CRUMB a card from devarno-cloud

Timeline & TimelineStatus Lifecycle

tnp beginner 5 min read

ELI5

A timeline is like a numbered ticket roll at a deli counter — every new operation tears off the next ticket. The status label on the roll says whether the counter is open for business (Official/Draft), running an experiment (Experimental), or closed (Archived).

Technical Deep Dive

Class Diagram

classDiagram
class TimelineID {
+bytes: Vec~u8~
+new(bytes: &[u8]) TimelineID
}
class TimelineStatus {
<<enumeration>>
Official
Draft
Experimental
Archived
}
class Timeline {
+id: TimelineID
+status: TimelineStatus
+nodes: Vec~NodeID~
+created_timestamp_ms: u64
+last_modified_ms: u64
+node_timestamps: HashMap~NodeID, u64~
+add_node(NodeID)
+nodes_before_timestamp(u64) Result~Vec~NodeID~~
+state_hash() Vec~u8~
+is_active() bool
+age_seconds() u64
+node_count() usize
}
Timeline "1" --> "1" TimelineID
Timeline "1" --> "1" TimelineStatus

Status State Machine

stateDiagram-v2
[*] --> Official : new canonical timeline
[*] --> Draft : fork_timeline(..., Draft)
[*] --> Experimental : fork_timeline(..., Experimental)
Official --> Archived : manual archive
Draft --> Archived : archive_threshold_days elapsed (inferred)
Experimental --> Archived : archive_threshold_days elapsed (inferred)
Archived --> [*]
note right of Official : is_active() = true
note right of Draft : is_active() = true
note right of Experimental : is_active() = false
note right of Archived : is_active() = false

Note: status transitions other than the initial construction are not shown in the current source; the state machine above reflects the intended model described in lib.rs comments and TNPConfig.archive_threshold_days.

add_node Behaviour

pub fn add_node(&mut self, node_id: NodeID) {
let now = /* SystemTime::now() as ms */;
if !self.nodes.contains(node_id) {
self.nodes.push(node_id.clone());
}
self.node_timestamps.insert(node_id, now);
self.last_modified_ms = now;
}

Dedup is positional (nodes.contains), so inserting the same NodeID twice only adds one entry to nodes but updates the timestamp map each time.

state_hash

XOR-folds each byte of each NodeID into a 32-byte buffer:

hash[i % 32] ^= byte

Deterministic given identical nodes in identical order. Order-dependent: two timelines with the same set of nodes in different insertion order will produce different hashes.

nodes_before_timestamp

Filters nodes by looking up each NodeID in node_timestamps and keeping only entries where ts <= timestamp_ms. Returns Ok([]) for an empty timeline (not an error).

Key Terms

  • TimelineStatus → enum with four variants; governs is_active() output and drives archiving logic
  • is_active → returns true only for Official and Draft statuses
  • node_timestamps → secondary map recording insertion wall-clock time per node; used by nodes_before_timestamp
  • state_hash → 32-byte XOR fingerprint of the timeline’s ordered node list

Q&A

Q: If a node is inserted twice, how many entries appear in node_timestamps vs nodes? A: node_timestamps gets a fresh timestamp (overwritten), while nodes still has exactly one entry (the duplicate is rejected by contains).

Q: Does age_seconds use created_timestamp_ms or last_modified_ms? A: created_timestamp_ms — it measures the timeline’s age since creation, not last activity.

Q: Does nodes_before_timestamp preserve insertion order? A: Yes — it iterates self.nodes in push order and filters; insertion order is preserved in the output.

Examples

let mut tl = Timeline::new(TimelineID::new(b"exp-branch"), TimelineStatus::Experimental);
assert!(!tl.is_active()); // Experimental is NOT active
tl.add_node(NodeID::new(b"op1".to_vec()));
let before = tl.nodes_before_timestamp(u64::MAX).unwrap();
assert_eq!(before.len(), 1);
let hash = tl.state_hash(); // 32-byte XOR fingerprint
assert_eq!(hash.len(), 32);

neighbors on the map