@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 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 pendingReplicaTransactions;
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 drainPendingReplicaTransactions;
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
- pendingReplicaTransactions = [];
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.pendingReplicaTransactions.length = 0;
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, and known peer changes must be
955
- // reconciled before a resident prediction can be reused.
956
- const reuseResident = preparedResult
957
- && preparedResult.base === this.replica.executionVersion()
958
- && preparedResult.pending === JSON.stringify(sharedPatches)
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 (!reuseResident && !uncoveredRead)
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.pendingReplicaTransactions.length = 0;
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.pendingReplicaTransactions.length = 0;
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.pendingReplicaTransactions.length = 0;
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.drainPendingReplicaTransactions();
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.pendingReplicaTransactions.length = 0;
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.pendingReplicaTransactions.length = 0;
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.pendingReplicaTransactions.push(message);
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.enqueueReplicaFrame(() => this.handleReplicaWatermark(message.revision));
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
- drainPendingReplicaTransactions() {
2488
- const pending = this.pendingReplicaTransactions.splice(0);
2489
- for (const message of pending)
2490
- this.enqueueReplicaTransaction(message);
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
- const cursor = this.replica.getWindow(subscription.key)?.cursor;
2536
- if (!cursor
2537
- || cursor.revision >= revision
2538
- || !subscription.isUpToDate
2567
+ if (!subscription.isUpToDate
2539
2568
  || subscription.opening
2540
- || !this.replica.getWindow(subscription.key)?.hashes)
2569
+ || !this.replica.windowCanAdvance(subscription.key, revision))
2541
2570
  continue;
2542
2571
  eligibleSignatures.push(subscription.key);
2543
2572
  }