@procedures/store 0.47.2 → 0.48.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 +17 -0
- package/README.md +6 -3
- package/dist/.tsbuildinfo +1 -1
- package/dist/browser.d.ts +1 -1
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +1 -1
- package/dist/browser.js.map +1 -1
- package/dist/indexeddb/document-storage.d.ts.map +1 -1
- package/dist/indexeddb/document-storage.js.map +1 -1
- package/dist/indexeddb/idb.d.ts +2 -12
- package/dist/indexeddb/idb.d.ts.map +1 -1
- package/dist/indexeddb/idb.js +7 -12
- package/dist/indexeddb/idb.js.map +1 -1
- package/dist/program-store/collection.d.ts.map +1 -1
- package/dist/program-store/collection.js.map +1 -1
- package/dist/program-store/file.d.ts.map +1 -1
- package/dist/program-store/file.js.map +1 -1
- package/dist/storage/factory.d.ts +1 -1
- package/dist/storage/file-queue.d.ts.map +1 -1
- package/dist/storage/file-queue.js.map +1 -1
- package/dist/storage/hybrid.d.ts +26 -10
- package/dist/storage/hybrid.d.ts.map +1 -1
- package/dist/storage/hybrid.js +41 -19
- 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 +1 -1
- package/dist/storage/index.js.map +1 -1
- package/dist/storage/types.d.ts +1 -1
- package/dist/storage/types.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/browser.ts +1 -0
- package/src/indexeddb/document-storage.ts +18 -6
- package/src/indexeddb/idb.ts +27 -12
- package/src/program-store/collection.ts +5 -1
- package/src/program-store/file.ts +5 -1
- package/src/storage/factory.ts +1 -1
- package/src/storage/file-queue.ts +5 -1
- package/src/storage/hybrid.ts +84 -31
- package/src/storage/index.ts +1 -0
- package/src/storage/types.ts +1 -1
package/src/indexeddb/idb.ts
CHANGED
|
@@ -142,18 +142,28 @@ export type IdbWork<R> = (transaction: IdbTransactionLike, fail: (error: unknown
|
|
|
142
142
|
* and a failed open tries again at the next operation.
|
|
143
143
|
*/
|
|
144
144
|
export class IdbConnection {
|
|
145
|
+
/** The name of the database. */
|
|
146
|
+
readonly database: string;
|
|
147
|
+
/** The object stores of the database. */
|
|
148
|
+
private readonly stores: readonly IdbStoreSchema[];
|
|
149
|
+
private readonly factory: IdbFactoryLike | undefined;
|
|
150
|
+
/** The function that writes the first records when the open makes the object stores. */
|
|
151
|
+
private readonly onCreate?: ((store: (name: string) => IdbObjectStoreLike) => void) | undefined;
|
|
152
|
+
|
|
145
153
|
private connection: Promise<IdbDatabaseLike> | undefined;
|
|
146
154
|
private closed = false;
|
|
147
155
|
|
|
148
156
|
constructor(
|
|
149
|
-
|
|
150
|
-
readonly
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
+
database: string,
|
|
158
|
+
stores: readonly IdbStoreSchema[],
|
|
159
|
+
factory: IdbFactoryLike | undefined,
|
|
160
|
+
onCreate?: (store: (name: string) => IdbObjectStoreLike) => void,
|
|
161
|
+
) {
|
|
162
|
+
this.database = database;
|
|
163
|
+
this.stores = stores;
|
|
164
|
+
this.factory = factory;
|
|
165
|
+
this.onCreate = onCreate;
|
|
166
|
+
}
|
|
157
167
|
|
|
158
168
|
/**
|
|
159
169
|
* This method runs work in one transaction over the object stores of `names`. The promise resolves
|
|
@@ -276,16 +286,21 @@ export class IdbConnection {
|
|
|
276
286
|
|
|
277
287
|
/** One object store in its own database, version 1. The stores of programs, queues and sync states use it. */
|
|
278
288
|
export class IdbStoreConnection {
|
|
289
|
+
/** The name of the database. */
|
|
290
|
+
readonly database: string;
|
|
291
|
+
/** The name of the object store. */
|
|
292
|
+
readonly store: string;
|
|
293
|
+
|
|
279
294
|
private readonly connection: IdbConnection;
|
|
280
295
|
|
|
281
296
|
constructor(
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
/** The name of the object store. */
|
|
285
|
-
readonly store: string,
|
|
297
|
+
database: string,
|
|
298
|
+
store: string,
|
|
286
299
|
storeOptions: { keyPath?: string; autoIncrement?: boolean },
|
|
287
300
|
factory: IdbFactoryLike | undefined,
|
|
288
301
|
) {
|
|
302
|
+
this.database = database;
|
|
303
|
+
this.store = store;
|
|
289
304
|
this.connection = new IdbConnection(database, [{ name: store, options: storeOptions }], factory);
|
|
290
305
|
}
|
|
291
306
|
|
|
@@ -33,13 +33,17 @@ function nextRevision(version: string | number | null | undefined): string {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export class CollectionProgramStore implements ProgramStore {
|
|
36
|
+
private readonly storage: CollectionStorage<StoredProgram>;
|
|
37
|
+
|
|
36
38
|
readonly kind = "collection";
|
|
37
39
|
|
|
38
40
|
/**
|
|
39
41
|
* @param storage - The storage of the records. The store closes it in `close()`. Give it the
|
|
40
42
|
* option `versionOf: storedProgramVersion`, so that it can test `expectedVersion`.
|
|
41
43
|
*/
|
|
42
|
-
constructor(
|
|
44
|
+
constructor(storage: CollectionStorage<StoredProgram>) {
|
|
45
|
+
this.storage = storage;
|
|
46
|
+
}
|
|
43
47
|
|
|
44
48
|
async get(key: string): Promise<StoredProgram | undefined> {
|
|
45
49
|
const record = await this.storage.get(key);
|
|
@@ -71,12 +71,16 @@ export async function retryOnContention<T>(operation: () => Promise<T>): Promise
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
export class FileProgramStore implements ProgramStore {
|
|
74
|
+
readonly path: string;
|
|
75
|
+
|
|
74
76
|
readonly kind = "file";
|
|
75
77
|
/** The writes of this process, one at a time (the lock file orders the processes). */
|
|
76
78
|
private queue: Promise<unknown> = Promise.resolve();
|
|
77
79
|
|
|
78
80
|
/** @param path - The JSON file. The store makes it, and its folder, at the first write. */
|
|
79
|
-
constructor(
|
|
81
|
+
constructor(path: string) {
|
|
82
|
+
this.path = path;
|
|
83
|
+
}
|
|
80
84
|
|
|
81
85
|
async get(key: string): Promise<StoredProgram | undefined> {
|
|
82
86
|
return (await this.read()).programs[key];
|
package/src/storage/factory.ts
CHANGED
|
@@ -51,7 +51,7 @@ export interface CreateSyncedRegistryConfig {
|
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
53
|
* The first mode of the hybrid storage (roadmap R28): `local-only`, `write-through`,
|
|
54
|
-
* `write-back` or `
|
|
54
|
+
* `write-back`, `remote-only` or `read-only`. `setMode()` of the registry changes it later. Only the type
|
|
55
55
|
* "hybrid" has modes.
|
|
56
56
|
* @default "write-through"
|
|
57
57
|
*/
|
|
@@ -21,11 +21,15 @@ export type { QueuedOperation, QueueStore } from "./queue.js";
|
|
|
21
21
|
|
|
22
22
|
/** A queue store in a JSON Lines file. */
|
|
23
23
|
export class FileQueueStore implements QueueStore {
|
|
24
|
+
readonly path: string;
|
|
25
|
+
|
|
24
26
|
/** The calls of this instance, one at a time */
|
|
25
27
|
private chain: Promise<unknown> = Promise.resolve();
|
|
26
28
|
|
|
27
29
|
/** @param path - The file. The store makes it, and its folder, at the first write. */
|
|
28
|
-
constructor(
|
|
30
|
+
constructor(path: string) {
|
|
31
|
+
this.path = path;
|
|
32
|
+
}
|
|
29
33
|
|
|
30
34
|
load(): Promise<QueuedOperation[]> {
|
|
31
35
|
return this.serial(async () => {
|
package/src/storage/hybrid.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Hybrid Storage Implementation
|
|
3
3
|
*
|
|
4
4
|
* A local copy (an `InMemoryStorage`, or the `local` storage that the application gives) in
|
|
5
|
-
* front of a remote `CollectionStorage` (often an `ApiStorage`). The storage has
|
|
5
|
+
* front of a remote `CollectionStorage` (often an `ApiStorage`). The storage has five modes, and
|
|
6
6
|
* `setMode()` changes the mode while the storage operates (roadmap R28):
|
|
7
7
|
*
|
|
8
8
|
* | Mode | Reads | Writes | Remote calls |
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* | `write-through` | the local copy, the remote on a miss | the local copy, then the remote | yes |
|
|
12
12
|
* | `write-back` | the local copy, the remote on a miss | the local copy and the queue, a timer sends it | yes |
|
|
13
13
|
* | `remote-only` | the remote | the remote | yes, and no local copy |
|
|
14
|
+
* | `read-only` | the local copy, the remote on a miss | refused with `ReadOnlyError` | reads and pulls only |
|
|
14
15
|
*
|
|
15
16
|
* - The queue is durable through the `queueStore` that the application gives.
|
|
16
17
|
* - A sync asks for the changes since a version when the remote has `getChangesSince`.
|
|
@@ -93,10 +94,19 @@ export type WriteStrategy = "write-through" | "write-back";
|
|
|
93
94
|
* - `write-through`: each write goes to the local copy, then to the remote.
|
|
94
95
|
* - `write-back`: each write goes to the local copy and to the queue. A timer sends the queue.
|
|
95
96
|
* - `remote-only`: each read and each write goes to the remote. The storage keeps no local copy.
|
|
97
|
+
* - `read-only`: the storage refuses each write with `ReadOnlyError` at once, with no change of
|
|
98
|
+
* the local copy, the queue or the remote. Reads, pulls and the feed work as in `write-through`
|
|
99
|
+
* (roadmap CLI-89). This mode sends no write: a queue that it holds waits for a mode that writes.
|
|
96
100
|
*/
|
|
97
|
-
export type HybridMode = "local-only" | "write-through" | "write-back" | "remote-only";
|
|
101
|
+
export type HybridMode = "local-only" | "write-through" | "write-back" | "remote-only" | "read-only";
|
|
98
102
|
|
|
99
|
-
const MODES: ReadonlySet<string> = new Set<HybridMode>(["local-only", "write-through", "write-back", "remote-only"]);
|
|
103
|
+
const MODES: ReadonlySet<string> = new Set<HybridMode>(["local-only", "write-through", "write-back", "remote-only", "read-only"]);
|
|
104
|
+
|
|
105
|
+
/** True for the modes that keep a local copy and read the remote changes into it. */
|
|
106
|
+
const pullsRemote = (mode: HybridMode): boolean => mode === "write-through" || mode === "write-back" || mode === "read-only";
|
|
107
|
+
|
|
108
|
+
/** True for the modes that send writes to the remote. */
|
|
109
|
+
const pushesRemote = (mode: HybridMode): boolean => mode !== "local-only" && mode !== "read-only";
|
|
100
110
|
|
|
101
111
|
/**
|
|
102
112
|
* Hybrid storage configuration options.
|
|
@@ -179,14 +189,14 @@ export interface HybridStorageOptions {
|
|
|
179
189
|
|
|
180
190
|
/**
|
|
181
191
|
* The interval of the periodic pull, for example `minutes(1)` (roadmap R28). A pull reads the
|
|
182
|
-
* remote changes into the local copy in the modes `write-through` and `
|
|
192
|
+
* remote changes into the local copy in the modes `write-through`, `write-back` and `read-only`.
|
|
183
193
|
* @default no periodic pull
|
|
184
194
|
*/
|
|
185
195
|
pullInterval?: Duration;
|
|
186
196
|
|
|
187
197
|
/**
|
|
188
198
|
* Initialize by syncing from remote on construction: send the loaded queue, then read the
|
|
189
|
-
* remote. The mode `local-only` makes no remote call.
|
|
199
|
+
* remote. The mode `local-only` makes no remote call. The mode `read-only` sends no queue.
|
|
190
200
|
* @default true
|
|
191
201
|
*/
|
|
192
202
|
syncOnInit?: boolean;
|
|
@@ -469,6 +479,8 @@ interface TimerOptions {
|
|
|
469
479
|
* process alive.
|
|
470
480
|
*/
|
|
471
481
|
class SyncTimer {
|
|
482
|
+
private readonly options: TimerOptions;
|
|
483
|
+
|
|
472
484
|
private cancel: Cancel | undefined;
|
|
473
485
|
private active = false;
|
|
474
486
|
/** True while `work` runs */
|
|
@@ -477,7 +489,9 @@ class SyncTimer {
|
|
|
477
489
|
private again = false;
|
|
478
490
|
private failures = 0;
|
|
479
491
|
|
|
480
|
-
constructor(
|
|
492
|
+
constructor(options: TimerOptions) {
|
|
493
|
+
this.options = options;
|
|
494
|
+
}
|
|
481
495
|
|
|
482
496
|
/**
|
|
483
497
|
* This method starts the timer. A timer that waits does not start again. A start during a run
|
|
@@ -590,6 +604,21 @@ export class QueueFullError extends Error {
|
|
|
590
604
|
}
|
|
591
605
|
}
|
|
592
606
|
|
|
607
|
+
/**
|
|
608
|
+
* The storage is in `read-only` mode, so it refuses the write (roadmap CLI-89). The local copy, the
|
|
609
|
+
* queue and the remote did not change. `operation` is the name of the refused operation.
|
|
610
|
+
*/
|
|
611
|
+
export class ReadOnlyError extends Error {
|
|
612
|
+
readonly operation: string;
|
|
613
|
+
|
|
614
|
+
override readonly name = "ReadOnlyError";
|
|
615
|
+
readonly code = "READ_ONLY";
|
|
616
|
+
constructor(operation: string) {
|
|
617
|
+
super(`The storage is in read-only mode: it refuses the operation ${operation}`);
|
|
618
|
+
this.operation = operation;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
593
622
|
/** The remote gave items without a key, so a sync cannot know which local items to keep. */
|
|
594
623
|
export class MissingKeysError extends Error {
|
|
595
624
|
override readonly name = "MissingKeysError";
|
|
@@ -616,10 +645,13 @@ export class QueueStoreError extends Error {
|
|
|
616
645
|
* their queued writes stay. `ids` are the ids of the conflicts.
|
|
617
646
|
*/
|
|
618
647
|
export class ConflictError extends Error {
|
|
648
|
+
readonly ids: string[];
|
|
649
|
+
|
|
619
650
|
override readonly name = "ConflictError";
|
|
620
651
|
readonly code = "SYNC_CONFLICT";
|
|
621
|
-
constructor(
|
|
652
|
+
constructor(ids: string[]) {
|
|
622
653
|
super(`Conflict detected between local and remote versions of ${ids.length} items: ${ids.slice(0, 10).join(", ")}`);
|
|
654
|
+
this.ids = ids;
|
|
623
655
|
}
|
|
624
656
|
}
|
|
625
657
|
|
|
@@ -628,10 +660,15 @@ export class ConflictError extends Error {
|
|
|
628
660
|
* error of the flush or of the sync.
|
|
629
661
|
*/
|
|
630
662
|
export class ModeSwitchError extends Error {
|
|
663
|
+
readonly from: HybridMode;
|
|
664
|
+
readonly to: HybridMode;
|
|
665
|
+
|
|
631
666
|
override readonly name = "ModeSwitchError";
|
|
632
667
|
readonly code = "MODE_SWITCH_FAILED";
|
|
633
|
-
constructor(
|
|
668
|
+
constructor(from: HybridMode, to: HybridMode, cause: unknown) {
|
|
634
669
|
super(`The storage could not change from ${from} to ${to}: the mode is still ${from}`, { cause });
|
|
670
|
+
this.from = from;
|
|
671
|
+
this.to = to;
|
|
635
672
|
}
|
|
636
673
|
}
|
|
637
674
|
|
|
@@ -653,10 +690,13 @@ export class SyncStateError extends Error {
|
|
|
653
690
|
* `apply` before the hook settled.
|
|
654
691
|
*/
|
|
655
692
|
export class RemoteApplyHookError extends Error {
|
|
693
|
+
readonly calls: number;
|
|
694
|
+
|
|
656
695
|
override readonly name = "RemoteApplyHookError";
|
|
657
696
|
readonly code = "REMOTE_APPLY_HOOK";
|
|
658
|
-
constructor(
|
|
697
|
+
constructor(calls: number) {
|
|
659
698
|
super(`The hook aroundRemoteApply called apply ${calls} times, not 1 time: the storage applied the changes 1 time`);
|
|
699
|
+
this.calls = calls;
|
|
660
700
|
}
|
|
661
701
|
}
|
|
662
702
|
|
|
@@ -688,7 +728,7 @@ const defaultKeyOf = (item: unknown): string | undefined => {
|
|
|
688
728
|
|
|
689
729
|
function checkMode(mode: unknown): HybridMode {
|
|
690
730
|
if (typeof mode !== "string" || !MODES.has(mode)) {
|
|
691
|
-
throw new TypeError(`Unknown mode ${JSON.stringify(mode)}: use local-only, write-through, write-back or
|
|
731
|
+
throw new TypeError(`Unknown mode ${JSON.stringify(mode)}: use local-only, write-through, write-back, remote-only or read-only`);
|
|
692
732
|
}
|
|
693
733
|
return mode as HybridMode;
|
|
694
734
|
}
|
|
@@ -792,15 +832,25 @@ interface EchoEntry {
|
|
|
792
832
|
* a write whose echo never comes does not hold an id long.
|
|
793
833
|
*/
|
|
794
834
|
class EchoLedger {
|
|
835
|
+
private readonly window: number;
|
|
836
|
+
private readonly now: () => number;
|
|
837
|
+
private readonly maxPerId: number;
|
|
838
|
+
private readonly maxIds: number;
|
|
839
|
+
|
|
795
840
|
/** The versions of each id, the oldest first. The ids are in the order of their last write. */
|
|
796
841
|
private readonly entries = new Map<string, EchoEntry[]>();
|
|
797
842
|
|
|
798
843
|
constructor(
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
) {
|
|
844
|
+
window: number,
|
|
845
|
+
now: () => number,
|
|
846
|
+
maxPerId = 20,
|
|
847
|
+
maxIds = 10_000,
|
|
848
|
+
) {
|
|
849
|
+
this.window = window;
|
|
850
|
+
this.now = now;
|
|
851
|
+
this.maxPerId = maxPerId;
|
|
852
|
+
this.maxIds = maxIds;
|
|
853
|
+
}
|
|
804
854
|
|
|
805
855
|
/** This method records the version that an own write of an id gives. `null`: a delete. */
|
|
806
856
|
wrote(id: string, version: string | null): void {
|
|
@@ -1360,7 +1410,7 @@ export class HybridStorage<T> implements CollectionStorage<T> {
|
|
|
1360
1410
|
private updateFeed(): void {
|
|
1361
1411
|
const feed = this.feed;
|
|
1362
1412
|
if (!feed) return;
|
|
1363
|
-
const wanted = !this.closed && (this.mode
|
|
1413
|
+
const wanted = !this.closed && pullsRemote(this.mode);
|
|
1364
1414
|
if (wanted === (this.feedStop !== undefined)) return;
|
|
1365
1415
|
const token = ++this.feedToken;
|
|
1366
1416
|
if (!wanted) {
|
|
@@ -1416,7 +1466,7 @@ export class HybridStorage<T> implements CollectionStorage<T> {
|
|
|
1416
1466
|
*/
|
|
1417
1467
|
private async applyFeed(changes: RemoteChanges<T>, mark: number): Promise<void> {
|
|
1418
1468
|
if (this.loadError) throw this.loadError;
|
|
1419
|
-
if (this.closed || (this.mode
|
|
1469
|
+
if (this.closed || !pullsRemote(this.mode)) return;
|
|
1420
1470
|
if (changes.version === this.cursor) return;
|
|
1421
1471
|
const follows = changes.since !== undefined && changes.since === this.cursor;
|
|
1422
1472
|
const hook = this.aroundRemoteApply;
|
|
@@ -1450,7 +1500,7 @@ export class HybridStorage<T> implements CollectionStorage<T> {
|
|
|
1450
1500
|
*/
|
|
1451
1501
|
private async applyHint(hint: RemoteChangeHint, mark: number): Promise<void> {
|
|
1452
1502
|
if (this.loadError) throw this.loadError;
|
|
1453
|
-
if (this.closed || (this.mode
|
|
1503
|
+
if (this.closed || !pullsRemote(this.mode)) return;
|
|
1454
1504
|
// The remote cannot tell what changed: a full pull
|
|
1455
1505
|
if (hint.reset === true) return this.requestPull(hint, true);
|
|
1456
1506
|
if (hint.version !== undefined && hint.version === this.cursor) return;
|
|
@@ -1491,7 +1541,7 @@ export class HybridStorage<T> implements CollectionStorage<T> {
|
|
|
1491
1541
|
if (this.closed) return Promise.resolve();
|
|
1492
1542
|
return this.track("pull", async () => {
|
|
1493
1543
|
if (this.loadError) throw this.loadError;
|
|
1494
|
-
if (this.mode
|
|
1544
|
+
if (pullsRemote(this.mode)) await this.pullThrough(hints.length > 0 ? "hint" : "gap", hints, full);
|
|
1495
1545
|
});
|
|
1496
1546
|
}),
|
|
1497
1547
|
)
|
|
@@ -1669,7 +1719,8 @@ export class HybridStorage<T> implements CollectionStorage<T> {
|
|
|
1669
1719
|
* - The switch waits for the operations that run. The operations that start later wait for
|
|
1670
1720
|
* the switch. Switches and syncs run one at a time.
|
|
1671
1721
|
* - A switch to a remote mode sends the queue first. A switch to `local-only` makes no remote
|
|
1672
|
-
* call: the queue stays until the next remote mode.
|
|
1722
|
+
* call: the queue stays until the next remote mode. A switch to `read-only` also sends the
|
|
1723
|
+
* queue first, so this mode starts with an empty queue, or the switch fails.
|
|
1673
1724
|
* - A switch to `remote-only` drops the local copy, after the queue reached the remote.
|
|
1674
1725
|
* - A switch from `local-only` or `remote-only` to a mode with a local copy syncs the local
|
|
1675
1726
|
* copy. The reads wait for the sync.
|
|
@@ -1732,7 +1783,7 @@ export class HybridStorage<T> implements CollectionStorage<T> {
|
|
|
1732
1783
|
|
|
1733
1784
|
/**
|
|
1734
1785
|
* This method starts and stops the timers for the mode. The push timer runs in `write-back`, the
|
|
1735
|
-
* pull timer in `write-through` and `
|
|
1786
|
+
* pull timer in `write-through`, `write-back` and `read-only`, and the send of the queue after a failure only
|
|
1736
1787
|
* in `write-through`. A failed run reports its error in the status, and the next delay grows.
|
|
1737
1788
|
*/
|
|
1738
1789
|
private updateTimers(): void {
|
|
@@ -1740,7 +1791,7 @@ export class HybridStorage<T> implements CollectionStorage<T> {
|
|
|
1740
1791
|
if (this.mode === "write-back") this.pushTimer?.start();
|
|
1741
1792
|
else this.pushTimer?.stop();
|
|
1742
1793
|
|
|
1743
|
-
if (this.mode
|
|
1794
|
+
if (pullsRemote(this.mode)) this.pullTimer?.start();
|
|
1744
1795
|
else this.pullTimer?.stop();
|
|
1745
1796
|
|
|
1746
1797
|
if (this.mode !== "write-through") this.drainTimer.stop();
|
|
@@ -1936,6 +1987,7 @@ export class HybridStorage<T> implements CollectionStorage<T> {
|
|
|
1936
1987
|
* One write, in the mode of the storage. The result is the result of the local copy, or of the
|
|
1937
1988
|
* remote in `remote-only` mode.
|
|
1938
1989
|
*
|
|
1990
|
+
* - `read-only`: the write throws `ReadOnlyError`, and nothing changes.
|
|
1939
1991
|
* - `remote-only`: the remote only. Its error passes through.
|
|
1940
1992
|
* - `write-back` and `local-only`: the local copy and the queue. A full queue refuses the write
|
|
1941
1993
|
* before the local change. The queue store keeps the operation before the write ends.
|
|
@@ -1958,6 +2010,7 @@ export class HybridStorage<T> implements CollectionStorage<T> {
|
|
|
1958
2010
|
applyRemote: () => Promise<R>,
|
|
1959
2011
|
): Promise<R> {
|
|
1960
2012
|
return this.run(false, async () => {
|
|
2013
|
+
if (this.mode === "read-only") throw new ReadOnlyError(type);
|
|
1961
2014
|
const op: QueuedOperation = { type, data, timestamp: Date.now() };
|
|
1962
2015
|
|
|
1963
2016
|
if (this.mode === "remote-only") {
|
|
@@ -2139,7 +2192,7 @@ export class HybridStorage<T> implements CollectionStorage<T> {
|
|
|
2139
2192
|
this.deferred.delete(id);
|
|
2140
2193
|
const mark = this.writeSeq;
|
|
2141
2194
|
let item = deferred.item;
|
|
2142
|
-
if (this.mode
|
|
2195
|
+
if (pullsRemote(this.mode)) {
|
|
2143
2196
|
try {
|
|
2144
2197
|
item = await this.remote.get(id);
|
|
2145
2198
|
this.isOnline = true;
|
|
@@ -2208,8 +2261,8 @@ export class HybridStorage<T> implements CollectionStorage<T> {
|
|
|
2208
2261
|
/**
|
|
2209
2262
|
* Sync all pending offline operations to remote.
|
|
2210
2263
|
* Processes queued operations in order. When one fails, it and the later operations stay in
|
|
2211
|
-
* the queue: the operations that reached the remote are not sent again. In `local-only`
|
|
2212
|
-
* the call does nothing.
|
|
2264
|
+
* the queue: the operations that reached the remote are not sent again. In the modes `local-only`
|
|
2265
|
+
* and `read-only`, the call does nothing.
|
|
2213
2266
|
*/
|
|
2214
2267
|
syncToRemote(): Promise<void> {
|
|
2215
2268
|
if (this.closed) return Promise.reject(this.closedError());
|
|
@@ -2218,7 +2271,7 @@ export class HybridStorage<T> implements CollectionStorage<T> {
|
|
|
2218
2271
|
|
|
2219
2272
|
/**
|
|
2220
2273
|
* This method sends the queue to the remote (roadmap R28). `ProcedureServer.useStorage` uses it. In
|
|
2221
|
-
* `local-only`
|
|
2274
|
+
* the modes `local-only` and `read-only`, it sends nothing: the queue waits for a mode that writes.
|
|
2222
2275
|
*/
|
|
2223
2276
|
flush(): Promise<void> {
|
|
2224
2277
|
return this.syncToRemote();
|
|
@@ -2252,7 +2305,7 @@ export class HybridStorage<T> implements CollectionStorage<T> {
|
|
|
2252
2305
|
this.track("pull", async () => {
|
|
2253
2306
|
await this.loaded;
|
|
2254
2307
|
if (this.loadError) throw this.loadError;
|
|
2255
|
-
if (this.mode
|
|
2308
|
+
if (pullsRemote(this.mode)) await this.pullThrough("pull", NO_HINTS);
|
|
2256
2309
|
}),
|
|
2257
2310
|
);
|
|
2258
2311
|
}
|
|
@@ -2263,7 +2316,7 @@ export class HybridStorage<T> implements CollectionStorage<T> {
|
|
|
2263
2316
|
this.track("push", async () => {
|
|
2264
2317
|
await this.loaded;
|
|
2265
2318
|
if (this.loadError) throw this.loadError;
|
|
2266
|
-
if (this.mode
|
|
2319
|
+
if (pushesRemote(this.mode)) await this.pushQueue();
|
|
2267
2320
|
}),
|
|
2268
2321
|
);
|
|
2269
2322
|
}
|
|
@@ -2281,7 +2334,7 @@ export class HybridStorage<T> implements CollectionStorage<T> {
|
|
|
2281
2334
|
* This method closes the storage.
|
|
2282
2335
|
*
|
|
2283
2336
|
* - It stops the timers, and waits for the operations, the mode switch and the syncs that run.
|
|
2284
|
-
* - It sends the queue (not in `local-only`
|
|
2337
|
+
* - It sends the queue (not in the modes `local-only` and `read-only`). The queue store keeps the operations that
|
|
2285
2338
|
* the remote did not take.
|
|
2286
2339
|
* - It closes the remote and the local copy. A durable local copy keeps its items.
|
|
2287
2340
|
* - After it, each operation rejects. A second `close()` does nothing: it gives the promise of
|
|
@@ -2310,7 +2363,7 @@ export class HybridStorage<T> implements CollectionStorage<T> {
|
|
|
2310
2363
|
// After the syncs that run: send the pending operations. The queue store keeps the ones that
|
|
2311
2364
|
// the remote did not take, and the status gets the failure.
|
|
2312
2365
|
await this.serialize(async () => {
|
|
2313
|
-
if (this.loadError || this.offlineOps.length === 0 || this.mode
|
|
2366
|
+
if (this.loadError || this.offlineOps.length === 0 || !pushesRemote(this.mode)) return;
|
|
2314
2367
|
await this.track("push", () => this.pushQueue()).catch(() => undefined);
|
|
2315
2368
|
});
|
|
2316
2369
|
await this.storeChain;
|
|
@@ -2442,7 +2495,7 @@ export class HybridStorage<T> implements CollectionStorage<T> {
|
|
|
2442
2495
|
if (this.options.syncOnInit && this.mode !== "local-only") {
|
|
2443
2496
|
// A failure goes to the status: the reads go on with the local copy
|
|
2444
2497
|
if (this.offlineOps.length > 0) await this.push().catch(() => undefined);
|
|
2445
|
-
if (!this.closed && (this.mode
|
|
2498
|
+
if (!this.closed && pullsRemote(this.mode)) {
|
|
2446
2499
|
await this.pull().catch(() => undefined);
|
|
2447
2500
|
}
|
|
2448
2501
|
}
|
package/src/storage/index.ts
CHANGED
package/src/storage/types.ts
CHANGED
|
@@ -139,7 +139,7 @@ export interface SyncStatus {
|
|
|
139
139
|
syncing: boolean;
|
|
140
140
|
|
|
141
141
|
/** The mode of a hybrid storage (roadmap R28), or undefined for a storage without modes */
|
|
142
|
-
mode?: "local-only" | "write-through" | "write-back" | "remote-only" | undefined;
|
|
142
|
+
mode?: "local-only" | "write-through" | "write-back" | "remote-only" | "read-only" | undefined;
|
|
143
143
|
|
|
144
144
|
/**
|
|
145
145
|
* The newest failure of the sync that a later success of the same work did not clear (roadmap
|