@procedures/store 0.51.0 → 0.52.1
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 +24 -0
- package/README.md +19 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/storage/hybrid.d.ts +30 -4
- package/dist/storage/hybrid.d.ts.map +1 -1
- package/dist/storage/hybrid.js +49 -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 +77 -9
- package/src/storage/index.ts +1 -0
package/src/storage/hybrid.ts
CHANGED
|
@@ -150,9 +150,11 @@ export interface HybridStorageOptions {
|
|
|
150
150
|
* three-way merge uses it. `null`: the remote had no item. The value is undefined when the
|
|
151
151
|
* storage does not know it. Examples are a storage with no `versionOf`, an operation of an
|
|
152
152
|
* older version, and a write after a restart, before a sync.
|
|
153
|
+
* @param context - `context.overlap(paths)` reports the paths where the merge kept only one side.
|
|
154
|
+
* The hook `onDropped` gets one call with the reason `overlap` for each call.
|
|
153
155
|
* @returns Merged value
|
|
154
156
|
*/
|
|
155
|
-
mergeFn?: <T>(local: T, remote: T, base: T | null | undefined) => T;
|
|
157
|
+
mergeFn?: <T>(local: T, remote: T, base: T | null | undefined, context: MergeContext) => T;
|
|
156
158
|
|
|
157
159
|
/**
|
|
158
160
|
* The key of a remote item, for the local cache. The default reads a string `id` field. Give
|
|
@@ -270,6 +272,9 @@ export interface HybridStorageOptions {
|
|
|
270
272
|
* `remote` decides, on a delete of the remote and on a refusal. The call comes after the local
|
|
271
273
|
* copy changed, so the hook can write the value again. An error of the hook does not stop the
|
|
272
274
|
* sync. The sync waits for the hook.
|
|
275
|
+
*
|
|
276
|
+
* The hook also gets each overlap that `mergeFn` reports, with the reason `overlap`. The merged write of an overlap
|
|
277
|
+
* stays in the queue.
|
|
273
278
|
*/
|
|
274
279
|
onDropped?: (drop: DroppedWrite) => void | Promise<void>;
|
|
275
280
|
|
|
@@ -603,6 +608,15 @@ export interface QueueRefusal {
|
|
|
603
608
|
restored: boolean;
|
|
604
609
|
}
|
|
605
610
|
|
|
611
|
+
/** The fourth argument of `mergeFn` (roadmap CLI-109). */
|
|
612
|
+
export interface MergeContext {
|
|
613
|
+
/**
|
|
614
|
+
* This method reports the paths where the merge kept only one side, for example `["title"]`. Each call during the
|
|
615
|
+
* merge gives one call of `onDropped` with the reason `overlap`. A call after the end of the merge changes nothing.
|
|
616
|
+
*/
|
|
617
|
+
overlap(paths: readonly string[]): void;
|
|
618
|
+
}
|
|
619
|
+
|
|
606
620
|
/**
|
|
607
621
|
* A queued write that left the queue with no send that the remote took: the argument of the hook
|
|
608
622
|
* `onDropped` (roadmap CLI-106). The hook can get a write again after a crash, because the store
|
|
@@ -618,13 +632,21 @@ export interface DroppedWrite {
|
|
|
618
632
|
value: unknown;
|
|
619
633
|
/**
|
|
620
634
|
* The cause: the policy took the remote value, the remote deleted the item, or the remote refused the write. The
|
|
621
|
-
* cause `too-large` tells that the write was larger than the limit of the remote (roadmap CLI-108).
|
|
635
|
+
* cause `too-large` tells that the write was larger than the limit of the remote (roadmap CLI-108). The cause
|
|
636
|
+
* `overlap` tells that `mergeFn` kept only one side of some paths (roadmap CLI-109). Its merged write stays in the
|
|
637
|
+
* queue.
|
|
622
638
|
*/
|
|
623
|
-
reason: "conflict" | "remote-delete" | "refused" | "too-large";
|
|
639
|
+
reason: "conflict" | "remote-delete" | "refused" | "too-large" | "overlap";
|
|
624
640
|
/** The sync that dropped the write. */
|
|
625
641
|
during: "pull" | "push";
|
|
626
|
-
/** The remote value that replaced the write, for the reason `conflict`. */
|
|
642
|
+
/** The remote value that replaced the write, for the reason `conflict`. The remote value of the merge, for `overlap`. */
|
|
627
643
|
remote?: unknown;
|
|
644
|
+
/** The base value of the merge, for the reason `overlap`. This value is undefined when the storage does not know it. */
|
|
645
|
+
base?: unknown;
|
|
646
|
+
/** The value that `mergeFn` gave, for the reason `overlap`. */
|
|
647
|
+
merged?: unknown;
|
|
648
|
+
/** The paths that `mergeFn` reported, for the reason `overlap`. */
|
|
649
|
+
paths?: string[];
|
|
628
650
|
/** The error of the remote, for the reasons `refused` and `too-large`. */
|
|
629
651
|
error?: unknown;
|
|
630
652
|
}
|
|
@@ -1032,7 +1054,7 @@ export class HybridStorage<T> implements CollectionStorage<T> {
|
|
|
1032
1054
|
private readonly queueStore: QueueStore;
|
|
1033
1055
|
private options: {
|
|
1034
1056
|
conflictResolution: ConflictResolution;
|
|
1035
|
-
mergeFn?: <T>(local: T, remote: T, base: T | null | undefined) => T;
|
|
1057
|
+
mergeFn?: <T>(local: T, remote: T, base: T | null | undefined, context: MergeContext) => T;
|
|
1036
1058
|
keyOf: (item: T) => string | undefined;
|
|
1037
1059
|
versionOf?: (item: T) => string | undefined;
|
|
1038
1060
|
sendBaseVersions: boolean;
|
|
@@ -1174,6 +1196,7 @@ export class HybridStorage<T> implements CollectionStorage<T> {
|
|
|
1174
1196
|
syncCount: 0,
|
|
1175
1197
|
conflicts: 0,
|
|
1176
1198
|
dropped: 0,
|
|
1199
|
+
overlaps: 0,
|
|
1177
1200
|
};
|
|
1178
1201
|
|
|
1179
1202
|
constructor(
|
|
@@ -2490,6 +2513,7 @@ export class HybridStorage<T> implements CollectionStorage<T> {
|
|
|
2490
2513
|
syncCount: this.stats.syncCount,
|
|
2491
2514
|
conflicts: this.stats.conflicts,
|
|
2492
2515
|
dropped: this.stats.dropped,
|
|
2516
|
+
overlaps: this.stats.overlaps,
|
|
2493
2517
|
pendingOps: this.offlineOps.length,
|
|
2494
2518
|
isOnline: this.isOnline,
|
|
2495
2519
|
mode: this.mode,
|
|
@@ -2873,9 +2897,11 @@ export class HybridStorage<T> implements CollectionStorage<T> {
|
|
|
2873
2897
|
conflicts.push(id);
|
|
2874
2898
|
return false;
|
|
2875
2899
|
}
|
|
2876
|
-
const
|
|
2900
|
+
const overlaps: string[][] = [];
|
|
2901
|
+
const resolved = this.resolveConflict(localItem, item, undefined, overlaps);
|
|
2877
2902
|
await this.localSet(id, resolved, this.syncOrigin);
|
|
2878
2903
|
this.stats.conflicts++;
|
|
2904
|
+
await this.reportOverlaps({ id, during: "pull", local: localItem, remote: item, base: undefined, merged: resolved }, overlaps);
|
|
2879
2905
|
}
|
|
2880
2906
|
if (remoteVersion !== undefined) {
|
|
2881
2907
|
this.bases.set(id, remoteVersion);
|
|
@@ -2950,13 +2976,16 @@ export class HybridStorage<T> implements CollectionStorage<T> {
|
|
|
2950
2976
|
return true;
|
|
2951
2977
|
case "merge": {
|
|
2952
2978
|
if (localItem === undefined) break;
|
|
2953
|
-
const
|
|
2979
|
+
const base = this.queuedBaseValue(id);
|
|
2980
|
+
const overlaps: string[][] = [];
|
|
2981
|
+
const merged = this.resolveConflict(localItem, remoteItem, base, overlaps);
|
|
2954
2982
|
if (!this.dropQueued(id)) break;
|
|
2955
2983
|
await this.localSet(id, merged, this.syncOrigin);
|
|
2956
2984
|
this.bases.set(id, remoteVersion);
|
|
2957
2985
|
this.noteBaseValue(id, remoteVersion, remoteItem);
|
|
2958
2986
|
// The merged write changes the remote item of this conflict: that item is its base
|
|
2959
2987
|
this.offlineOps.push({ type: "set", data: { id, value: merged }, timestamp: Date.now(), base: [[id, remoteVersion]], baseValues: [[id, remoteItem]] });
|
|
2988
|
+
await this.reportOverlaps({ id, during, local: localItem, remote: remoteItem, base, merged }, overlaps);
|
|
2960
2989
|
return true;
|
|
2961
2990
|
}
|
|
2962
2991
|
case "error":
|
|
@@ -2980,6 +3009,33 @@ export class HybridStorage<T> implements CollectionStorage<T> {
|
|
|
2980
3009
|
}
|
|
2981
3010
|
}
|
|
2982
3011
|
|
|
3012
|
+
/**
|
|
3013
|
+
* This method gives each overlap of a merge to the hook `onDropped` (roadmap CLI-109). It counts the overlaps, not the
|
|
3014
|
+
* dropped writes: the merged write stays in the queue. An error of the hook does not stop the sync.
|
|
3015
|
+
*/
|
|
3016
|
+
private async reportOverlaps(
|
|
3017
|
+
merge: { id: string; during: DroppedWrite["during"]; local: T; remote: T; base: T | null | undefined; merged: T },
|
|
3018
|
+
overlaps: readonly string[][],
|
|
3019
|
+
): Promise<void> {
|
|
3020
|
+
for (const paths of overlaps) {
|
|
3021
|
+
this.stats.overlaps++;
|
|
3022
|
+
try {
|
|
3023
|
+
await this.options.onDropped?.({
|
|
3024
|
+
id: merge.id,
|
|
3025
|
+
value: merge.local,
|
|
3026
|
+
reason: "overlap",
|
|
3027
|
+
during: merge.during,
|
|
3028
|
+
remote: merge.remote,
|
|
3029
|
+
base: merge.base,
|
|
3030
|
+
merged: merge.merged,
|
|
3031
|
+
paths,
|
|
3032
|
+
});
|
|
3033
|
+
} catch {
|
|
3034
|
+
// An error of the hook does not stop the sync
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
|
|
2983
3039
|
/**
|
|
2984
3040
|
* This method removes an id from the queued operations. It gives false when a queued clear touches it: nothing
|
|
2985
3041
|
* changes then.
|
|
@@ -3357,7 +3413,7 @@ export class HybridStorage<T> implements CollectionStorage<T> {
|
|
|
3357
3413
|
/**
|
|
3358
3414
|
* Resolve conflict between local and remote versions.
|
|
3359
3415
|
*/
|
|
3360
|
-
private resolveConflict(local: T, remote: T, base?: T | null): T {
|
|
3416
|
+
private resolveConflict(local: T, remote: T, base?: T | null, overlaps?: string[][]): T {
|
|
3361
3417
|
switch (this.options.conflictResolution) {
|
|
3362
3418
|
case "local":
|
|
3363
3419
|
return local;
|
|
@@ -3371,7 +3427,19 @@ export class HybridStorage<T> implements CollectionStorage<T> {
|
|
|
3371
3427
|
"Merge function required for conflict resolution strategy 'merge'"
|
|
3372
3428
|
);
|
|
3373
3429
|
}
|
|
3374
|
-
|
|
3430
|
+
{
|
|
3431
|
+
let open = true;
|
|
3432
|
+
const context: MergeContext = {
|
|
3433
|
+
overlap: (paths) => {
|
|
3434
|
+
if (open) overlaps?.push([...paths]);
|
|
3435
|
+
},
|
|
3436
|
+
};
|
|
3437
|
+
try {
|
|
3438
|
+
return this.options.mergeFn(local, remote, base, context);
|
|
3439
|
+
} finally {
|
|
3440
|
+
open = false;
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3375
3443
|
|
|
3376
3444
|
case "error":
|
|
3377
3445
|
throw new Error(
|