Skip to main content

thingd_core/
model.rs

1//! Data model types shared by storage adapters.
2
3use crate::{u64_to_i64, unix_timestamp_millis};
4
5/// Default queue lease duration in milliseconds.
6pub const DEFAULT_QUEUE_LEASE_MS: u64 = 30_000;
7
8/// Stable object key inside a collection.
9#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
10pub struct ObjectKey {
11    /// Collection name, such as `decisions`, `documents`, or `customers`.
12    pub collection: String,
13    /// Stable object identifier inside the collection.
14    pub id: String,
15}
16
17impl ObjectKey {
18    /// Create a new object key.
19    pub fn new(collection: impl Into<String>, id: impl Into<String>) -> Self {
20        Self {
21            collection: collection.into(),
22            id: id.into(),
23        }
24    }
25}
26
27/// An object stored in a thingd collection.
28#[derive(Clone, Debug, Eq, PartialEq)]
29pub struct MemoryObject {
30    /// Stable object key.
31    pub key: ObjectKey,
32    /// Serialized object body.
33    pub body: String,
34    /// Monotonic object version assigned by the store.
35    pub version: u64,
36    /// ISO 8601 creation timestamp, e.g. "2026-06-01T12:00:00.000Z". Empty if not set.
37    pub created_at: String,
38    /// ISO 8601 last-update timestamp. Empty if not set.
39    pub updated_at: String,
40}
41
42impl MemoryObject {
43    /// Create a new object record.
44    pub fn new(
45        collection: impl Into<String>,
46        id: impl Into<String>,
47        body: impl Into<String>,
48    ) -> Self {
49        Self {
50            key: ObjectKey::new(collection, id),
51            body: body.into(),
52            version: 0,
53            created_at: String::new(),
54            updated_at: String::new(),
55        }
56    }
57}
58
59/// An append-only event stored in a thingd stream.
60#[derive(Clone, Debug, Eq, PartialEq)]
61pub struct MemoryEvent {
62    /// Stream name, such as `project:thingd` or `customer:cus_123`.
63    pub stream: String,
64    /// Event kind, such as `decision.made`.
65    pub event_type: String,
66    /// Serialized event body.
67    pub body: String,
68    /// Monotonic sequence assigned by the event log.
69    pub sequence: u64,
70    /// ISO 8601 creation timestamp. Empty if not set.
71    pub created_at: String,
72}
73
74impl MemoryEvent {
75    /// Create a new event record.
76    pub fn new(
77        stream: impl Into<String>,
78        event_type: impl Into<String>,
79        body: impl Into<String>,
80    ) -> Self {
81        Self {
82            stream: stream.into(),
83            event_type: event_type.into(),
84            body: body.into(),
85            sequence: 0,
86            created_at: String::new(),
87        }
88    }
89}
90
91/// Queue job lifecycle state.
92#[derive(Clone, Copy, Debug, Eq, PartialEq)]
93pub enum QueueJobStatus {
94    /// Ready to be claimed by a worker.
95    Ready,
96    /// Claimed by a worker and awaiting ack/nack.
97    Leased,
98    /// Completed successfully.
99    Completed,
100    /// Exhausted retries and moved to the dead-letter set.
101    Dead,
102}
103
104/// A queued unit of work.
105#[derive(Clone, Debug, Eq, PartialEq)]
106pub struct QueueJob {
107    /// Queue name.
108    pub queue: String,
109    /// Stable job identifier.
110    pub id: String,
111    /// Serialized job payload.
112    pub body: String,
113    /// Number of attempts already made.
114    pub attempts: u32,
115    /// Maximum attempts before the job should be considered dead.
116    pub max_attempts: u32,
117    /// Current job status.
118    pub status: QueueJobStatus,
119    /// Unix timestamp in milliseconds when this job becomes claimable.
120    pub available_at_ms: i64,
121    /// Unix timestamp in milliseconds when this job was leased.
122    pub leased_at_ms: Option<i64>,
123    /// Unix timestamp in milliseconds when this job lease expires.
124    pub lease_expires_at_ms: Option<i64>,
125    /// Unix timestamp in milliseconds when this job completed.
126    pub completed_at_ms: Option<i64>,
127    /// Unix timestamp in milliseconds when this job moved to dead-letter state.
128    pub dead_at_ms: Option<i64>,
129    /// ISO 8601 creation timestamp. Empty if not set.
130    pub created_at: String,
131    /// Error message from last nack. Empty if not set.
132    pub last_error: String,
133}
134
135impl QueueJob {
136    /// Create a new ready job.
137    pub fn new(
138        queue: impl Into<String>,
139        id: impl Into<String>,
140        body: impl Into<String>,
141        max_attempts: u32,
142    ) -> Self {
143        Self {
144            queue: queue.into(),
145            id: id.into(),
146            body: body.into(),
147            attempts: 0,
148            max_attempts,
149            status: QueueJobStatus::Ready,
150            available_at_ms: 0,
151            leased_at_ms: None,
152            lease_expires_at_ms: None,
153            completed_at_ms: None,
154            dead_at_ms: None,
155            created_at: String::new(),
156            last_error: String::new(),
157        }
158    }
159
160    /// Make this job available after a delay.
161    #[must_use]
162    pub fn delay_by_ms(mut self, delay_ms: u64) -> Self {
163        self.available_at_ms = unix_timestamp_millis().saturating_add(u64_to_i64(delay_ms));
164        self
165    }
166
167    /// Set the exact Unix timestamp in milliseconds when this job is claimable.
168    #[must_use]
169    pub const fn available_at_ms(mut self, available_at_ms: i64) -> Self {
170        self.available_at_ms = available_at_ms;
171        self
172    }
173}
174
175/// Options for listing events.
176#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
177pub struct ListEventsOptions {
178    /// Only return events with sequence greater than this value.
179    pub from_sequence: Option<u64>,
180    /// Maximum number of events to return.
181    pub limit: Option<u64>,
182}
183
184/// Options used when claiming a queue job.
185#[derive(Clone, Copy, Debug, Eq, PartialEq)]
186pub struct QueueClaimOptions {
187    /// Lease duration in milliseconds.
188    pub lease_ms: u64,
189}
190
191impl Default for QueueClaimOptions {
192    fn default() -> Self {
193        Self {
194            lease_ms: DEFAULT_QUEUE_LEASE_MS,
195        }
196    }
197}
198
199impl QueueClaimOptions {
200    /// Create queue claim options with the given lease duration.
201    #[must_use]
202    pub const fn new(lease_ms: u64) -> Self {
203        Self { lease_ms }
204    }
205}
206
207/// Options used when rejecting a leased queue job.
208#[derive(Clone, Debug, Eq, PartialEq, Default)]
209pub struct QueueNackOptions {
210    /// Delay before a retry can be claimed.
211    pub delay_ms: u64,
212    /// Error message from the worker, stored as `last_error` on the job.
213    pub error: String,
214}
215
216impl QueueNackOptions {
217    /// Create queue nack options with the given retry delay.
218    #[must_use]
219    pub const fn new(delay_ms: u64) -> Self {
220        Self {
221            delay_ms,
222            error: String::new(),
223        }
224    }
225
226    /// Create queue nack options with retry delay and an error message.
227    #[must_use]
228    pub fn with_error(delay_ms: u64, error: impl Into<String>) -> Self {
229        Self {
230            delay_ms,
231            error: error.into(),
232        }
233    }
234}
235
236/// Options used when performing a search.
237#[derive(Clone, Debug, Eq, PartialEq, Default)]
238pub struct SearchOptions {
239    /// Limit search to these collection or stream names.
240    pub collections: Option<Vec<String>>,
241    /// Maximum number of hits to return.
242    pub limit: Option<usize>,
243    /// Metadata filters to match custom fields in the JSON body.
244    pub filter: Option<serde_json::Value>,
245}
246
247/// A single match returned by a search query.
248#[derive(Clone, Debug, PartialEq)]
249pub struct SearchHit {
250    /// Result kind: "object" or "event".
251    pub kind: String,
252    /// Collection or stream name.
253    pub collection: String,
254    /// Object id or event sequence number.
255    pub id: String,
256    /// The indexed text that matched.
257    pub text: String,
258    /// Relevancy score.
259    pub score: f64,
260    /// The serialized body.
261    pub body: String,
262    /// Object version (only populated for objects).
263    pub version: Option<u64>,
264    /// Created timestamp.
265    pub created_at: String,
266    /// Updated timestamp (only populated for objects).
267    pub updated_at: Option<String>,
268    /// Event type (only populated for events).
269    pub event_type: Option<String>,
270}
271
272/// A graph link connecting two references.
273#[derive(Clone, Debug, PartialEq)]
274pub struct Link {
275    /// Unique link identifier.
276    pub id: String,
277    /// Source reference (e.g. "collection/id" or "stream/sequence").
278    pub from_ref: String,
279    /// Relationship type (e.g. "supports", "`depends_on`", "`chunk_of`").
280    pub link_type: String,
281    /// Target reference.
282    pub to_ref: String,
283    /// Optional weight for ranking (0.0 to 1.0).
284    pub weight: Option<f64>,
285    /// Optional metadata as JSON string.
286    pub metadata_json: String,
287    /// ISO 8601 creation timestamp.
288    pub created_at: String,
289}
290
291impl Link {
292    /// Create a new graph link.
293    pub fn new(
294        from_ref: impl Into<String>,
295        link_type: impl Into<String>,
296        to_ref: impl Into<String>,
297    ) -> Self {
298        Self {
299            id: String::new(),
300            from_ref: from_ref.into(),
301            link_type: link_type.into(),
302            to_ref: to_ref.into(),
303            weight: None,
304            metadata_json: "{}".to_string(),
305            created_at: String::new(),
306        }
307    }
308
309    /// Set the link weight.
310    #[must_use]
311    pub const fn with_weight(mut self, weight: f64) -> Self {
312        self.weight = Some(weight);
313        self
314    }
315
316    /// Set the metadata JSON.
317    #[must_use]
318    pub fn with_metadata(mut self, metadata: impl Into<String>) -> Self {
319        self.metadata_json = metadata.into();
320        self
321    }
322}
323
324/// Options for querying graph links.
325#[derive(Clone, Debug, Default, Eq, PartialEq)]
326pub struct LinkQueryOptions {
327    /// Filter by relationship type.
328    pub link_type: Option<String>,
329    /// Maximum number of results.
330    pub limit: Option<usize>,
331}
332
333/// Direction for neighbor queries.
334#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
335pub enum LinkDirection {
336    /// Only outgoing links (`from_ref` matches).
337    Outgoing,
338    /// Only incoming links (`to_ref` matches).
339    Incoming,
340    /// Both directions.
341    #[default]
342    Both,
343}