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