@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.
- package/CHANGELOG.md +19 -0
- package/README.md +2 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/storage/hybrid.d.ts +32 -0
- package/dist/storage/hybrid.d.ts.map +1 -1
- package/dist/storage/hybrid.js +26 -4
- package/dist/storage/hybrid.js.map +1 -1
- package/dist/storage/index.d.ts +1 -1
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js.map +1 -1
- package/package.json +4 -4
- package/src/storage/hybrid.ts +59 -3
- package/src/storage/index.ts +1 -0
package/src/storage/hybrid.ts
CHANGED
|
@@ -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;
|