thingd-core 0.28.0

Core primitives for thingd, an object-shaped local memory engine for apps and agents.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
//! Data model types shared by storage adapters.

use crate::{u64_to_i64, unix_timestamp_millis};

/// Default queue lease duration in milliseconds.
pub const DEFAULT_QUEUE_LEASE_MS: u64 = 30_000;

/// Stable object key inside a collection.
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct ObjectKey {
    /// Collection name, such as `decisions`, `documents`, or `customers`.
    pub collection: String,
    /// Stable object identifier inside the collection.
    pub id: String,
}

impl ObjectKey {
    /// Create a new object key.
    pub fn new(collection: impl Into<String>, id: impl Into<String>) -> Self {
        Self {
            collection: collection.into(),
            id: id.into(),
        }
    }
}

/// An object stored in a thingd collection.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MemoryObject {
    /// Stable object key.
    pub key: ObjectKey,
    /// Serialized object body.
    pub body: String,
    /// Monotonic object version assigned by the store.
    pub version: u64,
    /// ISO 8601 creation timestamp, e.g. "2026-06-01T12:00:00.000Z". Empty if not set.
    pub created_at: String,
    /// ISO 8601 last-update timestamp. Empty if not set.
    pub updated_at: String,
}

impl MemoryObject {
    /// Create a new object record.
    pub fn new(
        collection: impl Into<String>,
        id: impl Into<String>,
        body: impl Into<String>,
    ) -> Self {
        Self {
            key: ObjectKey::new(collection, id),
            body: body.into(),
            version: 0,
            created_at: String::new(),
            updated_at: String::new(),
        }
    }
}

/// An append-only event stored in a thingd stream.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MemoryEvent {
    /// Stream name, such as `project:thingd` or `customer:cus_123`.
    pub stream: String,
    /// Event kind, such as `decision.made`.
    pub event_type: String,
    /// Serialized event body.
    pub body: String,
    /// Monotonic sequence assigned by the event log.
    pub sequence: u64,
    /// ISO 8601 creation timestamp. Empty if not set.
    pub created_at: String,
}

impl MemoryEvent {
    /// Create a new event record.
    pub fn new(
        stream: impl Into<String>,
        event_type: impl Into<String>,
        body: impl Into<String>,
    ) -> Self {
        Self {
            stream: stream.into(),
            event_type: event_type.into(),
            body: body.into(),
            sequence: 0,
            created_at: String::new(),
        }
    }
}

/// Queue job lifecycle state.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum QueueJobStatus {
    /// Ready to be claimed by a worker.
    Ready,
    /// Claimed by a worker and awaiting ack/nack.
    Leased,
    /// Completed successfully.
    Completed,
    /// Exhausted retries and moved to the dead-letter set.
    Dead,
}

/// A queued unit of work.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct QueueJob {
    /// Queue name.
    pub queue: String,
    /// Stable job identifier.
    pub id: String,
    /// Serialized job payload.
    pub body: String,
    /// Number of attempts already made.
    pub attempts: u32,
    /// Maximum attempts before the job should be considered dead.
    pub max_attempts: u32,
    /// Current job status.
    pub status: QueueJobStatus,
    /// Unix timestamp in milliseconds when this job becomes claimable.
    pub available_at_ms: i64,
    /// Unix timestamp in milliseconds when this job was leased.
    pub leased_at_ms: Option<i64>,
    /// Unix timestamp in milliseconds when this job lease expires.
    pub lease_expires_at_ms: Option<i64>,
    /// Unix timestamp in milliseconds when this job completed.
    pub completed_at_ms: Option<i64>,
    /// Unix timestamp in milliseconds when this job moved to dead-letter state.
    pub dead_at_ms: Option<i64>,
    /// ISO 8601 creation timestamp. Empty if not set.
    pub created_at: String,
    /// Error message from last nack. Empty if not set.
    pub last_error: String,
}

impl QueueJob {
    /// Create a new ready job.
    pub fn new(
        queue: impl Into<String>,
        id: impl Into<String>,
        body: impl Into<String>,
        max_attempts: u32,
    ) -> Self {
        Self {
            queue: queue.into(),
            id: id.into(),
            body: body.into(),
            attempts: 0,
            max_attempts,
            status: QueueJobStatus::Ready,
            available_at_ms: 0,
            leased_at_ms: None,
            lease_expires_at_ms: None,
            completed_at_ms: None,
            dead_at_ms: None,
            created_at: String::new(),
            last_error: String::new(),
        }
    }

    /// Make this job available after a delay.
    #[must_use]
    pub fn delay_by_ms(mut self, delay_ms: u64) -> Self {
        self.available_at_ms = unix_timestamp_millis().saturating_add(u64_to_i64(delay_ms));
        self
    }

    /// Set the exact Unix timestamp in milliseconds when this job is claimable.
    #[must_use]
    pub const fn available_at_ms(mut self, available_at_ms: i64) -> Self {
        self.available_at_ms = available_at_ms;
        self
    }
}

/// Options for listing events.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct ListEventsOptions {
    /// Only return events with sequence greater than this value.
    pub from_sequence: Option<u64>,
    /// Maximum number of events to return.
    pub limit: Option<u64>,
}

/// Sort direction for list queries.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum SortDirection {
    /// Ascending order (A→Z, oldest→newest, smallest→largest).
    #[default]
    Asc,
    /// Descending order (Z→A, newest→oldest, largest→smallest).
    Desc,
}

/// Sort specification for list queries.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SortBy {
    /// Field name: `id`, `collection`, `created_at`, `updated_at`, `version`.
    pub field: String,
    /// Sort direction.
    pub direction: SortDirection,
}

impl SortBy {
    /// Create ascending sort by field name.
    pub fn asc(field: impl Into<String>) -> Self {
        Self {
            field: field.into(),
            direction: SortDirection::Asc,
        }
    }

    /// Create descending sort by field name.
    pub fn desc(field: impl Into<String>) -> Self {
        Self {
            field: field.into(),
            direction: SortDirection::Desc,
        }
    }
}

/// Options for listing objects in a collection.
#[derive(Clone, Debug, Default)]
pub struct ListObjectsOptions {
    /// Filter key-value pairs serialised as JSON pairs: only objects whose body
    /// contains every listed top-level key with the exact JSON value are returned.
    /// Each string is `"key":<json-value>` without surrounding braces.
    pub filter: Vec<(String, serde_json::Value)>,
    /// Sort specification. Default is insertion order.
    pub sort_by: Option<SortBy>,
    /// Maximum number of objects to return.
    pub limit: Option<u64>,
    /// Number of objects to skip before returning results.
    pub offset: Option<u64>,
}

/// Options for putting an object.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct PutObjectOptions {
    /// Whether to update the FTS search index. Default: `true`.
    /// Set to `false` when only metadata changes (e.g. timestamp dedup)
    /// and the body text is identical — skips FTS DELETE + INSERT.
    pub index: bool,
}

impl Default for PutObjectOptions {
    fn default() -> Self {
        Self { index: true }
    }
}

/// Options used when claiming a queue job.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct QueueClaimOptions {
    /// Lease duration in milliseconds.
    pub lease_ms: u64,
}

impl Default for QueueClaimOptions {
    fn default() -> Self {
        Self {
            lease_ms: DEFAULT_QUEUE_LEASE_MS,
        }
    }
}

impl QueueClaimOptions {
    /// Create queue claim options with the given lease duration.
    #[must_use]
    pub const fn new(lease_ms: u64) -> Self {
        Self { lease_ms }
    }
}

/// Options used when rejecting a leased queue job.
#[derive(Clone, Debug, Eq, PartialEq, Default)]
pub struct QueueNackOptions {
    /// Delay before a retry can be claimed.
    pub delay_ms: u64,
    /// Error message from the worker, stored as `last_error` on the job.
    pub error: String,
}

impl QueueNackOptions {
    /// Create queue nack options with the given retry delay.
    #[must_use]
    pub const fn new(delay_ms: u64) -> Self {
        Self {
            delay_ms,
            error: String::new(),
        }
    }

    /// Create queue nack options with retry delay and an error message.
    #[must_use]
    pub fn with_error(delay_ms: u64, error: impl Into<String>) -> Self {
        Self {
            delay_ms,
            error: error.into(),
        }
    }
}

/// Options used when performing a search.
#[derive(Clone, Debug, Eq, PartialEq, Default)]
pub struct SearchOptions {
    /// Limit search to these collection or stream names.
    pub collections: Option<Vec<String>>,
    /// Maximum number of hits to return.
    pub limit: Option<usize>,
    /// Metadata filters to match custom fields in the JSON body.
    pub filter: Option<serde_json::Value>,
}

/// A single match returned by a search query.
#[derive(Clone, Debug, PartialEq)]
pub struct SearchHit {
    /// Result kind: "object" or "event".
    pub kind: String,
    /// Collection or stream name.
    pub collection: String,
    /// Object id or event sequence number.
    pub id: String,
    /// The indexed text that matched.
    pub text: String,
    /// Relevancy score.
    pub score: f64,
    /// The serialized body.
    pub body: String,
    /// Object version (only populated for objects).
    pub version: Option<u64>,
    /// Created timestamp.
    pub created_at: String,
    /// Updated timestamp (only populated for objects).
    pub updated_at: Option<String>,
    /// Event type (only populated for events).
    pub event_type: Option<String>,
}

/// A graph link connecting two references.
#[derive(Clone, Debug, PartialEq)]
pub struct Link {
    /// Unique link identifier.
    pub id: String,
    /// Source reference (e.g. "collection/id" or "stream/sequence").
    pub from_ref: String,
    /// Relationship type (e.g. "supports", "`depends_on`", "`chunk_of`").
    pub link_type: String,
    /// Target reference.
    pub to_ref: String,
    /// Optional weight for ranking (0.0 to 1.0).
    pub weight: Option<f64>,
    /// Optional metadata as JSON string.
    pub metadata_json: String,
    /// ISO 8601 creation timestamp.
    pub created_at: String,
}

impl Link {
    /// Create a new graph link.
    pub fn new(
        from_ref: impl Into<String>,
        link_type: impl Into<String>,
        to_ref: impl Into<String>,
    ) -> Self {
        Self {
            id: String::new(),
            from_ref: from_ref.into(),
            link_type: link_type.into(),
            to_ref: to_ref.into(),
            weight: None,
            metadata_json: "{}".to_string(),
            created_at: String::new(),
        }
    }

    /// Set the link weight.
    #[must_use]
    pub const fn with_weight(mut self, weight: f64) -> Self {
        self.weight = Some(weight);
        self
    }

    /// Set the metadata JSON.
    #[must_use]
    pub fn with_metadata(mut self, metadata: impl Into<String>) -> Self {
        self.metadata_json = metadata.into();
        self
    }
}

/// Options for querying graph links.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct LinkQueryOptions {
    /// Filter by relationship type.
    pub link_type: Option<String>,
    /// Maximum number of results.
    pub limit: Option<usize>,
}

/// Direction for neighbor queries.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum LinkDirection {
    /// Only outgoing links (`from_ref` matches).
    Outgoing,
    /// Only incoming links (`to_ref` matches).
    Incoming,
    /// Both directions.
    #[default]
    Both,
}