@syncular/client 0.15.48 → 0.17.0

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.
@@ -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);
@@ -91,7 +91,7 @@ export function startSyncWorker(overrides = {}) {
91
91
  autoSyncScheduled = false;
92
92
  if (closed ||
93
93
  client === undefined ||
94
- client.securityLifecycle === 'preflight') {
94
+ client.securityLifecycle() === 'preflight') {
95
95
  return;
96
96
  }
97
97
  const running = client;
@@ -113,7 +113,7 @@ export function startSyncWorker(overrides = {}) {
113
113
  if (!autoSync ||
114
114
  closed ||
115
115
  client === undefined ||
116
- client.securityLifecycle === 'preflight' ||
116
+ client.securityLifecycle() === 'preflight' ||
117
117
  intent.kind === 'none') {
118
118
  return;
119
119
  }
@@ -277,12 +277,12 @@ export function startSyncWorker(overrides = {}) {
277
277
  // `start()` may discover persisted subscriptions/outbox work. Its callback
278
278
  // fires before this worker publishes the initialized client, so consume
279
279
  // the durable state once here as well; coalescing makes this a single task.
280
- if (started.syncNeeded)
280
+ if (started.statusSnapshot().syncNeeded)
281
281
  consumeSyncIntent({ kind: 'interactive' });
282
282
  return { clientId: started.clientId };
283
283
  }
284
284
  const api = {
285
- securityLifecycle: () => requireClient().securityLifecycle,
285
+ securityLifecycle: () => requireClient().securityLifecycle(),
286
286
  beginSecurityPreflight: async () => {
287
287
  if (backgroundTimer !== undefined)
288
288
  clearTimeout(backgroundTimer);
@@ -336,15 +336,11 @@ export function startSyncWorker(overrides = {}) {
336
336
  realtime: snapshot.host.realtime,
337
337
  });
338
338
  },
339
- conflicts: () => requireClient().conflicts,
340
- rejections: () => requireClient().rejections,
339
+ conflicts: () => requireClient().conflicts(),
340
+ rejections: () => requireClient().rejections(),
341
341
  commitOutcome: (clientCommitId) => requireClient().commitOutcome(clientCommitId),
342
342
  commitOutcomes: (query) => requireClient().commitOutcomes(query),
343
343
  resolveCommitOutcome: (input) => requireClient().resolveCommitOutcome(input),
344
- schemaFloor: () => requireClient().schemaFloor,
345
- leaseState: () => requireClient().leaseState,
346
- upgrading: () => requireClient().upgrading,
347
- syncNeeded: () => requireClient().syncNeeded,
348
344
  pendingCommits: () => requireClient().pendingCommits(),
349
345
  subscriptions: () => requireClient().subscriptions(),
350
346
  subscription: (id) => requireClient().subscription(id),
@@ -1,3 +1,4 @@
1
+ import type { PromiseMethods } from './client.js';
1
2
  /**
2
3
  * Main-thread side of the worker mode and the
3
4
  * multi-tab topology.
@@ -23,7 +24,7 @@
23
24
  */
24
25
  import type { WakeReason } from '@syncular/core';
25
26
  import type { BlobRef, CachedBlob } from './blob.js';
26
- import type { ConflictRecord, LeaseState, MutationInput, PresencePeer, QueryReadSpec, QuerySnapshot, RejectionRecord, SchemaFloor, SecurityLifecycle, SubscribeInput, SyncClientLimits, SyncSummary, WindowState } from './client.js';
27
+ import type { ConflictRecord, MutationInput, PresencePeer, QueryReadSpec, QuerySnapshot, RejectionRecord, SecurityLifecycle, SubscribeInput, SyncClientLimits, SyncSummary, WindowState } from './client.js';
27
28
  import type { SqlRow, SqlValue } from './database.js';
28
29
  import { ClientDiagnosticsEmitter, type ClientDiagnosticsListener, type ClientDiagnosticsRequest, type ClientDiagnosticsSnapshot } from './diagnostics.js';
29
30
  import type { EncryptionKeyringConfig } from './encryption.js';
@@ -38,7 +39,7 @@ import type { CommitOutcome, CommitOutcomeQuery, ResolveCommitOutcomeInput } fro
38
39
  import type { ClientSchema } from './schema.js';
39
40
  import type { SubscriptionRecord } from './state.js';
40
41
  import type { WindowBase } from './window.js';
41
- import { type SyncWorkerEvent, type WorkerDatabaseInit, type WorkerEndpoints, type WorkerErrorShape, type WorkerSecurityActivation } from './worker-protocol.js';
42
+ import { type SyncWorkerEvent, type WorkerApi, type WorkerDatabaseInit, type WorkerEndpoints, type WorkerErrorShape, type WorkerSecurityActivation } from './worker-protocol.js';
42
43
  /** Classify startup without echoing a chunk URL or bundler text to UI/logs. */
43
44
  export declare function workerStartupError(message: unknown): ClientSyncError;
44
45
  export type HandleRole = 'leader' | 'follower';
@@ -132,7 +133,7 @@ interface LeaderCore {
132
133
  * promise. `role` is `'leader'` (owns the worker) or `'follower'` (proxies to
133
134
  * the leader over the channel). Constructed via {@link createSyncClientHandle}.
134
135
  */
135
- export declare class SyncClientHandle {
136
+ export declare class SyncClientHandle implements PromiseMethods<WorkerApi> {
136
137
  #private;
137
138
  /** True only for a leader handle. Kept for the pre-multiTab contract. */
138
139
  get isLeader(): boolean;
@@ -214,11 +215,7 @@ export declare class SyncClientHandle {
214
215
  commitOutcome(clientCommitId: string): Promise<CommitOutcome | undefined>;
215
216
  commitOutcomes(query?: CommitOutcomeQuery): Promise<readonly CommitOutcome[]>;
216
217
  resolveCommitOutcome(input: ResolveCommitOutcomeInput): Promise<CommitOutcome>;
217
- schemaFloor(): Promise<SchemaFloor | undefined>;
218
- leaseState(): Promise<LeaseState | undefined>;
219
218
  /** §7.4.5: true while a schema-bump reset + first re-bootstrap runs. */
220
- upgrading(): Promise<boolean>;
221
- syncNeeded(): Promise<boolean>;
222
219
  pendingCommits(): Promise<OutboxCommit[]>;
223
220
  subscriptions(): Promise<SubscriptionRecord[]>;
224
221
  subscription(id: string): Promise<SubscriptionRecord | undefined>;
@@ -104,12 +104,12 @@ export class SyncClientHandle {
104
104
  ref: this,
105
105
  clientId: () => this.#clientId,
106
106
  role: () => this.#role,
107
- outbox: async () => (await this.pendingCommits()).length,
107
+ outbox: async () => (await this.statusSnapshot()).outbox,
108
108
  subscriptions: () => this.subscriptions(),
109
109
  conflicts: async () => (await this.conflicts()).length,
110
110
  rejections: async () => (await this.rejections()).length,
111
- syncNeeded: () => this.syncNeeded(),
112
- upgrading: () => this.upgrading(),
111
+ syncNeeded: async () => (await this.statusSnapshot()).syncNeeded,
112
+ upgrading: async () => (await this.statusSnapshot()).upgrading,
113
113
  onInvalidate: (listener) => this.onInvalidate(listener),
114
114
  });
115
115
  }
@@ -310,19 +310,7 @@ export class SyncClientHandle {
310
310
  resolveCommitOutcome(input) {
311
311
  return this.#call('resolveCommitOutcome', [input]);
312
312
  }
313
- schemaFloor() {
314
- return this.#call('schemaFloor', []);
315
- }
316
- leaseState() {
317
- return this.#call('leaseState', []);
318
- }
319
313
  /** §7.4.5: true while a schema-bump reset + first re-bootstrap runs. */
320
- upgrading() {
321
- return this.#call('upgrading', []);
322
- }
323
- syncNeeded() {
324
- return this.#call('syncNeeded', []);
325
- }
326
314
  pendingCommits() {
327
315
  return this.#call('pendingCommits', []);
328
316
  }