@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.
- package/README.md +50 -0
- package/dist/bun-database.js +11 -0
- package/dist/client.d.ts +13 -20
- package/dist/client.js +78 -61
- package/dist/node-database.js +8 -0
- package/dist/outbox.d.ts +5 -1
- package/dist/outbox.js +26 -3
- package/dist/reactive-store.d.ts +12 -6
- package/dist/reactive-store.js +219 -45
- package/dist/realtime-supervisor.d.ts +1 -1
- package/dist/realtime-supervisor.js +2 -8
- package/dist/worker-entry.js +6 -10
- package/dist/worker-host.d.ts +4 -7
- package/dist/worker-host.js +3 -15
- package/dist/worker-protocol.d.ts +4 -18
- package/package.json +3 -11
- package/src/bun-database.ts +10 -0
- package/src/client.ts +124 -69
- package/src/node-database.ts +7 -0
- package/src/outbox.ts +39 -8
- package/src/reactive-store.ts +226 -56
- package/src/realtime-supervisor.ts +2 -11
- package/src/worker-entry.ts +7 -10
- package/src/worker-host.ts +5 -22
- package/src/worker-protocol.ts +7 -33
- package/dist/realtime-supervisor-observation.d.ts +0 -8
- package/dist/realtime-supervisor-observation.js +0 -15
- package/src/realtime-supervisor-observation.ts +0 -21
package/dist/reactive-store.js
CHANGED
|
@@ -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
|
-
#
|
|
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
|
|
208
|
-
this
|
|
209
|
-
|
|
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(
|
|
281
|
+
if (!this.#listeners.delete(notify))
|
|
223
282
|
return;
|
|
224
|
-
this.#
|
|
225
|
-
|
|
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 = () =>
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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.#
|
|
358
|
-
|
|
359
|
-
this.#
|
|
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
|
-
|
|
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
|
|
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 () =>
|
|
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.#
|
|
439
|
-
|
|
440
|
-
this
|
|
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
|
|
447
|
-
#windows = new
|
|
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
|
-
|
|
483
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
660
|
-
|
|
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
|
|
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
|
-
|
|
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/worker-entry.js
CHANGED
|
@@ -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),
|
package/dist/worker-host.d.ts
CHANGED
|
@@ -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,
|
|
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>;
|
package/dist/worker-host.js
CHANGED
|
@@ -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.
|
|
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.
|
|
112
|
-
upgrading: () => this.
|
|
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
|
}
|