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 ERROR: u8 = 0x1F;
39
40    pub const REQUEST_TYPES: &[u8] = &[REQ_HEADER, PUSH_COMMIT, PULL_HEADER, SUBSCRIPTION];
41    pub const RESPONSE_TYPES: &[u8] = &[
42        RESP_HEADER,
43        PUSH_RESULT,
44        SUB_START,
45        COMMIT,
46        SEGMENT_REF,
47        SEGMENT_INLINE,
48        SUB_END,
49        LEASE,
50        ERROR,
51    ];
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum Op {
56    Upsert,
57    Delete,
58}
59
60impl Op {
61    pub fn name(self) -> &'static str {
62        match self {
63            Op::Upsert => "upsert",
64            Op::Delete => "delete",
65        }
66    }
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum SubStatus {
71    Active,
72    Revoked,
73    Reset,
74}
75
76impl SubStatus {
77    pub fn name(self) -> &'static str {
78        match self {
79            SubStatus::Active => "active",
80            SubStatus::Revoked => "revoked",
81            SubStatus::Reset => "reset",
82        }
83    }
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub enum PushStatus {
88    Applied,
89    Cached,
90    Rejected,
91}
92
93impl PushStatus {
94    pub fn name(self) -> &'static str {
95        match self {
96            PushStatus::Applied => "applied",
97            PushStatus::Cached => "cached",
98            PushStatus::Rejected => "rejected",
99        }
100    }
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub enum MediaType {
105    Rows,
106    Sqlite,
107}
108
109impl MediaType {
110    pub fn name(self) -> &'static str {
111        match self {
112            MediaType::Rows => "rows",
113            MediaType::Sqlite => "sqlite",
114        }
115    }
116}
117
118/// Push operation record (§6.1).
119#[derive(Debug, Clone, PartialEq)]
120pub struct Operation {
121    pub table: String,
122    pub row_id: String,
123    pub op: Op,
124    pub base_version: Option<i64>,
125    /// Row-codec bytes, opaque at the envelope layer (§1.7).
126    pub payload: Option<Vec<u8>>,
127}
128
129/// Commit change record (§4.5).
130#[derive(Debug, Clone, PartialEq)]
131pub struct Change {
132    pub table_index: u16,
133    pub row_id: String,
134    pub op: Op,
135    pub row_version: Option<i64>,
136    pub scopes: Vec<(String, String)>,
137    /// Row-codec bytes, opaque at the envelope layer.
138    pub row: Option<Vec<u8>>,
139}
140
141/// Push result record — tagged union (§6.3).
142#[derive(Debug, Clone, PartialEq)]
143pub enum OpResult {
144    Applied {
145        op_index: i32,
146    },
147    Conflict {
148        op_index: i32,
149        code: String,
150        message: String,
151        server_version: i64,
152        server_row: Vec<u8>,
153    },
154    Error {
155        op_index: i32,
156        code: String,
157        message: String,
158        retryable: bool,
159    },
160}
161
162#[derive(Debug, Clone, PartialEq)]
163pub enum Frame {
164    ReqHeader {
165        client_id: String,
166        schema_version: i32,
167    },
168    PushCommit {
169        client_commit_id: String,
170        operations: Vec<Operation>,
171    },
172    PullHeader {
173        limit_commits: i32,
174        limit_snapshot_rows: i32,
175        max_snapshot_pages: i32,
176        accept: u8,
177    },
178    Subscription {
179        id: String,
180        table: String,
181        scopes: Vec<(String, Vec<String>)>,
182        params: Option<RawJson>,
183        cursor: i64,
184        bootstrap_state: Option<RawJson>,
185    },
186    RespHeader {
187        required_schema_version: Option<i32>,
188        latest_schema_version: Option<i32>,
189    },
190    /// §7.3.2: a server-issued auth lease delivered to the client (opaque).
191    Lease {
192        lease_id: String,
193        expires_at_ms: i64,
194    },
195    PushResult {
196        client_commit_id: String,
197        status: PushStatus,
198        commit_seq: Option<i64>,
199        results: Vec<OpResult>,
200    },
201    SubStart {
202        id: String,
203        status: SubStatus,
204        reason_code: String,
205        effective_scopes: Vec<(String, Vec<String>)>,
206        bootstrap: bool,
207    },
208    Commit {
209        commit_seq: i64,
210        created_at_ms: i64,
211        actor_id: String,
212        tables: Vec<String>,
213        changes: Vec<Change>,
214    },
215    SegmentRef {
216        segment_id: String,
217        media_type: MediaType,
218        table: String,
219        byte_length: i64,
220        row_count: i64,
221        as_of_commit_seq: i64,
222        scope_digest: String,
223        row_cursor: Option<String>,
224        next_row_cursor: Option<String>,
225        url: Option<String>,
226        url_expires_at_ms: Option<i64>,
227    },
228    /// Raw payload bytes, validated at decode as a structurally valid rows
229    /// segment (§5.7) and preserved verbatim for re-encoding.
230    SegmentInline { payload: Vec<u8> },
231    SubEnd {
232        next_cursor: i64,
233        bootstrap_state: Option<RawJson>,
234    },
235    Error {
236        code: String,
237        message: String,
238        category: String,
239        retryable: bool,
240        recommended_action: String,
241        details: Option<RawJson>,
242    },
243    /// Unknown frame preserved by the §1.2 rule-2 skip rule: never dropped,
244    /// re-encoded byte-for-byte in its original position.
245    Unknown { frame_type: u8, payload: Vec<u8> },
246}