@syncular/client 0.15.47 → 0.16.1

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/outbox.js CHANGED
@@ -26,17 +26,40 @@ export function appendOutboxCommit(db, clientCommitId, operations, nowMs, before
26
26
  ]);
27
27
  }
28
28
  }
29
- /** Pending commits in FIFO creation order (§7.1). */
29
+ /** Pending commits in FIFO creation order (§7.1). Full reads serve replay and the public listing. */
30
30
  export function listOutbox(db) {
31
31
  return db
32
32
  .query(`SELECT seq, client_commit_id, created_at_ms, operations
33
33
  FROM _syncular_outbox ORDER BY seq ASC`)
34
- .map((row) => ({
34
+ .map(decodeOutboxRow);
35
+ }
36
+ function decodeOutboxRow(row) {
37
+ return {
35
38
  seq: row.seq,
36
39
  clientCommitId: row.client_commit_id,
37
40
  createdAtMs: row.created_at_ms,
38
41
  operations: JSON.parse(row.operations),
39
- }));
42
+ };
43
+ }
44
+ /** Keyset pages bound staging; laziness decodes only commits consumed by the encoder. */
45
+ export function* iterateOutbox(db, throughSeq) {
46
+ let afterSeq = 0;
47
+ while (afterSeq < throughSeq) {
48
+ const rows = db.query(`SELECT seq, client_commit_id, created_at_ms, operations FROM _syncular_outbox
49
+ WHERE seq > ? AND seq <= ? ORDER BY seq ASC LIMIT 32`, [afterSeq, throughSeq]);
50
+ if (rows.length === 0)
51
+ return;
52
+ for (const row of rows) {
53
+ const commit = decodeOutboxRow(row);
54
+ afterSeq = commit.seq;
55
+ yield commit;
56
+ }
57
+ }
58
+ }
59
+ /** Routine status reads never load operation bodies. */
60
+ export function countOutbox(db) {
61
+ return db.query('SELECT COUNT(*) AS count FROM _syncular_outbox')[0]
62
+ .count;
40
63
  }
41
64
  export function deleteOutboxCommit(db, clientCommitId) {
42
65
  db.exec('DELETE FROM _syncular_outbox_before_images WHERE client_commit_id = ?', [clientCommitId]);
@@ -1,5 +1,5 @@
1
1
  import { type SyncAvailability } from './availability.js';
2
- import type { CommitOutcome, QueryReadSpec, QuerySnapshot, WindowCoverage, WindowState } from './client.js';
2
+ import type { CommitOutcome, ClientSnapshotReader, QueryReadSpec, QuerySnapshot, WindowCoverage, WindowState } from './client.js';
3
3
  import type { SqlValue } from './database.js';
4
4
  import type { ClientChangeListener, SyncStatusSnapshot } from './invalidation.js';
5
5
  import type { LeadershipState } from './multi-tab.js';
@@ -27,16 +27,14 @@ export interface LiveQueryResult<Row> {
27
27
  readonly isRefreshing: boolean;
28
28
  readonly availability: SyncAvailability;
29
29
  }
30
- export interface ReactiveQueryClient {
30
+ export interface ReactiveQueryClient extends Pick<ClientSnapshotReader, 'statusSnapshot' | 'commitOutcomes'> {
31
31
  readonly currentSchemaVersion?: number;
32
32
  onChange(listener: ClientChangeListener): () => void;
33
33
  querySnapshot<Row = Record<string, SqlValue>>(spec: QueryReadSpec): QuerySnapshot<Row> | Promise<QuerySnapshot<Row>>;
34
- statusSnapshot(): SyncStatusSnapshot | Promise<SyncStatusSnapshot>;
35
34
  leadershipSnapshot?(): LeadershipState | undefined;
36
35
  onLeadershipChange?(listener: (state: LeadershipState) => void): () => void;
37
- readonly conflicts: readonly unknown[] | (() => readonly unknown[] | Promise<readonly unknown[]>);
38
- readonly rejections: readonly unknown[] | (() => readonly unknown[] | Promise<readonly unknown[]>);
39
- commitOutcomes(): readonly CommitOutcome[] | Promise<readonly CommitOutcome[]>;
36
+ conflicts(): readonly unknown[] | Promise<readonly unknown[]>;
37
+ rejections(): readonly unknown[] | Promise<readonly unknown[]>;
40
38
  setWindow(base: WindowBase, units: readonly string[]): void | Promise<void>;
41
39
  windowState(base: WindowBase): WindowState | Promise<WindowState>;
42
40
  }
@@ -85,6 +83,14 @@ export declare class ReactiveClientStore {
85
83
  window(base: WindowBase): ExternalStoreEntry<WindowState>;
86
84
  setWindowClaim(owner: symbol, base: WindowBase, units: readonly string[]): Promise<void>;
87
85
  releaseWindowClaims(owner: symbol): void;
86
+ /** Retained observation counts for diagnostics and resource benchmarks. */
87
+ cacheStats(): {
88
+ queries: number;
89
+ activeQueries: number;
90
+ windows: number;
91
+ activeWindows: number;
92
+ windowClaims: number;
93
+ };
88
94
  start(): void;
89
95
  dispose(): void;
90
96
  }
@@ -1,11 +1,9 @@
1
1
  import { classifySyncAvailability, } from './availability.js';
2
+ import { ClientSyncError } from './errors.js';
2
3
  import { windowBaseKey } from './window.js';
3
4
  function errorOf(value) {
4
5
  return value instanceof Error ? value : new Error(String(value));
5
6
  }
6
- function readCollection(value) {
7
- return typeof value === 'function' ? value() : value;
8
- }
9
7
  function unsupportedCanonicalValue(value) {
10
8
  const description = Object.prototype.toString.call(value);
11
9
  throw new TypeError(`unsupported reactive cache-key value ${description}; use null, string, finite number, bigint, boolean, bytes, arrays, or plain objects`);
@@ -178,9 +176,58 @@ function reconcileRows(previous, fresh, rowKey) {
178
176
  ? previous
179
177
  : next;
180
178
  }
179
+ /** Shared ownership for query and window observations, including abandoned renders. */
180
+ class ObservationCache {
181
+ #entries = new Map();
182
+ #active = new Set();
183
+ get size() {
184
+ return this.#entries.size;
185
+ }
186
+ get activeSize() {
187
+ return this.#active.size;
188
+ }
189
+ get(key) {
190
+ return this.#entries.get(key);
191
+ }
192
+ add(key, entry) {
193
+ this.#entries.set(key, entry);
194
+ this.#cleanup(key, entry);
195
+ }
196
+ activate(key, candidate) {
197
+ const entry = this.#entries.get(key) ?? candidate;
198
+ this.#entries.set(key, entry);
199
+ this.#active.add(entry);
200
+ return entry;
201
+ }
202
+ deactivate(key, entry) {
203
+ this.#active.delete(entry);
204
+ this.#cleanup(key, entry);
205
+ }
206
+ #cleanup(key, entry) {
207
+ scheduleMicrotask(() => {
208
+ if (this.#active.has(entry))
209
+ return;
210
+ if (this.#entries.get(key) === entry)
211
+ this.#entries.delete(key);
212
+ entry.reset();
213
+ });
214
+ }
215
+ onChange(batch) {
216
+ for (const entry of this.#active)
217
+ entry.onChange(batch);
218
+ }
219
+ clear() {
220
+ for (const entry of this.#entries.values())
221
+ entry.dispose();
222
+ this.#entries.clear();
223
+ this.#active.clear();
224
+ }
225
+ }
181
226
  class QueryEntry {
182
227
  store;
183
228
  spec;
229
+ key;
230
+ cache;
184
231
  #owner = Symbol('query-window-claim');
185
232
  #listeners = new Set();
186
233
  #state = {
@@ -191,22 +238,31 @@ class QueryEntry {
191
238
  isRefreshing: false,
192
239
  availability: { state: 'ready' },
193
240
  };
194
- #subscribers = 0;
241
+ #delegate;
242
+ #generation = 0;
195
243
  #scheduled = false;
196
244
  #running = false;
197
245
  #requested = false;
198
246
  #desiredRevision = 0n;
199
247
  #claimReady = Promise.resolve();
200
248
  #offStatus;
201
- constructor(store, spec) {
249
+ constructor(store, spec, key, cache) {
202
250
  this.store = store;
203
251
  this.spec = spec;
252
+ this.key = key;
253
+ this.cache = cache;
204
254
  }
205
- getSnapshot = () => this.#state;
255
+ getSnapshot = () => this.#delegate?.getSnapshot() ?? this.#state;
206
256
  subscribe = (listener) => {
207
- this.#listeners.add(listener);
208
- this.#subscribers += 1;
209
- if (this.#subscribers === 1) {
257
+ const current = this.cache.activate(this.key, this);
258
+ if (current !== this) {
259
+ this.#delegate = current;
260
+ return this.#delegate.subscribe(listener);
261
+ }
262
+ this.#delegate = undefined;
263
+ const notify = () => listener();
264
+ this.#listeners.add(notify);
265
+ if (this.#listeners.size === 1) {
210
266
  this.#offStatus = this.store.status.subscribe(() => this.#onAvailabilityChange());
211
267
  this.#onAvailabilityChange();
212
268
  if (this.spec.claimCoverage !== false) {
@@ -215,28 +271,59 @@ class QueryEntry {
215
271
  claims.push(this.store.setWindowClaim(this.#owner, coverage.base, coverage.units));
216
272
  }
217
273
  this.#claimReady = Promise.all(claims).then(() => undefined);
274
+ // A render can lose its last subscriber before the read loop starts.
275
+ // The loop still observes the original rejection while it owns the claim.
276
+ void this.#claimReady.catch(() => undefined);
218
277
  }
219
278
  this.#requestRead();
220
279
  }
221
280
  return () => {
222
- if (!this.#listeners.delete(listener))
281
+ if (!this.#listeners.delete(notify))
223
282
  return;
224
- this.#subscribers -= 1;
225
- if (this.#subscribers === 0) {
283
+ if (this.#listeners.size === 0) {
284
+ this.cache.deactivate(this.key, this);
226
285
  this.#offStatus?.();
227
286
  this.#offStatus = undefined;
228
287
  this.store.releaseWindowClaims(this.#owner);
229
288
  }
230
289
  };
231
290
  };
232
- refresh = () => this.#requestRead(true);
291
+ refresh = () => {
292
+ if (this.#delegate !== undefined)
293
+ this.#delegate.refresh();
294
+ else
295
+ this.#requestRead(true);
296
+ };
297
+ reset() {
298
+ this.#generation += 1;
299
+ this.#scheduled = false;
300
+ this.#running = false;
301
+ this.#requested = false;
302
+ this.#desiredRevision = 0n;
303
+ this.#claimReady = Promise.resolve();
304
+ this.#state = {
305
+ rows: [],
306
+ phase: 'loading',
307
+ revision: undefined,
308
+ error: undefined,
309
+ isRefreshing: false,
310
+ availability: { state: 'ready' },
311
+ };
312
+ }
313
+ dispose() {
314
+ this.#listeners.clear();
315
+ this.#offStatus?.();
316
+ this.#offStatus = undefined;
317
+ this.store.releaseWindowClaims(this.#owner);
318
+ this.reset();
319
+ }
233
320
  onChange(batch) {
234
321
  if (!batchMatches(batch, this.spec))
235
322
  return;
236
323
  if (batch.revision > this.#desiredRevision) {
237
324
  this.#desiredRevision = batch.revision;
238
325
  }
239
- if (this.#subscribers > 0)
326
+ if (this.#listeners.size > 0)
240
327
  this.#requestRead();
241
328
  }
242
329
  #publish(next) {
@@ -253,6 +340,8 @@ class QueryEntry {
253
340
  listener();
254
341
  }
255
342
  #requestRead(refreshing = false) {
343
+ if (this.#listeners.size === 0)
344
+ return;
256
345
  this.#requested = true;
257
346
  if (refreshing &&
258
347
  this.#state.revision !== undefined &&
@@ -262,7 +351,10 @@ class QueryEntry {
262
351
  if (this.#scheduled || this.#running)
263
352
  return;
264
353
  this.#scheduled = true;
354
+ const generation = this.#generation;
265
355
  scheduleMicrotask(() => {
356
+ if (generation !== this.#generation)
357
+ return;
266
358
  this.#scheduled = false;
267
359
  void this.#readLoop();
268
360
  });
@@ -292,8 +384,9 @@ class QueryEntry {
292
384
  this.#requestRead();
293
385
  }
294
386
  async #readLoop() {
295
- if (this.#running || this.#subscribers === 0)
387
+ if (this.#running || this.#listeners.size === 0)
296
388
  return;
389
+ const generation = this.#generation;
297
390
  this.#running = true;
298
391
  try {
299
392
  do {
@@ -301,6 +394,8 @@ class QueryEntry {
301
394
  if (this.store.availabilitySnapshot().state === 'blocked')
302
395
  break;
303
396
  await this.#claimReady;
397
+ if (generation !== this.#generation || this.#listeners.size === 0)
398
+ return;
304
399
  const snapshot = await this.store.client.querySnapshot({
305
400
  sql: this.spec.sql,
306
401
  ...(this.spec.params !== undefined
@@ -310,6 +405,8 @@ class QueryEntry {
310
405
  ? { coverage: this.spec.coverage }
311
406
  : {}),
312
407
  });
408
+ if (generation !== this.#generation || this.#listeners.size === 0)
409
+ return;
313
410
  const availability = this.store.availabilitySnapshot();
314
411
  if (availability.state === 'blocked') {
315
412
  this.#publish({
@@ -341,9 +438,11 @@ class QueryEntry {
341
438
  isRefreshing: false,
342
439
  availability,
343
440
  });
344
- } while (this.#requested && this.#subscribers > 0);
441
+ } while (this.#requested && this.#listeners.size > 0);
345
442
  }
346
443
  catch (error) {
444
+ if (generation !== this.#generation || this.#listeners.size === 0)
445
+ return;
347
446
  const wrapped = errorOf(error);
348
447
  this.#publish({
349
448
  ...this.#state,
@@ -354,15 +453,18 @@ class QueryEntry {
354
453
  });
355
454
  }
356
455
  finally {
357
- this.#running = false;
358
- if (this.#requested && this.#subscribers > 0)
359
- this.#requestRead();
456
+ if (generation === this.#generation) {
457
+ this.#running = false;
458
+ if (this.#requested && this.#listeners.size > 0)
459
+ this.#requestRead();
460
+ }
360
461
  }
361
462
  }
362
463
  }
363
464
  class ValueEntry {
364
465
  value;
365
466
  read;
467
+ #generation = 0;
366
468
  #listeners = new Set();
367
469
  constructor(value, read) {
368
470
  this.value = value;
@@ -374,9 +476,17 @@ class ValueEntry {
374
476
  return () => this.#listeners.delete(listener);
375
477
  };
376
478
  refresh = () => {
377
- void this.read().then((next) => this.set(next));
479
+ const generation = ++this.#generation;
480
+ void this.read().then((next) => {
481
+ if (generation === this.#generation)
482
+ this.set(next);
483
+ });
378
484
  };
485
+ invalidate() {
486
+ this.#generation += 1;
487
+ }
379
488
  set(next) {
489
+ this.invalidate();
380
490
  if (next === this.value)
381
491
  return;
382
492
  this.value = next;
@@ -388,28 +498,59 @@ class WindowEntry {
388
498
  store;
389
499
  base;
390
500
  baseKey;
501
+ cache;
391
502
  #listeners = new Set();
392
503
  #state = { units: [], pending: [] };
504
+ #generation = 0;
505
+ #delegate;
393
506
  #running = false;
394
507
  #requested = false;
395
- constructor(store, base, baseKey) {
508
+ constructor(store, base, baseKey, cache) {
396
509
  this.store = store;
397
510
  this.base = base;
398
511
  this.baseKey = baseKey;
512
+ this.cache = cache;
399
513
  }
400
- getSnapshot = () => this.#state;
514
+ getSnapshot = () => this.#delegate?.getSnapshot() ?? this.#state;
401
515
  subscribe = (listener) => {
402
- this.#listeners.add(listener);
516
+ const current = this.cache.activate(this.baseKey, this);
517
+ if (current !== this) {
518
+ this.#delegate = current;
519
+ return this.#delegate.subscribe(listener);
520
+ }
521
+ this.#delegate = undefined;
522
+ const notify = () => listener();
523
+ this.#listeners.add(notify);
403
524
  if (this.#listeners.size === 1)
404
525
  this.refresh();
405
- return () => this.#listeners.delete(listener);
526
+ return () => {
527
+ if (this.#listeners.delete(notify) && this.#listeners.size === 0) {
528
+ this.cache.deactivate(this.baseKey, this);
529
+ }
530
+ };
406
531
  };
407
532
  refresh = () => {
533
+ if (this.#delegate !== undefined) {
534
+ this.#delegate.refresh();
535
+ return;
536
+ }
537
+ if (this.#listeners.size === 0)
538
+ return;
408
539
  this.#requested = true;
409
540
  if (this.#running)
410
541
  return;
411
542
  void this.#readLoop();
412
543
  };
544
+ reset() {
545
+ this.#generation += 1;
546
+ this.#running = false;
547
+ this.#requested = false;
548
+ this.#state = { units: [], pending: [] };
549
+ }
550
+ dispose() {
551
+ this.#listeners.clear();
552
+ this.reset();
553
+ }
413
554
  onChange(batch) {
414
555
  if (batch.windows.some((change) => change.baseKey === this.baseKey) &&
415
556
  this.#listeners.size > 0) {
@@ -417,34 +558,39 @@ class WindowEntry {
417
558
  }
418
559
  }
419
560
  async #readLoop() {
561
+ const generation = this.#generation;
420
562
  this.#running = true;
421
563
  try {
422
564
  do {
423
565
  this.#requested = false;
424
566
  const next = await this.store.client.windowState(this.base);
567
+ if (generation !== this.#generation || this.#listeners.size === 0)
568
+ return;
425
569
  if (canonicalValue(next.units) !== canonicalValue(this.#state.units) ||
426
570
  canonicalValue(next.pending) !== canonicalValue(this.#state.pending)) {
427
571
  this.#state = next;
428
572
  for (const listener of this.#listeners)
429
573
  listener();
430
574
  }
431
- } while (this.#requested);
575
+ } while (this.#requested && this.#listeners.size > 0);
432
576
  }
433
577
  catch {
434
578
  // WindowState predates the error-bearing query result. Keep the last
435
579
  // coherent snapshot; a later exact window event or refresh retries.
436
580
  }
437
581
  finally {
438
- this.#running = false;
439
- if (this.#requested)
440
- this.refresh();
582
+ if (generation === this.#generation) {
583
+ this.#running = false;
584
+ if (this.#requested)
585
+ this.refresh();
586
+ }
441
587
  }
442
588
  }
443
589
  }
444
590
  export class ReactiveClientStore {
445
591
  client;
446
- #queries = new Map();
447
- #windows = new Map();
592
+ #queries = new ObservationCache();
593
+ #windows = new ObservationCache();
448
594
  #windowClaims = new Map();
449
595
  #offChange;
450
596
  #offLeadership;
@@ -479,8 +625,8 @@ export class ReactiveClientStore {
479
625
  const conflicts = new ValueEntry({ conflicts: [], rejections: [], error: undefined, isLoading: true }, async () => {
480
626
  try {
481
627
  const [found, rejected] = await Promise.all([
482
- readCollection(client.conflicts),
483
- readCollection(client.rejections),
628
+ client.conflicts(),
629
+ client.rejections(),
484
630
  ]);
485
631
  return {
486
632
  conflicts: found,
@@ -513,9 +659,6 @@ export class ReactiveClientStore {
513
659
  this.status = status;
514
660
  this.conflicts = conflicts;
515
661
  this.outcomes = outcomes;
516
- status.refresh();
517
- conflicts.refresh();
518
- outcomes.refresh();
519
662
  this.start();
520
663
  }
521
664
  query(spec) {
@@ -541,8 +684,8 @@ export class ReactiveClientStore {
541
684
  });
542
685
  let entry = this.#queries.get(key);
543
686
  if (entry === undefined) {
544
- entry = new QueryEntry(this, spec);
545
- this.#queries.set(key, entry);
687
+ entry = new QueryEntry(this, spec, key, this.#queries);
688
+ this.#queries.add(key, entry);
546
689
  }
547
690
  return entry;
548
691
  }
@@ -580,8 +723,8 @@ export class ReactiveClientStore {
580
723
  const key = windowBaseKey(base);
581
724
  let entry = this.#windows.get(key);
582
725
  if (entry === undefined) {
583
- entry = new WindowEntry(this, base, key);
584
- this.#windows.set(key, entry);
726
+ entry = new WindowEntry(this, base, key, this.#windows);
727
+ this.#windows.add(key, entry);
585
728
  }
586
729
  return entry;
587
730
  }
@@ -624,7 +767,8 @@ export class ReactiveClientStore {
624
767
  });
625
768
  }
626
769
  async #flushWindow(group) {
627
- if (group.running)
770
+ const baseKey = windowBaseKey(group.base);
771
+ if (group.running || this.#windowClaims.get(baseKey) !== group)
628
772
  return;
629
773
  group.running = true;
630
774
  try {
@@ -636,6 +780,8 @@ export class ReactiveClientStore {
636
780
  const key = canonicalValue(units);
637
781
  if (key !== group.appliedKey) {
638
782
  await this.client.setWindow(group.base, units);
783
+ if (this.#windowClaims.get(baseKey) !== group)
784
+ return;
639
785
  group.appliedKey = key;
640
786
  }
641
787
  }
@@ -648,18 +794,33 @@ export class ReactiveClientStore {
648
794
  }
649
795
  finally {
650
796
  group.running = false;
651
- if (group.requested)
797
+ if (group.requested && this.#windowClaims.get(baseKey) === group)
652
798
  this.#scheduleWindow(group);
799
+ else if (group.claims.size === 0 &&
800
+ group.waiters.length === 0 &&
801
+ group.appliedKey === canonicalValue([])) {
802
+ const key = windowBaseKey(group.base);
803
+ if (this.#windowClaims.get(key) === group)
804
+ this.#windowClaims.delete(key);
805
+ }
653
806
  }
654
807
  }
808
+ /** Retained observation counts for diagnostics and resource benchmarks. */
809
+ cacheStats() {
810
+ return {
811
+ queries: this.#queries.size,
812
+ activeQueries: this.#queries.activeSize,
813
+ windows: this.#windows.size,
814
+ activeWindows: this.#windows.activeSize,
815
+ windowClaims: this.#windowClaims.size,
816
+ };
817
+ }
655
818
  start() {
656
819
  if (this.#offChange !== undefined)
657
820
  return;
658
821
  this.#offChange = this.client.onChange((batch) => {
659
- for (const entry of this.#queries.values())
660
- entry.onChange(batch);
661
- for (const entry of this.#windows.values())
662
- entry.onChange(batch);
822
+ this.#queries.onChange(batch);
823
+ this.#windows.onChange(batch);
663
824
  if (batch.status !== undefined) {
664
825
  this.status.set({
665
826
  status: batch.status,
@@ -680,14 +841,27 @@ export class ReactiveClientStore {
680
841
  ...previous,
681
842
  leadership,
682
843
  });
844
+ if (previous.isLoading)
845
+ this.status.refresh();
683
846
  });
847
+ this.status.refresh();
848
+ this.conflicts.refresh();
849
+ this.outcomes.refresh();
684
850
  }
685
851
  dispose() {
852
+ this.#queries.clear();
853
+ this.#windows.clear();
854
+ for (const entry of [this.status, this.conflicts, this.outcomes]) {
855
+ entry.invalidate();
856
+ }
686
857
  this.#offChange?.();
687
858
  this.#offChange = undefined;
688
859
  this.#offLeadership?.();
689
860
  this.#offLeadership = undefined;
690
861
  for (const group of this.#windowClaims.values()) {
862
+ for (const waiter of group.waiters.splice(0)) {
863
+ waiter.reject(new ClientSyncError('client.reactive_store_disposed', 'reactive store disposed'));
864
+ }
691
865
  // Releasing a window is best-effort teardown. A resource owner may have
692
866
  // already closed the underlying worker/native handle before React effect
693
867
  // cleanup runs (notably during schema-changing HMR). Do not let that
@@ -1,6 +1,5 @@
1
1
  import type { SecurityLifecycle } from './client.js';
2
2
  import type { ClientDiagnosticsConnectivity, ClientDiagnosticsListener, ClientDiagnosticsSnapshot } from './diagnostics.js';
3
- export { linkRealtimeSupervisorObservation } from './realtime-supervisor-observation.js';
4
3
  type CancelTimer = () => void;
5
4
  export interface RealtimeSupervisorClient {
6
5
  connectRealtime(): Promise<void>;
@@ -91,3 +90,4 @@ export declare class RealtimeSupervisor {
91
90
  export declare function installRealtimeSupervisor<T extends RealtimeSupervisorClient>(client: T, options?: RealtimeSupervisorOptions): T;
92
91
  export declare function realtimeSupervisorSnapshot(client: object): RealtimeSupervisorSnapshot;
93
92
  export declare function subscribeRealtimeSupervisor(client: object, listener: () => void): () => void;
93
+ export {};
@@ -1,5 +1,3 @@
1
- import { realtimeSupervisorObservationSource } from './realtime-supervisor-observation.js';
2
- export { linkRealtimeSupervisorObservation } from './realtime-supervisor-observation.js';
3
1
  const REALTIME_SUPERVISOR_KEY = Symbol.for('syncular.realtime-supervisor.v1');
4
2
  const DEFAULT_INITIAL_DELAY_MS = 1_000;
5
3
  const DEFAULT_MAXIMUM_DELAY_MS = 30_000;
@@ -8,16 +6,12 @@ const UNSUPPORTED_SNAPSHOT = Object.freeze({
8
6
  phase: 'unsupported',
9
7
  attempt: 0,
10
8
  });
11
- function attachment(client, visited = new Set()) {
12
- if (visited.has(client))
13
- return undefined;
14
- visited.add(client);
9
+ function attachment(client) {
15
10
  const candidate = Reflect.get(client, REALTIME_SUPERVISOR_KEY);
16
11
  if (candidate?.version === 1 && candidate.supervisor) {
17
12
  return candidate;
18
13
  }
19
- const source = realtimeSupervisorObservationSource(client);
20
- return source === undefined ? undefined : attachment(source, visited);
14
+ return undefined;
21
15
  }
22
16
  function scheduleTimer(callback, delayMs) {
23
17
  const timer = globalThis.setTimeout(callback, delayMs);
package/dist/remote.d.ts CHANGED
@@ -18,6 +18,8 @@ export interface SyncRemoteClientConfig {
18
18
  readonly operations?: RemoteOperationTransport;
19
19
  readonly operationRealtime?: RemoteOperationRealtimeConnector;
20
20
  readonly encryption?: EncryptionConfig;
21
+ /** Acquired partition log epoch for restore-safe ordinary commits (§2.1). */
22
+ readonly logEpoch?: string;
21
23
  }
22
24
  export interface RemoteCommitInput {
23
25
  /** Stable caller-owned idempotency identity for this logical commit. */