@procedures/store 0.14.0 → 0.15.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.
- package/CHANGELOG.md +58 -0
- package/README.md +46 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/browser.d.ts +26 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +22 -0
- package/dist/browser.js.map +1 -0
- package/dist/storage/api.d.ts +21 -6
- package/dist/storage/api.d.ts.map +1 -1
- package/dist/storage/api.js +47 -7
- package/dist/storage/api.js.map +1 -1
- package/dist/storage/feed.d.ts +1 -1
- package/dist/storage/feed.d.ts.map +1 -1
- package/dist/storage/feed.js +1 -1
- package/dist/storage/feed.js.map +1 -1
- package/dist/storage/hybrid.d.ts +65 -7
- package/dist/storage/hybrid.d.ts.map +1 -1
- package/dist/storage/hybrid.js +188 -20
- package/dist/storage/hybrid.js.map +1 -1
- package/package.json +13 -5
- package/src/browser.ts +43 -0
- package/src/storage/api.ts +60 -10
- package/src/storage/feed.ts +1 -1
- package/src/storage/hybrid.ts +206 -22
package/src/storage/feed.ts
CHANGED
package/src/storage/hybrid.ts
CHANGED
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
* - The queue is durable through the `queueStore` that the application gives.
|
|
16
16
|
* - A sync asks for the changes since a version when the remote has `getChangesSince`.
|
|
17
17
|
* - With `versionOf`, a conflict is a change on both sides, and `conflictResolution` decides.
|
|
18
|
+
* - With `versionOf`, the push of a queued write sends its base version as `expectedVersion`
|
|
19
|
+
* (roadmap CLI-16). A `VERSION_CONFLICT` of the remote goes to `conflictResolution`, as a
|
|
20
|
+
* conflict of a pull does. The option `sendBaseVersions: false` stops it.
|
|
18
21
|
* - `pullInterval` keeps the local copy fresh.
|
|
19
22
|
* - Each write takes a copy of the value (`structuredClone`), for the local copy, the queue and the
|
|
20
23
|
* remote. The reads give the copies of the local copy, or of the remote in `remote-only` mode.
|
|
@@ -42,23 +45,27 @@
|
|
|
42
45
|
|
|
43
46
|
import type {
|
|
44
47
|
CollectionStorage,
|
|
48
|
+
StorageBatchWriteOptions,
|
|
45
49
|
StorageChangeBatch,
|
|
46
50
|
StorageChangeEvent,
|
|
47
51
|
StorageChanges,
|
|
52
|
+
StorageItemWriteOptions,
|
|
48
53
|
StorageMetadata,
|
|
49
54
|
StorageObserver,
|
|
50
55
|
StorageWriteOptions,
|
|
56
|
+
VersionConflict,
|
|
51
57
|
} from "@procedures/collections";
|
|
52
58
|
import { InMemoryStorage, StorageListeners, type ListenerErrorHandler } from "@procedures/collections";
|
|
53
59
|
import {
|
|
54
60
|
queryEntries,
|
|
55
61
|
queryOptionsProblems,
|
|
56
62
|
selectorProblems,
|
|
63
|
+
versionConflictsOf,
|
|
57
64
|
type StorageQueryOptions,
|
|
58
65
|
type StorageQueryPolicy,
|
|
59
66
|
type StorageSelector,
|
|
60
67
|
} from "@procedures/client/query";
|
|
61
|
-
import { optionalMs, realClock, type Cancel, type Clock, type Duration } from "@procedures/client";
|
|
68
|
+
import { optionalMs, realClock, type Cancel, type Clock, type Duration } from "@procedures/client/browser";
|
|
62
69
|
import { backoffDelay } from "@mark1russell7/service/backoff";
|
|
63
70
|
import { MemoryQueueStore, idsOf, withoutId, type QueuedOperation, type QueueStore } from "./queue.js";
|
|
64
71
|
import type { SyncError, SyncStatus } from "./types.js";
|
|
@@ -147,6 +154,19 @@ export interface HybridStorageOptions {
|
|
|
147
154
|
*/
|
|
148
155
|
versionOf?: (item: any) => string | undefined;
|
|
149
156
|
|
|
157
|
+
/**
|
|
158
|
+
* With `versionOf`: true sends the base version of each queued write to the remote as its
|
|
159
|
+
* `expectedVersion` (roadmap CLI-16). The remote then refuses a write over a change of another
|
|
160
|
+
* writer with `VERSION_CONFLICT`, and `conflictResolution` decides, as for a pull. The base
|
|
161
|
+
* version is the remote version of the id when the local write was queued. False: the push
|
|
162
|
+
* sends no precondition, and the queued write replaces the remote item, as before CLI-16.
|
|
163
|
+
*
|
|
164
|
+
* The remote must test the precondition: an `ApiStorage` of the core collection procedures over
|
|
165
|
+
* a storage with the same `versionOf`. A remote that cannot test it refuses each push.
|
|
166
|
+
* @default true
|
|
167
|
+
*/
|
|
168
|
+
sendBaseVersions?: boolean;
|
|
169
|
+
|
|
150
170
|
/**
|
|
151
171
|
* Automatic sync interval (for write-back mode), for example `seconds(5)`.
|
|
152
172
|
* Set to 0 to disable automatic sync. In write-through mode, it is also the delay before the
|
|
@@ -530,6 +550,15 @@ type PulledChanges<T> = Omit<StorageChanges<T>, "version"> & { version?: string
|
|
|
530
550
|
/** The data of a queued operation of each type. */
|
|
531
551
|
type OperationData<T> = { id: string; value: T; ids: string[]; items: Array<[string, T]> };
|
|
532
552
|
|
|
553
|
+
/** True when two values have the same JSON. A value without JSON is never the same. */
|
|
554
|
+
function sameJson(a: unknown, b: unknown): boolean {
|
|
555
|
+
try {
|
|
556
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
557
|
+
} catch {
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
533
562
|
/** A change with an origin, when there is one. */
|
|
534
563
|
function withOrigin<T>(change: StorageChangeEvent<T>, origin: string | undefined): StorageChangeEvent<T> {
|
|
535
564
|
return origin === undefined ? change : { ...change, origin };
|
|
@@ -752,6 +781,7 @@ export class HybridStorage<T> implements CollectionStorage<T> {
|
|
|
752
781
|
mergeFn?: <T>(local: T, remote: T) => T;
|
|
753
782
|
keyOf: (item: T) => string | undefined;
|
|
754
783
|
versionOf?: (item: T) => string | undefined;
|
|
784
|
+
sendBaseVersions: boolean;
|
|
755
785
|
syncInterval: number;
|
|
756
786
|
pullInterval: number;
|
|
757
787
|
syncOnInit: boolean;
|
|
@@ -874,7 +904,10 @@ export class HybridStorage<T> implements CollectionStorage<T> {
|
|
|
874
904
|
remote: CollectionStorage<T>,
|
|
875
905
|
options: HybridStorageOptions = {}
|
|
876
906
|
) {
|
|
877
|
-
|
|
907
|
+
// The local copy tests the preconditions of the callers (roadmap CLI-16): it gets `versionOf`
|
|
908
|
+
this.local =
|
|
909
|
+
(options.local as LocalCopyStorage<T> | undefined) ??
|
|
910
|
+
new InMemoryStorage<T>(options.versionOf !== undefined ? { versionOf: options.versionOf as (item: T) => string | undefined } : {});
|
|
878
911
|
this.remote = remote;
|
|
879
912
|
this.queueStore = options.queueStore ?? new MemoryQueueStore();
|
|
880
913
|
this.mode = checkMode(options.mode ?? options.writeStrategy ?? "write-through");
|
|
@@ -883,6 +916,7 @@ export class HybridStorage<T> implements CollectionStorage<T> {
|
|
|
883
916
|
...(options.mergeFn !== undefined && { mergeFn: options.mergeFn }),
|
|
884
917
|
keyOf: options.keyOf ?? defaultKeyOf,
|
|
885
918
|
...(options.versionOf !== undefined && { versionOf: options.versionOf }),
|
|
919
|
+
sendBaseVersions: options.sendBaseVersions ?? true,
|
|
886
920
|
syncInterval: optionalMs(options.syncInterval, "syncInterval") ?? 5000,
|
|
887
921
|
pullInterval: optionalMs(options.pullInterval, "pullInterval") ?? 0,
|
|
888
922
|
syncOnInit: options.syncOnInit ?? true,
|
|
@@ -1603,22 +1637,34 @@ export class HybridStorage<T> implements CollectionStorage<T> {
|
|
|
1603
1637
|
* This method sets an item. The storage takes a copy of the value, so a later change of the
|
|
1604
1638
|
* value does not change the local copy, the queue or the remote write. The origin of the options
|
|
1605
1639
|
* goes to the batches of `observe()`. The local copy and the remote get the origin of the storage.
|
|
1640
|
+
*
|
|
1641
|
+
* An `expectedVersion` (roadmap CLI-16) is a precondition on the item as the storage reads it.
|
|
1642
|
+
* The local copy tests it (the default local copy needs `versionOf`). In `write-through` mode, the
|
|
1643
|
+
* remote write also gets it. In `remote-only` mode, only the remote tests it.
|
|
1606
1644
|
*/
|
|
1607
|
-
async set(id: string, written: T, options?:
|
|
1645
|
+
async set(id: string, written: T, options?: StorageItemWriteOptions): Promise<void> {
|
|
1608
1646
|
const value = structuredClone(written);
|
|
1609
1647
|
const origin = options?.origin;
|
|
1648
|
+
const expected = this.expectedOf(options);
|
|
1610
1649
|
await this.write("set", { id, value }, [id], origin, async () => {
|
|
1611
1650
|
this.bump(id);
|
|
1612
|
-
await this.localSet(id, value, origin);
|
|
1613
|
-
}, () => this.remote.set(id, value, this.own));
|
|
1651
|
+
await this.localSet(id, value, origin, expected);
|
|
1652
|
+
}, () => this.remote.set(id, value, { ...this.own, ...expected }));
|
|
1614
1653
|
}
|
|
1615
1654
|
|
|
1616
|
-
|
|
1655
|
+
/** This method deletes an item. An `expectedVersion` is a precondition, as for `set`. */
|
|
1656
|
+
async delete(id: string, options?: StorageItemWriteOptions): Promise<boolean> {
|
|
1617
1657
|
const origin = options?.origin;
|
|
1658
|
+
const expected = this.expectedOf(options);
|
|
1618
1659
|
return this.write("delete", { id }, [id], origin, async () => {
|
|
1619
1660
|
this.bump(id);
|
|
1620
|
-
return this.localDelete(id, origin);
|
|
1621
|
-
}, () => this.remote.delete(id, this.own));
|
|
1661
|
+
return this.localDelete(id, origin, expected);
|
|
1662
|
+
}, () => this.remote.delete(id, { ...this.own, ...expected }));
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
/** The precondition of the caller: the field `expectedVersion`, or no field. */
|
|
1666
|
+
private expectedOf(options: StorageItemWriteOptions | undefined): { expectedVersion?: string | null } {
|
|
1667
|
+
return options?.expectedVersion === undefined ? {} : { expectedVersion: options.expectedVersion };
|
|
1622
1668
|
}
|
|
1623
1669
|
|
|
1624
1670
|
async clear(options?: StorageWriteOptions): Promise<void> {
|
|
@@ -1633,14 +1679,18 @@ export class HybridStorage<T> implements CollectionStorage<T> {
|
|
|
1633
1679
|
// ═══ Bulk Operations ═══
|
|
1634
1680
|
//
|
|
1635
1681
|
|
|
1636
|
-
/**
|
|
1637
|
-
|
|
1682
|
+
/**
|
|
1683
|
+
* This method sets several items. The storage takes a copy of each value, as `set` does. The
|
|
1684
|
+
* `expectedVersions` are preconditions, as `expectedVersion` of `set` is.
|
|
1685
|
+
*/
|
|
1686
|
+
async setBatch(written: Array<[string, T]>, options?: StorageBatchWriteOptions): Promise<void> {
|
|
1638
1687
|
const items = written.map(([id, value]): [string, T] => [id, structuredClone(value)]);
|
|
1639
1688
|
const origin = options?.origin;
|
|
1689
|
+
const expected = options?.expectedVersions !== undefined && options.expectedVersions.length > 0 ? { expectedVersions: options.expectedVersions } : {};
|
|
1640
1690
|
await this.write("setBatch", { items }, items.map(([id]) => id), origin, async () => {
|
|
1641
1691
|
for (const [id] of items) this.bump(id);
|
|
1642
|
-
await this.localSetBatch(items, origin);
|
|
1643
|
-
}, () => this.remote.setBatch(items, this.own));
|
|
1692
|
+
await this.localSetBatch(items, origin, expected);
|
|
1693
|
+
}, () => this.remote.setBatch(items, { ...this.own, ...expected }));
|
|
1644
1694
|
}
|
|
1645
1695
|
|
|
1646
1696
|
async deleteBatch(ids: string[], options?: StorageWriteOptions): Promise<number> {
|
|
@@ -2248,19 +2298,22 @@ export class HybridStorage<T> implements CollectionStorage<T> {
|
|
|
2248
2298
|
// The writes of the local copy have the origin of this storage, so the capture does not queue
|
|
2249
2299
|
// them. The observers get each change with the origin of the caller.
|
|
2250
2300
|
|
|
2251
|
-
|
|
2252
|
-
|
|
2301
|
+
// A precondition of a caller goes to the local copy, which tests it and writes in one step
|
|
2302
|
+
// (roadmap CLI-16).
|
|
2303
|
+
|
|
2304
|
+
private async localSet(id: string, value: T, origin: string | undefined, expected: { expectedVersion?: string | null } = {}): Promise<void> {
|
|
2305
|
+
await this.writeLocal(() => [[id, this.recordedVersion(value)]], () => this.local.set(id, value, { ...this.own, ...expected }));
|
|
2253
2306
|
if (this.observers.size > 0) this.emit([setChange(id, value, origin)]);
|
|
2254
2307
|
}
|
|
2255
2308
|
|
|
2256
|
-
private async localDelete(id: string, origin: string | undefined): Promise<boolean> {
|
|
2257
|
-
const existed = await this.writeLocal(() => [[id, undefined]], () => this.local.delete(id, this.own));
|
|
2309
|
+
private async localDelete(id: string, origin: string | undefined, expected: { expectedVersion?: string | null } = {}): Promise<boolean> {
|
|
2310
|
+
const existed = await this.writeLocal(() => [[id, undefined]], () => this.local.delete(id, { ...this.own, ...expected }));
|
|
2258
2311
|
if (existed) this.emit([withOrigin<T>({ type: "delete", id }, origin)]);
|
|
2259
2312
|
return existed;
|
|
2260
2313
|
}
|
|
2261
2314
|
|
|
2262
|
-
private async localSetBatch(items: Array<[string, T]>, origin: string | undefined): Promise<void> {
|
|
2263
|
-
await this.writeLocal(() => items.map(([id, value]) => [id, this.recordedVersion(value)]), () => this.local.setBatch(items, this.own));
|
|
2315
|
+
private async localSetBatch(items: Array<[string, T]>, origin: string | undefined, expected: StorageBatchWriteOptions = {}): Promise<void> {
|
|
2316
|
+
await this.writeLocal(() => items.map(([id, value]) => [id, this.recordedVersion(value)]), () => this.local.setBatch(items, { ...this.own, ...expected }));
|
|
2264
2317
|
if (this.observers.size > 0) this.emit(items.map(([id, value]) => setChange(id, value, origin)));
|
|
2265
2318
|
}
|
|
2266
2319
|
|
|
@@ -2576,6 +2629,8 @@ export class HybridStorage<T> implements CollectionStorage<T> {
|
|
|
2576
2629
|
private async pushQueue(): Promise<void> {
|
|
2577
2630
|
this.pushing = true;
|
|
2578
2631
|
let sent = 0;
|
|
2632
|
+
// The operations whose conflicts this push resolved once (roadmap CLI-16)
|
|
2633
|
+
const resolved = new Set<QueuedOperation>();
|
|
2579
2634
|
try {
|
|
2580
2635
|
while (this.offlineOps.length > 0) {
|
|
2581
2636
|
const op = this.offlineOps[0]!;
|
|
@@ -2588,8 +2643,16 @@ export class HybridStorage<T> implements CollectionStorage<T> {
|
|
|
2588
2643
|
this.echoSent(op);
|
|
2589
2644
|
await this.executeOperation(op);
|
|
2590
2645
|
} catch (error) {
|
|
2646
|
+
// The remote refused a base version: the conflict policy decides, then the push goes on
|
|
2647
|
+
const conflicts = this.sendsBaseVersions() ? versionConflictsOf(error) : undefined;
|
|
2648
|
+
if (conflicts !== undefined && conflicts.length > 0 && !resolved.has(op)) {
|
|
2649
|
+
resolved.add(op);
|
|
2650
|
+
this.isOnline = true;
|
|
2651
|
+
await this.resolvePushConflicts(op, conflicts);
|
|
2652
|
+
continue;
|
|
2653
|
+
}
|
|
2591
2654
|
// This operation and the later ones stay queued
|
|
2592
|
-
this.isOnline = false;
|
|
2655
|
+
if (conflicts === undefined) this.isOnline = false;
|
|
2593
2656
|
throw error;
|
|
2594
2657
|
}
|
|
2595
2658
|
this.noteRemoteWrite(op);
|
|
@@ -2914,15 +2977,15 @@ export class HybridStorage<T> implements CollectionStorage<T> {
|
|
|
2914
2977
|
|
|
2915
2978
|
switch (op.type) {
|
|
2916
2979
|
case "set":
|
|
2917
|
-
await this.remote.set(data.id, data.value, this.
|
|
2980
|
+
await this.remote.set(data.id, data.value, this.withBase(data.id));
|
|
2918
2981
|
break;
|
|
2919
2982
|
|
|
2920
2983
|
case "delete":
|
|
2921
|
-
await this.remote.delete(data.id, this.
|
|
2984
|
+
await this.remote.delete(data.id, this.withBase(data.id));
|
|
2922
2985
|
break;
|
|
2923
2986
|
|
|
2924
2987
|
case "setBatch":
|
|
2925
|
-
await this.remote.setBatch(data.items, this.
|
|
2988
|
+
await this.remote.setBatch(data.items, this.withBases(data.items));
|
|
2926
2989
|
break;
|
|
2927
2990
|
|
|
2928
2991
|
case "deleteBatch":
|
|
@@ -2935,6 +2998,127 @@ export class HybridStorage<T> implements CollectionStorage<T> {
|
|
|
2935
2998
|
}
|
|
2936
2999
|
}
|
|
2937
3000
|
|
|
3001
|
+
//
|
|
3002
|
+
// ═══ The base versions as preconditions (roadmap CLI-16) ═══
|
|
3003
|
+
//
|
|
3004
|
+
|
|
3005
|
+
/** True when a push sends the base versions of its operations as expected versions. */
|
|
3006
|
+
private sendsBaseVersions(): boolean {
|
|
3007
|
+
return this.options.sendBaseVersions && this.options.versionOf !== undefined;
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
/**
|
|
3011
|
+
* The options of the push of a set or a delete of one id: the base version of the id as its
|
|
3012
|
+
* expected version. The first queued operation of an id changes the remote version in `bases`.
|
|
3013
|
+
* An id without a known base version gets no precondition: the queued write wins, as before.
|
|
3014
|
+
*/
|
|
3015
|
+
private withBase(id: string): StorageItemWriteOptions {
|
|
3016
|
+
if (!this.sendsBaseVersions()) return this.own;
|
|
3017
|
+
const base = this.bases.get(id);
|
|
3018
|
+
return base === undefined ? this.own : { ...this.own, expectedVersion: base };
|
|
3019
|
+
}
|
|
3020
|
+
|
|
3021
|
+
/** The options of the push of a batch: the base version of each id that has one. */
|
|
3022
|
+
private withBases(items: Array<[string, T]>): StorageBatchWriteOptions {
|
|
3023
|
+
if (!this.sendsBaseVersions()) return this.own;
|
|
3024
|
+
const expectedVersions: Array<[string, string | null]> = [];
|
|
3025
|
+
for (const id of new Set(items.map(([id]) => id))) {
|
|
3026
|
+
const base = this.bases.get(id);
|
|
3027
|
+
if (base !== undefined) expectedVersions.push([id, base]);
|
|
3028
|
+
}
|
|
3029
|
+
return expectedVersions.length === 0 ? this.own : { ...this.own, expectedVersions };
|
|
3030
|
+
}
|
|
3031
|
+
|
|
3032
|
+
/**
|
|
3033
|
+
* The value that a queued operation writes for an id: the value of the last item of the id, or
|
|
3034
|
+
* `null` for a delete. Undefined when the operation does not write the id.
|
|
3035
|
+
*/
|
|
3036
|
+
private writtenValue(op: QueuedOperation, id: string): { value: T } | null | undefined {
|
|
3037
|
+
const data = op.data as OperationData<T>;
|
|
3038
|
+
switch (op.type) {
|
|
3039
|
+
case "set":
|
|
3040
|
+
return data.id === id ? { value: data.value } : undefined;
|
|
3041
|
+
case "setBatch": {
|
|
3042
|
+
const item = data.items.findLast(([itemId]) => itemId === id);
|
|
3043
|
+
return item ? { value: item[1] } : undefined;
|
|
3044
|
+
}
|
|
3045
|
+
case "delete":
|
|
3046
|
+
return data.id === id ? null : undefined;
|
|
3047
|
+
default:
|
|
3048
|
+
return undefined;
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
|
|
3052
|
+
/**
|
|
3053
|
+
* This method applies the conflicts of a push that the remote refused (`VERSION_CONFLICT`). Each
|
|
3054
|
+
* conflict goes to `conflictResolution`, as a conflict of a pull does:
|
|
3055
|
+
*
|
|
3056
|
+
* - The remote item is the value of the operation itself: an earlier send of it reached the
|
|
3057
|
+
* remote, and its answer was lost. The base becomes the current version, and the push sends
|
|
3058
|
+
* the operation again.
|
|
3059
|
+
* - `remote`: the remote item replaces the local item, and the queued writes of the id go.
|
|
3060
|
+
* - `merge`: the merged item replaces the local item, and a new queued write sends it.
|
|
3061
|
+
* - `local`: the base becomes the current version, so the next send replaces the remote item.
|
|
3062
|
+
* - `error`: the queue keeps the operation, and the push throws `ConflictError` with the ids.
|
|
3063
|
+
*/
|
|
3064
|
+
private async resolvePushConflicts(op: QueuedOperation, conflicts: readonly VersionConflict[]): Promise<void> {
|
|
3065
|
+
const ids: string[] = [];
|
|
3066
|
+
let queueChanged = false;
|
|
3067
|
+
for (const conflict of conflicts) {
|
|
3068
|
+
const { id } = conflict;
|
|
3069
|
+
const written = this.writtenValue(op, id);
|
|
3070
|
+
if (written === undefined) continue;
|
|
3071
|
+
if (conflict.found) {
|
|
3072
|
+
const remoteItem = conflict.value as T;
|
|
3073
|
+
const remoteVersion = conflict.version ?? this.versionOfItem(remoteItem);
|
|
3074
|
+
// The echo of this operation, or a remote item without a version: the queued write wins
|
|
3075
|
+
if (remoteVersion === undefined || (written !== null && sameJson(written.value, remoteItem))) {
|
|
3076
|
+
if (remoteVersion === undefined) this.bases.delete(id);
|
|
3077
|
+
else this.bases.set(id, remoteVersion);
|
|
3078
|
+
continue;
|
|
3079
|
+
}
|
|
3080
|
+
const localItem = await this.readLocal(id);
|
|
3081
|
+
if (await this.resolvePending(id, localItem, remoteItem, remoteVersion, ids)) queueChanged = true;
|
|
3082
|
+
continue;
|
|
3083
|
+
}
|
|
3084
|
+
// The remote has no item: the echo of a delete, or a delete of another writer
|
|
3085
|
+
if (written === null) {
|
|
3086
|
+
this.bases.set(id, null);
|
|
3087
|
+
continue;
|
|
3088
|
+
}
|
|
3089
|
+
if (await this.resolveRemoteDelete(id, ids)) queueChanged = true;
|
|
3090
|
+
}
|
|
3091
|
+
if (queueChanged) {
|
|
3092
|
+
this.storeDirty = true;
|
|
3093
|
+
// The status gets the failure of the queue store. A later direct write repairs the store first.
|
|
3094
|
+
await this.persistAll().catch(() => undefined);
|
|
3095
|
+
}
|
|
3096
|
+
if (ids.length > 0) throw new ConflictError(ids);
|
|
3097
|
+
}
|
|
3098
|
+
|
|
3099
|
+
/**
|
|
3100
|
+
* A queued write of an id that the remote deleted: a conflict, and the policy decides, as for
|
|
3101
|
+
* a delete of a pull. True when the queue changed.
|
|
3102
|
+
*/
|
|
3103
|
+
private async resolveRemoteDelete(id: string, conflicts: string[]): Promise<boolean> {
|
|
3104
|
+
this.stats.conflicts++;
|
|
3105
|
+
switch (this.options.conflictResolution) {
|
|
3106
|
+
case "remote":
|
|
3107
|
+
if (!this.dropQueued(id)) break;
|
|
3108
|
+
await this.localDelete(id, this.syncOrigin);
|
|
3109
|
+
this.bases.set(id, null);
|
|
3110
|
+
return true;
|
|
3111
|
+
case "error":
|
|
3112
|
+
conflicts.push(id);
|
|
3113
|
+
return false;
|
|
3114
|
+
default:
|
|
3115
|
+
// "local", and "merge" with one deleted side: the queued write wins
|
|
3116
|
+
break;
|
|
3117
|
+
}
|
|
3118
|
+
this.bases.set(id, null);
|
|
3119
|
+
return false;
|
|
3120
|
+
}
|
|
3121
|
+
|
|
2938
3122
|
/**
|
|
2939
3123
|
* Get current online status.
|
|
2940
3124
|
*/
|