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}