1use serde_json::{json, Value};
17use ssp2::segment::{decode_rows_segment, encode_rows_segment};
18use ssp2::{
19 decode_message, encode_message, parse_control, render_message, render_rows_segment,
20 ControlMessage,
21};
22use syncular_client::{ClientLimits, Mutation, SyncClient, Transport, WindowBase};
23
24pub fn bytes_to_hex(bytes: &[u8]) -> String {
27 syncular_client::values::bytes_to_hex(bytes)
28}
29
30pub fn hex_to_bytes(hex: &str) -> Result<Vec<u8>, String> {
31 syncular_client::values::hex_to_bytes(hex)
32}
33
34pub fn bytes_value(bytes: &[u8]) -> Value {
35 json!({ "$bytes": bytes_to_hex(bytes) })
36}
37
38pub fn value_bytes(value: Option<&Value>) -> Result<Vec<u8>, String> {
39 let hex = value
40 .and_then(|v| v.get("$bytes"))
41 .and_then(Value::as_str)
42 .ok_or_else(|| "expected a {\"$bytes\": hex} value".to_owned())?;
43 hex_to_bytes(hex)
44}
45
46pub type CommandError = (String, String);
48
49#[derive(Debug, Default, Clone)]
53pub struct CreateEffects {
54 pub signed_urls: bool,
57}
58
59fn client_err(message: String) -> CommandError {
60 ("client.failed".to_owned(), message)
61}
62
63fn need_client(client: &mut Option<SyncClient>) -> Result<&mut SyncClient, CommandError> {
64 client
65 .as_mut()
66 .ok_or_else(|| client_err("no client instance created".to_owned()))
67}
68
69pub fn parse_limits(value: Option<&Value>) -> ClientLimits {
70 let mut limits = ClientLimits::default();
71 let Some(object) = value.and_then(Value::as_object) else {
72 return limits;
73 };
74 limits.limit_commits = object
75 .get("limitCommits")
76 .and_then(Value::as_i64)
77 .map(|v| v as i32);
78 limits.limit_snapshot_rows = object
79 .get("limitSnapshotRows")
80 .and_then(Value::as_i64)
81 .map(|v| v as i32);
82 limits.max_snapshot_pages = object
83 .get("maxSnapshotPages")
84 .and_then(Value::as_i64)
85 .map(|v| v as i32);
86 limits.accept = object
87 .get("accept")
88 .and_then(Value::as_u64)
89 .map(|v| v as u8);
90 limits.blob_cache_max_bytes = object.get("blobCacheMaxBytes").and_then(Value::as_i64);
91 limits
92}
93
94pub fn parse_encryption(
97 value: &Value,
98) -> Result<syncular_client::values::EncryptionConfig, String> {
99 let mut config = syncular_client::values::EncryptionConfig::default();
100 let Some(keys) = value.get("keys").and_then(Value::as_object) else {
101 return Ok(config);
102 };
103 for (key_id, key_val) in keys {
104 let bytes =
105 value_bytes(Some(key_val)).map_err(|e| format!("encryption key {key_id:?}: {e}"))?;
106 if bytes.len() != 32 {
107 return Err(format!(
108 "encryption key {key_id:?} must be 32 bytes, got {}",
109 bytes.len()
110 ));
111 }
112 config.keys.insert(key_id.clone(), bytes);
113 }
114 Ok(config)
115}
116
117pub fn parse_mutations(value: Option<&Value>) -> Result<Vec<Mutation>, String> {
118 let list = value
119 .and_then(Value::as_array)
120 .ok_or_else(|| "mutations must be a list".to_owned())?;
121 let mut out = Vec::with_capacity(list.len());
122 for entry in list {
123 let op = entry
124 .get("op")
125 .and_then(Value::as_str)
126 .ok_or_else(|| "mutation missing op".to_owned())?;
127 let table = entry
128 .get("table")
129 .and_then(Value::as_str)
130 .ok_or_else(|| "mutation missing table".to_owned())?
131 .to_owned();
132 let base_version = entry.get("baseVersion").and_then(Value::as_i64);
133 match op {
134 "upsert" => {
135 let values = entry
136 .get("values")
137 .and_then(Value::as_object)
138 .cloned()
139 .ok_or_else(|| "upsert missing values".to_owned())?;
140 out.push(Mutation::Upsert {
141 table,
142 values,
143 base_version,
144 });
145 }
146 "delete" => {
147 let row_id = entry
148 .get("rowId")
149 .and_then(Value::as_str)
150 .ok_or_else(|| "delete missing rowId".to_owned())?
151 .to_owned();
152 out.push(Mutation::Delete {
153 table,
154 row_id,
155 base_version,
156 });
157 }
158 other => return Err(format!("unknown mutation op {other:?}")),
159 }
160 }
161 Ok(out)
162}
163
164fn scopes_from_params(value: Option<&Value>) -> Result<Vec<(String, Vec<String>)>, String> {
165 match value {
166 Some(v) => syncular_client::values::json_to_scope_map(v),
167 None => Ok(Vec::new()),
168 }
169}
170
171fn window_base_from_params(value: Option<&Value>) -> Result<WindowBase, String> {
174 let object = value
175 .and_then(Value::as_object)
176 .ok_or_else(|| "setWindow/windowState missing base object".to_owned())?;
177 let table = object
178 .get("table")
179 .and_then(Value::as_str)
180 .ok_or_else(|| "window base missing table".to_owned())?
181 .to_owned();
182 let variable = object
183 .get("variable")
184 .and_then(Value::as_str)
185 .ok_or_else(|| "window base missing variable".to_owned())?
186 .to_owned();
187 let fixed_scopes = scopes_from_params(object.get("fixedScopes"))?;
188 let params = object
189 .get("params")
190 .and_then(Value::as_str)
191 .map(str::to_owned);
192 Ok(WindowBase {
193 table,
194 variable,
195 fixed_scopes,
196 params,
197 })
198}
199
200#[cfg(feature = "crdt-yjs")]
204fn crdt_target(params: &Value) -> Result<(String, String, String, String), CommandError> {
205 let table = params
206 .get("table")
207 .and_then(Value::as_str)
208 .ok_or_else(|| client_err("crdt command missing table".to_owned()))?
209 .to_owned();
210 let row_id = params
211 .get("rowId")
212 .and_then(Value::as_str)
213 .ok_or_else(|| client_err("crdt command missing rowId".to_owned()))?
214 .to_owned();
215 let column = params
216 .get("column")
217 .and_then(Value::as_str)
218 .ok_or_else(|| client_err("crdt command missing column".to_owned()))?
219 .to_owned();
220 let name = params
221 .get("name")
222 .and_then(Value::as_str)
223 .unwrap_or("text")
224 .to_owned();
225 Ok((table, row_id, column, name))
226}
227
228pub fn dispatch<T: Transport>(
238 transport: &mut T,
239 client: &mut Option<SyncClient>,
240 effects: &mut CreateEffects,
241 method: &str,
242 params: &Value,
243) -> Result<Value, CommandError> {
244 match method {
245 "create" => {
246 let client_id = params
247 .get("clientId")
248 .and_then(Value::as_str)
249 .ok_or_else(|| client_err("create missing clientId".to_owned()))?
250 .to_owned();
251 let schema = params
252 .get("schema")
253 .ok_or_else(|| client_err("create missing schema".to_owned()))?;
254 let limits = parse_limits(params.get("limits"));
255 let mut instance = match params.get("dbPath").and_then(Value::as_str) {
259 Some(path) => {
260 SyncClient::open_path(client_id, schema, limits, path).map_err(client_err)?
261 }
262 None => SyncClient::new(client_id, schema, limits).map_err(client_err)?,
263 };
264 if let Some(now_ms) = params.get("nowMs").and_then(Value::as_i64) {
266 instance.set_now_ms(now_ms);
267 }
268 effects.signed_urls = params
271 .get("signedUrls")
272 .and_then(Value::as_bool)
273 .unwrap_or(false);
274 if let Some(enc) = params.get("encryption") {
277 let config = parse_encryption(enc).map_err(client_err)?;
278 instance.set_encryption(config);
279 }
280 *client = Some(instance);
281 Ok(json!({}))
282 }
283 "subscribe" => {
284 let id = params
285 .get("id")
286 .and_then(Value::as_str)
287 .ok_or_else(|| client_err("subscribe missing id".to_owned()))?
288 .to_owned();
289 let table = params
290 .get("table")
291 .and_then(Value::as_str)
292 .ok_or_else(|| client_err("subscribe missing table".to_owned()))?
293 .to_owned();
294 let scopes = scopes_from_params(params.get("scopes")).map_err(client_err)?;
295 let sub_params = params
296 .get("params")
297 .and_then(Value::as_str)
298 .map(str::to_owned);
299 need_client(client)?
300 .subscribe(id, table, scopes, sub_params)
301 .map_err(client_err)?;
302 Ok(json!({}))
303 }
304 "unsubscribe" => {
305 let id = params
306 .get("id")
307 .and_then(Value::as_str)
308 .ok_or_else(|| client_err("unsubscribe missing id".to_owned()))?;
309 need_client(client)?.unsubscribe(id);
310 Ok(json!({}))
311 }
312 "setWindow" => {
313 let base = window_base_from_params(params.get("base")).map_err(client_err)?;
314 let units: Vec<String> = params
315 .get("units")
316 .and_then(Value::as_array)
317 .map(|arr| {
318 arr.iter()
319 .filter_map(|v| v.as_str().map(str::to_owned))
320 .collect()
321 })
322 .unwrap_or_default();
323 need_client(client)?
324 .set_window(&base, &units)
325 .map_err(client_err)?;
326 Ok(json!({}))
327 }
328 "windowState" => {
329 let base = window_base_from_params(params.get("base")).map_err(client_err)?;
330 let units = need_client(client)?.window_state(&base);
331 Ok(json!({ "units": units }))
332 }
333 "mutate" => {
334 let mutations = parse_mutations(params.get("mutations")).map_err(client_err)?;
335 let id = need_client(client)?.mutate(mutations).map_err(client_err)?;
336 Ok(json!({ "clientCommitId": id }))
337 }
338 "sync" => {
339 let outcome = need_client(client)?.sync(transport);
340 Ok(outcome.to_json())
341 }
342 "syncUntilIdle" => {
343 let max_rounds = params
344 .get("maxRounds")
345 .and_then(Value::as_u64)
346 .map(|v| v as u32);
347 let outcome = need_client(client)?.sync_until_idle(transport, max_rounds);
348 Ok(outcome.to_json())
349 }
350 "readRows" => {
351 let table = params
352 .get("table")
353 .and_then(Value::as_str)
354 .ok_or_else(|| client_err("readRows missing table".to_owned()))?;
355 let rows = need_client(client)?.read_rows(table).map_err(client_err)?;
356 Ok(json!({ "rows": rows }))
357 }
358 "query" => {
359 let sql = params
363 .get("sql")
364 .and_then(Value::as_str)
365 .ok_or_else(|| client_err("query missing sql".to_owned()))?;
366 let bind = match params.get("params") {
367 Some(Value::Array(list)) => list.clone(),
368 None | Some(Value::Null) => Vec::new(),
369 Some(_) => return Err(client_err("query params must be a list".to_owned())),
370 };
371 let rows = need_client(client)?.query(sql, &bind).map_err(client_err)?;
372 Ok(json!({ "rows": rows }))
373 }
374 #[cfg(feature = "crdt-yjs")]
380 "crdtText" => {
381 let (table, row_id, column, name) = crdt_target(params)?;
382 let text = need_client(client)?
383 .crdt_text(&table, &row_id, &column, &name)
384 .map_err(client_err)?;
385 Ok(json!({ "text": text }))
386 }
387 #[cfg(feature = "crdt-yjs")]
388 "crdtInsertText" => {
389 let (table, row_id, column, name) = crdt_target(params)?;
390 let index = params
391 .get("index")
392 .and_then(Value::as_u64)
393 .ok_or_else(|| client_err("crdtInsertText missing index".to_owned()))?
394 as u32;
395 let value = params
396 .get("value")
397 .and_then(Value::as_str)
398 .ok_or_else(|| client_err("crdtInsertText missing value".to_owned()))?;
399 let id = need_client(client)?
400 .crdt_insert_text(&table, &row_id, &column, &name, index, value)
401 .map_err(client_err)?;
402 Ok(json!({ "clientCommitId": id }))
403 }
404 #[cfg(feature = "crdt-yjs")]
405 "crdtDeleteText" => {
406 let (table, row_id, column, name) = crdt_target(params)?;
407 let index = params
408 .get("index")
409 .and_then(Value::as_u64)
410 .ok_or_else(|| client_err("crdtDeleteText missing index".to_owned()))?
411 as u32;
412 let len = params
413 .get("len")
414 .and_then(Value::as_u64)
415 .ok_or_else(|| client_err("crdtDeleteText missing len".to_owned()))?
416 as u32;
417 let id = need_client(client)?
418 .crdt_delete_text(&table, &row_id, &column, &name, index, len)
419 .map_err(client_err)?;
420 Ok(json!({ "clientCommitId": id }))
421 }
422 #[cfg(feature = "crdt-yjs")]
423 "crdtApplyUpdate" => {
424 let (table, row_id, column, _name) = crdt_target(params)?;
425 let update = value_bytes(params.get("update")).map_err(client_err)?;
426 let id = need_client(client)?
427 .crdt_apply_update(&table, &row_id, &column, &update)
428 .map_err(client_err)?;
429 Ok(json!({ "clientCommitId": id }))
430 }
431 #[cfg(not(feature = "crdt-yjs"))]
432 "crdtText" | "crdtInsertText" | "crdtDeleteText" | "crdtApplyUpdate" => Err((
433 "client.crdt_unavailable".to_owned(),
434 "native CRDT support requires the `crdt-yjs` feature (§5.10.5)".to_owned(),
435 )),
436
437 "uploadBlob" => {
438 let bytes = value_bytes(params.get("bytes")).map_err(client_err)?;
439 let media_type = params
440 .get("mediaType")
441 .and_then(Value::as_str)
442 .map(str::to_owned);
443 let name = params
444 .get("name")
445 .and_then(Value::as_str)
446 .map(str::to_owned);
447 let reference = need_client(client)?
448 .upload_blob(&bytes, media_type, name)
449 .map_err(client_err)?;
450 Ok(json!({ "ref": reference }))
451 }
452 "fetchBlob" => {
453 let blob = params
454 .get("blob")
455 .and_then(Value::as_str)
456 .ok_or_else(|| client_err("fetchBlob missing blob".to_owned()))?
457 .to_owned();
458 let value = need_client(client)?.fetch_blob(transport, &blob)?;
461 Ok(json!({ "blob": value }))
462 }
463 "conflicts" => {
464 let conflicts = need_client(client)?.conflicts().to_vec();
465 Ok(json!({ "conflicts": conflicts }))
466 }
467 "rejections" => {
468 let rejections = need_client(client)?.rejections().to_vec();
469 Ok(json!({ "rejections": rejections }))
470 }
471 "pendingCommitIds" => {
472 let ids = need_client(client)?.pending_commit_ids();
473 Ok(json!({ "ids": ids }))
474 }
475 "subscriptionState" => {
476 let id = params
477 .get("id")
478 .and_then(Value::as_str)
479 .ok_or_else(|| client_err("subscriptionState missing id".to_owned()))?;
480 let state = need_client(client)?.subscription_state(id);
481 Ok(json!({ "state": state }))
482 }
483 "schemaFloor" => {
484 let floor = need_client(client)?.schema_floor().cloned();
485 Ok(json!({ "floor": floor }))
486 }
487 "leaseState" => {
488 let lease = need_client(client)?.lease_state().cloned();
489 Ok(json!({ "lease": lease }))
490 }
491 "upgrading" => {
492 let value = need_client(client)?.upgrading();
494 Ok(json!({ "value": value }))
495 }
496 "recreateWithSchema" => {
497 let schema = params
501 .get("schema")
502 .ok_or_else(|| client_err("recreateWithSchema missing schema".to_owned()))?;
503 need_client(client)?
504 .recreate_with_schema(schema)
505 .map_err(client_err)?;
506 Ok(json!({}))
507 }
508 "connectRealtime" => {
509 need_client(client)?
510 .connect_realtime(transport)
511 .map_err(client_err)?;
512 Ok(json!({}))
513 }
514 "disconnectRealtime" => {
515 need_client(client)?.disconnect_realtime(transport);
516 Ok(json!({}))
517 }
518 "syncNeeded" => {
519 let value = need_client(client)?.sync_needed();
520 Ok(json!({ "value": value }))
521 }
522 "setPresence" => {
523 let scope_key = params
524 .get("scopeKey")
525 .and_then(Value::as_str)
526 .ok_or_else(|| client_err("setPresence missing scopeKey".to_owned()))?
527 .to_owned();
528 let doc = params.get("doc");
530 let doc_ref = match doc {
531 None | Some(Value::Null) => None,
532 Some(v) => Some(v),
533 };
534 need_client(client)?
535 .set_presence(transport, &scope_key, doc_ref)
536 .map_err(client_err)?;
537 Ok(json!({}))
538 }
539 "presence" => {
540 let scope_key = params
541 .get("scopeKey")
542 .and_then(Value::as_str)
543 .ok_or_else(|| client_err("presence missing scopeKey".to_owned()))?;
544 let peers = need_client(client)?.presence(scope_key);
545 Ok(json!({ "peers": peers }))
546 }
547
548 "messageRoundtrip" => {
550 let bytes = value_bytes(params.get("bytes")).map_err(client_err)?;
551 match decode_message(&bytes) {
552 Ok(message) => Ok(json!({
553 "ok": true,
554 "bytes": bytes_value(&encode_message(&message)),
555 "renderedJson": render_message(&message).to_string(),
556 })),
557 Err(error) => Ok(json!({ "ok": false, "errorCode": error.code.as_str() })),
558 }
559 }
560 "segmentRoundtrip" => {
561 let bytes = value_bytes(params.get("bytes")).map_err(client_err)?;
562 match decode_rows_segment(&bytes) {
563 Ok(segment) => Ok(json!({
564 "ok": true,
565 "bytes": bytes_value(&encode_rows_segment(&segment)),
566 "renderedJson": render_rows_segment(&segment).to_string(),
567 })),
568 Err(error) => Ok(json!({ "ok": false, "errorCode": error.code.as_str() })),
569 }
570 }
571 "realtimeKnown" => {
572 let text = params
573 .get("text")
574 .and_then(Value::as_str)
575 .ok_or_else(|| client_err("realtimeKnown missing text".to_owned()))?;
576 let known = matches!(
577 parse_control(text),
578 Ok(ControlMessage::Hello { .. })
579 | Ok(ControlMessage::Wake { .. })
580 | Ok(ControlMessage::Heartbeat { .. })
581 | Ok(ControlMessage::Presence { .. })
582 );
583 Ok(json!({ "value": known }))
584 }
585
586 other => Err(client_err(format!("unknown method {other:?}"))),
587 }
588}