Skip to main content

moire_types/
primitives.rs

1use facet::Facet;
2#[cfg(feature = "rusqlite")]
3use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSql, ToSqlOutput, ValueRef};
4use std::fmt;
5use std::sync::OnceLock;
6use std::sync::atomic::{AtomicU64, Ordering};
7use std::time::{Instant, SystemTime, UNIX_EPOCH};
8
9#[derive(Facet, Clone, Copy, Debug, PartialEq, Eq)]
10#[repr(u8)]
11#[facet(rename_all = "snake_case")]
12pub enum Direction {
13    Incoming,
14    Outgoing,
15}
16
17#[derive(Facet, Clone, Copy, Debug, PartialEq, Eq)]
18#[repr(u8)]
19#[facet(rename_all = "snake_case")]
20pub enum ChannelDir {
21    Tx,
22    Rx,
23}
24
25/// Raw JSON text payload used by request/response entities.
26///
27/// This wrapper preserves the exact JSON source string that was captured
28/// at instrumentation boundaries.
29#[derive(Facet, Clone, Debug, PartialEq, Eq)]
30#[facet(transparent)]
31pub struct Json(pub(crate) String);
32
33impl Json {
34    pub fn new(text: impl Into<String>) -> Self {
35        Self(text.into())
36    }
37
38    pub fn as_str(&self) -> &str {
39        self.0.as_str()
40    }
41
42    pub fn into_string(self) -> String {
43        self.0
44    }
45}
46
47#[cfg(feature = "rusqlite")]
48impl ToSql for Json {
49    fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
50        Ok(self.as_str().into())
51    }
52}
53
54#[cfg(feature = "rusqlite")]
55impl FromSql for Json {
56    fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
57        Ok(Json::new(String::column_result(value)?))
58    }
59}
60
61/// First-use monotonic anchor for process-relative timestamps.
62/// "Process birth" is defined as the first call to `PTime::now()`.
63fn ptime_anchor() -> &'static Instant {
64    static PTIME_ANCHOR: OnceLock<Instant> = OnceLock::new();
65    PTIME_ANCHOR.get_or_init(Instant::now)
66}
67
68// r[impl model.ptime]
69/// process start time + N milliseconds
70#[derive(Facet, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
71#[facet(transparent)]
72pub struct PTime(u64);
73
74impl PTime {
75    pub fn now() -> Self {
76        let elapsed_ms = ptime_anchor().elapsed().as_millis().min(u64::MAX as u128) as u64;
77        Self(elapsed_ms)
78    }
79
80    pub fn as_millis(&self) -> u64 {
81        self.0
82    }
83}
84
85#[cfg(feature = "rusqlite")]
86impl ToSql for PTime {
87    fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
88        let as_i64 = i64::try_from(self.0).map_err(|_| {
89            rusqlite::Error::ToSqlConversionFailure(Box::new(std::io::Error::new(
90                std::io::ErrorKind::InvalidData,
91                format!("PTime out of SQLite i64 range: {}", self.0),
92            )))
93        })?;
94        Ok(as_i64.into())
95    }
96}
97
98#[cfg(feature = "rusqlite")]
99impl FromSql for PTime {
100    fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
101        let raw_i64 = i64::column_result(value)?;
102        let raw_u64 = u64::try_from(raw_i64).map_err(|_| FromSqlError::OutOfRange(raw_i64))?;
103        Ok(PTime(raw_u64))
104    }
105}
106
107/// Opaque textual entity identifier suitable for wire formats and JS runtimes.
108#[derive(Facet, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
109#[facet(transparent)]
110pub struct EntityId(pub(crate) String);
111
112/// Opaque textual scope identifier suitable for wire formats and JS runtimes.
113#[derive(Facet, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
114#[facet(transparent)]
115pub struct ScopeId(pub(crate) String);
116
117/// Opaque textual event identifier suitable for wire formats and JS runtimes.
118#[derive(Facet, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
119#[facet(transparent)]
120pub struct EventId(pub(crate) String);
121
122#[derive(Facet, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
123#[facet(transparent)]
124pub struct ConnectionId(pub(crate) u64);
125
126#[derive(Facet, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
127#[facet(transparent)]
128pub struct ProcessId(pub(crate) String);
129
130#[derive(Facet, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
131#[facet(transparent)]
132pub struct SessionId(pub(crate) String);
133
134impl EntityId {
135    pub fn new(id: impl Into<String>) -> Self {
136        Self(id.into())
137    }
138
139    pub fn as_str(&self) -> &str {
140        self.0.as_str()
141    }
142}
143
144impl ScopeId {
145    pub fn new(id: impl Into<String>) -> Self {
146        Self(id.into())
147    }
148
149    pub fn as_str(&self) -> &str {
150        self.0.as_str()
151    }
152}
153
154impl EventId {
155    pub fn new(id: impl Into<String>) -> Self {
156        Self(id.into())
157    }
158
159    pub fn as_str(&self) -> &str {
160        self.0.as_str()
161    }
162}
163
164impl ConnectionId {
165    pub fn new(id: u64) -> Self {
166        assert!(
167            id > 0 && id <= moire_trace_types::JS_SAFE_INT_MAX_U64,
168            "invariant violated: connection_id must be in 1..={}, got {id}",
169            moire_trace_types::JS_SAFE_INT_MAX_U64
170        );
171        Self(id)
172    }
173
174    pub fn next(self) -> Self {
175        Self::new(self.0.saturating_add(1))
176    }
177}
178
179impl fmt::Display for ConnectionId {
180    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181        write!(f, "CONNECTION#{:x}", self.0)
182    }
183}
184
185impl ProcessId {
186    pub fn new(id: impl Into<String>) -> Self {
187        Self(id.into())
188    }
189
190    pub fn as_str(&self) -> &str {
191        self.0.as_str()
192    }
193}
194
195impl SessionId {
196    pub fn new(id: impl Into<String>) -> Self {
197        Self(id.into())
198    }
199
200    pub fn from_ordinal(value: u64) -> Self {
201        Self(format!("session:{value}"))
202    }
203
204    pub fn as_str(&self) -> &str {
205        self.0.as_str()
206    }
207}
208
209#[cfg(feature = "rusqlite")]
210impl ToSql for ProcessId {
211    fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
212        Ok(self.as_str().into())
213    }
214}
215
216#[cfg(feature = "rusqlite")]
217impl FromSql for ProcessId {
218    fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
219        Ok(ProcessId::new(String::column_result(value)?))
220    }
221}
222
223#[cfg(feature = "rusqlite")]
224impl ToSql for ConnectionId {
225    fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
226        let value = i64::try_from(self.0).map_err(|_| {
227            rusqlite::Error::ToSqlConversionFailure(Box::new(std::io::Error::new(
228                std::io::ErrorKind::InvalidData,
229                format!("ConnectionId out of SQLite i64 range: {}", self.0),
230            )))
231        })?;
232        Ok(value.into())
233    }
234}
235
236#[cfg(feature = "rusqlite")]
237impl FromSql for ConnectionId {
238    fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
239        let raw_i64 = i64::column_result(value)?;
240        let raw_u64 = u64::try_from(raw_i64).map_err(|_| FromSqlError::OutOfRange(raw_i64))?;
241        Ok(ConnectionId::new(raw_u64))
242    }
243}
244
245#[cfg(feature = "rusqlite")]
246impl ToSql for EntityId {
247    fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
248        Ok(self.as_str().into())
249    }
250}
251
252#[cfg(feature = "rusqlite")]
253impl FromSql for EntityId {
254    fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
255        Ok(EntityId::new(String::column_result(value)?))
256    }
257}
258
259#[cfg(feature = "rusqlite")]
260impl ToSql for ScopeId {
261    fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
262        Ok(self.as_str().into())
263    }
264}
265
266#[cfg(feature = "rusqlite")]
267impl FromSql for ScopeId {
268    fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
269        Ok(ScopeId::new(String::column_result(value)?))
270    }
271}
272
273#[cfg(feature = "rusqlite")]
274impl ToSql for EventId {
275    fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
276        Ok(self.as_str().into())
277    }
278}
279
280#[cfg(feature = "rusqlite")]
281impl FromSql for EventId {
282    fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
283        Ok(EventId::new(String::column_result(value)?))
284    }
285}
286
287pub(crate) fn next_entity_id() -> EntityId {
288    EntityId(next_opaque_id())
289}
290
291pub(crate) fn next_scope_id() -> ScopeId {
292    ScopeId(next_opaque_id())
293}
294
295pub(crate) fn next_event_id() -> EventId {
296    EventId(next_opaque_id())
297}
298
299pub fn next_process_id() -> ProcessId {
300    ProcessId(next_opaque_id())
301}
302
303pub fn process_prefix_u16() -> u16 {
304    static PROCESS_PREFIX: OnceLock<u16> = OnceLock::new();
305    *PROCESS_PREFIX.get_or_init(|| {
306        let pid = std::process::id() as u64;
307        let seed = SystemTime::now()
308            .duration_since(UNIX_EPOCH)
309            .map(|d| d.as_nanos() as u64)
310            .unwrap_or(0);
311        ((seed ^ pid) & 0xFFFF) as u16
312    })
313}
314
315// r[impl model.id.uniqueness]
316fn next_opaque_id() -> String {
317    static COUNTER: AtomicU64 = AtomicU64::new(1);
318
319    let prefix = process_prefix_u16();
320
321    let counter = COUNTER.fetch_add(1, Ordering::Relaxed) & 0x0000_FFFF_FFFF_FFFF;
322    let raw = ((prefix as u64) << 48) | counter;
323    MoireHex(raw).to_string()
324}
325
326// r[impl model.id.format]
327/// `moire-hex` formatter:
328/// lowercase hex with `a..f` remapped to `p,e,s,P,E,S`.
329struct MoireHex(u64);
330
331impl fmt::Display for MoireHex {
332    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
333        const DIGITS: &[u8; 16] = b"0123456789pesPES";
334        let mut out = [0u8; 16];
335        for (idx, shift) in (0..16).zip((0..64).step_by(4).rev()) {
336            let nibble = ((self.0 >> shift) & 0xF) as usize;
337            out[idx] = DIGITS[nibble];
338        }
339        // SAFETY: DIGITS only contains ASCII bytes.
340        f.write_str(unsafe { std::str::from_utf8_unchecked(&out) })
341    }
342}