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