@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.
Files changed (41) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +6 -3
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/browser.d.ts +1 -1
  5. package/dist/browser.d.ts.map +1 -1
  6. package/dist/browser.js +1 -1
  7. package/dist/browser.js.map +1 -1
  8. package/dist/indexeddb/document-storage.d.ts.map +1 -1
  9. package/dist/indexeddb/document-storage.js.map +1 -1
  10. package/dist/indexeddb/idb.d.ts +2 -12
  11. package/dist/indexeddb/idb.d.ts.map +1 -1
  12. package/dist/indexeddb/idb.js +7 -12
  13. package/dist/indexeddb/idb.js.map +1 -1
  14. package/dist/program-store/collection.d.ts.map +1 -1
  15. package/dist/program-store/collection.js.map +1 -1
  16. package/dist/program-store/file.d.ts.map +1 -1
  17. package/dist/program-store/file.js.map +1 -1
  18. package/dist/storage/factory.d.ts +1 -1
  19. package/dist/storage/file-queue.d.ts.map +1 -1
  20. package/dist/storage/file-queue.js.map +1 -1
  21. package/dist/storage/hybrid.d.ts +26 -10
  22. package/dist/storage/hybrid.d.ts.map +1 -1
  23. package/dist/storage/hybrid.js +41 -19
  24. package/dist/storage/hybrid.js.map +1 -1
  25. package/dist/storage/index.d.ts +1 -1
  26. package/dist/storage/index.d.ts.map +1 -1
  27. package/dist/storage/index.js +1 -1
  28. package/dist/storage/index.js.map +1 -1
  29. package/dist/storage/types.d.ts +1 -1
  30. package/dist/storage/types.d.ts.map +1 -1
  31. package/package.json +5 -5
  32. package/src/browser.ts +1 -0
  33. package/src/indexeddb/document-storage.ts +18 -6
  34. package/src/indexeddb/idb.ts +27 -12
  35. package/src/program-store/collection.ts +5 -1
  36. package/src/program-store/file.ts +5 -1
  37. package/src/storage/factory.ts +1 -1
  38. package/src/storage/file-queue.ts +5 -1
  39. package/src/storage/hybrid.ts +84 -31
  40. package/src/storage/index.ts +1 -0
  41. package/src/storage/types.ts +1 -1
@@ -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
- /** The name of the database. */
150
- readonly database: string,
151
- /** The object stores of the database. */
152
- private readonly stores: readonly IdbStoreSchema[],
153
- private readonly factory: IdbFactoryLike | undefined,
154
- /** The function that writes the first records when the open makes the object stores. */
155
- private readonly onCreate?: (store: (name: string) => IdbObjectStoreLike) => void,
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
- /** The name of the database. */
283
- readonly database: string,
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(private readonly storage: CollectionStorage<StoredProgram>) {}
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(readonly path: string) {}
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];
@@ -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 `remote-only`. `setMode()` of the registry changes it later. Only the type
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(readonly path: string) {}
30
+ constructor(path: string) {
31
+ this.path = path;
32
+ }
29
33
 
30
34
  load(): Promise<QueuedOperation[]> {
31
35
  return this.serial(async () => {
@@ -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 four modes, and
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 `write-back`.
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(private readonly options: TimerOptions) {}
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(readonly ids: string[]) {
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(readonly from: HybridMode, readonly to: HybridMode, cause: unknown) {
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(readonly calls: number) {
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 remote-only`);
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
- private readonly window: number,
800
- private readonly now: () => number,
801
- private readonly maxPerId = 20,
802
- private readonly maxIds = 10_000,
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 === "write-through" || this.mode === "write-back");
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 !== "write-through" && this.mode !== "write-back")) return;
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 !== "write-through" && this.mode !== "write-back")) return;
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 === "write-through" || this.mode === "write-back") await this.pullThrough(hints.length > 0 ? "hint" : "gap", hints, full);
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 `write-back`, and the send of the queue after a failure only
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 === "write-through" || this.mode === "write-back") this.pullTimer?.start();
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 === "write-through" || this.mode === "write-back") {
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` mode,
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` mode, it sends nothing: the queue waits for a remote mode.
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 === "write-through" || this.mode === "write-back") await this.pullThrough("pull", NO_HINTS);
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 !== "local-only") await this.pushQueue();
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` mode). The queue store keeps the operations that
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 === "local-only") return;
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 === "write-through" || this.mode === "write-back")) {
2498
+ if (!this.closed && pullsRemote(this.mode)) {
2446
2499
  await this.pull().catch(() => undefined);
2447
2500
  }
2448
2501
  }
@@ -41,6 +41,7 @@ export {
41
41
  HybridStorage,
42
42
  PendingWriteError,
43
43
  QueueFullError,
44
+ ReadOnlyError,
44
45
  MissingKeysError,
45
46
  QueueStoreError,
46
47
  ConflictError,
@@ -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