@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/dist/storage/hybrid.js
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`.
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 `
|
|
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
|
|
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
|
|
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`
|
|
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`
|
|
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
|
|
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
|
|
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`
|
|
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
|
|
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
|
|
2043
|
+
if (!this.closed && pullsRemote(this.mode)) {
|
|
2022
2044
|
await this.pull().catch(() => undefined);
|
|
2023
2045
|
}
|
|
2024
2046
|
}
|