spacetimedb_lib/
connection_id.rs

1use anyhow::Context as _;
2use core::{fmt, net::Ipv6Addr};
3use spacetimedb_bindings_macro::{Deserialize, Serialize};
4use spacetimedb_lib::from_hex_pad;
5use spacetimedb_sats::hex::HexString;
6use spacetimedb_sats::{impl_deserialize, impl_serialize, impl_st, AlgebraicType, AlgebraicValue};
7
8/// A unique identifier for a client connection to a SpacetimeDB database.
9///
10/// This is a special type.
11///
12/// A `ConnectionId` is a 128-bit unsigned integer. This can be serialized in various ways.
13/// - In JSON, an `ConnectionId` is represented as a BARE DECIMAL number.
14///   This requires some care when deserializing; see
15///   <https://stackoverflow.com/questions/69644298/how-to-make-json-parse-to-treat-all-the-numbers-as-bigint>
16/// - In BSATN, a `ConnectionId` is represented as a LITTLE-ENDIAN number 16 bytes long.
17/// - In memory, a `ConnectionId` is stored as a 128-bit number with the endianness of the host system.
18//
19// If you are manually converting a hexadecimal string to a byte array like so:
20// ```ignore
21// "0xb0b1b2..."
22// ->
23// [0xb0, 0xb1, 0xb2, ...]
24// ```
25// Make sure you call `ConnectionId::from_be_byte_array` and NOT `ConnectionId::from_le_byte_array`.
26// The standard way of writing hexadecimal numbers follows a big-endian convention, if you
27// index the characters in written text in increasing order from left to right.
28#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
29pub struct ConnectionId {
30    __connection_id__: u128,
31}
32
33impl_st!([] ConnectionId, AlgebraicType::connection_id());
34
35#[cfg(feature = "metrics_impls")]
36impl spacetimedb_metrics::typed_prometheus::AsPrometheusLabel for ConnectionId {
37    fn as_prometheus_str(&self) -> impl AsRef<str> + '_ {
38        self.to_hex()
39    }
40}
41
42impl fmt::Display for ConnectionId {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        f.pad(&self.to_hex())
45    }
46}
47
48impl fmt::Debug for ConnectionId {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        f.debug_tuple("ConnectionId").field(&format_args!("{self}")).finish()
51    }
52}
53
54impl ConnectionId {
55    pub const ZERO: Self = Self::from_u128(0);
56
57    pub const fn from_u128(__connection_id__: u128) -> Self {
58        Self { __connection_id__ }
59    }
60
61    pub const fn to_u128(&self) -> u128 {
62        self.__connection_id__
63    }
64
65    /// Create an `ConnectionId` from a little-endian byte array.
66    ///
67    /// If you are parsing an `ConnectionId` from a string,
68    /// you probably want [`Self::from_be_byte_array`] instead.
69    /// But if you need to convert a hexadecimal string to a `ConnectionId`,
70    /// just use [`Self::from_hex`].
71    pub const fn from_le_byte_array(arr: [u8; 16]) -> Self {
72        Self::from_u128(u128::from_le_bytes(arr))
73    }
74
75    /// Create an `ConnectionId` from a big-endian byte array.
76    ///
77    /// This method is the correct choice
78    /// if you have converted the bytes of a hexadecimal-formatted `ConnectionId`
79    /// to a byte array in the following way:
80    ///
81    /// ```ignore
82    /// "0xb0b1b2..."
83    /// ->
84    /// [0xb0, 0xb1, 0xb2, ...]
85    /// ```
86    ///
87    /// But if you need to convert a hexadecimal string to a `ConnectionId`,
88    /// just use [`Self::from_hex`].
89    pub const fn from_be_byte_array(arr: [u8; 16]) -> Self {
90        Self::from_u128(u128::from_be_bytes(arr))
91    }
92
93    /// Convert a `ConnectionId` to a little-endian byte array.
94    pub const fn as_le_byte_array(&self) -> [u8; 16] {
95        self.__connection_id__.to_le_bytes()
96    }
97
98    /// Convert a `ConnectionId` to a big-endian byte array.
99    ///
100    /// This is a format suitable for printing as a hexadecimal string.
101    /// But if you need to convert a `ConnectionId` to a hexadecimal string,
102    /// just use [`Self::to_hex`].
103    pub const fn as_be_byte_array(&self) -> [u8; 16] {
104        self.__connection_id__.to_be_bytes()
105    }
106
107    /// Parse a hexadecimal string into a `ConnectionId`.
108    pub fn from_hex(hex: &str) -> Result<Self, anyhow::Error> {
109        from_hex_pad::<[u8; 16], _>(hex)
110            .context("ConnectionIds must be 32 hex characters (16 bytes) in length.")
111            .map(Self::from_be_byte_array)
112    }
113
114    /// Convert this `ConnectionId` to a hexadecimal string.
115    pub fn to_hex(self) -> HexString<16> {
116        spacetimedb_sats::hex::encode(&self.as_be_byte_array())
117    }
118
119    /// Extract the first 8 bytes of this `ConnectionId` as if it was stored in big-endian
120    /// format. (That is, the most significant bytes.)
121    pub fn abbreviate(&self) -> [u8; 8] {
122        self.as_be_byte_array()[..8].try_into().unwrap()
123    }
124
125    /// Extract the first 16 characters of this `ConnectionId`'s hexadecimal representation.
126    pub fn to_abbreviated_hex(self) -> HexString<8> {
127        spacetimedb_sats::hex::encode(&self.abbreviate())
128    }
129
130    /// Create an `ConnectionId` from a slice, assumed to be in big-endian format.
131    pub fn from_be_slice(slice: impl AsRef<[u8]>) -> Self {
132        let slice = slice.as_ref();
133        let mut dst = [0u8; 16];
134        dst.copy_from_slice(slice);
135        Self::from_be_byte_array(dst)
136    }
137
138    pub fn to_ipv6(self) -> Ipv6Addr {
139        Ipv6Addr::from(self.__connection_id__)
140    }
141
142    #[allow(dead_code)]
143    pub fn to_ipv6_string(self) -> String {
144        self.to_ipv6().to_string()
145    }
146
147    pub fn none_if_zero(self) -> Option<Self> {
148        (self != Self::ZERO).then_some(self)
149    }
150}
151
152impl From<u128> for ConnectionId {
153    fn from(value: u128) -> Self {
154        Self::from_u128(value)
155    }
156}
157
158impl From<ConnectionId> for AlgebraicValue {
159    fn from(value: ConnectionId) -> Self {
160        AlgebraicValue::product([value.to_u128().into()])
161    }
162}
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
165pub struct ConnectionIdForUrl(u128);
166
167impl From<ConnectionId> for ConnectionIdForUrl {
168    fn from(addr: ConnectionId) -> Self {
169        ConnectionIdForUrl(addr.to_u128())
170    }
171}
172
173impl From<ConnectionIdForUrl> for ConnectionId {
174    fn from(addr: ConnectionIdForUrl) -> Self {
175        ConnectionId::from_u128(addr.0)
176    }
177}
178
179impl_serialize!([] ConnectionIdForUrl, (self, ser) => self.0.serialize(ser));
180impl_deserialize!([] ConnectionIdForUrl, de => u128::deserialize(de).map(Self));
181impl_st!([] ConnectionIdForUrl, AlgebraicType::U128);
182
183#[cfg(feature = "serde")]
184impl serde::Serialize for ConnectionIdForUrl {
185    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
186    where
187        S: serde::Serializer,
188    {
189        spacetimedb_sats::ser::serde::serialize_to(&ConnectionId::from(*self).as_be_byte_array(), serializer)
190    }
191}
192
193#[cfg(feature = "serde")]
194impl<'de> serde::Deserialize<'de> for ConnectionIdForUrl {
195    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
196    where
197        D: serde::Deserializer<'de>,
198    {
199        let arr = spacetimedb_sats::de::serde::deserialize_from(deserializer)?;
200        Ok(ConnectionId::from_be_byte_array(arr).into())
201    }
202}
203
204#[cfg(feature = "serde")]
205impl serde::Serialize for ConnectionId {
206    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
207    where
208        S: serde::Serializer,
209    {
210        spacetimedb_sats::ser::serde::serialize_to(&self.as_be_byte_array(), serializer)
211    }
212}
213
214#[cfg(feature = "serde")]
215impl<'de> serde::Deserialize<'de> for ConnectionId {
216    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
217    where
218        D: serde::Deserializer<'de>,
219    {
220        let arr = spacetimedb_sats::de::serde::deserialize_from(deserializer)?;
221        Ok(ConnectionId::from_be_byte_array(arr))
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228    use proptest::prelude::*;
229    use spacetimedb_sats::bsatn;
230    use spacetimedb_sats::ser::serde::SerializeWrapper;
231    use spacetimedb_sats::GroundSpacetimeType as _;
232
233    #[test]
234    fn connection_id_json_serialization_big_endian() {
235        let conn_id = ConnectionId::from_be_byte_array([0xff, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]);
236
237        let hex = conn_id.to_hex();
238        assert!(
239            hex.as_str().starts_with("ff01"),
240            "expected {hex:?} to start with \"ff01\""
241        );
242
243        let json1 = serde_json::to_string(&conn_id).unwrap();
244        let json2 = serde_json::to_string(&ConnectionIdForUrl::from(conn_id)).unwrap();
245
246        assert!(
247            json1.contains(hex.as_str()),
248            "expected {json1} to contain {hex} but it didn't"
249        );
250        assert!(
251            json2.contains(hex.as_str()),
252            "expected {json2} to contain {hex} but it didn't"
253        );
254
255        // Serde made the slightly odd choice to serialize u128 as decimals in JSON.
256        // So we have an incompatibility between our formats here :/
257        // The implementation of serialization for `sats` types via `SerializeWrapper` just calls
258        // the `serde` implementation to serialize primitives, so we can't fix this
259        // unless we make a custom implementation of `Serialize` and `Deserialize` for `ConnectionId`.
260        let decimal = conn_id.to_u128().to_string();
261        let json3 = serde_json::to_string(SerializeWrapper::from_ref(&conn_id)).unwrap();
262        assert!(
263            json3.contains(decimal.as_str()),
264            "expected {json3} to contain {decimal} but it didn't"
265        );
266    }
267
268    proptest! {
269        #[test]
270        fn test_bsatn_roundtrip(val: u128) {
271            let conn_id = ConnectionId::from_u128(val);
272            let ser = bsatn::to_vec(&conn_id).unwrap();
273            let de = bsatn::from_slice(&ser).unwrap();
274            assert_eq!(conn_id, de);
275        }
276
277        #[test]
278        fn connection_id_conversions(a: u128) {
279            let v = ConnectionId::from_u128(a);
280
281            prop_assert_eq!(ConnectionId::from_le_byte_array(v.as_le_byte_array()), v);
282            prop_assert_eq!(ConnectionId::from_be_byte_array(v.as_be_byte_array()), v);
283            prop_assert_eq!(ConnectionId::from_hex(v.to_hex().as_str()).unwrap(), v);
284        }
285    }
286
287    #[test]
288    fn connection_id_is_special() {
289        assert!(ConnectionId::get_type().is_special());
290    }
291
292    #[cfg(feature = "serde")]
293    mod serde {
294        use super::*;
295        use crate::sats::{algebraic_value::de::ValueDeserializer, de::Deserialize, Typespace};
296        use crate::ser::serde::SerializeWrapper;
297        use crate::WithTypespace;
298
299        proptest! {
300            /// Tests the round-trip used when using the `spacetime subscribe`
301            /// CLI command.
302            /// Somewhat confusingly, this is distinct from the ser-de path
303            /// in `test_serde_roundtrip`.
304            #[test]
305            fn test_wrapper_roundtrip(val: u128) {
306                let conn_id = ConnectionId::from_u128(val);
307                let wrapped = SerializeWrapper::new(&conn_id);
308
309                let ser = serde_json::to_string(&wrapped).unwrap();
310                let empty = Typespace::default();
311                let conn_id_ty = ConnectionId::get_type();
312                let conn_id_ty = WithTypespace::new(&empty, &conn_id_ty);
313                let row = serde_json::from_str::<serde_json::Value>(&ser[..])?;
314                let de = ::serde::de::DeserializeSeed::deserialize(
315                    crate::de::serde::SeedWrapper(
316                        conn_id_ty
317                    ),
318                    row)?;
319                let de = ConnectionId::deserialize(ValueDeserializer::new(de)).unwrap();
320                prop_assert_eq!(conn_id, de);
321            }
322        }
323
324        proptest! {
325            #[test]
326            fn test_serde_roundtrip(val: u128) {
327                let conn_id = ConnectionId::from_u128(val);
328                let to_url = ConnectionIdForUrl::from(conn_id);
329                let ser = serde_json::to_vec(&to_url).unwrap();
330                let de = serde_json::from_slice::<ConnectionIdForUrl>(&ser).unwrap();
331                let from_url = ConnectionId::from(de);
332                prop_assert_eq!(conn_id, from_url);
333            }
334        }
335    }
336}