@spooky-sync/core 0.0.1-canary.204 → 0.0.1-canary.205
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 +148 -40
- package/dist/index.js +601 -173
- package/dist/tabs-broker-worker.js +51 -13
- package/dist/types.d.ts +52 -13
- package/package.json +3 -3
- package/src/modules/cache/index.ts +21 -4
- package/src/modules/devtools/index.ts +3 -1
- package/src/modules/sync/engine.ts +76 -58
- package/src/modules/sync/sync.ts +65 -8
- package/src/services/database/cache-engine.ts +33 -0
- package/src/services/database/sqlite-cache-engine.test.ts +58 -0
- package/src/services/database/sqlite-cache-engine.ts +71 -1
- package/src/services/database/surreal-cache-engine.ts +20 -0
- package/src/services/stream-processor/index.ts +471 -225
- package/src/services/stream-processor/stream-processor.batch.test.ts +3 -13
- package/src/services/stream-processor/stream-processor.prime.test.ts +198 -0
- package/src/services/stream-processor/stream-processor.reset.test.ts +77 -67
- package/src/services/stream-processor/wasm-types.ts +32 -1
- package/src/services/tabs/broker.test.ts +49 -0
- package/src/services/tabs/coordinator.test.ts +6 -1
- package/src/services/tabs/coordinator.ts +35 -3
- package/src/services/tabs/tabs-broker-worker.ts +66 -12
- package/src/sp00ky.ts +54 -8
- package/src/types.ts +21 -13
package/dist/index.d.ts
CHANGED
|
@@ -286,10 +286,34 @@ type StreamProcessorEvents = {
|
|
|
286
286
|
interface StreamUpdateReceiver {
|
|
287
287
|
onStreamUpdate(update: StreamUpdate): void;
|
|
288
288
|
}
|
|
289
|
+
/** One row change in the shape `ingestMany` consumes. */
|
|
290
|
+
interface IngestRecord {
|
|
291
|
+
table: string;
|
|
292
|
+
/** `MERGE` overlays the given fields on the stored row (projection widening). */
|
|
293
|
+
op: 'CREATE' | 'UPDATE' | 'DELETE' | 'MERGE';
|
|
294
|
+
id: string;
|
|
295
|
+
record: any;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* What the boot-time prime needs from the client: which tables to walk, how
|
|
299
|
+
* to recognise a snapshot written under a different schema, and which rows'
|
|
300
|
+
* local `_00_rv` must not be reported as the server's.
|
|
301
|
+
*/
|
|
302
|
+
interface CircuitPrimeContext {
|
|
303
|
+
tables: string[];
|
|
304
|
+
schemaHash: string;
|
|
305
|
+
/** Encoded ids with an unsettled local mutation (their `_00_rv` was bumped
|
|
306
|
+
* locally and may exceed the server's next version). */
|
|
307
|
+
pendingIds: Set<string>;
|
|
308
|
+
/** Receives every `(id, rv)` the prime put into the circuit, per table, so
|
|
309
|
+
* the sync layer can skip re-downloading bodies it already has. */
|
|
310
|
+
onVersions?: (table: string, entries: [string, number][]) => void;
|
|
311
|
+
}
|
|
312
|
+
/** Storage key of the circuit snapshot inside the local store. */
|
|
313
|
+
|
|
289
314
|
declare class StreamProcessorService {
|
|
290
315
|
events: EventSystem<StreamProcessorEvents>;
|
|
291
316
|
private db;
|
|
292
|
-
private persistenceClient;
|
|
293
317
|
private logger;
|
|
294
318
|
private processor;
|
|
295
319
|
private isInitialized;
|
|
@@ -297,15 +321,21 @@ declare class StreamProcessorService {
|
|
|
297
321
|
private batching;
|
|
298
322
|
private batchBuffer;
|
|
299
323
|
private sessionAuth;
|
|
300
|
-
private stateKeySuffix;
|
|
301
324
|
private stateGeneration;
|
|
302
325
|
private persistState;
|
|
303
326
|
private persistCircuit;
|
|
304
327
|
private checkpointMs;
|
|
305
328
|
private checkpointTimer;
|
|
306
329
|
private snapshotDirty;
|
|
307
|
-
private
|
|
308
|
-
|
|
330
|
+
private dirtyRows;
|
|
331
|
+
private hideHandler;
|
|
332
|
+
private checkpointInFlight;
|
|
333
|
+
private projection;
|
|
334
|
+
private primed;
|
|
335
|
+
private schemaHash;
|
|
336
|
+
private widenQueue;
|
|
337
|
+
private widenPending;
|
|
338
|
+
constructor(events: EventSystem<StreamProcessorEvents>, db: LocalStore, logger: Logger);
|
|
309
339
|
/**
|
|
310
340
|
* Add a receiver for stream updates.
|
|
311
341
|
* Multiple receivers can be registered (DataManager, DevTools, etc.)
|
|
@@ -314,23 +344,21 @@ declare class StreamProcessorService {
|
|
|
314
344
|
private notifyUpdates;
|
|
315
345
|
private dispatchUpdates;
|
|
316
346
|
/**
|
|
317
|
-
* Ingest a batch of record changes
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
347
|
+
* Ingest a batch of record changes, firing one coalesced `StreamUpdate` per
|
|
348
|
+
* affected query once every record has been ingested. Use this whenever
|
|
349
|
+
* multiple records land at once (sync fetching N rows, the boot prime).
|
|
350
|
+
*
|
|
351
|
+
* The batch is fed to the wasm side in chunks of {@link INGEST_CHUNK}: one
|
|
352
|
+
* circuit step per chunk (a step walks every registered view, so per-record
|
|
353
|
+
* ingest paid that fixed cost N times), but never the whole batch at once,
|
|
354
|
+
* because the wasm side has to hold every parsed row of a call at the same
|
|
355
|
+
* time and wasm32 dlmalloc never returns that peak.
|
|
323
356
|
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
357
|
+
* Returns the records that were ingested. A chunk that fails is reported
|
|
358
|
+
* and skipped, not retried (a retry would double-apply whatever the failed
|
|
359
|
+
* step already committed), and the remaining chunks still run.
|
|
327
360
|
*/
|
|
328
|
-
ingestMany(records:
|
|
329
|
-
table: string;
|
|
330
|
-
op: 'CREATE' | 'UPDATE' | 'DELETE';
|
|
331
|
-
id: string;
|
|
332
|
-
record: any;
|
|
333
|
-
}>): void;
|
|
361
|
+
ingestMany(records: IngestRecord[]): IngestRecord[];
|
|
334
362
|
/**
|
|
335
363
|
* Open a coalescing window. While open, the per-record stream updates
|
|
336
364
|
* emitted by `ingest` are buffered (one entry per queryHash) instead of
|
|
@@ -352,17 +380,16 @@ declare class StreamProcessorService {
|
|
|
352
380
|
* This must be called before using other methods.
|
|
353
381
|
*/
|
|
354
382
|
init(): Promise<void>;
|
|
355
|
-
/** Route the persisted circuit snapshot to a per-bucket key. */
|
|
356
|
-
setStateKeySuffix(bucketId: string): void;
|
|
357
|
-
private stateKey;
|
|
358
383
|
/**
|
|
359
384
|
* Drop the current WASM processor and start a fresh, empty circuit. Used on
|
|
360
385
|
* local-bucket switches: the old circuit holds the previous user's rows AND
|
|
361
386
|
* views registered with the previous `$auth` context, so neither may survive.
|
|
362
|
-
* Deliberately
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
*
|
|
387
|
+
* Deliberately loads nothing: the snapshot in the store being swapped away
|
|
388
|
+
* from belongs to the previous bucket; the caller primes the new bucket's
|
|
389
|
+
* circuit (`primeFromLocal`) once its store is open, and the DataModule
|
|
390
|
+
* rebind re-registers every live view against this fresh processor. Caller
|
|
391
|
+
* must re-seed `setPermissions` afterwards (a fresh circuit default-denies
|
|
392
|
+
* every table).
|
|
366
393
|
*/
|
|
367
394
|
reset(): Promise<void>;
|
|
368
395
|
/**
|
|
@@ -379,21 +406,61 @@ declare class StreamProcessorService {
|
|
|
379
406
|
/** Toggle circuit-state persistence (shared-tabs follower/leader role). */
|
|
380
407
|
setPersistenceEnabled(enabled: boolean): void;
|
|
381
408
|
/**
|
|
382
|
-
*
|
|
383
|
-
*
|
|
384
|
-
*
|
|
409
|
+
* Snapshot persistence (`persistCircuit`). When on, the circuit's store is
|
|
410
|
+
* written to the local store on a checkpoint interval and when the page
|
|
411
|
+
* goes hidden, and restored by {@link primeFromLocal} on the next boot.
|
|
385
412
|
*/
|
|
386
413
|
configureCircuitPersistence(enabled: boolean, checkpointMs?: number): void;
|
|
387
414
|
/**
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
|
|
415
|
+
* Field projection (`circuitProjection`, default on). Takes effect on the
|
|
416
|
+
* next processor (`init`/`reset`) and on rows written after that.
|
|
417
|
+
*/
|
|
418
|
+
configureProjection(enabled: boolean): void;
|
|
419
|
+
private applyProjection;
|
|
420
|
+
/** Resolves once the boot-time prime has finished (or was skipped). */
|
|
421
|
+
whenPrimed(): Promise<void>;
|
|
422
|
+
/**
|
|
423
|
+
* Fill the circuit from the LOCAL store, in the background.
|
|
424
|
+
*
|
|
425
|
+
* With a usable snapshot: install it under whatever views have registered
|
|
426
|
+
* meanwhile (`load_store_state` re-primes them), then `reconcile` each table
|
|
427
|
+
* against the store's `(id, rv)` list so rows deleted since the checkpoint
|
|
428
|
+
* are stepped out and only rows added or changed since are read back and
|
|
429
|
+
* ingested. Without one: read every row and ingest it, chunked.
|
|
430
|
+
*
|
|
431
|
+
* Either way the circuit ends up equal to the local store without touching
|
|
432
|
+
* the network, so the first sync diff is a real delta rather than "fetch
|
|
433
|
+
* everything". The returned promise never rejects; `whenPrimed` gates on it.
|
|
434
|
+
*/
|
|
435
|
+
primeFromLocal(ctx: CircuitPrimeContext): Promise<void>;
|
|
436
|
+
private runPrime;
|
|
437
|
+
/** Publish wasm updates produced outside an ingest (restore, reconcile). */
|
|
438
|
+
private dispatchWasmUpdates;
|
|
439
|
+
/**
|
|
440
|
+
* Record that the circuit changed by `rows` rows. Cheap; the snapshot is
|
|
441
|
+
* deferred to the checkpoint timer and skipped entirely when `persistCircuit`
|
|
442
|
+
* is off.
|
|
391
443
|
*/
|
|
392
444
|
private markSnapshotDirty;
|
|
393
445
|
private startCheckpoints;
|
|
394
|
-
/** Stop checkpointing and drop the
|
|
446
|
+
/** Stop checkpointing and drop the visibility listeners. */
|
|
395
447
|
stopCheckpoints(): void;
|
|
396
|
-
|
|
448
|
+
/**
|
|
449
|
+
* Write the circuit's store to the local store as a snapshot. Compacts the
|
|
450
|
+
* row arena first when dead bytes outweigh live ones. Serialised: a second
|
|
451
|
+
* call while one is in flight joins it. No-op unless persistence is on, this
|
|
452
|
+
* tab owns the store, and the engine can hold a snapshot.
|
|
453
|
+
*/
|
|
454
|
+
checkpoint(reason?: string): Promise<void>;
|
|
455
|
+
private runCheckpoint;
|
|
456
|
+
/**
|
|
457
|
+
* Projection widening: a newly registered view evaluates fields that rows
|
|
458
|
+
* already in the circuit were stored without. Merge just those fields in,
|
|
459
|
+
* table by table, from the local store. The view registered against what
|
|
460
|
+
* was present and converges as the merges step through.
|
|
461
|
+
*/
|
|
462
|
+
private scheduleWiden;
|
|
463
|
+
private runWiden;
|
|
397
464
|
/**
|
|
398
465
|
* Seed per-table `select` permission predicates ({ [table]: whereText }).
|
|
399
466
|
* Must run after the processor exists and before any `register_view`, else
|
|
@@ -411,13 +478,12 @@ declare class StreamProcessorService {
|
|
|
411
478
|
* "requires $auth but registration params lack it".
|
|
412
479
|
*/
|
|
413
480
|
setSessionAuth(authId: string | null, access: string | null): void;
|
|
414
|
-
saveState(): Promise<void>;
|
|
415
481
|
/**
|
|
416
482
|
* Ingest a record change into the processor.
|
|
417
483
|
* Emits 'stream_update' event if materialized views are affected.
|
|
418
484
|
* @param isOptimistic true = local mutation (increment versions), false = remote sync (keep versions)
|
|
419
485
|
*/
|
|
420
|
-
ingest(table: string, op: '
|
|
486
|
+
ingest(table: string, op: IngestRecord['op'], id: string, record: any): WasmStreamUpdate[];
|
|
421
487
|
/**
|
|
422
488
|
* Register a new query plan.
|
|
423
489
|
* Emits 'stream_update' with the initial result.
|
|
@@ -489,6 +555,14 @@ declare class CacheModule implements StreamUpdateReceiver {
|
|
|
489
555
|
*/
|
|
490
556
|
applyRelayedIngest(tuples: CacheIngestTuple[]): void;
|
|
491
557
|
lookup(recordId: string): number;
|
|
558
|
+
/**
|
|
559
|
+
* Seed the version memo from rows the circuit was primed with out of the
|
|
560
|
+
* local store, so the first post-reload sync diff does not re-download
|
|
561
|
+
* bodies the browser already has. Only rows the prime actually put into the
|
|
562
|
+
* circuit belong here: a memo entry with no circuit row would make the diff
|
|
563
|
+
* flag the id forever while nothing ever fetches it.
|
|
564
|
+
*/
|
|
565
|
+
primeVersions(entries: [string, number][]): void;
|
|
492
566
|
/** Drop the version cache on a bucket switch — a stale version would make
|
|
493
567
|
* the sync diff skip fetching a body the new bucket legitimately needs. */
|
|
494
568
|
clearVersionLookups(): void;
|
|
@@ -1240,11 +1314,37 @@ declare class Sp00kySync<S extends SchemaStructure> {
|
|
|
1240
1314
|
/** Set BEFORE init(): shapes what init boots (a follower loads no outbox and
|
|
1241
1315
|
* never starts LIVE; its own registration/poll paths stay untouched). */
|
|
1242
1316
|
setTabContext(role: 'solo' | 'leader' | 'follower', tabId: string | null): void;
|
|
1317
|
+
/** In-flight {@link resumeLeaderDuties}, so a second call joins the first
|
|
1318
|
+
* instead of double-draining the outbox. */
|
|
1319
|
+
private leaderDutiesInFlight;
|
|
1320
|
+
/** Resolves once the in-browser circuit has been primed from the local
|
|
1321
|
+
* store. Every sync diff waits on it: diffing against an empty circuit
|
|
1322
|
+
* classifies the whole working set as missing and re-downloads it. */
|
|
1323
|
+
private primeGate;
|
|
1324
|
+
/** The prime we last waited on. `whenPrimed` hands out one promise per
|
|
1325
|
+
* prime, so a new identity means a new prime (boot, bucket switch) ran. */
|
|
1326
|
+
private settledPrime;
|
|
1327
|
+
setPrimeGate(gate: () => Promise<void>): void;
|
|
1328
|
+
/**
|
|
1329
|
+
* Leader WIRING only, and deliberately synchronous.
|
|
1330
|
+
*
|
|
1331
|
+
* The coordinator publishes the leader role and tells the broker
|
|
1332
|
+
* `leader-ready` the moment the store is adopted, and the broker can mint a
|
|
1333
|
+
* follower's ports on the very next tick. So the follower-message handler
|
|
1334
|
+
* has to be live before this returns, or a mutation forwarded in that window
|
|
1335
|
+
* is dropped. Everything that can block (outbox reload, LIVE restart) moved
|
|
1336
|
+
* to {@link resumeLeaderDuties}: a promotion that waits on the network holds
|
|
1337
|
+
* `leader-ready` back, and a broker whose leader never reports ready serves
|
|
1338
|
+
* no follower ports and re-elects no one, which wedges the whole namespace.
|
|
1339
|
+
*/
|
|
1340
|
+
promoteToLeader(hub: LeaderSyncHub): void;
|
|
1243
1341
|
/** Leader duties: drain the shared outbox, own the single list_ref LIVE,
|
|
1244
|
-
* relay LIVE events and rollbacks to followers
|
|
1245
|
-
*
|
|
1246
|
-
*
|
|
1247
|
-
|
|
1342
|
+
* relay LIVE events and rollbacks to followers. Idempotent for a boot-time
|
|
1343
|
+
* leader; a runtime promotion (failover) reloads the outbox, which now
|
|
1344
|
+
* holds EVERY tab's rows, and restarts LIVE under this session. Runs in the
|
|
1345
|
+
* background off the promotion path, so however long it takes (or if it
|
|
1346
|
+
* never finishes) the tab is already a working leader. */
|
|
1347
|
+
resumeLeaderDuties(): Promise<void>;
|
|
1248
1348
|
/** Follower duties: no outbox drain, no LIVE. Mutations forward to the
|
|
1249
1349
|
* leader; everything else (registration, per-query sync, poll) runs
|
|
1250
1350
|
* against this tab's own remote session as usual. */
|
|
@@ -2452,6 +2552,14 @@ declare class Sp00kyClient<S extends SchemaStructure> {
|
|
|
2452
2552
|
* the former to `encodeRecordId` reads `.table` off a string and throws
|
|
2453
2553
|
* during boot.
|
|
2454
2554
|
*/
|
|
2555
|
+
/**
|
|
2556
|
+
* Prime the in-browser circuit from the local store. Builds the context the
|
|
2557
|
+
* stream processor needs: every synced table (the app schema plus the
|
|
2558
|
+
* server-written meta tables that sync down), a schema hash so a snapshot
|
|
2559
|
+
* projected under another schema is not trusted, and the ids whose local
|
|
2560
|
+
* `_00_rv` was bumped by an unsettled mutation.
|
|
2561
|
+
*/
|
|
2562
|
+
private primeCircuit;
|
|
2455
2563
|
private sessionAuthId;
|
|
2456
2564
|
private mintSessionSalt;
|
|
2457
2565
|
}
|