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