@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 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 pagehideHandler;
308
- constructor(events: EventSystem<StreamProcessorEvents>, db: LocalStore, persistenceClient: PersistenceClient, logger: Logger);
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 as a single bulk operation, firing only
318
- * one coalesced `StreamUpdate` per affected query once every record has been
319
- * ingested (instead of one update per record). Use this whenever multiple
320
- * records land at once — e.g. sync fetching N missing rows — so a list query
321
- * re-runs and the UI re-renders once for the whole batch rather than
322
- * row-by-row.
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
- * Internally opens a coalescing window, ingests each record, then flushes;
325
- * processor state is persisted once for the whole batch. No-op for an empty
326
- * batch.
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: Array<{
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 does NOT `loadState()` a persisted snapshot references views
363
- * under a dead sessionId salt; the DataModule rebind re-registers every live
364
- * view against this fresh processor. Caller must re-seed `setPermissions`
365
- * afterwards (a fresh circuit default-denies every table).
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
- * Opt into snapshot persistence (`persistCircuit`). Off by default: see the
383
- * `persistCircuit` field comment for why per-ingest snapshots were removed.
384
- * Must be called before `init()` for a snapshot to be restored at boot.
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
- * Record that the circuit changed. Cheap and O(1), the expensive snapshot is
389
- * deferred to the checkpoint timer, and skipped entirely when
390
- * `persistCircuit` is off (the default).
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 `pagehide` listener. */
446
+ /** Stop checkpointing and drop the visibility listeners. */
395
447
  stopCheckpoints(): void;
396
- loadState(): Promise<void>;
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: 'CREATE' | 'UPDATE' | 'DELETE', id: string, record: any): WasmStreamUpdate[];
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 via `hub`. Idempotent for a
1245
- * boot-time leader; a runtime promotion (failover) reloads the outbox,
1246
- * which now holds EVERY tab's rows, and restarts LIVE under this session. */
1247
- promoteToLeader(hub: LeaderSyncHub): Promise<void>;
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
  }