kaafil-js 0.6.2 → 0.8.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.
@@ -1017,6 +1017,31 @@ interface CreateDrainerOptions {
1017
1017
  /** Fed every observed `meta.serverTime` so the share layer can age a token
1018
1018
  * without a device clock (`06 §8`). */
1019
1019
  readonly onServerTime?: (serverTime: string) => void;
1020
+ /**
1021
+ * Called with the lanes a pass actually APPLIED something on, so the engine
1022
+ * can pull them straight afterwards.
1023
+ *
1024
+ * ── WHY A DRAIN HAS TO BE FOLLOWED BY A PULL ──────────────────────────────
1025
+ *
1026
+ * A write leaves the snapshot alone unless it was enqueued with an
1027
+ * `entity`/`optimistic` pairing, and several deliberately are not: a write
1028
+ * whose response carries server-assigned fields no client can compute
1029
+ * (`floatMovementId`, a resolved `receiptRequired` threshold, whether a
1030
+ * payment cleared a negative-float guard) is better left unguessed than
1031
+ * painted wrong and silently corrected later. Those writes reach the server
1032
+ * correctly and the snapshot simply does not know yet.
1033
+ *
1034
+ * Until now nothing closed that gap: `sync.drained` was emitted and the pass
1035
+ * ended. A host reading a snapshot-backed list saw its own successful write
1036
+ * missing from it — indefinitely, since nothing else schedules a pull — and
1037
+ * the fix was to reopen the screen. Observed in the field: an expense logged
1038
+ * against a trip's float moved the balance and stayed absent from the list
1039
+ * beside it.
1040
+ *
1041
+ * A callback rather than a direct dependency on `./pull.ts`, so this module
1042
+ * keeps knowing nothing about pulling and stays testable without one.
1043
+ */
1044
+ readonly onLanesApplied?: (lanes: readonly string[]) => void;
1020
1045
  }
1021
1046
  declare function createDrainer(options: CreateDrainerOptions): Drainer;
1022
1047
 
@@ -1017,6 +1017,31 @@ interface CreateDrainerOptions {
1017
1017
  /** Fed every observed `meta.serverTime` so the share layer can age a token
1018
1018
  * without a device clock (`06 §8`). */
1019
1019
  readonly onServerTime?: (serverTime: string) => void;
1020
+ /**
1021
+ * Called with the lanes a pass actually APPLIED something on, so the engine
1022
+ * can pull them straight afterwards.
1023
+ *
1024
+ * ── WHY A DRAIN HAS TO BE FOLLOWED BY A PULL ──────────────────────────────
1025
+ *
1026
+ * A write leaves the snapshot alone unless it was enqueued with an
1027
+ * `entity`/`optimistic` pairing, and several deliberately are not: a write
1028
+ * whose response carries server-assigned fields no client can compute
1029
+ * (`floatMovementId`, a resolved `receiptRequired` threshold, whether a
1030
+ * payment cleared a negative-float guard) is better left unguessed than
1031
+ * painted wrong and silently corrected later. Those writes reach the server
1032
+ * correctly and the snapshot simply does not know yet.
1033
+ *
1034
+ * Until now nothing closed that gap: `sync.drained` was emitted and the pass
1035
+ * ended. A host reading a snapshot-backed list saw its own successful write
1036
+ * missing from it — indefinitely, since nothing else schedules a pull — and
1037
+ * the fix was to reopen the screen. Observed in the field: an expense logged
1038
+ * against a trip's float moved the balance and stayed absent from the list
1039
+ * beside it.
1040
+ *
1041
+ * A callback rather than a direct dependency on `./pull.ts`, so this module
1042
+ * keeps knowing nothing about pulling and stays testable without one.
1043
+ */
1044
+ readonly onLanesApplied?: (lanes: readonly string[]) => void;
1020
1045
  }
1021
1046
  declare function createDrainer(options: CreateDrainerOptions): Drainer;
1022
1047
 
@@ -1979,11 +1979,35 @@ async function runWithLadder(ctx, request, send) {
1979
1979
  throw error;
1980
1980
  }
1981
1981
  }
1982
+ var inFlightReads = /* @__PURE__ */ new WeakMap();
1983
+ function readKey(request) {
1984
+ return [
1985
+ request.operationId,
1986
+ request.path,
1987
+ JSON.stringify(request.pathParams ?? {}),
1988
+ JSON.stringify(request.query ?? {}),
1989
+ JSON.stringify(request.headers ?? {})
1990
+ ].join("|");
1991
+ }
1982
1992
  function execute(ctx, request) {
1983
- return runWithLadder(ctx, request, async (idempotencyKey) => {
1993
+ const run = () => runWithLadder(ctx, request, async (idempotencyKey) => {
1984
1994
  const { data, meta, facets } = await sendOnce(ctx, request, idempotencyKey);
1985
1995
  return attachMeta(data, meta, facets);
1986
1996
  });
1997
+ if (request.method !== "GET" || request.signal !== void 0) return run();
1998
+ let perClient = inFlightReads.get(ctx);
1999
+ if (perClient === void 0) {
2000
+ perClient = /* @__PURE__ */ new Map();
2001
+ inFlightReads.set(ctx, perClient);
2002
+ }
2003
+ const key = readKey(request);
2004
+ const existing = perClient.get(key);
2005
+ if (existing !== void 0) return existing;
2006
+ const started = run().finally(() => {
2007
+ perClient.delete(key);
2008
+ });
2009
+ perClient.set(key, started);
2010
+ return started;
1987
2011
  }
1988
2012
  function executeBinary(ctx, request) {
1989
2013
  return runWithLadder(
@@ -2851,6 +2875,9 @@ function createDrainer(options) {
2851
2875
  remaining: report.remaining,
2852
2876
  usedBatchTransport: report.usedBatchTransport
2853
2877
  });
2878
+ if (report.applied > 0) {
2879
+ options.onLanesApplied?.(report.lanes);
2880
+ }
2854
2881
  return report;
2855
2882
  }
2856
2883
  let kickTimer;
@@ -3144,15 +3171,15 @@ function createReadCacheStore(options) {
3144
3171
  await options.storage.set(key, JSON.stringify(shape));
3145
3172
  }
3146
3173
  return {
3147
- get(readKey) {
3148
- return reads.get(readKey);
3174
+ get(readKey2) {
3175
+ return reads.get(readKey2);
3149
3176
  },
3150
- async set(readKey, value, serverTime) {
3151
- reads.set(readKey, { value, syncedAt: serverTime });
3177
+ async set(readKey2, value, serverTime) {
3178
+ reads.set(readKey2, { value, syncedAt: serverTime });
3152
3179
  await persist();
3153
3180
  },
3154
- async purge(readKey) {
3155
- if (!reads.delete(readKey)) {
3181
+ async purge(readKey2) {
3182
+ if (!reads.delete(readKey2)) {
3156
3183
  return;
3157
3184
  }
3158
3185
  await persist();
@@ -3169,9 +3196,9 @@ function createReadCacheStore(options) {
3169
3196
  } catch {
3170
3197
  return;
3171
3198
  }
3172
- for (const [readKey, entry] of Object.entries(parsed.reads ?? {})) {
3199
+ for (const [readKey2, entry] of Object.entries(parsed.reads ?? {})) {
3173
3200
  if (entry !== null && typeof entry === "object" && typeof entry.syncedAt === "string") {
3174
- reads.set(readKey, entry);
3201
+ reads.set(readKey2, entry);
3175
3202
  }
3176
3203
  }
3177
3204
  }
@@ -3574,22 +3601,45 @@ function createOfflineEngine(options) {
3574
3601
  ...options.share.monotonic !== void 0 ? { monotonic: options.share.monotonic } : {}
3575
3602
  });
3576
3603
  }
3604
+ const pull = createPullCoordinator({
3605
+ snapshot,
3606
+ transport: options.transport,
3607
+ events,
3608
+ onServerTime
3609
+ });
3577
3610
  const drainer = createDrainer({
3578
3611
  outbox,
3579
3612
  snapshot,
3580
3613
  transport: options.transport,
3581
3614
  events,
3582
3615
  onServerTime,
3616
+ /*
3617
+ * A DRAIN IS FOLLOWED BY A PULL, for the lanes it actually changed.
3618
+ *
3619
+ * Not every write rebases the snapshot: one enqueued without an
3620
+ * `entity`/`optimistic` pairing — deliberately, wherever the response
3621
+ * carries server-assigned fields a client cannot compute — reaches the
3622
+ * server and leaves the local snapshot untouched. Nothing used to close
3623
+ * that gap, so a host reading a snapshot-backed list saw its own
3624
+ * successful write missing from it until something reopened the screen.
3625
+ *
3626
+ * Failures are swallowed on purpose. This is a background reconcile behind
3627
+ * a write that has already succeeded; a pull that cannot reach the server
3628
+ * changes nothing the caller needs to hear about, and `pullTrip` has
3629
+ * already emitted whatever the host subscribes to. Rethrowing here would
3630
+ * surface a sync failure as if the WRITE had failed, which is the one
3631
+ * reading of it that is untrue.
3632
+ */
3633
+ onLanesApplied: (lanes) => {
3634
+ for (const tripRef of lanes) {
3635
+ void pull.pullTrip({ tripRef }).catch(() => {
3636
+ });
3637
+ }
3638
+ },
3583
3639
  ...options.conflictResolver !== void 0 ? { conflictResolver: options.conflictResolver } : {},
3584
3640
  ...options.now !== void 0 ? { now: options.now } : {},
3585
3641
  ...options.random !== void 0 ? { random: options.random } : {}
3586
3642
  });
3587
- const pull = createPullCoordinator({
3588
- snapshot,
3589
- transport: options.transport,
3590
- events,
3591
- onServerTime
3592
- });
3593
3643
  const blobs = options.blob === void 0 ? void 0 : createBlobLane({
3594
3644
  storage: options.storage,
3595
3645
  keyPrefix,