kaafil-js 0.1.0 → 0.3.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.
@@ -2585,6 +2585,11 @@ function createDrainer(options) {
2585
2585
  row: outcome.entity,
2586
2586
  ...outcome.serverTime !== void 0 ? { syncedAt: outcome.serverTime } : {}
2587
2587
  });
2588
+ const temporaryId = entity.temporaryId;
2589
+ if (temporaryId !== void 0 && temporaryId !== outcome.entity.id) {
2590
+ await snapshot.purgeEntity(op.tripRef, entity.listName, temporaryId);
2591
+ changed = true;
2592
+ }
2588
2593
  }
2589
2594
  if (changed && outcome.serverTime !== void 0) {
2590
2595
  events.emit("snapshot.updated", {
@@ -2684,6 +2689,16 @@ function createDrainer(options) {
2684
2689
  switch (result.action) {
2685
2690
  case "drop":
2686
2691
  await outbox.drop(op.id);
2692
+ if (op.entity?.temporaryId !== void 0) {
2693
+ await snapshot.purgeEntity(op.tripRef, op.entity.listName, op.entity.temporaryId);
2694
+ if (outcome.serverTime !== void 0) {
2695
+ events.emit("snapshot.updated", {
2696
+ tripRef: op.tripRef,
2697
+ lists: [op.entity.listName],
2698
+ serverTime: outcome.serverTime
2699
+ });
2700
+ }
2701
+ }
2687
2702
  return "terminal";
2688
2703
  case "reissue":
2689
2704
  await outbox.reissue(op.id, {
@@ -2964,7 +2979,35 @@ var SYNC_PULL_SECTIONS = [
2964
2979
  "itinerary",
2965
2980
  "closeout"
2966
2981
  ];
2967
- function extractSectionRows(data) {
2982
+ var SECTION_ROW_KEY = {
2983
+ // Delta-shaped `rooms[]`; `windows[]`/`unassigned[]` are derived
2984
+ // projections — the long note above is still exactly right about this one.
2985
+ rooming: "rooms",
2986
+ // The same decision in the same shape: `vehicles[]` is the feed,
2987
+ // `unassignedPool[]` the projection that must never be cached as rows.
2988
+ seating: "vehicles",
2989
+ expenses: "items",
2990
+ float: "data",
2991
+ // The one that was silently dead. `sections[]` is a grouping projection and
2992
+ // `availableTemplates[]` is agency-level catalogue; `items[]` is the trip's
2993
+ // own entity list and the only one of the three carrying tombstones.
2994
+ checklist: "items",
2995
+ // `days[]` is the day-by-day projection the engine recomputes per read;
2996
+ // `items[]` is the entity list.
2997
+ itinerary: "items",
2998
+ closeout: null
2999
+ // `stayWindows`, `pickups`, `bookings` and `collections` send a top-level
3000
+ // array and never reach this table.
3001
+ };
3002
+ function extractSectionRows(data, section) {
3003
+ if (section !== void 0 && section in SECTION_ROW_KEY) {
3004
+ const key = SECTION_ROW_KEY[section];
3005
+ if (key === null || key === void 0 || typeof data !== "object" || data === null) {
3006
+ return void 0;
3007
+ }
3008
+ const value = data[key];
3009
+ return Array.isArray(value) && isRowArray(value) ? value : void 0;
3010
+ }
2968
3011
  if (Array.isArray(data)) {
2969
3012
  return isRowArray(data) ? data : void 0;
2970
3013
  }
@@ -3034,7 +3077,7 @@ function createPullCoordinator(options) {
3034
3077
  absent.push(name);
3035
3078
  continue;
3036
3079
  }
3037
- const rows = extractSectionRows(section.data);
3080
+ const rows = extractSectionRows(section.data, name);
3038
3081
  if (rows === void 0) {
3039
3082
  unmapped.push(name);
3040
3083
  continue;
@@ -3071,6 +3114,49 @@ function createPullCoordinator(options) {
3071
3114
  };
3072
3115
  }
3073
3116
 
3117
+ // src/offline/reads.ts
3118
+ function createReadCacheStore(options) {
3119
+ const key = `${options.keyPrefix}:reads`;
3120
+ let reads = /* @__PURE__ */ new Map();
3121
+ async function persist() {
3122
+ const shape = { reads: Object.fromEntries(reads) };
3123
+ await options.storage.set(key, JSON.stringify(shape));
3124
+ }
3125
+ return {
3126
+ get(readKey) {
3127
+ return reads.get(readKey);
3128
+ },
3129
+ async set(readKey, value, serverTime) {
3130
+ reads.set(readKey, { value, syncedAt: serverTime });
3131
+ await persist();
3132
+ },
3133
+ async purge(readKey) {
3134
+ if (!reads.delete(readKey)) {
3135
+ return;
3136
+ }
3137
+ await persist();
3138
+ },
3139
+ async load() {
3140
+ reads = /* @__PURE__ */ new Map();
3141
+ const raw = await options.storage.get(key);
3142
+ if (raw === void 0) {
3143
+ return;
3144
+ }
3145
+ let parsed;
3146
+ try {
3147
+ parsed = JSON.parse(raw);
3148
+ } catch {
3149
+ return;
3150
+ }
3151
+ for (const [readKey, entry] of Object.entries(parsed.reads ?? {})) {
3152
+ if (entry !== null && typeof entry === "object" && typeof entry.syncedAt === "string") {
3153
+ reads.set(readKey, entry);
3154
+ }
3155
+ }
3156
+ }
3157
+ };
3158
+ }
3159
+
3074
3160
  // src/offline/share.ts
3075
3161
  var SHARE_WRITE_GRACE_MS = 7 * 24 * 60 * 60 * 1e3;
3076
3162
  var KaafilShareLinkExpiredError = class _KaafilShareLinkExpiredError extends Error {
@@ -3445,6 +3531,8 @@ function createOfflineEngine(options) {
3445
3531
  ...options.onEventHandlerError !== void 0 ? { onHandlerError: (error) => options.onEventHandlerError?.(error) } : {}
3446
3532
  });
3447
3533
  const snapshot = createSnapshotStore({ storage: options.storage, keyPrefix });
3534
+ const reads = createReadCacheStore({ storage: options.storage, keyPrefix });
3535
+ const mintEntityId = () => `tmp_${options.newId?.() ?? newIdempotencyKey()}`;
3448
3536
  const outbox = createOutbox({
3449
3537
  storage: options.storage,
3450
3538
  keyPrefix,
@@ -3493,6 +3581,13 @@ function createOfflineEngine(options) {
3493
3581
  let unbind;
3494
3582
  async function enqueue(input) {
3495
3583
  share?.assertWritable();
3584
+ let entity = input.entity;
3585
+ let temporaryId;
3586
+ if (input.creates === true && input.entity !== void 0) {
3587
+ const { temporaryId: _ignored, ...ref } = input.entity;
3588
+ temporaryId = mintEntityId();
3589
+ entity = { ...ref, id: temporaryId, temporaryId };
3590
+ }
3496
3591
  const op = await outbox.enqueue({
3497
3592
  tripRef: input.tripRef,
3498
3593
  method: input.method,
@@ -3500,16 +3595,25 @@ function createOfflineEngine(options) {
3500
3595
  operationId: input.operationId,
3501
3596
  ...input.body !== void 0 ? { body: input.body } : {},
3502
3597
  ...input.ifMatch !== void 0 ? { ifMatch: input.ifMatch } : {},
3503
- ...input.entity !== void 0 ? { entity: input.entity } : {},
3598
+ ...entity !== void 0 ? { entity } : {},
3504
3599
  ...input.idempotencyKey !== void 0 ? { idempotencyKey: input.idempotencyKey } : {},
3505
3600
  ...input.enqueuedAt !== void 0 ? { enqueuedAt: input.enqueuedAt } : {}
3506
3601
  });
3507
- if (input.optimistic !== void 0 && input.entity !== void 0) {
3602
+ if (input.optimistic !== void 0 && entity !== void 0) {
3508
3603
  await snapshot.upsert({
3509
3604
  tripRef: input.tripRef,
3510
- listName: input.entity.listName,
3511
- row: input.optimistic
3605
+ listName: entity.listName,
3606
+ // A create's row is keyed on the minted id rather than on whatever
3607
+ // `id` the caller happened to put in the optimistic row — those two
3608
+ // disagreeing is what would leave an orphan the drainer cannot find.
3609
+ row: temporaryId === void 0 ? input.optimistic : { ...input.optimistic, id: temporaryId }
3512
3610
  });
3611
+ if (temporaryId !== void 0) {
3612
+ events.emit("snapshot.updated", {
3613
+ tripRef: input.tripRef,
3614
+ lists: [entity.listName]
3615
+ });
3616
+ }
3513
3617
  }
3514
3618
  drainer.kick();
3515
3619
  return op;
@@ -3517,17 +3621,39 @@ function createOfflineEngine(options) {
3517
3621
  return {
3518
3622
  events,
3519
3623
  snapshot,
3624
+ reads,
3520
3625
  outbox,
3521
3626
  drainer,
3522
3627
  pull,
3523
3628
  blobs,
3524
3629
  share,
3525
3630
  async open() {
3526
- await Promise.all([snapshot.load(), outbox.load(), blobs?.load() ?? Promise.resolve()]);
3631
+ await Promise.all([
3632
+ snapshot.load(),
3633
+ reads.load(),
3634
+ outbox.load(),
3635
+ blobs?.load() ?? Promise.resolve()
3636
+ ]);
3527
3637
  unbind = drainer.bindOpportunisticTriggers();
3528
3638
  drainer.kick();
3529
3639
  },
3530
3640
  enqueue,
3641
+ async discard(opId) {
3642
+ const op = outbox.get(opId);
3643
+ if (op === void 0) {
3644
+ return;
3645
+ }
3646
+ await outbox.drop(opId);
3647
+ const temporaryId = op.entity?.temporaryId;
3648
+ if (op.entity === void 0 || temporaryId === void 0) {
3649
+ return;
3650
+ }
3651
+ await snapshot.purgeEntity(op.tripRef, op.entity.listName, temporaryId);
3652
+ events.emit("snapshot.updated", {
3653
+ tripRef: op.tripRef,
3654
+ lists: [op.entity.listName]
3655
+ });
3656
+ },
3531
3657
  async enqueueWithBlob(input) {
3532
3658
  const { blob, ...rest } = input;
3533
3659
  share?.assertWritable();
@@ -7511,6 +7637,7 @@ exports.createOfflineEngine = createOfflineEngine;
7511
7637
  exports.createOfflineEvents = createOfflineEvents;
7512
7638
  exports.createOutbox = createOutbox;
7513
7639
  exports.createPullCoordinator = createPullCoordinator;
7640
+ exports.createReadCacheStore = createReadCacheStore;
7514
7641
  exports.createShareGuard = createShareGuard;
7515
7642
  exports.createSnapshotStore = createSnapshotStore;
7516
7643
  exports.createSyncResource = createSyncResource;