@procedures/store 0.48.0 → 0.48.2

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.
@@ -252,6 +252,15 @@ export interface HybridStorageOptions {
252
252
  */
253
253
  onRefused?: (refusal: QueueRefusal) => void | Promise<void>;
254
254
 
255
+ /**
256
+ * This hook gets each queued write that leaves the queue with no send that the remote took
257
+ * (roadmap CLI-106). The storage gives one call for each id, on a conflict that the policy
258
+ * `remote` decides, on a delete of the remote and on a refusal. The call comes after the local
259
+ * copy changed, so the hook can write the value again. An error of the hook does not stop the
260
+ * sync. The sync waits for the hook.
261
+ */
262
+ onDropped?: (drop: DroppedWrite) => void | Promise<void>;
263
+
255
264
  /**
256
265
  * The storage of the local copy (roadmap R45). A durable storage, for example `SqliteStorage`
257
266
  * of `@procedures/sqlite`, keeps the local copy after a restart. Then a new storage in
@@ -582,6 +591,29 @@ export interface QueueRefusal {
582
591
  restored: boolean;
583
592
  }
584
593
 
594
+ /**
595
+ * A queued write that left the queue with no send that the remote took: the argument of the hook
596
+ * `onDropped` (roadmap CLI-106). The hook can get a write again after a crash, because the store
597
+ * of the queue gets the change after the hook.
598
+ */
599
+ export interface DroppedWrite {
600
+ /** The id of the write, or `"*"` for a refused clear. */
601
+ id: string;
602
+ /**
603
+ * The value of the write when it left the queue. Undefined for a queued delete and for a clear.
604
+ * The last queued write gives the value when an id has more than one.
605
+ */
606
+ value: unknown;
607
+ /** The cause: the policy took the remote value, the remote deleted the item, or the remote refused the write. */
608
+ reason: "conflict" | "remote-delete" | "refused";
609
+ /** The sync that dropped the write. */
610
+ during: "pull" | "push";
611
+ /** The remote value that replaced the write, for the reason `conflict`. */
612
+ remote?: unknown;
613
+ /** The error of the remote, for the reason `refused`. */
614
+ error?: unknown;
615
+ }
616
+
585
617
  /**
586
618
  * The remote did not take a write of the write-through mode. The write is in the local cache and
587
619
  * in the queue, and the storage sends it later (roadmap R2). `cause` is the error of the remote.
@@ -989,6 +1021,7 @@ export class HybridStorage<T> implements CollectionStorage<T> {
989
1021
  isUnavailable: (error: unknown) => boolean;
990
1022
  isRefusal: (error: unknown) => boolean;
991
1023
  onRefused?: (refusal: QueueRefusal) => void | Promise<void>;
1024
+ onDropped?: (drop: DroppedWrite) => void | Promise<void>;
992
1025
  };
993
1026
  private mode: HybridMode;
994
1027
  private offlineOps: QueuedOperation[] = [];
@@ -1105,6 +1138,7 @@ export class HybridStorage<T> implements CollectionStorage<T> {
1105
1138
  cacheMisses: 0,
1106
1139
  syncCount: 0,
1107
1140
  conflicts: 0,
1141
+ dropped: 0,
1108
1142
  };
1109
1143
 
1110
1144
  constructor(
@@ -1132,6 +1166,7 @@ export class HybridStorage<T> implements CollectionStorage<T> {
1132
1166
  isUnavailable: options.isUnavailable ?? defaultIsUnavailable,
1133
1167
  isRefusal: options.isRefusal ?? defaultIsRefusal,
1134
1168
  ...(options.onRefused !== undefined && { onRefused: options.onRefused }),
1169
+ ...(options.onDropped !== undefined && { onDropped: options.onDropped }),
1135
1170
  };
1136
1171
  this.statusListeners = new StorageListeners<SyncStatus>(options.onListenerError);
1137
1172
  this.queryPolicy = options.queryPolicy;
@@ -2405,6 +2440,7 @@ export class HybridStorage<T> implements CollectionStorage<T> {
2405
2440
  cacheMisses: this.stats.cacheMisses,
2406
2441
  syncCount: this.stats.syncCount,
2407
2442
  conflicts: this.stats.conflicts,
2443
+ dropped: this.stats.dropped,
2408
2444
  pendingOps: this.offlineOps.length,
2409
2445
  isOnline: this.isOnline,
2410
2446
  mode: this.mode,
@@ -2760,7 +2796,7 @@ export class HybridStorage<T> implements CollectionStorage<T> {
2760
2796
  this.bases.set(id, remoteVersion);
2761
2797
  return false;
2762
2798
  }
2763
- return this.resolvePending(id, localItem, item, remoteVersion, conflicts);
2799
+ return this.resolvePending(id, localItem, item, remoteVersion, conflicts, "pull");
2764
2800
  }
2765
2801
 
2766
2802
  if (knownVersion !== undefined) {
@@ -2822,6 +2858,7 @@ export class HybridStorage<T> implements CollectionStorage<T> {
2822
2858
  if (!this.dropQueued(id)) break;
2823
2859
  await this.localDelete(id, this.syncOrigin);
2824
2860
  this.bases.set(id, null);
2861
+ await this.reportDropped({ id, value: localItem, reason: "remote-delete", during: "pull" });
2825
2862
  return true;
2826
2863
  case "error":
2827
2864
  conflicts.push(id);
@@ -2844,6 +2881,7 @@ export class HybridStorage<T> implements CollectionStorage<T> {
2844
2881
  remoteItem: T,
2845
2882
  remoteVersion: string,
2846
2883
  conflicts: string[],
2884
+ during: DroppedWrite["during"],
2847
2885
  ): Promise<boolean> {
2848
2886
  this.stats.conflicts++;
2849
2887
  switch (this.options.conflictResolution) {
@@ -2852,6 +2890,7 @@ export class HybridStorage<T> implements CollectionStorage<T> {
2852
2890
  if (!this.dropQueued(id)) break;
2853
2891
  await this.localSet(id, remoteItem, this.syncOrigin);
2854
2892
  this.bases.set(id, remoteVersion);
2893
+ await this.reportDropped({ id, value: localItem, reason: "conflict", during, remote: remoteItem });
2855
2894
  return true;
2856
2895
  case "merge": {
2857
2896
  if (localItem === undefined) break;
@@ -2873,6 +2912,16 @@ export class HybridStorage<T> implements CollectionStorage<T> {
2873
2912
  return false;
2874
2913
  }
2875
2914
 
2915
+ /** This method counts a dropped write and gives it to the hook `onDropped`. An error of the hook does not stop the sync. */
2916
+ private async reportDropped(drop: DroppedWrite): Promise<void> {
2917
+ this.stats.dropped++;
2918
+ try {
2919
+ await this.options.onDropped?.(drop);
2920
+ } catch {
2921
+ // An error of the hook does not stop the sync
2922
+ }
2923
+ }
2924
+
2876
2925
  /**
2877
2926
  * This method removes an id from the queued operations. It gives false when a queued clear touches it: nothing
2878
2927
  * changes then.
@@ -3014,6 +3063,10 @@ export class HybridStorage<T> implements CollectionStorage<T> {
3014
3063
  } catch {
3015
3064
  // An error of the hook does not stop the queue
3016
3065
  }
3066
+ for (const id of all ? ["*"] : ids) {
3067
+ const written = this.writtenValue(op, id);
3068
+ await this.reportDropped({ id, value: written ? written.value : undefined, reason: "refused", during: "push", error });
3069
+ }
3017
3070
  }
3018
3071
 
3019
3072
  /**
@@ -3449,7 +3502,7 @@ export class HybridStorage<T> implements CollectionStorage<T> {
3449
3502
  continue;
3450
3503
  }
3451
3504
  const localItem = await this.readLocal(id);
3452
- if (await this.resolvePending(id, localItem, remoteItem, remoteVersion, ids)) queueChanged = true;
3505
+ if (await this.resolvePending(id, localItem, remoteItem, remoteVersion, ids, "push")) queueChanged = true;
3453
3506
  continue;
3454
3507
  }
3455
3508
  // The remote has no item: the echo of a delete, or a delete of another writer
@@ -3478,11 +3531,14 @@ export class HybridStorage<T> implements CollectionStorage<T> {
3478
3531
  private async resolveRemoteDelete(id: string, conflicts: string[]): Promise<boolean> {
3479
3532
  this.stats.conflicts++;
3480
3533
  switch (this.options.conflictResolution) {
3481
- case "remote":
3534
+ case "remote": {
3535
+ const value = await this.readLocal(id);
3482
3536
  if (!this.dropQueued(id)) break;
3483
3537
  await this.localDelete(id, this.syncOrigin);
3484
3538
  this.bases.set(id, null);
3539
+ await this.reportDropped({ id, value, reason: "remote-delete", during: "push" });
3485
3540
  return true;
3541
+ }
3486
3542
  case "error":
3487
3543
  conflicts.push(id);
3488
3544
  return false;
@@ -50,6 +50,7 @@ export {
50
50
  RemoteApplyHookError,
51
51
  } from "./hybrid.js";
52
52
  export type {
53
+ DroppedWrite,
53
54
  HybridStorageOptions,
54
55
  HybridMode,
55
56
  LocalCopyStorage,