syncular_client/transport.rs
1//! The transport seam handed TO the client by its host (the conformance
2//! harness, an app shell, …). Bytes and strings only — mirroring the
3//! `ClientEndpoints` inversion of the conformance driver contract. The
4//! client is synchronous: the driver protocol is request/response.
5
6/// A transport-level or request-level failure (§1.1 HTTP-JSON surface).
7#[derive(Debug, Clone)]
8pub struct TransportError {
9 pub code: String,
10 pub message: String,
11 pub details: Option<serde_json::Value>,
12}
13
14impl TransportError {
15 pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
16 TransportError {
17 code: code.into(),
18 message: message.into(),
19 details: None,
20 }
21 }
22}
23
24/// Direct-endpoint segment fetch (§5.5). Signed-URL descriptors never
25/// reach this call: the §5.4 resolution lives in the client core, which
26/// routes url-carrying descriptors through `fetch_url` instead.
27#[derive(Debug, Clone)]
28pub struct SegmentRequest {
29 pub segment_id: String,
30 pub table: String,
31 /// Canonical JSON (§11.2) of the requested scope map (§5.5).
32 pub requested_scopes_json: String,
33}
34
35/// A §5.9.5 blob download result: inline bytes, or a presigned url the client
36/// fetches directly (always-issue). `url_expires_at_ms` is present iff `url`.
37#[derive(Debug, Clone)]
38pub enum BlobDownload {
39 Bytes(Vec<u8>),
40 Url {
41 url: String,
42 url_expires_at_ms: Option<i64>,
43 },
44}
45
46/// A §5.9.3 presigned-upload grant: a single PUT url, an already-present
47/// marker (skip the PUT), or none (stream through the direct endpoint).
48#[derive(Debug, Clone)]
49pub enum BlobUploadGrant {
50 Url {
51 url: String,
52 url_expires_at_ms: Option<i64>,
53 },
54 Present,
55 None,
56}
57
58pub trait Transport {
59 /// One combined push+pull round trip (§1.5) over the request/response
60 /// binding (`POST /sync`, loopback, …).
61 fn sync(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError>;
62 /// One registered authoritative query or command request. Hosts that do
63 /// not expose `<mount>/operations` keep the default fail-loud behavior.
64 fn remote_operation(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
65 let _ = request;
66 Err(TransportError::new(
67 "client.remote_operations_unconfigured",
68 "this transport has no remote operation endpoint",
69 ))
70 }
71 /// One combined push+pull round over the realtime socket (§8.7). The
72 /// host owns the WS-binding mechanics — channel tags, chunk assembly
73 /// to the response's END — and returns the assembled response bytes.
74 /// The client calls this instead of `sync` whenever realtime is
75 /// connected (the socket IS the sync-round
76 /// transport, not a fallback pair); the server registers the round's
77 /// subscriptions on the connection at round end, so no reconnect is
78 /// needed after subscription changes.
79 fn realtime_sync(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError>;
80 /// Segment download via the direct endpoint (§5.5).
81 fn download_segment(
82 &mut self,
83 request: &SegmentRequest,
84 _on_progress: &mut dyn FnMut(u64),
85 ) -> Result<Vec<u8>, TransportError>;
86 /// §5.4 direct URL fetch capability: `true` makes the client
87 /// advertise accept bit 3 (capability negotiation, §4.2). Default:
88 /// not capable.
89 fn supports_url_fetch(&self) -> bool {
90 false
91 }
92 /// Plain GET of a signed URL (§5.4). The URL is the entire grant —
93 /// implementations MUST NOT attach sync-server authentication or the
94 /// `X-Syncular-Scopes` header. Only called when `supports_url_fetch`
95 /// returned `true`.
96 fn fetch_url(
97 &mut self,
98 url: &str,
99 _on_progress: &mut dyn FnMut(u64),
100 ) -> Result<Vec<u8>, TransportError> {
101 let _ = url;
102 Err(TransportError::new(
103 "sync.invalid_request",
104 "this transport has no direct URL fetch (§5.4)",
105 ))
106 }
107 /// The I/O executor records when a signed segment fetch began. Apply
108 /// checks expiry against that instant, preserving the fetch-start rule.
109 fn segment_fetch_started_at(&self, _url: &str) -> Option<i64> {
110 None
111 }
112 /// §5.9.3 blob upload: host-authenticated `PUT <mount>/blobs/{blobId}`.
113 /// The server verifies the content address. Default: unsupported.
114 fn blob_upload(
115 &mut self,
116 blob_id: &str,
117 bytes: &[u8],
118 media_type: Option<&str>,
119 ) -> Result<(), TransportError> {
120 let _ = (blob_id, bytes, media_type);
121 Err(TransportError::new(
122 "sync.invalid_request",
123 "this transport has no blob upload (§5.9)",
124 ))
125 }
126 /// §5.9.5 blob download: host-authenticated `GET <mount>/blobs/{blobId}`,
127 /// re-authorized server-side against referencing rows. Returns inline
128 /// bytes OR (always-issue, presign configured) a signed url the client
129 /// fetches directly. Default: none.
130 fn blob_download(&mut self, blob_id: &str) -> Result<BlobDownload, TransportError> {
131 let _ = blob_id;
132 Err(TransportError::new(
133 "blob.not_found",
134 "this transport has no blob download (§5.9)",
135 ))
136 }
137 /// §5.9.5 presigned-download fetch: a bare GET of the signed url. MUST
138 /// attach NO host authentication — the url is the entire grant (§5.4).
139 /// Only called when `blob_download` returned a `Url` arm.
140 fn fetch_blob_url(&mut self, url: &str) -> Result<Vec<u8>, TransportError> {
141 let _ = url;
142 Err(TransportError::new(
143 "sync.invalid_request",
144 "this transport has no blob url fetch (§5.9.5)",
145 ))
146 }
147 /// §5.9.3 presigned-upload grant: `POST /blobs/{blobId}/upload-grant` with
148 /// the declared size. Absent (`None`) ⇒ the client always streams through
149 /// `blob_upload`. A `Url` grant is PUT via `blob_put_url`.
150 fn blob_upload_grant(
151 &mut self,
152 blob_id: &str,
153 byte_length: u64,
154 media_type: Option<&str>,
155 ) -> Result<BlobUploadGrant, TransportError> {
156 let _ = (blob_id, byte_length, media_type);
157 Ok(BlobUploadGrant::None)
158 }
159 /// §5.9.3 direct-to-storage PUT of the granted url. MUST attach NO host
160 /// authentication — the presigned url is the entire grant (§5.4). Only
161 /// called when `blob_upload_grant` returned a `Url` arm.
162 fn blob_put_url(
163 &mut self,
164 url: &str,
165 bytes: &[u8],
166 media_type: Option<&str>,
167 ) -> Result<(), TransportError> {
168 let _ = (url, bytes, media_type);
169 Err(TransportError::new(
170 "sync.invalid_request",
171 "this transport has no blob put url (§5.9.3)",
172 ))
173 }
174 /// Realtime attach (§8.1). Inbound traffic is delivered by the host via
175 /// `SyncClient::on_realtime_text` / `on_realtime_binary`.
176 /// Legacy host-configured realtime connection. Implementations which do
177 /// not bind identity at the URL layer may implement only this method.
178 fn realtime_connect(&mut self) -> Result<(), TransportError>;
179 /// Open realtime for this exact persisted client identity. Native socket
180 /// servers bind registrations/cursors to the URL-level client id before
181 /// any protocol frame is exchanged, so client-owned hosts override this.
182 /// The default preserves compatibility with connectors that already close
183 /// over their identity.
184 fn realtime_connect_for_client(&mut self, _client_id: &str) -> Result<(), TransportError> {
185 self.realtime_connect()
186 }
187 /// The whole-round network deadline this transport would apply, as a
188 /// budget. `None` (the default) means no round deadline. The client
189 /// anchors the returned budget once per round and scopes it to each
190 /// `PreparedSyncRound::exchange`, so a reused transport never carries
191 /// stale deadline state.
192 fn round_deadline(&self) -> Option<std::time::Duration> {
193 None
194 }
195 /// Scope the transport to one absolute round deadline for the duration of
196 /// a single exchange; `None` clears it. The default ignores it.
197 fn set_round_deadline(&mut self, _deadline: Option<std::time::Instant>) {}
198 /// Client → server JSON control message (acks, §8.2).
199 fn realtime_send(&mut self, text: &str) -> Result<(), TransportError>;
200 fn realtime_close(&mut self) -> Result<(), TransportError>;
201}