Skip to main content

ssp2/
model.rs

1//! Decoded SSP2 message model (SPEC.md §1.2, §1.5, §1.6, §4–§6).
2//!
3//! The model preserves everything needed for byte-identical re-encoding:
4//! raw `json` strings, unknown frames in their original positions, map entry
5//! order (validated canonical at decode), and opaque row payload bytes (the
6//! envelope codec never decodes rows — §1.7).
7
8use crate::primitives::RawJson;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum MsgKind {
12    Request,
13    Response,
14}
15
16#[derive(Debug, Clone, PartialEq)]
17pub struct Message {
18    pub msg_kind: MsgKind,
19    pub frames: Vec<Frame>,
20}
21
22/// Frame types, wire version 1 (§1.2 registry). `END` is structural and not
23/// represented in the model.
24pub mod frame_type {
25    pub const END: u8 = 0x00;
26    pub const REQ_HEADER: u8 = 0x01;
27    pub const PUSH_COMMIT: u8 = 0x02;
28    pub const PULL_HEADER: u8 = 0x03;
29    pub const SUBSCRIPTION: u8 = 0x04;
30    pub const RESP_HEADER: u8 = 0x10;
31    pub const PUSH_RESULT: u8 = 0x11;
32    pub const SUB_START: u8 = 0x12;
33    pub const COMMIT: u8 = 0x13;
34    pub const SEGMENT_REF: u8 = 0x14;
35    pub const SEGMENT_INLINE: u8 = 0x15;
36    pub const SUB_END: u8 = 0x16;
37    pub const LEASE: u8 = 0x19;
38    pub const PUSH_RESULT_DETAILS: u8 = 0x1B;
39    pub const ERROR: u8 = 0x1F;
40
41    pub const REQUEST_TYPES: &[u8] = &[REQ_HEADER, PUSH_COMMIT, PULL_HEADER, SUBSCRIPTION];
42    pub const RESPONSE_TYPES: &[u8] = &[
43        RESP_HEADER,
44        PUSH_RESULT,
45        SUB_START,
46        COMMIT,
47        SEGMENT_REF,
48        SEGMENT_INLINE,
49        SUB_END,
50        LEASE,
51        PUSH_RESULT_DETAILS,
52        ERROR,
53    ];
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum Op {
58    Upsert,
59    Delete,
60}
61
62impl Op {
63    pub fn name(self) -> &'static str {
64        match self {
65            Op::Upsert => "upsert",
66            Op::Delete => "delete",
67        }
68    }
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum SubStatus {
73    Active,
74    Revoked,
75    Reset,
76}
77
78impl SubStatus {
79    pub fn name(self) -> &'static str {
80        match self {
81            SubStatus::Active => "active",
82            SubStatus::Revoked => "revoked",
83            SubStatus::Reset => "reset",
84        }
85    }
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89pub enum PushStatus {
90    Applied,
91    Cached,
92    Rejected,
93}
94
95impl PushStatus {
96    pub fn name(self) -> &'static str {
97        match self {
98            PushStatus::Applied => "applied",
99            PushStatus::Cached => "cached",
100            PushStatus::Rejected => "rejected",
101        }
102    }
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106pub enum MediaType {
107    Rows,
108    Sqlite,
109}
110
111impl MediaType {
112    pub fn name(self) -> &'static str {
113        match self {
114            MediaType::Rows => "rows",
115            MediaType::Sqlite => "sqlite",
116        }
117    }
118}
119
120/// Push operation record (§6.1).
121#[derive(Debug, Clone, PartialEq)]
122pub struct Operation {
123    pub table: String,
124    pub row_id: String,
125    pub op: Op,
126    pub base_version: Option<i64>,
127    /// Row-codec bytes, opaque at the envelope layer (§1.7).
128    pub payload: Option<Vec<u8>>,
129}
130
131/// Commit change record (§4.5).
132#[derive(Debug, Clone, PartialEq)]
133pub struct Change {
134    pub table_index: u16,
135    pub row_id: String,
136    pub op: Op,
137    pub row_version: Option<i64>,
138    pub scopes: Vec<(String, String)>,
139    /// Row-codec bytes, opaque at the envelope layer.
140    pub row: Option<Vec<u8>>,
141}
142
143/// Push result record — tagged union (§6.3).
144#[derive(Debug, Clone, PartialEq)]
145pub enum OpResult {
146    Applied {
147        op_index: i32,
148    },
149    Conflict {
150        op_index: i32,
151        code: String,
152        message: String,
153        server_version: i64,
154        server_row: Vec<u8>,
155    },
156    Error {
157        op_index: i32,
158        code: String,
159        message: String,
160        retryable: bool,
161    },
162}
163
164#[derive(Debug, Clone, PartialEq, Eq)]
165pub struct PushResultDetail {
166    pub op_index: i32,
167    pub details: RawJson,
168}
169
170#[derive(Debug, Clone, PartialEq)]
171pub enum Frame {
172    ReqHeader {
173        client_id: String,
174        schema_version: i32,
175    },
176    PushCommit {
177        client_commit_id: String,
178        operations: Vec<Operation>,
179    },
180    PullHeader {
181        limit_commits: i32,
182        limit_snapshot_rows: i32,
183        max_snapshot_pages: i32,
184        accept: u8,
185    },
186    Subscription {
187        id: String,
188        table: String,
189        scopes: Vec<(String, Vec<String>)>,
190        params: Option<RawJson>,
191        cursor: i64,
192        bootstrap_state: Option<RawJson>,
193    },
194    RespHeader {
195        required_schema_version: Option<i32>,
196        latest_schema_version: Option<i32>,
197    },
198    /// §7.3.2: a server-issued auth lease delivered to the client (opaque).
199    Lease {
200        lease_id: String,
201        expires_at_ms: i64,
202    },
203    PushResult {
204        client_commit_id: String,
205        status: PushStatus,
206        commit_seq: Option<i64>,
207        results: Vec<OpResult>,
208    },
209    /// Additive host-safe metadata for rejection records. Older clients skip
210    /// this frame under the unknown-frame rule and still process PUSH_RESULT.
211    PushResultDetails {
212        client_commit_id: String,
213        entries: Vec<PushResultDetail>,
214    },
215    SubStart {
216        id: String,
217        status: SubStatus,
218        reason_code: String,
219        effective_scopes: Vec<(String, Vec<String>)>,
220        bootstrap: bool,
221    },
222    Commit {
223        commit_seq: i64,
224        created_at_ms: i64,
225        actor_id: String,
226        tables: Vec<String>,
227        changes: Vec<Change>,
228    },
229    SegmentRef {
230        segment_id: String,
231        media_type: MediaType,
232        table: String,
233        byte_length: i64,
234        row_count: i64,
235        as_of_commit_seq: i64,
236        scope_digest: String,
237        row_cursor: Option<String>,
238        next_row_cursor: Option<String>,
239        url: Option<String>,
240        url_expires_at_ms: Option<i64>,
241    },
242    /// Raw payload bytes, validated at decode as a structurally valid rows
243    /// segment (§5.7) and preserved verbatim for re-encoding.
244    SegmentInline { payload: Vec<u8> },
245    SubEnd {
246        next_cursor: i64,
247        bootstrap_state: Option<RawJson>,
248    },
249    Error {
250        code: String,
251        message: String,
252        category: String,
253        retryable: bool,
254        recommended_action: String,
255        details: Option<RawJson>,
256    },
257    /// Unknown frame preserved by the §1.2 rule-2 skip rule: never dropped,
258    /// re-encoded byte-for-byte in its original position.
259    Unknown { frame_type: u8, payload: Vec<u8> },
260}