@spooky-sync/core 0.0.1-canary.331 → 0.0.1-canary.332

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/types.ts CHANGED
@@ -258,14 +258,20 @@ export interface Sp00kyConfig<S extends SchemaStructure> {
258
258
  */
259
259
  streamDebounceTime?: number;
260
260
  /**
261
- * Debounce time in milliseconds for syncing collaborative (CRDT) field
262
- * edits to the server. Local state is kept on every change (so reload and
263
- * offline work), but a field's own new ops go out as one edit-log row per
264
- * pause of this length (at most 2 s apart while typing continues).
265
- * Lower = snappier propagation and more rows; higher = fewer rows and more
266
- * lag for other collaborators. Defaults to 300ms.
261
+ * How long in milliseconds the first edit to a collaborative (CRDT) field
262
+ * after a quiet spell waits before it goes out, so a burst of keystrokes
263
+ * travels as one edit-log row. Local state is kept on every change (so
264
+ * reload and offline work). Defaults to 50ms.
267
265
  */
268
266
  crdtDebounceMs?: number;
267
+ /**
268
+ * The shortest gap in milliseconds between two edit-log rows of one CRDT
269
+ * field: while someone keeps typing, their edits go out on this cadence
270
+ * instead of waiting for a pause. Lower = other collaborators see the text
271
+ * sooner and more rows are written; higher = fewer rows and more lag.
272
+ * Defaults to 200ms.
273
+ */
274
+ crdtSendIntervalMs?: number;
269
275
  /**
270
276
  * Enable collaborative CRDT fields. When `true`, the `loro-crdt` engine is
271
277
  * preloaded at client startup (fetched as a separate chunk on page load) so
@@ -299,15 +305,16 @@ export interface Sp00kyConfig<S extends SchemaStructure> {
299
305
  * back for it.
300
306
  *
301
307
  * The client holds exactly one live subscription, on its `_00_list_ref`
302
- * table, and today it is a doorbell: the notification is reduced to the
303
- * query hash and the row is then pulled with `SELECT * FROM $ids`. The edge
308
+ * table. Without this, the notification names the row and the client then
309
+ * pulls it with `SELECT * FROM $ids`. The edge
304
310
  * is a real graph edge, so `FETCH out` makes SurrealDB resolve the row into
305
311
  * the notification and the body fetch disappears.
306
312
  *
307
313
  * The push stays advisory: the body is landed exactly the way a fetched body
308
314
  * is (store write, circuit ingest, version recorded), so a notification the
309
- * server drops costs nothing but the old round trip. Membership is still
310
- * read back; only the body fetch is saved.
315
+ * server drops costs nothing but the old round trip. A row joining a live
316
+ * view is committed to membership from the notification either way (no
317
+ * re-read); with this on, the body fetch is saved as well.
311
318
  *
312
319
  * Default `false`. The cost is wire bytes: one edit bumps the edge in every
313
320
  * view holding the row, and under `FETCH` each of those notifications
@@ -542,6 +549,18 @@ export interface InlineRow {
542
549
  record: Record<string, unknown>;
543
550
  }
544
551
 
552
+ /**
553
+ * A row that joined a query's view, or moved to a newer version, as one LIVE
554
+ * notification on `_00_list_ref` reports it: the edge's `out` and `version`.
555
+ * The client commits it to the query's membership straight away instead of
556
+ * re-reading the edges (see `sync/live.saga.ts`).
557
+ */
558
+ export interface EdgeDelta {
559
+ hash: QueryHash;
560
+ id: string;
561
+ version: number;
562
+ }
563
+
545
564
  // Flat array format: [[record-id, version], [record-id, version], ...]
546
565
  export type RecordVersionArray = Array<[string, number]>;
547
566
 
@@ -1,5 +1,5 @@
1
1
  import { RecordId } from 'surrealdb';
2
- import type { InlineRow } from '../types';
2
+ import type { EdgeDelta, InlineRow } from '../types';
3
3
  import { encodeRecordId } from './index';
4
4
 
5
5
  /**
@@ -37,3 +37,21 @@ export function rowOfEdge(value: unknown): InlineRow | null {
37
37
  if (version === null) return null;
38
38
  return { id, version, record: out as Record<string, unknown> };
39
39
  }
40
+
41
+ /**
42
+ * The membership change a `CREATE` or `UPDATE` notification reports: the
43
+ * edge's target joined the view (or its version moved). Null for anything a
44
+ * re-read must settle instead: a `DELETE` (a row leaving, or every edge of a
45
+ * view being rewritten), a subquery child (`parent` set), or an edge missing
46
+ * its target or version. With `FETCH out` the target is the joined row, whose
47
+ * `id` is the record id.
48
+ */
49
+ export function deltaOfEdge(action: string, value: unknown): EdgeDelta | null {
50
+ if (action !== 'CREATE' && action !== 'UPDATE') return null;
51
+ const hash = hashOfEdge(value);
52
+ const edge = value as { out?: unknown; version?: unknown; parent?: unknown };
53
+ if (!hash || edge.parent != null || typeof edge.version !== 'number') return null;
54
+ const out = edge.out instanceof RecordId ? edge.out : (edge.out as { id?: unknown } | null | undefined)?.id;
55
+ if (!(out instanceof RecordId)) return null;
56
+ return { hash, id: encodeRecordId(out), version: edge.version };
57
+ }