@syncular/client 0.30.1 → 0.30.3
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/reactive-store.js +34 -13
- package/package.json +3 -3
- package/src/reactive-store.ts +36 -13
package/dist/reactive-store.js
CHANGED
|
@@ -233,9 +233,9 @@ class ObservationCache {
|
|
|
233
233
|
for (const entry of this.#active)
|
|
234
234
|
entry.onChange(batch);
|
|
235
235
|
}
|
|
236
|
-
|
|
236
|
+
reclaim() {
|
|
237
237
|
for (const entry of this.#active)
|
|
238
|
-
entry.
|
|
238
|
+
entry.reclaim?.();
|
|
239
239
|
}
|
|
240
240
|
clear() {
|
|
241
241
|
for (const entry of this.#entries.values())
|
|
@@ -368,6 +368,8 @@ class QueryEntry {
|
|
|
368
368
|
refresh = () => {
|
|
369
369
|
if (this.#delegate !== undefined)
|
|
370
370
|
this.#delegate.refresh();
|
|
371
|
+
else if (this.#claimError !== undefined)
|
|
372
|
+
this.reclaim();
|
|
371
373
|
else
|
|
372
374
|
this.#requestRead(true);
|
|
373
375
|
};
|
|
@@ -390,9 +392,9 @@ class QueryEntry {
|
|
|
390
392
|
this.store.releaseWindowClaims(this.#owner);
|
|
391
393
|
this.reset();
|
|
392
394
|
}
|
|
393
|
-
/**
|
|
394
|
-
*
|
|
395
|
-
|
|
395
|
+
/** SPEC §7.7: a rejected claim is claimed again; its stored error would
|
|
396
|
+
* fail every read until the last subscriber leaves. */
|
|
397
|
+
reclaim() {
|
|
396
398
|
if (this.#delegate !== undefined || this.#claimError === undefined)
|
|
397
399
|
return;
|
|
398
400
|
this.#claimError = undefined;
|
|
@@ -405,7 +407,14 @@ class QueryEntry {
|
|
|
405
407
|
if (batch.revision > this.#desiredRevision) {
|
|
406
408
|
this.#desiredRevision = batch.revision;
|
|
407
409
|
}
|
|
408
|
-
if (this.#listeners.size
|
|
410
|
+
if (this.#listeners.size === 0)
|
|
411
|
+
return;
|
|
412
|
+
// A committed row change of a dependency claims again; a window-only
|
|
413
|
+
// batch, which a failed window edit can itself produce, does not.
|
|
414
|
+
const rowChange = batch.tables.some((change) => this.spec.dependencies.some((dependency) => dependency.table === change.table));
|
|
415
|
+
if (rowChange && this.#claimError !== undefined)
|
|
416
|
+
this.reclaim();
|
|
417
|
+
else
|
|
409
418
|
this.#requestRead();
|
|
410
419
|
}
|
|
411
420
|
#publish(next) {
|
|
@@ -894,6 +903,16 @@ export class ReactiveClientStore {
|
|
|
894
903
|
this.#scheduleWindow(group);
|
|
895
904
|
}
|
|
896
905
|
}
|
|
906
|
+
/** Apply every claimed window group again and claim again for query entries
|
|
907
|
+
* whose claim was rejected. An applied group compares equal and makes no
|
|
908
|
+
* call; a group whose edit is in flight applies once more after it. */
|
|
909
|
+
#reclaimWindows() {
|
|
910
|
+
for (const group of this.#windowClaims.values()) {
|
|
911
|
+
if (group.claims.size > 0)
|
|
912
|
+
this.#scheduleWindow(group);
|
|
913
|
+
}
|
|
914
|
+
this.#queries.reclaim();
|
|
915
|
+
}
|
|
897
916
|
#scheduleWindow(group) {
|
|
898
917
|
group.requested = true;
|
|
899
918
|
if (group.scheduled || group.running)
|
|
@@ -985,7 +1004,14 @@ export class ReactiveClientStore {
|
|
|
985
1004
|
this.outcomes.refresh();
|
|
986
1005
|
});
|
|
987
1006
|
const roundFailure = this.roundFailure;
|
|
1007
|
+
let claimedAttempt;
|
|
988
1008
|
this.#offProgress = this.client.onProgress((progress) => {
|
|
1009
|
+
// A new sync attempt is the client's retry schedule (§7.6): window
|
|
1010
|
+
// claims that failed are claimed again once per attempt.
|
|
1011
|
+
if (progress.state === 'running' && progress.attempt !== claimedAttempt) {
|
|
1012
|
+
claimedAttempt = progress.attempt;
|
|
1013
|
+
this.#reclaimWindows();
|
|
1014
|
+
}
|
|
989
1015
|
const current = roundFailure.getSnapshot();
|
|
990
1016
|
if (progress.state === 'failed' && current?.attempt !== progress.attempt)
|
|
991
1017
|
roundFailure.set(new SyncRoundFailedError(progress.errorCode ?? 'client.unknown_failure', progress.attempt, progress.retryDelayMs));
|
|
@@ -1004,13 +1030,8 @@ export class ReactiveClientStore {
|
|
|
1004
1030
|
this.status.refresh();
|
|
1005
1031
|
// Window edits that failed while no leader served this tab apply now;
|
|
1006
1032
|
// an applied group compares equal and makes no call.
|
|
1007
|
-
if (leadership.state === 'leader' || leadership.state === 'follower')
|
|
1008
|
-
|
|
1009
|
-
if (group.claims.size > 0)
|
|
1010
|
-
this.#scheduleWindow(group);
|
|
1011
|
-
}
|
|
1012
|
-
this.#queries.onLeaderServing();
|
|
1013
|
-
}
|
|
1033
|
+
if (leadership.state === 'leader' || leadership.state === 'follower')
|
|
1034
|
+
this.#reclaimWindows();
|
|
1014
1035
|
});
|
|
1015
1036
|
this.status.refresh();
|
|
1016
1037
|
this.conflicts.refresh();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncular/client",
|
|
3
|
-
"version": "0.30.
|
|
3
|
+
"version": "0.30.3",
|
|
4
4
|
"description": "Syncular TypeScript client core — offline-first sync over SQLite (WASM/OPFS, Bun, Node)",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Benjamin Kniffler",
|
|
@@ -89,10 +89,10 @@
|
|
|
89
89
|
},
|
|
90
90
|
"dependencies": {
|
|
91
91
|
"@sqlite.org/sqlite-wasm": "^3.53.0-build1",
|
|
92
|
-
"@syncular/core": "0.30.
|
|
92
|
+
"@syncular/core": "0.30.3"
|
|
93
93
|
},
|
|
94
94
|
"devDependencies": {
|
|
95
|
-
"@syncular/server": "0.30.
|
|
95
|
+
"@syncular/server": "0.30.3",
|
|
96
96
|
"playwright": "1.63.0"
|
|
97
97
|
}
|
|
98
98
|
}
|
package/src/reactive-store.ts
CHANGED
|
@@ -316,7 +316,7 @@ function reconcileRows<Row>(
|
|
|
316
316
|
interface CachedObservation {
|
|
317
317
|
onChange(batch: ClientChangeBatch): void;
|
|
318
318
|
/** A leader serves this tab again (it leads or follows a bound leader). */
|
|
319
|
-
|
|
319
|
+
reclaim?(): void;
|
|
320
320
|
reset(): void;
|
|
321
321
|
dispose(): void;
|
|
322
322
|
}
|
|
@@ -358,8 +358,8 @@ class ObservationCache {
|
|
|
358
358
|
onChange(batch: ClientChangeBatch): void {
|
|
359
359
|
for (const entry of this.#active) entry.onChange(batch);
|
|
360
360
|
}
|
|
361
|
-
|
|
362
|
-
for (const entry of this.#active) entry.
|
|
361
|
+
reclaim(): void {
|
|
362
|
+
for (const entry of this.#active) entry.reclaim?.();
|
|
363
363
|
}
|
|
364
364
|
clear(): void {
|
|
365
365
|
for (const entry of this.#entries.values()) entry.dispose();
|
|
@@ -504,6 +504,7 @@ class QueryEntry<Row> implements ExternalStoreEntry<LiveQueryResult<Row>> {
|
|
|
504
504
|
|
|
505
505
|
refresh = (): void => {
|
|
506
506
|
if (this.#delegate !== undefined) this.#delegate.refresh();
|
|
507
|
+
else if (this.#claimError !== undefined) this.reclaim();
|
|
507
508
|
else this.#requestRead(true);
|
|
508
509
|
};
|
|
509
510
|
|
|
@@ -528,9 +529,9 @@ class QueryEntry<Row> implements ExternalStoreEntry<LiveQueryResult<Row>> {
|
|
|
528
529
|
this.reset();
|
|
529
530
|
}
|
|
530
531
|
|
|
531
|
-
/**
|
|
532
|
-
*
|
|
533
|
-
|
|
532
|
+
/** SPEC §7.7: a rejected claim is claimed again; its stored error would
|
|
533
|
+
* fail every read until the last subscriber leaves. */
|
|
534
|
+
reclaim(): void {
|
|
534
535
|
if (this.#delegate !== undefined || this.#claimError === undefined) return;
|
|
535
536
|
this.#claimError = undefined;
|
|
536
537
|
this.#claimCoverage();
|
|
@@ -542,7 +543,16 @@ class QueryEntry<Row> implements ExternalStoreEntry<LiveQueryResult<Row>> {
|
|
|
542
543
|
if (batch.revision > this.#desiredRevision) {
|
|
543
544
|
this.#desiredRevision = batch.revision;
|
|
544
545
|
}
|
|
545
|
-
if (this.#listeners.size
|
|
546
|
+
if (this.#listeners.size === 0) return;
|
|
547
|
+
// A committed row change of a dependency claims again; a window-only
|
|
548
|
+
// batch, which a failed window edit can itself produce, does not.
|
|
549
|
+
const rowChange = batch.tables.some((change) =>
|
|
550
|
+
this.spec.dependencies.some(
|
|
551
|
+
(dependency) => dependency.table === change.table,
|
|
552
|
+
),
|
|
553
|
+
);
|
|
554
|
+
if (rowChange && this.#claimError !== undefined) this.reclaim();
|
|
555
|
+
else this.#requestRead();
|
|
546
556
|
}
|
|
547
557
|
|
|
548
558
|
#publish(next: LiveQueryResult<Row>): void {
|
|
@@ -1078,6 +1088,16 @@ export class ReactiveClientStore {
|
|
|
1078
1088
|
}
|
|
1079
1089
|
}
|
|
1080
1090
|
|
|
1091
|
+
/** Apply every claimed window group again and claim again for query entries
|
|
1092
|
+
* whose claim was rejected. An applied group compares equal and makes no
|
|
1093
|
+
* call; a group whose edit is in flight applies once more after it. */
|
|
1094
|
+
#reclaimWindows(): void {
|
|
1095
|
+
for (const group of this.#windowClaims.values()) {
|
|
1096
|
+
if (group.claims.size > 0) this.#scheduleWindow(group);
|
|
1097
|
+
}
|
|
1098
|
+
this.#queries.reclaim();
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1081
1101
|
#scheduleWindow(group: WindowClaimGroup): void {
|
|
1082
1102
|
group.requested = true;
|
|
1083
1103
|
if (group.scheduled || group.running) return;
|
|
@@ -1166,7 +1186,14 @@ export class ReactiveClientStore {
|
|
|
1166
1186
|
const roundFailure = this.roundFailure as ValueEntry<
|
|
1167
1187
|
SyncRoundFailedError | undefined
|
|
1168
1188
|
>;
|
|
1189
|
+
let claimedAttempt: number | undefined;
|
|
1169
1190
|
this.#offProgress = this.client.onProgress((progress) => {
|
|
1191
|
+
// A new sync attempt is the client's retry schedule (§7.6): window
|
|
1192
|
+
// claims that failed are claimed again once per attempt.
|
|
1193
|
+
if (progress.state === 'running' && progress.attempt !== claimedAttempt) {
|
|
1194
|
+
claimedAttempt = progress.attempt;
|
|
1195
|
+
this.#reclaimWindows();
|
|
1196
|
+
}
|
|
1170
1197
|
const current = roundFailure.getSnapshot();
|
|
1171
1198
|
if (progress.state === 'failed' && current?.attempt !== progress.attempt)
|
|
1172
1199
|
roundFailure.set(
|
|
@@ -1192,12 +1219,8 @@ export class ReactiveClientStore {
|
|
|
1192
1219
|
if (previous.isLoading) this.status.refresh();
|
|
1193
1220
|
// Window edits that failed while no leader served this tab apply now;
|
|
1194
1221
|
// an applied group compares equal and makes no call.
|
|
1195
|
-
if (leadership.state === 'leader' || leadership.state === 'follower')
|
|
1196
|
-
|
|
1197
|
-
if (group.claims.size > 0) this.#scheduleWindow(group);
|
|
1198
|
-
}
|
|
1199
|
-
this.#queries.onLeaderServing();
|
|
1200
|
-
}
|
|
1222
|
+
if (leadership.state === 'leader' || leadership.state === 'follower')
|
|
1223
|
+
this.#reclaimWindows();
|
|
1201
1224
|
});
|
|
1202
1225
|
this.status.refresh();
|
|
1203
1226
|
this.conflicts.refresh();
|