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::{
23 ClientLimits, CommandEffects, Mutation, SyncClient, Transport, WindowBase, WindowCoverage,
24};
25
26pub fn bytes_to_hex(bytes: &[u8]) -> String {
29 syncular_client::values::bytes_to_hex(bytes)
30}
31
32pub fn hex_to_bytes(hex: &str) -> Result<Vec<u8>, String> {
33 syncular_client::values::hex_to_bytes(hex)
34}
35
36pub fn bytes_value(bytes: &[u8]) -> Value {
37 json!({ "$bytes": bytes_to_hex(bytes) })
38}
39
40pub fn value_bytes(value: Option<&Value>) -> Result<Vec<u8>, String> {
41 let hex = value
42 .and_then(|v| v.get("$bytes"))
43 .and_then(Value::as_str)
44 .ok_or_else(|| "expected a {\"$bytes\": hex} value".to_owned())?;
45 hex_to_bytes(hex)
46}
47
48pub type CommandError = (String, String);
50
51#[derive(Debug, Default, Clone)]
55pub struct CreateEffects {
56 pub signed_urls: bool,
59}
60
61fn client_err(message: String) -> CommandError {
62 let code = message
63 .split_once(':')
64 .map(|(candidate, _)| candidate)
65 .filter(|candidate| candidate.starts_with("client."))
66 .unwrap_or("client.failed");
67 (code.to_owned(), message)
68}
69
70fn need_client(client: &mut Option<SyncClient>) -> Result<&mut SyncClient, CommandError> {
71 client
72 .as_mut()
73 .ok_or_else(|| client_err("no client instance created".to_owned()))
74}
75
76pub fn parse_limits(value: Option<&Value>) -> ClientLimits {
77 let mut limits = ClientLimits::default();
78 let Some(object) = value.and_then(Value::as_object) else {
79 return limits;
80 };
81 limits.limit_commits = object
82 .get("limitCommits")
83 .and_then(Value::as_i64)
84 .map(|v| v as i32);
85 limits.limit_snapshot_rows = object
86 .get("limitSnapshotRows")
87 .and_then(Value::as_i64)
88 .map(|v| v as i32);
89 limits.max_snapshot_pages = object
90 .get("maxSnapshotPages")
91 .and_then(Value::as_i64)
92 .map(|v| v as i32);
93 limits.accept = object
94 .get("accept")
95 .and_then(Value::as_u64)
96 .map(|v| v as u8);
97 limits.blob_cache_max_bytes = object.get("blobCacheMaxBytes").and_then(Value::as_i64);
98 limits
99}
100
101pub fn parse_encryption(
104 value: &Value,
105) -> Result<syncular_client::values::EncryptionConfig, String> {
106 let mut config = syncular_client::values::EncryptionConfig::default();
107 let Some(keys) = value.get("keys").and_then(Value::as_object) else {
108 return Ok(config);
109 };
110 for (key_id, key_val) in keys {
111 let bytes =
112 value_bytes(Some(key_val)).map_err(|e| format!("encryption key {key_id:?}: {e}"))?;
113 if bytes.len() != 32 {
114 return Err(format!(
115 "encryption key {key_id:?} must be 32 bytes, got {}",
116 bytes.len()
117 ));
118 }
119 config.keys.insert(key_id.clone(), bytes);
120 }
121 Ok(config)
122}
123
124pub fn parse_mutations(value: Option<&Value>) -> Result<Vec<Mutation>, String> {
125 let list = value
126 .and_then(Value::as_array)
127 .ok_or_else(|| "mutations must be a list".to_owned())?;
128 let mut out = Vec::with_capacity(list.len());
129 for entry in list {
130 let op = entry
131 .get("op")
132 .and_then(Value::as_str)
133 .ok_or_else(|| "mutation missing op".to_owned())?;
134 let table = entry
135 .get("table")
136 .and_then(Value::as_str)
137 .ok_or_else(|| "mutation missing table".to_owned())?
138 .to_owned();
139 let base_version = entry.get("baseVersion").and_then(Value::as_i64);
140 match op {
141 "upsert" => {
142 let mut values = entry
143 .get("values")
144 .and_then(Value::as_object)
145 .cloned()
146 .ok_or_else(|| "upsert missing values".to_owned())?;
147 decode_bigint_members(&mut values)?;
148 out.push(Mutation::Upsert {
149 table,
150 values,
151 base_version,
152 });
153 }
154 "delete" => {
155 let row_id = entry
156 .get("rowId")
157 .and_then(Value::as_str)
158 .ok_or_else(|| "delete missing rowId".to_owned())?
159 .to_owned();
160 out.push(Mutation::Delete {
161 table,
162 row_id,
163 base_version,
164 });
165 }
166 other => return Err(format!("unknown mutation op {other:?}")),
167 }
168 }
169 Ok(out)
170}
171
172fn decode_bigint_members(values: &mut serde_json::Map<String, Value>) -> Result<(), String> {
173 for value in values.values_mut() {
174 let Some(decimal) = value.get("$bigint").and_then(Value::as_str) else {
175 continue;
176 };
177 let integer = decimal
178 .parse::<i64>()
179 .map_err(|_| format!("bigint value {decimal:?} is outside SQLite's i64 range"))?;
180 *value = Value::from(integer);
181 }
182 Ok(())
183}
184
185fn scopes_from_params(value: Option<&Value>) -> Result<Vec<(String, Vec<String>)>, String> {
186 match value {
187 Some(v) => syncular_client::values::json_to_scope_map(v),
188 None => Ok(Vec::new()),
189 }
190}
191
192fn window_base_from_params(value: Option<&Value>) -> Result<WindowBase, String> {
195 let object = value
196 .and_then(Value::as_object)
197 .ok_or_else(|| "setWindow/windowState missing base object".to_owned())?;
198 let table = object
199 .get("table")
200 .and_then(Value::as_str)
201 .ok_or_else(|| "window base missing table".to_owned())?
202 .to_owned();
203 let variable = object
204 .get("variable")
205 .and_then(Value::as_str)
206 .ok_or_else(|| "window base missing variable".to_owned())?
207 .to_owned();
208 let fixed_scopes = scopes_from_params(object.get("fixedScopes"))?;
209 let params = object
210 .get("params")
211 .and_then(Value::as_str)
212 .map(str::to_owned);
213 Ok(WindowBase {
214 table,
215 variable,
216 fixed_scopes,
217 params,
218 })
219}
220
221#[cfg(feature = "crdt-yjs")]
225fn crdt_target(params: &Value) -> Result<(String, String, String, String), CommandError> {
226 let table = params
227 .get("table")
228 .and_then(Value::as_str)
229 .ok_or_else(|| client_err("crdt command missing table".to_owned()))?
230 .to_owned();
231 let row_id = params
232 .get("rowId")
233 .and_then(Value::as_str)
234 .ok_or_else(|| client_err("crdt command missing rowId".to_owned()))?
235 .to_owned();
236 let column = params
237 .get("column")
238 .and_then(Value::as_str)
239 .ok_or_else(|| client_err("crdt command missing column".to_owned()))?
240 .to_owned();
241 let name = params
242 .get("name")
243 .and_then(Value::as_str)
244 .unwrap_or("text")
245 .to_owned();
246 Ok((table, row_id, column, name))
247}
248
249pub fn dispatch<T: Transport>(
259 transport: &mut T,
260 client: &mut Option<SyncClient>,
261 effects: &mut CreateEffects,
262 method: &str,
263 params: &Value,
264) -> Result<Value, CommandError> {
265 match method {
266 "create" => {
267 let client_id = params
268 .get("clientId")
269 .and_then(Value::as_str)
270 .map(str::to_owned);
271 let schema = params
272 .get("schema")
273 .ok_or_else(|| client_err("create missing schema".to_owned()))?;
274 let limits = parse_limits(params.get("limits"));
275 let mut instance = match params.get("dbPath").and_then(Value::as_str) {
279 Some(path) => SyncClient::open_path_with_identity(client_id, schema, limits, path)
280 .map_err(client_err)?,
281 None => {
282 SyncClient::new_with_identity(client_id, schema, limits).map_err(client_err)?
283 }
284 };
285 if let Some(now_ms) = params.get("nowMs").and_then(Value::as_i64) {
287 instance.set_now_ms(now_ms);
288 }
289 effects.signed_urls = params
292 .get("signedUrls")
293 .and_then(Value::as_bool)
294 .unwrap_or(false);
295 if let Some(enc) = params.get("encryption") {
298 let config = parse_encryption(enc).map_err(client_err)?;
299 instance.set_encryption(config);
300 }
301 *client = Some(instance);
302 Ok(json!({}))
303 }
304 "subscribe" => {
305 let id = params
306 .get("id")
307 .and_then(Value::as_str)
308 .ok_or_else(|| client_err("subscribe missing id".to_owned()))?
309 .to_owned();
310 let table = params
311 .get("table")
312 .and_then(Value::as_str)
313 .ok_or_else(|| client_err("subscribe missing table".to_owned()))?
314 .to_owned();
315 let scopes = scopes_from_params(params.get("scopes")).map_err(client_err)?;
316 let sub_params = params
317 .get("params")
318 .and_then(Value::as_str)
319 .map(str::to_owned);
320 need_client(client)?
321 .subscribe(id, table, scopes, sub_params)
322 .map_err(client_err)?;
323 Ok(json!({}))
324 }
325 "unsubscribe" => {
326 let id = params
327 .get("id")
328 .and_then(Value::as_str)
329 .ok_or_else(|| client_err("unsubscribe missing id".to_owned()))?;
330 need_client(client)?.unsubscribe(id);
331 Ok(json!({}))
332 }
333 "setWindow" => {
334 let base = window_base_from_params(params.get("base")).map_err(client_err)?;
335 let units: Vec<String> = params
336 .get("units")
337 .and_then(Value::as_array)
338 .map(|arr| {
339 arr.iter()
340 .filter_map(|v| v.as_str().map(str::to_owned))
341 .collect()
342 })
343 .unwrap_or_default();
344 let command_effects = need_client(client)?
345 .set_window(&base, &units)
346 .map_err(client_err)?;
347 Ok(json!({ "effects": command_effects }))
348 }
349 "windowState" => {
350 let base = window_base_from_params(params.get("base")).map_err(client_err)?;
351 let state = need_client(client)?.window_state(&base);
352 Ok(json!({ "units": state.units, "pending": state.pending }))
353 }
354 "mutate" => {
355 let mutations = parse_mutations(params.get("mutations")).map_err(client_err)?;
356 let id = need_client(client)?.mutate(mutations).map_err(client_err)?;
357 Ok(json!({
358 "clientCommitId": id,
359 "effects": CommandEffects::interactive()
360 }))
361 }
362 "patch" => {
363 let table = params
364 .get("table")
365 .and_then(Value::as_str)
366 .ok_or_else(|| client_err("patch missing table".to_owned()))?;
367 let row_id = params
368 .get("rowId")
369 .and_then(Value::as_str)
370 .ok_or_else(|| client_err("patch missing rowId".to_owned()))?;
371 let mut partial = params
372 .get("partial")
373 .and_then(Value::as_object)
374 .cloned()
375 .ok_or_else(|| client_err("patch missing partial object".to_owned()))?;
376 decode_bigint_members(&mut partial).map_err(client_err)?;
377 let base_version = params.get("baseVersion").and_then(Value::as_i64);
378 let id = need_client(client)?
379 .patch(table, row_id, partial, base_version)
380 .map_err(client_err)?;
381 Ok(json!({
382 "clientCommitId": id,
383 "effects": CommandEffects::interactive()
384 }))
385 }
386 "sync" => {
387 let outcome = need_client(client)?.sync(transport);
388 Ok(outcome.to_json())
389 }
390 "syncUntilIdle" => {
391 let max_rounds = params
392 .get("maxRounds")
393 .and_then(Value::as_u64)
394 .map(|v| v as u32);
395 let outcome = need_client(client)?.sync_until_idle(transport, max_rounds);
396 Ok(outcome.to_json())
397 }
398 "readRows" => {
399 let table = params
400 .get("table")
401 .and_then(Value::as_str)
402 .ok_or_else(|| client_err("readRows missing table".to_owned()))?;
403 let rows = need_client(client)?.read_rows(table).map_err(client_err)?;
404 Ok(json!({ "rows": rows }))
405 }
406 "query" => {
407 let sql = params
411 .get("sql")
412 .and_then(Value::as_str)
413 .ok_or_else(|| client_err("query missing sql".to_owned()))?;
414 let bind = match params.get("params") {
415 Some(Value::Array(list)) => list.clone(),
416 None | Some(Value::Null) => Vec::new(),
417 Some(_) => return Err(client_err("query params must be a list".to_owned())),
418 };
419 let rows = need_client(client)?.query(sql, &bind).map_err(client_err)?;
420 Ok(json!({ "rows": rows }))
421 }
422 "querySnapshot" => {
423 let sql = params
424 .get("sql")
425 .and_then(Value::as_str)
426 .ok_or_else(|| client_err("querySnapshot missing sql".to_owned()))?;
427 let bind = match params.get("params") {
428 Some(Value::Array(list)) => list.clone(),
429 None | Some(Value::Null) => Vec::new(),
430 Some(_) => {
431 return Err(client_err("querySnapshot params must be a list".to_owned()))
432 }
433 };
434 let mut coverage = Vec::new();
435 for entry in params
436 .get("coverage")
437 .and_then(Value::as_array)
438 .into_iter()
439 .flatten()
440 {
441 let base = window_base_from_params(entry.get("base")).map_err(client_err)?;
442 let units = entry
443 .get("units")
444 .and_then(Value::as_array)
445 .map(|values| {
446 values
447 .iter()
448 .filter_map(|value| value.as_str().map(str::to_owned))
449 .collect()
450 })
451 .unwrap_or_default();
452 coverage.push(WindowCoverage { base, units });
453 }
454 let snapshot = need_client(client)?
455 .query_snapshot(sql, &bind, &coverage)
456 .map_err(client_err)?;
457 Ok(serde_json::to_value(snapshot).expect("snapshot serializes"))
458 }
459 "localRevision" => Ok(json!({
460 "revision": need_client(client)?.local_revision().to_string()
461 })),
462 "statusSnapshot" => Ok(serde_json::to_value(need_client(client)?.status_snapshot())
463 .expect("status serializes")),
464 "drainChangeBatches" => Ok(json!({
469 "batches": need_client(client)?.drain_change_batches()
470 })),
471 "drainSyncIntents" => Ok(json!({
472 "intents": need_client(client)?.drain_sync_intents()
473 })),
474 #[cfg(feature = "crdt-yjs")]
480 "crdtText" => {
481 let (table, row_id, column, name) = crdt_target(params)?;
482 let text = need_client(client)?
483 .crdt_text(&table, &row_id, &column, &name)
484 .map_err(client_err)?;
485 Ok(json!({ "text": text }))
486 }
487 #[cfg(feature = "crdt-yjs")]
488 "crdtInsertText" => {
489 let (table, row_id, column, name) = crdt_target(params)?;
490 let index = params
491 .get("index")
492 .and_then(Value::as_u64)
493 .ok_or_else(|| client_err("crdtInsertText missing index".to_owned()))?
494 as u32;
495 let value = params
496 .get("value")
497 .and_then(Value::as_str)
498 .ok_or_else(|| client_err("crdtInsertText missing value".to_owned()))?;
499 let id = need_client(client)?
500 .crdt_insert_text(&table, &row_id, &column, &name, index, value)
501 .map_err(client_err)?;
502 Ok(json!({ "clientCommitId": id }))
503 }
504 #[cfg(feature = "crdt-yjs")]
505 "crdtDeleteText" => {
506 let (table, row_id, column, name) = crdt_target(params)?;
507 let index = params
508 .get("index")
509 .and_then(Value::as_u64)
510 .ok_or_else(|| client_err("crdtDeleteText missing index".to_owned()))?
511 as u32;
512 let len = params
513 .get("len")
514 .and_then(Value::as_u64)
515 .ok_or_else(|| client_err("crdtDeleteText missing len".to_owned()))?
516 as u32;
517 let id = need_client(client)?
518 .crdt_delete_text(&table, &row_id, &column, &name, index, len)
519 .map_err(client_err)?;
520 Ok(json!({ "clientCommitId": id }))
521 }
522 #[cfg(feature = "crdt-yjs")]
523 "crdtApplyUpdate" => {
524 let (table, row_id, column, _name) = crdt_target(params)?;
525 let update = value_bytes(params.get("update")).map_err(client_err)?;
526 let id = need_client(client)?
527 .crdt_apply_update(&table, &row_id, &column, &update)
528 .map_err(client_err)?;
529 Ok(json!({ "clientCommitId": id }))
530 }
531 #[cfg(not(feature = "crdt-yjs"))]
532 "crdtText" | "crdtInsertText" | "crdtDeleteText" | "crdtApplyUpdate" => Err((
533 "client.crdt_unavailable".to_owned(),
534 "native CRDT support requires the `crdt-yjs` feature (§5.10.5)".to_owned(),
535 )),
536
537 "uploadBlob" => {
538 let bytes = value_bytes(params.get("bytes")).map_err(client_err)?;
539 let media_type = params
540 .get("mediaType")
541 .and_then(Value::as_str)
542 .map(str::to_owned);
543 let name = params
544 .get("name")
545 .and_then(Value::as_str)
546 .map(str::to_owned);
547 let reference = need_client(client)?
548 .upload_blob(&bytes, media_type, name)
549 .map_err(client_err)?;
550 Ok(json!({ "ref": reference }))
551 }
552 "fetchBlob" => {
553 let blob = params
554 .get("blob")
555 .and_then(Value::as_str)
556 .ok_or_else(|| client_err("fetchBlob missing blob".to_owned()))?
557 .to_owned();
558 let value = need_client(client)?.fetch_blob(transport, &blob)?;
561 Ok(json!({ "blob": value }))
562 }
563 "conflicts" => {
564 let conflicts = need_client(client)?.conflicts().to_vec();
565 Ok(json!({ "conflicts": conflicts }))
566 }
567 "rejections" => {
568 let rejections = need_client(client)?.rejections().to_vec();
569 Ok(json!({ "rejections": rejections }))
570 }
571 "pendingCommitIds" => {
572 let ids = need_client(client)?.pending_commit_ids();
573 Ok(json!({ "ids": ids }))
574 }
575 "subscriptionState" => {
576 let id = params
577 .get("id")
578 .and_then(Value::as_str)
579 .ok_or_else(|| client_err("subscriptionState missing id".to_owned()))?;
580 let state = need_client(client)?.subscription_state(id);
581 Ok(json!({ "state": state }))
582 }
583 "schemaFloor" => {
584 let floor = need_client(client)?.schema_floor().cloned();
585 Ok(json!({ "floor": floor }))
586 }
587 "leaseState" => {
588 let lease = need_client(client)?.lease_state().cloned();
589 Ok(json!({ "lease": lease }))
590 }
591 "upgrading" => {
592 let value = need_client(client)?.upgrading();
594 Ok(json!({ "value": value }))
595 }
596 "recreateWithSchema" => {
597 let schema = params
601 .get("schema")
602 .ok_or_else(|| client_err("recreateWithSchema missing schema".to_owned()))?;
603 need_client(client)?
604 .recreate_with_schema(schema)
605 .map_err(client_err)?;
606 Ok(json!({}))
607 }
608 "connectRealtime" => {
609 need_client(client)?
610 .connect_realtime(transport)
611 .map_err(client_err)?;
612 Ok(json!({}))
613 }
614 "disconnectRealtime" => {
615 need_client(client)?.disconnect_realtime(transport);
616 Ok(json!({}))
617 }
618 "syncNeeded" => {
619 let value = need_client(client)?.sync_needed();
620 Ok(json!({ "value": value }))
621 }
622 "setPresence" => {
623 let scope_key = params
624 .get("scopeKey")
625 .and_then(Value::as_str)
626 .ok_or_else(|| client_err("setPresence missing scopeKey".to_owned()))?
627 .to_owned();
628 let doc = params.get("doc");
630 let doc_ref = match doc {
631 None | Some(Value::Null) => None,
632 Some(v) => Some(v),
633 };
634 need_client(client)?
635 .set_presence(transport, &scope_key, doc_ref)
636 .map_err(client_err)?;
637 Ok(json!({}))
638 }
639 "presence" => {
640 let scope_key = params
641 .get("scopeKey")
642 .and_then(Value::as_str)
643 .ok_or_else(|| client_err("presence missing scopeKey".to_owned()))?;
644 let peers = need_client(client)?.presence(scope_key);
645 Ok(json!({ "peers": peers }))
646 }
647
648 "messageRoundtrip" => {
650 let bytes = value_bytes(params.get("bytes")).map_err(client_err)?;
651 match decode_message(&bytes) {
652 Ok(message) => Ok(json!({
653 "ok": true,
654 "bytes": bytes_value(&encode_message(&message)),
655 "renderedJson": render_message(&message).to_string(),
656 })),
657 Err(error) => Ok(json!({ "ok": false, "errorCode": error.code.as_str() })),
658 }
659 }
660 "segmentRoundtrip" => {
661 let bytes = value_bytes(params.get("bytes")).map_err(client_err)?;
662 match decode_rows_segment(&bytes) {
663 Ok(segment) => Ok(json!({
664 "ok": true,
665 "bytes": bytes_value(&encode_rows_segment(&segment)),
666 "renderedJson": render_rows_segment(&segment).to_string(),
667 })),
668 Err(error) => Ok(json!({ "ok": false, "errorCode": error.code.as_str() })),
669 }
670 }
671 "realtimeKnown" => {
672 let text = params
673 .get("text")
674 .and_then(Value::as_str)
675 .ok_or_else(|| client_err("realtimeKnown missing text".to_owned()))?;
676 let known = matches!(
677 parse_control(text),
678 Ok(ControlMessage::Hello { .. })
679 | Ok(ControlMessage::Wake { .. })
680 | Ok(ControlMessage::Heartbeat { .. })
681 | Ok(ControlMessage::Presence { .. })
682 );
683 Ok(json!({ "value": known }))
684 }
685
686 other => Err(client_err(format!("unknown method {other:?}"))),
687 }
688}