@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
@@ -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`.
@@ -50,7 +51,11 @@ import { optionalMs, realClock } from "@procedures/client/browser";
50
51
  import { backoffDelay } from "@mark1russell7/service/backoff";
51
52
  import { MemoryQueueStore, idsOf, withoutId } from "./queue.js";
52
53
  import { isSyncState } from "./sync-state.js";
53
- const MODES = new Set(["local-only", "write-through", "write-back", "remote-only"]);
54
+ const MODES = new Set(["local-only", "write-through", "write-back", "remote-only", "read-only"]);
55
+ /** True for the modes that keep a local copy and read the remote changes into it. */
56
+ const pullsRemote = (mode) => mode === "write-through" || mode === "write-back" || mode === "read-only";
57
+ /** True for the modes that send writes to the remote. */
58
+ const pushesRemote = (mode) => mode !== "local-only" && mode !== "read-only";
54
59
  /** The hints of an apply without hints. */
55
60
  const NO_HINTS = Object.freeze([]);
56
61
  /** The most hints that a pull keeps for the hook. A pull drops the older ones. */
@@ -178,6 +183,19 @@ export class QueueFullError extends Error {
178
183
  super(`The queue of operations is full (${max} operations): the storage refuses the write`);
179
184
  }
180
185
  }
186
+ /**
187
+ * The storage is in `read-only` mode, so it refuses the write (roadmap CLI-89). The local copy, the
188
+ * queue and the remote did not change. `operation` is the name of the refused operation.
189
+ */
190
+ export class ReadOnlyError extends Error {
191
+ operation;
192
+ name = "ReadOnlyError";
193
+ code = "READ_ONLY";
194
+ constructor(operation) {
195
+ super(`The storage is in read-only mode: it refuses the operation ${operation}`);
196
+ this.operation = operation;
197
+ }
198
+ }
181
199
  /** The remote gave items without a key, so a sync cannot know which local items to keep. */
182
200
  export class MissingKeysError extends Error {
183
201
  name = "MissingKeysError";
@@ -273,7 +291,7 @@ const defaultKeyOf = (item) => {
273
291
  };
274
292
  function checkMode(mode) {
275
293
  if (typeof mode !== "string" || !MODES.has(mode)) {
276
- throw new TypeError(`Unknown mode ${JSON.stringify(mode)}: use local-only, write-through, write-back or remote-only`);
294
+ throw new TypeError(`Unknown mode ${JSON.stringify(mode)}: use local-only, write-through, write-back, remote-only or read-only`);
277
295
  }
278
296
  return mode;
279
297
  }
@@ -924,7 +942,7 @@ export class HybridStorage {
924
942
  const feed = this.feed;
925
943
  if (!feed)
926
944
  return;
927
- const wanted = !this.closed && (this.mode === "write-through" || this.mode === "write-back");
945
+ const wanted = !this.closed && pullsRemote(this.mode);
928
946
  if (wanted === (this.feedStop !== undefined))
929
947
  return;
930
948
  const token = ++this.feedToken;
@@ -988,7 +1006,7 @@ export class HybridStorage {
988
1006
  async applyFeed(changes, mark) {
989
1007
  if (this.loadError)
990
1008
  throw this.loadError;
991
- if (this.closed || (this.mode !== "write-through" && this.mode !== "write-back"))
1009
+ if (this.closed || !pullsRemote(this.mode))
992
1010
  return;
993
1011
  if (changes.version === this.cursor)
994
1012
  return;
@@ -1025,7 +1043,7 @@ export class HybridStorage {
1025
1043
  async applyHint(hint, mark) {
1026
1044
  if (this.loadError)
1027
1045
  throw this.loadError;
1028
- if (this.closed || (this.mode !== "write-through" && this.mode !== "write-back"))
1046
+ if (this.closed || !pullsRemote(this.mode))
1029
1047
  return;
1030
1048
  // The remote cannot tell what changed: a full pull
1031
1049
  if (hint.reset === true)
@@ -1073,7 +1091,7 @@ export class HybridStorage {
1073
1091
  return this.track("pull", async () => {
1074
1092
  if (this.loadError)
1075
1093
  throw this.loadError;
1076
- if (this.mode === "write-through" || this.mode === "write-back")
1094
+ if (pullsRemote(this.mode))
1077
1095
  await this.pullThrough(hints.length > 0 ? "hint" : "gap", hints, full);
1078
1096
  });
1079
1097
  }))
@@ -1254,7 +1272,8 @@ export class HybridStorage {
1254
1272
  * - The switch waits for the operations that run. The operations that start later wait for
1255
1273
  * the switch. Switches and syncs run one at a time.
1256
1274
  * - A switch to a remote mode sends the queue first. A switch to `local-only` makes no remote
1257
- * call: the queue stays until the next remote mode.
1275
+ * call: the queue stays until the next remote mode. A switch to `read-only` also sends the
1276
+ * queue first, so this mode starts with an empty queue, or the switch fails.
1258
1277
  * - A switch to `remote-only` drops the local copy, after the queue reached the remote.
1259
1278
  * - A switch from `local-only` or `remote-only` to a mode with a local copy syncs the local
1260
1279
  * copy. The reads wait for the sync.
@@ -1321,7 +1340,7 @@ export class HybridStorage {
1321
1340
  }
1322
1341
  /**
1323
1342
  * This method starts and stops the timers for the mode. The push timer runs in `write-back`, the
1324
- * pull timer in `write-through` and `write-back`, and the send of the queue after a failure only
1343
+ * pull timer in `write-through`, `write-back` and `read-only`, and the send of the queue after a failure only
1325
1344
  * in `write-through`. A failed run reports its error in the status, and the next delay grows.
1326
1345
  */
1327
1346
  updateTimers() {
@@ -1331,7 +1350,7 @@ export class HybridStorage {
1331
1350
  this.pushTimer?.start();
1332
1351
  else
1333
1352
  this.pushTimer?.stop();
1334
- if (this.mode === "write-through" || this.mode === "write-back")
1353
+ if (pullsRemote(this.mode))
1335
1354
  this.pullTimer?.start();
1336
1355
  else
1337
1356
  this.pullTimer?.stop();
@@ -1510,6 +1529,7 @@ export class HybridStorage {
1510
1529
  * One write, in the mode of the storage. The result is the result of the local copy, or of the
1511
1530
  * remote in `remote-only` mode.
1512
1531
  *
1532
+ * - `read-only`: the write throws `ReadOnlyError`, and nothing changes.
1513
1533
  * - `remote-only`: the remote only. Its error passes through.
1514
1534
  * - `write-back` and `local-only`: the local copy and the queue. A full queue refuses the write
1515
1535
  * before the local change. The queue store keeps the operation before the write ends.
@@ -1525,6 +1545,8 @@ export class HybridStorage {
1525
1545
  */
1526
1546
  write(type, data, ids, origin, applyLocal, applyRemote) {
1527
1547
  return this.run(false, async () => {
1548
+ if (this.mode === "read-only")
1549
+ throw new ReadOnlyError(type);
1528
1550
  const op = { type, data, timestamp: Date.now() };
1529
1551
  if (this.mode === "remote-only") {
1530
1552
  const send = async () => {
@@ -1709,7 +1731,7 @@ export class HybridStorage {
1709
1731
  this.deferred.delete(id);
1710
1732
  const mark = this.writeSeq;
1711
1733
  let item = deferred.item;
1712
- if (this.mode === "write-through" || this.mode === "write-back") {
1734
+ if (pullsRemote(this.mode)) {
1713
1735
  try {
1714
1736
  item = await this.remote.get(id);
1715
1737
  this.isOnline = true;
@@ -1782,8 +1804,8 @@ export class HybridStorage {
1782
1804
  /**
1783
1805
  * Sync all pending offline operations to remote.
1784
1806
  * Processes queued operations in order. When one fails, it and the later operations stay in
1785
- * the queue: the operations that reached the remote are not sent again. In `local-only` mode,
1786
- * the call does nothing.
1807
+ * the queue: the operations that reached the remote are not sent again. In the modes `local-only`
1808
+ * and `read-only`, the call does nothing.
1787
1809
  */
1788
1810
  syncToRemote() {
1789
1811
  if (this.closed)
@@ -1792,7 +1814,7 @@ export class HybridStorage {
1792
1814
  }
1793
1815
  /**
1794
1816
  * This method sends the queue to the remote (roadmap R28). `ProcedureServer.useStorage` uses it. In
1795
- * `local-only` mode, it sends nothing: the queue waits for a remote mode.
1817
+ * the modes `local-only` and `read-only`, it sends nothing: the queue waits for a mode that writes.
1796
1818
  */
1797
1819
  flush() {
1798
1820
  return this.syncToRemote();
@@ -1824,7 +1846,7 @@ export class HybridStorage {
1824
1846
  await this.loaded;
1825
1847
  if (this.loadError)
1826
1848
  throw this.loadError;
1827
- if (this.mode === "write-through" || this.mode === "write-back")
1849
+ if (pullsRemote(this.mode))
1828
1850
  await this.pullThrough("pull", NO_HINTS);
1829
1851
  }));
1830
1852
  }
@@ -1834,7 +1856,7 @@ export class HybridStorage {
1834
1856
  await this.loaded;
1835
1857
  if (this.loadError)
1836
1858
  throw this.loadError;
1837
- if (this.mode !== "local-only")
1859
+ if (pushesRemote(this.mode))
1838
1860
  await this.pushQueue();
1839
1861
  }));
1840
1862
  }
@@ -1849,7 +1871,7 @@ export class HybridStorage {
1849
1871
  * This method closes the storage.
1850
1872
  *
1851
1873
  * - It stops the timers, and waits for the operations, the mode switch and the syncs that run.
1852
- * - It sends the queue (not in `local-only` mode). The queue store keeps the operations that
1874
+ * - It sends the queue (not in the modes `local-only` and `read-only`). The queue store keeps the operations that
1853
1875
  * the remote did not take.
1854
1876
  * - It closes the remote and the local copy. A durable local copy keeps its items.
1855
1877
  * - After it, each operation rejects. A second `close()` does nothing: it gives the promise of
@@ -1877,7 +1899,7 @@ export class HybridStorage {
1877
1899
  // After the syncs that run: send the pending operations. The queue store keeps the ones that
1878
1900
  // the remote did not take, and the status gets the failure.
1879
1901
  await this.serialize(async () => {
1880
- if (this.loadError || this.offlineOps.length === 0 || this.mode === "local-only")
1902
+ if (this.loadError || this.offlineOps.length === 0 || !pushesRemote(this.mode))
1881
1903
  return;
1882
1904
  await this.track("push", () => this.pushQueue()).catch(() => undefined);
1883
1905
  });
@@ -2018,7 +2040,7 @@ export class HybridStorage {
2018
2040
  // A failure goes to the status: the reads go on with the local copy
2019
2041
  if (this.offlineOps.length > 0)
2020
2042
  await this.push().catch(() => undefined);
2021
- if (!this.closed && (this.mode === "write-through" || this.mode === "write-back")) {
2043
+ if (!this.closed && pullsRemote(this.mode)) {
2022
2044
  await this.pull().catch(() => undefined);
2023
2045
  }
2024
2046
  }