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 /// §5.9.3 blob upload: host-authenticated `PUT <mount>/blobs/{blobId}`.
108 /// The server verifies the content address. Default: unsupported.
109 fn blob_upload(
110 &mut self,
111 blob_id: &str,
112 bytes: &[u8],
113 media_type: Option<&str>,
114 ) -> Result<(), TransportError> {
115 let _ = (blob_id, bytes, media_type);
116 Err(TransportError::new(
117 "sync.invalid_request",
118 "this transport has no blob upload (§5.9)",
119 ))
120 }
121 /// §5.9.5 blob download: host-authenticated `GET <mount>/blobs/{blobId}`,
122 /// re-authorized server-side against referencing rows. Returns inline
123 /// bytes OR (always-issue, presign configured) a signed url the client
124 /// fetches directly. Default: none.
125 fn blob_download(&mut self, blob_id: &str) -> Result<BlobDownload, TransportError> {
126 let _ = blob_id;
127 Err(TransportError::new(
128 "blob.not_found",
129 "this transport has no blob download (§5.9)",
130 ))
131 }
132 /// §5.9.5 presigned-download fetch: a bare GET of the signed url. MUST
133 /// attach NO host authentication — the url is the entire grant (§5.4).
134 /// Only called when `blob_download` returned a `Url` arm.
135 fn fetch_blob_url(&mut self, url: &str) -> Result<Vec<u8>, TransportError> {
136 let _ = url;
137 Err(TransportError::new(
138 "sync.invalid_request",
139 "this transport has no blob url fetch (§5.9.5)",
140 ))
141 }
142 /// §5.9.3 presigned-upload grant: `POST /blobs/{blobId}/upload-grant` with
143 /// the declared size. Absent (`None`) ⇒ the client always streams through
144 /// `blob_upload`. A `Url` grant is PUT via `blob_put_url`.
145 fn blob_upload_grant(
146 &mut self,
147 blob_id: &str,
148 byte_length: u64,
149 media_type: Option<&str>,
150 ) -> Result<BlobUploadGrant, TransportError> {
151 let _ = (blob_id, byte_length, media_type);
152 Ok(BlobUploadGrant::None)
153 }
154 /// §5.9.3 direct-to-storage PUT of the granted url. MUST attach NO host
155 /// authentication — the presigned url is the entire grant (§5.4). Only
156 /// called when `blob_upload_grant` returned a `Url` arm.
157 fn blob_put_url(
158 &mut self,
159 url: &str,
160 bytes: &[u8],
161 media_type: Option<&str>,
162 ) -> Result<(), TransportError> {
163 let _ = (url, bytes, media_type);
164 Err(TransportError::new(
165 "sync.invalid_request",
166 "this transport has no blob put url (§5.9.3)",
167 ))
168 }
169 /// Realtime attach (§8.1). Inbound traffic is delivered by the host via
170 /// `SyncClient::on_realtime_text` / `on_realtime_binary`.
171 /// Legacy host-configured realtime connection. Implementations which do
172 /// not bind identity at the URL layer may implement only this method.
173 fn realtime_connect(&mut self) -> Result<(), TransportError>;
174 /// Open realtime for this exact persisted client identity. Native socket
175 /// servers bind registrations/cursors to the URL-level client id before
176 /// any protocol frame is exchanged, so client-owned hosts override this.
177 /// The default preserves compatibility with connectors that already close
178 /// over their identity.
179 fn realtime_connect_for_client(&mut self, _client_id: &str) -> Result<(), TransportError> {
180 self.realtime_connect()
181 }
182 /// Client → server JSON control message (acks, §8.2).
183 fn realtime_send(&mut self, text: &str) -> Result<(), TransportError>;
184 fn realtime_close(&mut self) -> Result<(), TransportError>;
185}