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 for listing objects in a collection.
185#[derive(Clone, Debug, Default)]
186pub struct ListObjectsOptions {
187    /// Filter key-value pairs serialised as JSON pairs: only objects whose body
188    /// contains every listed top-level key with the exact JSON value are returned.
189    /// Each string is `"key":<json-value>` without surrounding braces.
190    pub filter: Vec<(String, serde_json::Value)>,
191    /// Maximum number of objects to return.
192    pub limit: Option<u64>,
193    /// Number of objects to skip before returning results.
194    pub offset: Option<u64>,
195}
196
197/// Options used when claiming a queue job.
198#[derive(Clone, Copy, Debug, Eq, PartialEq)]
199pub struct QueueClaimOptions {
200    /// Lease duration in milliseconds.
201    pub lease_ms: u64,
202}
203
204impl Default for QueueClaimOptions {
205    fn default() -> Self {
206        Self {
207            lease_ms: DEFAULT_QUEUE_LEASE_MS,
208        }
209    }
210}
211
212impl QueueClaimOptions {
213    /// Create queue claim options with the given lease duration.
214    #[must_use]
215    pub const fn new(lease_ms: u64) -> Self {
216        Self { lease_ms }
217    }
218}
219
220/// Options used when rejecting a leased queue job.
221#[derive(Clone, Debug, Eq, PartialEq, Default)]
222pub struct QueueNackOptions {
223    /// Delay before a retry can be claimed.
224    pub delay_ms: u64,
225    /// Error message from the worker, stored as `last_error` on the job.
226    pub error: String,
227}
228
229impl QueueNackOptions {
230    /// Create queue nack options with the given retry delay.
231    #[must_use]
232    pub const fn new(delay_ms: u64) -> Self {
233        Self {
234            delay_ms,
235            error: String::new(),
236        }
237    }
238
239    /// Create queue nack options with retry delay and an error message.
240    #[must_use]
241    pub fn with_error(delay_ms: u64, error: impl Into<String>) -> Self {
242        Self {
243            delay_ms,
244            error: error.into(),
245        }
246    }
247}
248
249/// Options used when performing a search.
250#[derive(Clone, Debug, Eq, PartialEq, Default)]
251pub struct SearchOptions {
252    /// Limit search to these collection or stream names.
253    pub collections: Option<Vec<String>>,
254    /// Maximum number of hits to return.
255    pub limit: Option<usize>,
256    /// Metadata filters to match custom fields in the JSON body.
257    pub filter: Option<serde_json::Value>,
258}
259
260/// A single match returned by a search query.
261#[derive(Clone, Debug, PartialEq)]
262pub struct SearchHit {
263    /// Result kind: "object" or "event".
264    pub kind: String,
265    /// Collection or stream name.
266    pub collection: String,
267    /// Object id or event sequence number.
268    pub id: String,
269    /// The indexed text that matched.
270    pub text: String,
271    /// Relevancy score.
272    pub score: f64,
273    /// The serialized body.
274    pub body: String,
275    /// Object version (only populated for objects).
276    pub version: Option<u64>,
277    /// Created timestamp.
278    pub created_at: String,
279    /// Updated timestamp (only populated for objects).
280    pub updated_at: Option<String>,
281    /// Event type (only populated for events).
282    pub event_type: Option<String>,
283}
284
285/// A graph link connecting two references.
286#[derive(Clone, Debug, PartialEq)]
287pub struct Link {
288    /// Unique link identifier.
289    pub id: String,
290    /// Source reference (e.g. "collection/id" or "stream/sequence").
291    pub from_ref: String,
292    /// Relationship type (e.g. "supports", "`depends_on`", "`chunk_of`").
293    pub link_type: String,
294    /// Target reference.
295    pub to_ref: String,
296    /// Optional weight for ranking (0.0 to 1.0).
297    pub weight: Option<f64>,
298    /// Optional metadata as JSON string.
299    pub metadata_json: String,
300    /// ISO 8601 creation timestamp.
301    pub created_at: String,
302}
303
304impl Link {
305    /// Create a new graph link.
306    pub fn new(
307        from_ref: impl Into<String>,
308        link_type: impl Into<String>,
309        to_ref: impl Into<String>,
310    ) -> Self {
311        Self {
312            id: String::new(),
313            from_ref: from_ref.into(),
314            link_type: link_type.into(),
315            to_ref: to_ref.into(),
316            weight: None,
317            metadata_json: "{}".to_string(),
318            created_at: String::new(),
319        }
320    }
321
322    /// Set the link weight.
323    #[must_use]
324    pub const fn with_weight(mut self, weight: f64) -> Self {
325        self.weight = Some(weight);
326        self
327    }
328
329    /// Set the metadata JSON.
330    #[must_use]
331    pub fn with_metadata(mut self, metadata: impl Into<String>) -> Self {
332        self.metadata_json = metadata.into();
333        self
334    }
335}
336
337/// Options for querying graph links.
338#[derive(Clone, Debug, Default, Eq, PartialEq)]
339pub struct LinkQueryOptions {
340    /// Filter by relationship type.
341    pub link_type: Option<String>,
342    /// Maximum number of results.
343    pub limit: Option<usize>,
344}
345
346/// Direction for neighbor queries.
347#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
348pub enum LinkDirection {
349    /// Only outgoing links (`from_ref` matches).
350    Outgoing,
351    /// Only incoming links (`to_ref` matches).
352    Incoming,
353    /// Both directions.
354    #[default]
355    Both,
356}