Skip to main content

graphrefly_core/
message.rs

1//! Message protocol — the unit of communication between nodes.
2//!
3//! Mirrors `~/src/graphrefly-ts/src/core/messages.ts` and `GRAPHREFLY-SPEC.md` §1
4//! under the handle-protocol cleaving plane: payload-bearing variants
5//! (`Data`, `Error`) carry a [`HandleId`] rather than a raw user value.
6//! The Core never sees `T`; the binding-side registry resolves handles to values.
7//!
8//! # Tier table (per canonical spec R1.3.7.b, post-13.6.A lock)
9//!
10//! | Tier | Variants                          | Purpose                |
11//! |------|-----------------------------------|------------------------|
12//! | 0    | [`Message::Start`]                | Subscribe handshake    |
13//! | 1    | [`Message::Dirty`]                | Phase 1 control        |
14//! | 2    | [`Message::Pause`], [`Message::Resume`] | Pause coord       |
15//! | 3    | [`Message::Data`], [`Message::Resolved`] | Value delivery   |
16//! | 4    | [`Message::Invalidate`]           | Cache clear            |
17//! | 5    | [`Message::Complete`], [`Message::Error`] | Termination     |
18//! | 6    | [`Message::Teardown`]             | Permanent destruction  |
19//!
20//! Tier ordering is enforced globally during dispatch (R1.3.1.b two-phase push,
21//! R1.3.7 batch coalescing) — DIRTY drains system-wide before tier-3, etc.
22//!
23//! # Open type set (R1.2.2)
24//!
25//! The canonical spec allows custom message types. M1 first-slice ships only
26//! the 10 built-ins; custom types arrive as a follow-up via a registry shape
27//! mirroring `MessageTypeRegistration` in messages.ts.
28
29use crate::handle::{HandleId, LockId};
30
31/// A protocol message.
32///
33/// Payload-bearing variants carry [`HandleId`] (not user values) per the
34/// handle-protocol cleaving plane. `Pause`/`Resume` carry [`LockId`]
35/// (mandatory per R1.2.6; bare `[PAUSE]` / `[RESUME]` is a protocol violation).
36#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
37pub enum Message {
38    /// Subscribe-time handshake. Per-subscription; not forwarded through
39    /// intermediate nodes (R1.2.3). Tier 0.
40    Start,
41
42    /// Phase 1: value about to change. Tier 1 — immediate.
43    Dirty,
44
45    /// Phase 2: dirty pass complete, value unchanged (or equals-substituted).
46    /// Tier 3 — deferred inside batch.
47    Resolved,
48
49    /// Value delivery. The handle never carries the sentinel — `[DATA, undefined]`
50    /// / `[DATA, None]` is a protocol violation per R1.2.4. Tier 3.
51    Data(HandleId),
52
53    /// Cache clear; does not auto-emit. Tier 4 — deferred.
54    Invalidate,
55
56    /// Suspend activity. `lockId` mandatory (R1.2.6). Tier 2 — synchronous.
57    Pause(LockId),
58
59    /// Resume after pause. Unknown lockId is an idempotent no-op. Tier 2.
60    Resume(LockId),
61
62    /// Clean termination. Tier 5 — deferred.
63    Complete,
64
65    /// Error termination. Payload handle MUST resolve to a non-sentinel
66    /// value per R1.2.5. Tier 5 — deferred.
67    Error(HandleId),
68
69    /// Permanent cleanup; auto-precedes [`Message::Complete`] when delivered
70    /// to a non-terminal node (R2.6.4 / Lock 6.F). Tier 6 — deferred (drains last).
71    Teardown,
72}
73
74impl Message {
75    /// Per-message tier (0–6) per R1.3.7.b. Drives ordering in the dispatcher
76    /// and gating thresholds (e.g. auto-checkpoint on `tier >= 3`).
77    #[must_use]
78    pub const fn tier(self) -> u8 {
79        match self {
80            Self::Start => 0,
81            Self::Dirty => 1,
82            Self::Pause(_) | Self::Resume(_) => 2,
83            Self::Data(_) | Self::Resolved => 3,
84            Self::Invalidate => 4,
85            Self::Complete | Self::Error(_) => 5,
86            Self::Teardown => 6,
87        }
88    }
89
90    /// True for messages that carry a value handle (`Data`, `Error`).
91    /// Useful for the auto-DIRTY-prefix logic (R1.3.1.a) and for the
92    /// binding-layer refcount path: payload handles get a refcount bump
93    /// at emit time, decremented when the message is consumed.
94    #[must_use]
95    pub const fn payload_handle(self) -> Option<HandleId> {
96        match self {
97            Self::Data(h) | Self::Error(h) => Some(h),
98            _ => None,
99        }
100    }
101
102    /// True for the "value already moved" terminal variants.
103    /// `Complete` and `Error` are the lifecycle terminators per R1.3.4.a.
104    /// `Teardown` is destruction, not termination of message flow per se.
105    #[must_use]
106    pub const fn is_terminal(self) -> bool {
107        matches!(self, Self::Complete | Self::Error(_))
108    }
109}
110
111/// A batch of messages delivered as one wire emission.
112///
113/// Per R1.1.1, all inter-node communication uses `[[Type, Data?], ...]` — the
114/// outer batch is mandatory even for a single message. In Rust, we model a
115/// batch as a slice; allocation policy is up to the caller (`Vec`, `SmallVec`,
116/// stack arrays, or interned static slices for payload-free common cases).
117pub type Messages<'a> = &'a [Message];
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122    use crate::handle::{HandleId, LockId};
123
124    const HANDLE_42: HandleId = HandleId::new(42);
125    const LOCK_7: LockId = LockId::new(7);
126
127    #[test]
128    fn tier_table_matches_canonical_spec_r1_3_7b() {
129        // Tier 0
130        assert_eq!(Message::Start.tier(), 0);
131        // Tier 1
132        assert_eq!(Message::Dirty.tier(), 1);
133        // Tier 2
134        assert_eq!(Message::Pause(LOCK_7).tier(), 2);
135        assert_eq!(Message::Resume(LOCK_7).tier(), 2);
136        // Tier 3
137        assert_eq!(Message::Data(HANDLE_42).tier(), 3);
138        assert_eq!(Message::Resolved.tier(), 3);
139        // Tier 4
140        assert_eq!(Message::Invalidate.tier(), 4);
141        // Tier 5
142        assert_eq!(Message::Complete.tier(), 5);
143        assert_eq!(Message::Error(HANDLE_42).tier(), 5);
144        // Tier 6
145        assert_eq!(Message::Teardown.tier(), 6);
146    }
147
148    #[test]
149    fn payload_handle_only_for_data_and_error() {
150        assert_eq!(Message::Data(HANDLE_42).payload_handle(), Some(HANDLE_42));
151        assert_eq!(Message::Error(HANDLE_42).payload_handle(), Some(HANDLE_42));
152        assert_eq!(Message::Start.payload_handle(), None);
153        assert_eq!(Message::Dirty.payload_handle(), None);
154        assert_eq!(Message::Resolved.payload_handle(), None);
155        assert_eq!(Message::Invalidate.payload_handle(), None);
156        assert_eq!(Message::Pause(LOCK_7).payload_handle(), None);
157        assert_eq!(Message::Resume(LOCK_7).payload_handle(), None);
158        assert_eq!(Message::Complete.payload_handle(), None);
159        assert_eq!(Message::Teardown.payload_handle(), None);
160    }
161
162    #[test]
163    fn is_terminal_only_complete_and_error() {
164        assert!(Message::Complete.is_terminal());
165        assert!(Message::Error(HANDLE_42).is_terminal());
166        // Teardown is destruction, not lifecycle termination per R1.3.4.a.
167        assert!(!Message::Teardown.is_terminal());
168        assert!(!Message::Start.is_terminal());
169        assert!(!Message::Data(HANDLE_42).is_terminal());
170        assert!(!Message::Invalidate.is_terminal());
171    }
172
173    #[test]
174    fn copy_and_eq_round_trip() {
175        let m = Message::Data(HANDLE_42);
176        let copy = m;
177        assert_eq!(m, copy);
178        assert_eq!(Message::Resolved, Message::Resolved);
179    }
180}