@gonvex/client 0.5.2-staging.20 → 0.5.2-staging.21
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 +4 -2
- package/dist/index.js +55 -26
- package/dist/index.js.map +1 -1
- package/dist/indexeddb-replica.js +4 -11
- package/dist/indexeddb-replica.js.map +1 -1
- package/dist/local-replica.d.ts +2 -0
- package/dist/local-replica.js +5 -0
- package/dist/local-replica.js.map +1 -1
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
|
@@ -421,8 +421,9 @@ export declare class GonvexClient {
|
|
|
421
421
|
private hasAuthoritativeReplicaScope;
|
|
422
422
|
private replicaReady;
|
|
423
423
|
private replicaFrames;
|
|
424
|
+
private queuedReplicaWatermark?;
|
|
424
425
|
private processedReplicaWatermarkRevision;
|
|
425
|
-
private readonly
|
|
426
|
+
private readonly pendingReplicaFrames;
|
|
426
427
|
private readonly unsubscribeOutbox;
|
|
427
428
|
private readonly sharedOutboxStore?;
|
|
428
429
|
private readonly unsubscribePeerOutbox?;
|
|
@@ -639,8 +640,9 @@ export declare class GonvexClient {
|
|
|
639
640
|
};
|
|
640
641
|
private handleReplicaMessage;
|
|
641
642
|
private enqueueReplicaFrame;
|
|
643
|
+
private enqueueReplicaWatermark;
|
|
642
644
|
private enqueueReplicaTransaction;
|
|
643
|
-
private
|
|
645
|
+
private drainPendingReplicaFrames;
|
|
644
646
|
private acceptReplicaReady;
|
|
645
647
|
private handleReplicaWatermark;
|
|
646
648
|
private emitReplicaMessage;
|
package/dist/index.js
CHANGED
|
@@ -213,8 +213,9 @@ export class GonvexClient {
|
|
|
213
213
|
hasAuthoritativeReplicaScope = false;
|
|
214
214
|
replicaReady = Promise.resolve();
|
|
215
215
|
replicaFrames = Promise.resolve();
|
|
216
|
+
queuedReplicaWatermark;
|
|
216
217
|
processedReplicaWatermarkRevision = 0;
|
|
217
|
-
|
|
218
|
+
pendingReplicaFrames = [];
|
|
218
219
|
unsubscribeOutbox;
|
|
219
220
|
sharedOutboxStore;
|
|
220
221
|
unsubscribePeerOutbox;
|
|
@@ -507,7 +508,7 @@ export class GonvexClient {
|
|
|
507
508
|
if (subscription.socketGeneration !== undefined)
|
|
508
509
|
this.send({ type: "query.unsubscribe", id: subscription.id });
|
|
509
510
|
}
|
|
510
|
-
this.
|
|
511
|
+
this.pendingReplicaFrames.length = 0;
|
|
511
512
|
await this.replica.clear(replicaScope);
|
|
512
513
|
this.resetReplicaScopeState();
|
|
513
514
|
this.rotateSubscriptionScopes();
|
|
@@ -951,17 +952,18 @@ export class GonvexClient {
|
|
|
951
952
|
// A fully resident prediction reads the already published atomic replica.
|
|
952
953
|
// Unrelated snapshots may still be persisting; waiting for those here
|
|
953
954
|
// makes a small edit inherit their disk latency. The shared intent lock
|
|
954
|
-
// and journal still order every edit
|
|
955
|
-
//
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
955
|
+
// and journal still order every edit. Preparation proves resident
|
|
956
|
+
// coverage, even when a published commit invalidates its result while
|
|
957
|
+
// acquiring the lock. Re-execute against the latest atomic resident
|
|
958
|
+
// snapshot below; only known peer changes or committed shared intents
|
|
959
|
+
// require synchronizing storage first.
|
|
960
|
+
const canExecuteResident = preparedResult
|
|
959
961
|
&& !this.peerRefreshDirty && !this.peerRefreshScheduled
|
|
960
962
|
&& !sharedEntries.some(entry => entry.state === "committed");
|
|
961
963
|
const uncoveredRead = this.isUncoveredLocalRead(preparedFailure)
|
|
962
964
|
&& !this.peerRefreshDirty && !this.peerRefreshScheduled
|
|
963
965
|
&& !sharedEntries.some(entry => entry.state === "committed");
|
|
964
|
-
if (!
|
|
966
|
+
if (!canExecuteResident && !uncoveredRead)
|
|
965
967
|
await this.replica.synchronizeStorage();
|
|
966
968
|
if (sharedEntries)
|
|
967
969
|
this.replaceLocalPredictions(sharedEntries.filter(entry => entry.state !== "committed").map(entry => ({ commandId: entry.idempotencyKey, patches: entry.patches ?? [] })));
|
|
@@ -1246,7 +1248,7 @@ export class GonvexClient {
|
|
|
1246
1248
|
|| (hasOwn(auth, "identity") && !sameAuthTokenIdentity(this.auth, nextAuth));
|
|
1247
1249
|
if (scopeMayChange) {
|
|
1248
1250
|
this.pendingMessages.length = 0;
|
|
1249
|
-
this.
|
|
1251
|
+
this.pendingReplicaFrames.length = 0;
|
|
1250
1252
|
this.rejectPendingCalls((call) => new GonvexClientError(`Authentication scope changed while waiting for ${call.kind} ${call.path}`, { code: "superseded", path: call.path, operation: call.kind }));
|
|
1251
1253
|
for (const query of this.oneShotQueries.values()) {
|
|
1252
1254
|
if (query.timeoutTimer)
|
|
@@ -1327,7 +1329,7 @@ export class GonvexClient {
|
|
|
1327
1329
|
// drop them too — flushing them after reconnect would fire writes whose
|
|
1328
1330
|
// callers already saw a rejection.
|
|
1329
1331
|
this.pendingMessages.length = 0;
|
|
1330
|
-
this.
|
|
1332
|
+
this.pendingReplicaFrames.length = 0;
|
|
1331
1333
|
// Reducers/actions must fail closed on transport loss: silently
|
|
1332
1334
|
// replaying a non-idempotent write after reconnect is unsafe, and
|
|
1333
1335
|
// leaving the promise pending hangs the caller forever.
|
|
@@ -1390,7 +1392,7 @@ export class GonvexClient {
|
|
|
1390
1392
|
// subscription. Rotate their routing IDs immediately so delayed
|
|
1391
1393
|
// Replica errors from that discarded scope cannot reach current
|
|
1392
1394
|
// React listeners.
|
|
1393
|
-
this.
|
|
1395
|
+
this.pendingReplicaFrames.length = 0;
|
|
1394
1396
|
this.quarantineReplicaScope();
|
|
1395
1397
|
this.activeAuthFrameId = undefined;
|
|
1396
1398
|
// If credentials are already being revalidated, that queued auth
|
|
@@ -1504,7 +1506,7 @@ export class GonvexClient {
|
|
|
1504
1506
|
// Replica partition has been activated locally. Only now may
|
|
1505
1507
|
// tenant calls and subscriptions leave the client.
|
|
1506
1508
|
this.authInFlight = false;
|
|
1507
|
-
this.
|
|
1509
|
+
this.drainPendingReplicaFrames();
|
|
1508
1510
|
// Authentication is replaced in-place on the same socket during
|
|
1509
1511
|
// normal token rotation. The runtime clears its subscription
|
|
1510
1512
|
// maps for every accepted auth frame, so same-generation Live
|
|
@@ -1519,7 +1521,7 @@ export class GonvexClient {
|
|
|
1519
1521
|
})
|
|
1520
1522
|
.catch((error) => {
|
|
1521
1523
|
this.authInFlight = false;
|
|
1522
|
-
this.
|
|
1524
|
+
this.pendingReplicaFrames.length = 0;
|
|
1523
1525
|
this.settleManagedAuthAttempt(message.id, error instanceof Error ? error.message : "Runtime returned an invalid Local Replica scope");
|
|
1524
1526
|
this.rejectReplicaDirective(error);
|
|
1525
1527
|
this.flushPendingMessages();
|
|
@@ -1528,7 +1530,7 @@ export class GonvexClient {
|
|
|
1528
1530
|
}
|
|
1529
1531
|
else {
|
|
1530
1532
|
this.authInFlight = false;
|
|
1531
|
-
this.
|
|
1533
|
+
this.pendingReplicaFrames.length = 0;
|
|
1532
1534
|
const fetcher = this.auth.fetchToken;
|
|
1533
1535
|
if (fetcher && !this.authRetriedAfterError) {
|
|
1534
1536
|
// The installed token was rejected — typically expired while the
|
|
@@ -1558,7 +1560,7 @@ export class GonvexClient {
|
|
|
1558
1560
|
// Replica frames carry no tenant/scope field. During auth renewal we
|
|
1559
1561
|
// cannot safely attribute a late frame to either side of the switch.
|
|
1560
1562
|
if (this.authInFlight) {
|
|
1561
|
-
this.
|
|
1563
|
+
this.pendingReplicaFrames.push(message);
|
|
1562
1564
|
return;
|
|
1563
1565
|
}
|
|
1564
1566
|
this.enqueueReplicaTransaction(message);
|
|
@@ -1570,7 +1572,11 @@ export class GonvexClient {
|
|
|
1570
1572
|
// stream. Keep both on the same client queue so a watermark cannot
|
|
1571
1573
|
// advance the cursor past a transaction that was received first but
|
|
1572
1574
|
// has not finished applying to durable Local Replica storage yet.
|
|
1573
|
-
this.
|
|
1575
|
+
if (this.authInFlight) {
|
|
1576
|
+
this.pendingReplicaFrames.push(message);
|
|
1577
|
+
return;
|
|
1578
|
+
}
|
|
1579
|
+
this.enqueueReplicaWatermark(message.revision);
|
|
1574
1580
|
}
|
|
1575
1581
|
return;
|
|
1576
1582
|
}
|
|
@@ -2460,12 +2466,34 @@ export class GonvexClient {
|
|
|
2460
2466
|
}
|
|
2461
2467
|
this.emitReplicaMessage(subscription, message, scope);
|
|
2462
2468
|
}
|
|
2463
|
-
enqueueReplicaFrame(operation) {
|
|
2469
|
+
enqueueReplicaFrame(operation, watermark = false) {
|
|
2470
|
+
// Any entity/window frame seals the adjacent watermark batch. Advancing
|
|
2471
|
+
// across that barrier could make an unapplied transaction look stale.
|
|
2472
|
+
if (!watermark)
|
|
2473
|
+
this.queuedReplicaWatermark = undefined;
|
|
2464
2474
|
const processing = this.replicaFrames.then(operation);
|
|
2465
2475
|
this.replicaFrames = processing.catch(() => {
|
|
2466
2476
|
this.replica.setFreshness("verifying");
|
|
2467
2477
|
});
|
|
2468
2478
|
}
|
|
2479
|
+
enqueueReplicaWatermark(revision) {
|
|
2480
|
+
if (!Number.isSafeInteger(revision) || revision < 0)
|
|
2481
|
+
return;
|
|
2482
|
+
const pending = this.queuedReplicaWatermark;
|
|
2483
|
+
if (pending && pending.scope === this.replicaScope) {
|
|
2484
|
+
pending.revision = Math.max(pending.revision, revision);
|
|
2485
|
+
return;
|
|
2486
|
+
}
|
|
2487
|
+
const batch = { revision, scope: this.replicaScope };
|
|
2488
|
+
this.queuedReplicaWatermark = batch;
|
|
2489
|
+
this.enqueueReplicaFrame(async () => {
|
|
2490
|
+
if (this.queuedReplicaWatermark === batch)
|
|
2491
|
+
this.queuedReplicaWatermark = undefined;
|
|
2492
|
+
if (batch.scope !== this.replicaScope)
|
|
2493
|
+
return;
|
|
2494
|
+
await this.handleReplicaWatermark(batch.revision);
|
|
2495
|
+
}, true);
|
|
2496
|
+
}
|
|
2469
2497
|
enqueueReplicaTransaction(message) {
|
|
2470
2498
|
const scope = this.replicaScope;
|
|
2471
2499
|
this.enqueueReplicaFrame(async () => {
|
|
@@ -2484,10 +2512,14 @@ export class GonvexClient {
|
|
|
2484
2512
|
}
|
|
2485
2513
|
});
|
|
2486
2514
|
}
|
|
2487
|
-
|
|
2488
|
-
const pending = this.
|
|
2489
|
-
for (const message of pending)
|
|
2490
|
-
|
|
2515
|
+
drainPendingReplicaFrames() {
|
|
2516
|
+
const pending = this.pendingReplicaFrames.splice(0);
|
|
2517
|
+
for (const message of pending) {
|
|
2518
|
+
if (message.type === "replica.transaction")
|
|
2519
|
+
this.enqueueReplicaTransaction(message);
|
|
2520
|
+
else
|
|
2521
|
+
this.enqueueReplicaWatermark(message.revision);
|
|
2522
|
+
}
|
|
2491
2523
|
}
|
|
2492
2524
|
async acceptReplicaReady(subscription, message, hashes, hashesWereStored, scope = this.replicaScope) {
|
|
2493
2525
|
this.clearReplicaRetry(subscription, true);
|
|
@@ -2532,12 +2564,9 @@ export class GonvexClient {
|
|
|
2532
2564
|
return;
|
|
2533
2565
|
const eligibleSignatures = [];
|
|
2534
2566
|
for (const subscription of this.replicaSubscriptions.values()) {
|
|
2535
|
-
|
|
2536
|
-
if (!cursor
|
|
2537
|
-
|| cursor.revision >= revision
|
|
2538
|
-
|| !subscription.isUpToDate
|
|
2567
|
+
if (!subscription.isUpToDate
|
|
2539
2568
|
|| subscription.opening
|
|
2540
|
-
|| !this.replica.
|
|
2569
|
+
|| !this.replica.windowCanAdvance(subscription.key, revision))
|
|
2541
2570
|
continue;
|
|
2542
2571
|
eligibleSignatures.push(subscription.key);
|
|
2543
2572
|
}
|