@spooky-sync/core 0.0.1-canary.66 → 0.0.1-canary.67
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/dist/index.d.ts +105 -17
- package/dist/index.js +754 -133
- package/dist/otel/index.d.ts +1 -1
- package/dist/types.d.ts +29 -3
- package/package.json +2 -2
- package/src/modules/cache/index.ts +6 -1
- package/src/modules/crdt/crdt-field.ts +115 -23
- package/src/modules/crdt/crdt-hydration.test.ts +206 -0
- package/src/modules/crdt/index.ts +207 -68
- package/src/modules/data/index.ts +170 -17
- package/src/modules/ref-tables.test.ts +56 -0
- package/src/modules/ref-tables.ts +57 -0
- package/src/modules/sync/engine.ts +47 -11
- package/src/modules/sync/sync.ts +304 -13
- package/src/modules/sync/utils.test.ts +157 -0
- package/src/modules/sync/utils.ts +79 -0
- package/src/services/stream-processor/index.ts +25 -0
- package/src/sp00ky.auth-order.test.ts +65 -0
- package/src/sp00ky.ts +112 -31
- package/src/types.ts +29 -2
- package/src/utils/surql.ts +9 -0
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as
|
|
1
|
+
import { C as UpdateOptions, E as EventSystem, S as StoreType, T as EventDefinition, _ as RecordVersionDiff, a as MutationCallback, b as Sp00kyQueryResult, c as PersistenceClient, d as QueryConfigRecord, f as QueryHash, g as RecordVersionArray, h as QueryUpdateCallback, i as MATERIALIZATION_SAMPLE_WINDOW, l as PinoTransmit, m as QueryTimeToLive, n as EventSubscriptionOptions, o as MutationEvent, p as QueryState, r as Level, s as MutationEventType, t as DebounceOptions, u as QueryConfig, v as RunOptions, w as Logger$1, x as Sp00kyQueryResultPromise, y as Sp00kyConfig } from "./types.js";
|
|
2
2
|
import * as surrealdb0 from "surrealdb";
|
|
3
3
|
import { Duration, RecordId, Surreal as Surreal$1, SurrealTransaction } from "surrealdb";
|
|
4
4
|
import { AccessDefinition, BackendNames, BackendRoutes, BucketNames, ColumnSchema, GetTable, QueryBuilder, QueryOptions, RoutePayload, SchemaStructure, TableModel, TableNames, TypeNameToTypeMap } from "@spooky-sync/query-builder";
|
|
@@ -97,6 +97,12 @@ interface StreamUpdate {
|
|
|
97
97
|
queryHash: string;
|
|
98
98
|
localArray: RecordVersionArray;
|
|
99
99
|
op?: 'CREATE' | 'UPDATE' | 'DELETE';
|
|
100
|
+
/**
|
|
101
|
+
* End-to-end ingest latency for the WASM call that produced this update,
|
|
102
|
+
* in milliseconds. Populated by StreamProcessorService.ingest. Undefined
|
|
103
|
+
* for the initial register_view snapshot.
|
|
104
|
+
*/
|
|
105
|
+
materializationTimeMs?: number;
|
|
100
106
|
}
|
|
101
107
|
type StreamProcessorEvents = {
|
|
102
108
|
stream_update: EventDefinition<'stream_update', StreamUpdate[]>;
|
|
@@ -202,33 +208,55 @@ declare class CrdtField {
|
|
|
202
208
|
private fieldName;
|
|
203
209
|
private doc;
|
|
204
210
|
private pushTimer;
|
|
211
|
+
private local;
|
|
205
212
|
private remote;
|
|
206
213
|
private recordId;
|
|
214
|
+
private sessionId;
|
|
207
215
|
private unsubscribe;
|
|
208
216
|
private lastPushTime;
|
|
209
217
|
private lastCursorPushTime;
|
|
210
218
|
private loadedFromCrdt;
|
|
211
219
|
private pushRetryCount;
|
|
212
220
|
private logger;
|
|
221
|
+
private cursorsEnabled;
|
|
222
|
+
/** Remote-push debounce. Local writes happen immediately on every Loro
|
|
223
|
+
* update; the remote UPSERT is coalesced over this window. Configured
|
|
224
|
+
* via `Sp00kyConfig.crdtDebounceMs`, default 500. */
|
|
225
|
+
private remoteDebounceMs;
|
|
213
226
|
private _onCursorUpdate;
|
|
214
227
|
private pendingCursorUpdate;
|
|
215
228
|
/** Callback set by the editor to receive remote cursor updates.
|
|
216
229
|
* Any cursor data that arrived before this callback was set will be replayed. */
|
|
217
230
|
set onCursorUpdate(cb: ((data: Uint8Array) => void) | null);
|
|
218
231
|
get onCursorUpdate(): ((data: Uint8Array) => void) | null;
|
|
219
|
-
constructor(fieldName: string, initialState?:
|
|
232
|
+
constructor(fieldName: string, cursorsEnabled: boolean, initialState?: Uint8Array, logger?: Logger$1 | null);
|
|
220
233
|
getDoc(): LoroDoc;
|
|
221
234
|
/** Whether the LoroDoc was loaded from saved CRDT state */
|
|
222
235
|
hasContent(): boolean;
|
|
223
|
-
startSync(remote: RemoteDatabaseService, recordId: string): void;
|
|
236
|
+
startSync(local: LocalDatabaseService, remote: RemoteDatabaseService, recordId: string, sessionId: string, debounceMs: number): void;
|
|
224
237
|
stopSync(): void;
|
|
225
|
-
importRemote(
|
|
226
|
-
exportSnapshot():
|
|
227
|
-
/** Push
|
|
238
|
+
importRemote(state: Uint8Array): void;
|
|
239
|
+
exportSnapshot(): Uint8Array;
|
|
240
|
+
/** Push this session's cursor blob into the parent row at
|
|
241
|
+
* `<field>.cursors[$sid]`. No-op when cursors aren't enabled on this
|
|
242
|
+
* field — the editor still calls this method optimistically, but
|
|
243
|
+
* without `@cursor` on the schema there's nowhere to store the blob.
|
|
244
|
+
* The UPDATE itself fires the parent table's LIVE feed, so other
|
|
245
|
+
* browsers receive the cursor change without a separate `_00_rv` bump. */
|
|
228
246
|
pushCursorState(encoded: Uint8Array): Promise<void>;
|
|
229
247
|
/** Import remote cursor state (called by CrdtManager from LIVE SELECT) */
|
|
230
248
|
importRemoteCursor(base64State: string): void;
|
|
231
|
-
private
|
|
249
|
+
private scheduleRemotePush;
|
|
250
|
+
/** SET path inside a parent row for the current snapshot. `@crdt`-only
|
|
251
|
+
* fields hold the snapshot directly (`<field>`); `@crdt @cursor`
|
|
252
|
+
* fields hold a `{ state, cursors }` object so the snapshot lives at
|
|
253
|
+
* `<field>.state` next to per-session cursor blobs. */
|
|
254
|
+
private statePath;
|
|
255
|
+
/** Mirror the LoroDoc snapshot into the parent row locally. Runs on
|
|
256
|
+
* every local update and every remote import so reloads (online or
|
|
257
|
+
* offline) see the freshest content immediately. Failures are
|
|
258
|
+
* swallowed — a stale local write must never block user input. */
|
|
259
|
+
private persistLocal;
|
|
232
260
|
private pushToRemote;
|
|
233
261
|
}
|
|
234
262
|
//#endregion
|
|
@@ -236,16 +264,40 @@ declare class CrdtField {
|
|
|
236
264
|
/**
|
|
237
265
|
* CrdtManager manages active CrdtField instances and their sync channels.
|
|
238
266
|
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
267
|
+
* Collaborative state lives in two dedicated tables (defined in
|
|
268
|
+
* `apps/cli/src/meta_tables_remote.surql`):
|
|
269
|
+
* - `_00_crdt` { record_id, field, state } — one row per (record, field)
|
|
270
|
+
* - `_00_cursor` { record_id, session_id, field, state } — one row per
|
|
271
|
+
* (record, session, field)
|
|
272
|
+
*
|
|
273
|
+
* Splitting them off the parent row is what makes offline edits mergeable:
|
|
274
|
+
* each (record, field) gets its own row, so concurrent offline writes don't
|
|
275
|
+
* collide on the parent's last-write-wins semantics.
|
|
276
|
+
*
|
|
277
|
+
* Cross-browser delivery still rides the parent table's existing LIVE feed
|
|
278
|
+
* to avoid SurrealDB v3 LIVE bugs around dereference-based permission rules
|
|
279
|
+
* (issues 3602, 4026). On every meta UPSERT the writer also bumps the
|
|
280
|
+
* parent's `_00_rv` (a no-op assignment); that fires the parent's LIVE
|
|
281
|
+
* feed, and the receiver pulls the matching `_00_crdt` / `_00_cursor` rows
|
|
282
|
+
* via subquery. Permission inheritance happens server-side via
|
|
283
|
+
* `record_id.id != NONE` (SELECT) and `fn::can_update_record` (UPDATE).
|
|
241
284
|
*/
|
|
242
285
|
declare class CrdtManager {
|
|
243
286
|
private schema;
|
|
287
|
+
private local;
|
|
244
288
|
private remote;
|
|
289
|
+
private debounceMs;
|
|
245
290
|
private fields;
|
|
246
|
-
private
|
|
291
|
+
private liveByTable;
|
|
292
|
+
private pendingLive;
|
|
247
293
|
private logger;
|
|
248
|
-
|
|
294
|
+
private sessionId;
|
|
295
|
+
constructor(schema: SchemaStructure, local: LocalDatabaseService, remote: RemoteDatabaseService, logger: Logger$1, debounceMs?: number);
|
|
296
|
+
/** Set the session id that scopes this client's cursor entries. Must be
|
|
297
|
+
* called before `open()` for cursors to be pushed under a stable key.
|
|
298
|
+
* Passed in from `sp00ky.ts` at boot (it already fetches `session::id()`
|
|
299
|
+
* for the data-module salt). */
|
|
300
|
+
setSessionId(sessionId: string): void;
|
|
249
301
|
/**
|
|
250
302
|
* Open a CRDT field for collaborative editing.
|
|
251
303
|
*
|
|
@@ -258,8 +310,32 @@ declare class CrdtManager {
|
|
|
258
310
|
open(table: string, recordId: string, field: string, fallbackText?: string): Promise<CrdtField>;
|
|
259
311
|
close(table: string, recordId: string, field: string): void;
|
|
260
312
|
closeAll(): void;
|
|
261
|
-
|
|
262
|
-
|
|
313
|
+
/** Ensure a single `LIVE SELECT * FROM <table>` is running, shared across
|
|
314
|
+
* every open CrdtField on `table`. */
|
|
315
|
+
private ensureTableSubscription;
|
|
316
|
+
/** Apply a parent-row payload from a non-LIVE source (e.g. the
|
|
317
|
+
* list_ref-driven sync engine, when the cross-user LIVE on the
|
|
318
|
+
* parent table is filtered out by the SurrealDB cross-session
|
|
319
|
+
* permission gap). Same semantics as the internal `dispatchRow`. */
|
|
320
|
+
applyRow(table: string, row: Record<string, unknown>): void;
|
|
321
|
+
/** Dispatch a parent-row LIVE event to every open CrdtField on that
|
|
322
|
+
* record. Each open field reads its slice of the row directly — the
|
|
323
|
+
* CRDT snapshot is a column on the parent now, so there is no
|
|
324
|
+
* follow-up subquery. */
|
|
325
|
+
private dispatchRow;
|
|
326
|
+
/** One-shot remote fetch for a row whose CRDT field hasn't synced
|
|
327
|
+
* locally yet (fresh device, memory-backed local DB after reload, …).
|
|
328
|
+
* Used by `open()` when the local read came up empty. Subsequent
|
|
329
|
+
* cross-browser updates ride `dispatchRow` via the parent LIVE feed. */
|
|
330
|
+
private fetchAndDispatchRow;
|
|
331
|
+
/** Schema lookup: does `<table>.<field>` carry a `@cursor` annotation?
|
|
332
|
+
* Determines the on-disk shape (plain snapshot vs. `{ state, cursors }`). */
|
|
333
|
+
private fieldHasCursor;
|
|
334
|
+
/** Pull the LoroDoc snapshot bytes out of a row slice. For `@crdt`-only
|
|
335
|
+
* the slice IS the snapshot (Uint8Array); for `@crdt @cursor` it's
|
|
336
|
+
* `{ state, cursors }` where `state` carries the snapshot bytes. */
|
|
337
|
+
private extractSnapshot;
|
|
338
|
+
private killTableSubscription;
|
|
263
339
|
private makeKey;
|
|
264
340
|
/**
|
|
265
341
|
* Throws if `<table>.<field>` is not annotated `@crdt` in the schema. Catches
|
|
@@ -300,6 +376,12 @@ declare class Sp00kyClient<S extends SchemaStructure> {
|
|
|
300
376
|
get remoteClient(): surrealdb0.Surreal;
|
|
301
377
|
get localClient(): surrealdb0.Surreal;
|
|
302
378
|
get pendingMutationCount(): number;
|
|
379
|
+
/** Number of times the initial list_ref LIVE subscription retried on
|
|
380
|
+
* the most recent `setCurrentUserId` call. 0 when the SSP's
|
|
381
|
+
* pre-emptive user-table creation got there first; >0 when LIVE
|
|
382
|
+
* registration hit a "table not found" race. Exposed so the e2e
|
|
383
|
+
* suite can guard the pre-emptive path against regression. */
|
|
384
|
+
get liveRetryCount(): number;
|
|
303
385
|
subscribeToPendingMutations(cb: (count: number) => void): () => void;
|
|
304
386
|
constructor(config: Sp00kyConfig<S>);
|
|
305
387
|
/**
|
|
@@ -312,7 +394,8 @@ declare class Sp00kyClient<S extends SchemaStructure> {
|
|
|
312
394
|
/**
|
|
313
395
|
* Open a CRDT field for collaborative editing.
|
|
314
396
|
* Returns a CrdtField with a LoroDoc that can be bound to any editor.
|
|
315
|
-
* Also starts a LIVE SELECT on
|
|
397
|
+
* Also starts a LIVE SELECT on the parent table for real-time sync;
|
|
398
|
+
* incoming events trigger a subquery fetch of `_00_crdt` / `_00_cursor`.
|
|
316
399
|
*/
|
|
317
400
|
openCrdtField(table: string, recordId: string, field: string, fallbackText?: string): Promise<CrdtField>;
|
|
318
401
|
/**
|
|
@@ -334,8 +417,13 @@ declare class Sp00kyClient<S extends SchemaStructure> {
|
|
|
334
417
|
}>;
|
|
335
418
|
delete(table: string, id: string): Promise<void>;
|
|
336
419
|
useRemote<T>(fn: (client: Surreal) => Promise<T> | T): Promise<T>;
|
|
337
|
-
|
|
338
|
-
|
|
420
|
+
/**
|
|
421
|
+
* Fetch SurrealDB's `session::id()` as a string. Used as a salt for
|
|
422
|
+
* query-id hashing so two sessions for the same user get distinct
|
|
423
|
+
* `_00_query` rows. Returns empty string if the query fails (we still
|
|
424
|
+
* boot, just without session scoping for IDs).
|
|
425
|
+
*/
|
|
426
|
+
private fetchSessionId;
|
|
339
427
|
}
|
|
340
428
|
//#endregion
|
|
341
429
|
//#region src/utils/index.d.ts
|
|
@@ -350,4 +438,4 @@ declare function textToHtml(text: string): string;
|
|
|
350
438
|
*/
|
|
351
439
|
|
|
352
440
|
//#endregion
|
|
353
|
-
export { AuthEventSystem, AuthEventTypeMap, AuthEventTypes, AuthService, BucketHandle, CURSOR_COLORS, CrdtField, CrdtManager, DebounceOptions, EventSubscriptionOptions, Level, MutationCallback, MutationEvent, MutationEventType, PersistenceClient, PinoTransmit, QueryConfig, QueryConfigRecord, QueryHash, QueryState, QueryTimeToLive, QueryUpdateCallback, RecordVersionArray, RecordVersionDiff, RunOptions, Sp00kyClient, Sp00kyConfig, Sp00kyQueryResult, Sp00kyQueryResultPromise, StoreType, UpdateOptions, createAuthEventSystem, cursorColorFromName, fileToUint8Array, textToHtml };
|
|
441
|
+
export { AuthEventSystem, AuthEventTypeMap, AuthEventTypes, AuthService, BucketHandle, CURSOR_COLORS, CrdtField, CrdtManager, DebounceOptions, EventSubscriptionOptions, Level, MATERIALIZATION_SAMPLE_WINDOW, MutationCallback, MutationEvent, MutationEventType, PersistenceClient, PinoTransmit, QueryConfig, QueryConfigRecord, QueryHash, QueryState, QueryTimeToLive, QueryUpdateCallback, RecordVersionArray, RecordVersionDiff, RunOptions, Sp00kyClient, Sp00kyConfig, Sp00kyQueryResult, Sp00kyQueryResultPromise, StoreType, UpdateOptions, createAuthEventSystem, cursorColorFromName, fileToUint8Array, textToHtml };
|