Skip to main content

pulsedb/
types.rs

1//! Core type definitions for PulseDB identifiers and timestamps.
2//!
3//! This module defines the fundamental ID types used throughout PulseDB.
4//! All ID types use UUID v7 for time-ordered unique identification.
5
6use std::fmt;
7
8use serde::{Deserialize, Serialize};
9use uuid::Uuid;
10
11/// Collective identifier (UUID v7 for time-ordering).
12///
13/// Collectives are isolated namespaces for agent experiences, typically one per project.
14/// Each collective has its own HNSW index and embedding dimension.
15///
16/// # Example
17/// ```
18/// use pulsedb::CollectiveId;
19///
20/// let id = CollectiveId::new();
21/// println!("Created collective: {}", id);
22/// ```
23#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
24pub struct CollectiveId(pub Uuid);
25
26impl CollectiveId {
27    /// Creates a new CollectiveId with a UUID v7 (time-ordered).
28    #[inline]
29    pub fn new() -> Self {
30        Self(Uuid::now_v7())
31    }
32
33    /// Creates a nil (all zeros) CollectiveId.
34    /// Useful for testing or sentinel values.
35    #[inline]
36    pub fn nil() -> Self {
37        Self(Uuid::nil())
38    }
39
40    /// Returns the raw UUID bytes for storage.
41    #[inline]
42    pub fn as_bytes(&self) -> &[u8; 16] {
43        self.0.as_bytes()
44    }
45
46    /// Creates a CollectiveId from raw bytes.
47    #[inline]
48    pub fn from_bytes(bytes: [u8; 16]) -> Self {
49        Self(Uuid::from_bytes(bytes))
50    }
51}
52
53impl Default for CollectiveId {
54    /// Returns a nil (all zeros) CollectiveId.
55    ///
56    /// For a new unique ID, use [`CollectiveId::new()`].
57    fn default() -> Self {
58        Self::nil()
59    }
60}
61
62impl fmt::Display for CollectiveId {
63    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64        write!(f, "{}", self.0)
65    }
66}
67
68/// Experience identifier (UUID v7 for time-ordering).
69///
70/// Experiences are the core unit of learned knowledge in PulseDB.
71/// Each experience belongs to exactly one collective.
72#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
73pub struct ExperienceId(pub Uuid);
74
75impl ExperienceId {
76    /// Creates a new ExperienceId with a UUID v7 (time-ordered).
77    #[inline]
78    pub fn new() -> Self {
79        Self(Uuid::now_v7())
80    }
81
82    /// Creates a nil (all zeros) ExperienceId.
83    #[inline]
84    pub fn nil() -> Self {
85        Self(Uuid::nil())
86    }
87
88    /// Returns the raw UUID bytes for storage.
89    #[inline]
90    pub fn as_bytes(&self) -> &[u8; 16] {
91        self.0.as_bytes()
92    }
93
94    /// Creates an ExperienceId from raw bytes.
95    #[inline]
96    pub fn from_bytes(bytes: [u8; 16]) -> Self {
97        Self(Uuid::from_bytes(bytes))
98    }
99}
100
101impl Default for ExperienceId {
102    /// Returns a nil (all zeros) ExperienceId.
103    ///
104    /// For a new unique ID, use [`ExperienceId::new()`].
105    fn default() -> Self {
106        Self::nil()
107    }
108}
109
110impl fmt::Display for ExperienceId {
111    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112        write!(f, "{}", self.0)
113    }
114}
115
116/// Unique identifier for a PulseDB database instance.
117///
118/// Each database mints one stable instance id on first open and stores it in
119/// metadata. Temporal decay uses this id as the G-counter key for local
120/// reinforcement counts; the sync protocol also uses it to identify peers.
121#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
122pub struct InstanceId(pub Uuid);
123
124impl InstanceId {
125    /// Creates a new InstanceId with a UUID v7 (time-ordered).
126    #[inline]
127    pub fn new() -> Self {
128        Self(Uuid::now_v7())
129    }
130
131    /// Creates a nil (all zeros) InstanceId.
132    ///
133    /// Useful for tests and explicitly reserved sentinel values.
134    #[inline]
135    pub fn nil() -> Self {
136        Self(Uuid::nil())
137    }
138
139    /// Returns the raw UUID bytes for storage.
140    #[inline]
141    pub fn as_bytes(&self) -> &[u8; 16] {
142        self.0.as_bytes()
143    }
144
145    /// Creates an InstanceId from raw bytes.
146    #[inline]
147    pub fn from_bytes(bytes: [u8; 16]) -> Self {
148        Self(Uuid::from_bytes(bytes))
149    }
150}
151
152impl Default for InstanceId {
153    /// Returns a nil (all zeros) InstanceId.
154    ///
155    /// For a new unique ID, use [`InstanceId::new()`].
156    fn default() -> Self {
157        Self::nil()
158    }
159}
160
161impl fmt::Display for InstanceId {
162    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163        write!(f, "{}", self.0)
164    }
165}
166
167/// Unix timestamp in milliseconds.
168///
169/// Using i64 allows representing dates far into the future and past.
170/// Millisecond precision is sufficient for agent operations.
171#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
172pub struct Timestamp(pub i64);
173
174impl Timestamp {
175    /// Creates a timestamp for the current moment.
176    ///
177    /// If the system clock is before the Unix epoch (should never happen
178    /// in practice), returns a timestamp of 0 (epoch) rather than panicking.
179    #[inline]
180    pub fn now() -> Self {
181        use std::time::{SystemTime, UNIX_EPOCH};
182        let duration = SystemTime::now()
183            .duration_since(UNIX_EPOCH)
184            .unwrap_or_default();
185        Self(duration.as_millis() as i64)
186    }
187
188    /// Creates a timestamp from Unix milliseconds.
189    #[inline]
190    pub const fn from_millis(millis: i64) -> Self {
191        Self(millis)
192    }
193
194    /// Returns the timestamp as Unix milliseconds.
195    #[inline]
196    pub const fn as_millis(&self) -> i64 {
197        self.0
198    }
199
200    /// Returns big-endian bytes for storage (enables lexicographic ordering).
201    #[inline]
202    pub fn to_be_bytes(&self) -> [u8; 8] {
203        self.0.to_be_bytes()
204    }
205}
206
207impl fmt::Display for Timestamp {
208    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
209        write!(f, "{}", self.0)
210    }
211}
212
213/// Relation identifier (UUID v7 for time-ordering).
214///
215/// Relations connect two experiences within the same collective,
216/// enabling agents to understand how knowledge connects.
217#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
218pub struct RelationId(pub Uuid);
219
220impl RelationId {
221    /// Creates a new RelationId with a UUID v7 (time-ordered).
222    #[inline]
223    pub fn new() -> Self {
224        Self(Uuid::now_v7())
225    }
226
227    /// Creates a nil (all zeros) RelationId.
228    #[inline]
229    pub fn nil() -> Self {
230        Self(Uuid::nil())
231    }
232
233    /// Returns the raw UUID bytes for storage.
234    #[inline]
235    pub fn as_bytes(&self) -> &[u8; 16] {
236        self.0.as_bytes()
237    }
238
239    /// Creates a RelationId from raw bytes.
240    #[inline]
241    pub fn from_bytes(bytes: [u8; 16]) -> Self {
242        Self(Uuid::from_bytes(bytes))
243    }
244}
245
246impl Default for RelationId {
247    /// Returns a nil (all zeros) RelationId.
248    ///
249    /// For a new unique ID, use [`RelationId::new()`].
250    fn default() -> Self {
251        Self::nil()
252    }
253}
254
255impl fmt::Display for RelationId {
256    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
257        write!(f, "{}", self.0)
258    }
259}
260
261/// Insight identifier (UUID v7 for time-ordering).
262///
263/// Insights are derived knowledge synthesized from multiple experiences.
264/// Each insight belongs to exactly one collective.
265///
266/// # Example
267/// ```
268/// use pulsedb::InsightId;
269///
270/// let id = InsightId::new();
271/// println!("Created insight: {}", id);
272/// ```
273#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
274pub struct InsightId(pub Uuid);
275
276impl InsightId {
277    /// Creates a new InsightId with a UUID v7 (time-ordered).
278    #[inline]
279    pub fn new() -> Self {
280        Self(Uuid::now_v7())
281    }
282
283    /// Creates a nil (all zeros) InsightId.
284    #[inline]
285    pub fn nil() -> Self {
286        Self(Uuid::nil())
287    }
288
289    /// Returns the raw UUID bytes for storage.
290    #[inline]
291    pub fn as_bytes(&self) -> &[u8; 16] {
292        self.0.as_bytes()
293    }
294
295    /// Creates an InsightId from raw bytes.
296    #[inline]
297    pub fn from_bytes(bytes: [u8; 16]) -> Self {
298        Self(Uuid::from_bytes(bytes))
299    }
300}
301
302impl Default for InsightId {
303    /// Returns a nil (all zeros) InsightId.
304    ///
305    /// For a new unique ID, use [`InsightId::new()`].
306    fn default() -> Self {
307        Self::nil()
308    }
309}
310
311impl fmt::Display for InsightId {
312    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
313        write!(f, "{}", self.0)
314    }
315}
316
317/// Opaque user identifier.
318///
319/// PulseDB doesn't handle authentication - the consumer provides user IDs.
320/// This allows integration with any auth system (OAuth, API keys, etc.).
321#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
322pub struct UserId(pub String);
323
324impl UserId {
325    /// Creates a new UserId from a string.
326    pub fn new(id: impl Into<String>) -> Self {
327        Self(id.into())
328    }
329
330    /// Returns the user ID as a string slice.
331    pub fn as_str(&self) -> &str {
332        &self.0
333    }
334}
335
336impl fmt::Display for UserId {
337    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
338        write!(f, "{}", self.0)
339    }
340}
341
342/// Agent identifier.
343///
344/// Identifies a specific AI agent instance within a collective.
345/// Multiple agents can operate on the same collective simultaneously.
346#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
347pub struct AgentId(pub String);
348
349impl AgentId {
350    /// Creates a new AgentId from a string.
351    pub fn new(id: impl Into<String>) -> Self {
352        Self(id.into())
353    }
354
355    /// Returns the agent ID as a string slice.
356    pub fn as_str(&self) -> &str {
357        &self.0
358    }
359}
360
361impl fmt::Display for AgentId {
362    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
363        write!(f, "{}", self.0)
364    }
365}
366
367/// Task identifier.
368///
369/// Identifies a specific task or job that an agent is working on.
370/// Used for tracking which experiences came from which task.
371#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
372pub struct TaskId(pub String);
373
374impl TaskId {
375    /// Creates a new TaskId from a string.
376    pub fn new(id: impl Into<String>) -> Self {
377        Self(id.into())
378    }
379
380    /// Returns the task ID as a string slice.
381    pub fn as_str(&self) -> &str {
382        &self.0
383    }
384}
385
386impl fmt::Display for TaskId {
387    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
388        write!(f, "{}", self.0)
389    }
390}
391
392/// Embedding vector type alias.
393///
394/// Embeddings are f32 vectors of fixed dimension (typically 384 or 768).
395pub type Embedding = Vec<f32>;
396
397#[cfg(test)]
398mod tests {
399    use super::*;
400
401    #[test]
402    fn test_collective_id_new_is_unique() {
403        let id1 = CollectiveId::new();
404        let id2 = CollectiveId::new();
405        assert_ne!(id1, id2);
406    }
407
408    #[test]
409    fn test_collective_id_nil() {
410        let id = CollectiveId::nil();
411        assert_eq!(id.0, Uuid::nil());
412    }
413
414    #[test]
415    fn test_collective_id_bytes_roundtrip() {
416        let id = CollectiveId::new();
417        let bytes = *id.as_bytes();
418        let restored = CollectiveId::from_bytes(bytes);
419        assert_eq!(id, restored);
420    }
421
422    #[test]
423    fn test_collective_id_serialization() {
424        let id = CollectiveId::new();
425        let bytes = postcard::to_stdvec(&id).unwrap();
426        let restored: CollectiveId = postcard::from_bytes(&bytes).unwrap();
427        assert_eq!(id, restored);
428    }
429
430    #[test]
431    fn test_experience_id_new_is_unique() {
432        let id1 = ExperienceId::new();
433        let id2 = ExperienceId::new();
434        assert_ne!(id1, id2);
435    }
436
437    #[test]
438    fn test_experience_id_serialization() {
439        let id = ExperienceId::new();
440        let bytes = postcard::to_stdvec(&id).unwrap();
441        let restored: ExperienceId = postcard::from_bytes(&bytes).unwrap();
442        assert_eq!(id, restored);
443    }
444
445    #[test]
446    fn test_relation_id_new_is_unique() {
447        let id1 = RelationId::new();
448        let id2 = RelationId::new();
449        assert_ne!(id1, id2);
450    }
451
452    #[test]
453    fn test_relation_id_nil() {
454        let id = RelationId::nil();
455        assert_eq!(id.0, Uuid::nil());
456    }
457
458    #[test]
459    fn test_relation_id_bytes_roundtrip() {
460        let id = RelationId::new();
461        let bytes = *id.as_bytes();
462        let restored = RelationId::from_bytes(bytes);
463        assert_eq!(id, restored);
464    }
465
466    #[test]
467    fn test_relation_id_serialization() {
468        let id = RelationId::new();
469        let bytes = postcard::to_stdvec(&id).unwrap();
470        let restored: RelationId = postcard::from_bytes(&bytes).unwrap();
471        assert_eq!(id, restored);
472    }
473
474    #[test]
475    fn test_insight_id_new_is_unique() {
476        let id1 = InsightId::new();
477        let id2 = InsightId::new();
478        assert_ne!(id1, id2);
479    }
480
481    #[test]
482    fn test_insight_id_nil() {
483        let id = InsightId::nil();
484        assert_eq!(id.0, Uuid::nil());
485    }
486
487    #[test]
488    fn test_insight_id_bytes_roundtrip() {
489        let id = InsightId::new();
490        let bytes = *id.as_bytes();
491        let restored = InsightId::from_bytes(bytes);
492        assert_eq!(id, restored);
493    }
494
495    #[test]
496    fn test_insight_id_serialization() {
497        let id = InsightId::new();
498        let bytes = postcard::to_stdvec(&id).unwrap();
499        let restored: InsightId = postcard::from_bytes(&bytes).unwrap();
500        assert_eq!(id, restored);
501    }
502
503    #[test]
504    fn test_timestamp_now() {
505        let t1 = Timestamp::now();
506        std::thread::sleep(std::time::Duration::from_millis(1));
507        let t2 = Timestamp::now();
508        assert!(t1 < t2, "Timestamps should be ordered");
509    }
510
511    #[test]
512    fn test_timestamp_ordering() {
513        let t1 = Timestamp::from_millis(1000);
514        let t2 = Timestamp::from_millis(2000);
515        assert!(t1 < t2);
516    }
517
518    #[test]
519    fn test_timestamp_be_bytes() {
520        // Big-endian ensures lexicographic ordering matches numeric ordering
521        let t1 = Timestamp::from_millis(100);
522        let t2 = Timestamp::from_millis(200);
523        assert!(t1.to_be_bytes() < t2.to_be_bytes());
524    }
525
526    #[test]
527    fn test_user_id() {
528        let id = UserId::new("user-123");
529        assert_eq!(id.as_str(), "user-123");
530        assert_eq!(format!("{}", id), "user-123");
531    }
532
533    #[test]
534    fn test_agent_id() {
535        let id = AgentId::new("claude-opus");
536        assert_eq!(id.as_str(), "claude-opus");
537    }
538
539    #[test]
540    fn test_task_id() {
541        let id = TaskId::new("task-456");
542        assert_eq!(id.as_str(), "task-456");
543    }
544}