@procedures/store 0.2.3 → 0.4.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 +79 -0
- package/README.md +109 -2
- package/dist/.tsbuildinfo +1 -1
- package/dist/indexeddb/idb.d.ts +100 -0
- package/dist/indexeddb/idb.d.ts.map +1 -0
- package/dist/indexeddb/idb.js +137 -0
- package/dist/indexeddb/idb.js.map +1 -0
- package/dist/indexeddb/program-store.d.ts +42 -0
- package/dist/indexeddb/program-store.d.ts.map +1 -0
- package/dist/indexeddb/program-store.js +71 -0
- package/dist/indexeddb/program-store.js.map +1 -0
- package/dist/indexeddb/queue-store.d.ts +38 -0
- package/dist/indexeddb/queue-store.d.ts.map +1 -0
- package/dist/indexeddb/queue-store.js +73 -0
- package/dist/indexeddb/queue-store.js.map +1 -0
- package/dist/indexeddb.d.ts +19 -0
- package/dist/indexeddb.d.ts.map +1 -0
- package/dist/indexeddb.js +17 -0
- package/dist/indexeddb.js.map +1 -0
- package/dist/program-store/file.d.ts +5 -0
- package/dist/program-store/file.d.ts.map +1 -1
- package/dist/program-store/file.js +5 -2
- package/dist/program-store/file.js.map +1 -1
- package/dist/storage/api.d.ts +18 -7
- package/dist/storage/api.d.ts.map +1 -1
- package/dist/storage/api.js +34 -3
- package/dist/storage/api.js.map +1 -1
- package/dist/storage/factory.d.ts +13 -2
- package/dist/storage/factory.d.ts.map +1 -1
- package/dist/storage/factory.js +12 -4
- package/dist/storage/factory.js.map +1 -1
- package/dist/storage/file-queue.d.ts +32 -0
- package/dist/storage/file-queue.d.ts.map +1 -0
- package/dist/storage/file-queue.js +101 -0
- package/dist/storage/file-queue.js.map +1 -0
- package/dist/storage/hybrid.d.ts +361 -42
- package/dist/storage/hybrid.d.ts.map +1 -1
- package/dist/storage/hybrid.js +1102 -251
- package/dist/storage/hybrid.js.map +1 -1
- package/dist/storage/index.d.ts +4 -2
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +5 -1
- package/dist/storage/index.js.map +1 -1
- package/dist/storage/queue.d.ts +67 -0
- package/dist/storage/queue.d.ts.map +1 -0
- package/dist/storage/queue.js +85 -0
- package/dist/storage/queue.js.map +1 -0
- package/dist/storage/synced-registry.d.ts +18 -2
- package/dist/storage/synced-registry.d.ts.map +1 -1
- package/dist/storage/synced-registry.js +37 -3
- package/dist/storage/synced-registry.js.map +1 -1
- package/dist/storage/types.d.ts +5 -2
- package/dist/storage/types.d.ts.map +1 -1
- package/package.json +16 -6
- package/src/indexeddb/idb.ts +190 -0
- package/src/indexeddb/program-store.ts +80 -0
- package/src/indexeddb/queue-store.ts +80 -0
- package/src/indexeddb.ts +19 -0
- package/src/program-store/file.ts +5 -2
- package/src/storage/api.ts +50 -10
- package/src/storage/factory.ts +21 -4
- package/src/storage/file-queue.ts +103 -0
- package/src/storage/hybrid.ts +1151 -275
- package/src/storage/index.ts +15 -2
- package/src/storage/queue.ts +128 -0
- package/src/storage/synced-registry.ts +49 -4
- package/src/storage/types.ts +6 -2
package/src/storage/index.ts
CHANGED
|
@@ -36,8 +36,21 @@ export type { SerializeOptions, DeserializeOptions } from "./serialization.js";
|
|
|
36
36
|
// The collection storages that need a Client (before: "@procedures/client/collections")
|
|
37
37
|
export { ApiStorage } from "./api.js";
|
|
38
38
|
export type { ApiStorageOptions } from "./api.js";
|
|
39
|
-
export {
|
|
40
|
-
|
|
39
|
+
export {
|
|
40
|
+
HybridStorage,
|
|
41
|
+
PendingWriteError,
|
|
42
|
+
QueueFullError,
|
|
43
|
+
MissingKeysError,
|
|
44
|
+
QueueStoreError,
|
|
45
|
+
ConflictError,
|
|
46
|
+
ModeSwitchError,
|
|
47
|
+
} from "./hybrid.js";
|
|
48
|
+
export type { HybridStorageOptions, HybridMode, LocalCopyStorage } from "./hybrid.js";
|
|
49
|
+
// The queue of HybridStorage and its store (roadmap R28). FileQueueStore is in
|
|
50
|
+
// "@procedures/store/file-queue": it uses node:fs. IndexedDbQueueStore is in
|
|
51
|
+
// "@procedures/store/indexeddb": it uses the IndexedDB of a browser (roadmap R45).
|
|
52
|
+
export { MemoryQueueStore } from "./queue.js";
|
|
53
|
+
export type { QueuedOperation, QueueStore } from "./queue.js";
|
|
41
54
|
|
|
42
55
|
// Adapter
|
|
43
56
|
export { ProcedureStorageAdapter } from "./adapter.js";
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The queue of `HybridStorage` and the store that keeps it (roadmap R28).
|
|
3
|
+
*
|
|
4
|
+
* A write that waits for the remote is a `QueuedOperation`. The storage keeps the queue in
|
|
5
|
+
* memory and writes each change to a `QueueStore` before it acknowledges the write. A new storage
|
|
6
|
+
* with the same store loads the queue again, so a write that waits survives a restart.
|
|
7
|
+
*
|
|
8
|
+
* - `MemoryQueueStore` keeps the queue in memory. It is the default. A new storage that gets the
|
|
9
|
+
* same instance loads its queue, so the tests can simulate a restart.
|
|
10
|
+
* - `FileQueueStore` keeps the queue in a JSON Lines file. It is in `@procedures/store/file-queue`,
|
|
11
|
+
* because it uses `node:fs`. The root of the package stays usable in a browser.
|
|
12
|
+
* - `IndexedDbQueueStore` keeps the queue in IndexedDB, for a browser (roadmap R45). It is in
|
|
13
|
+
* `@procedures/store/indexeddb`, and it implements the four methods of `QueueStore` on one
|
|
14
|
+
* object store.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** A write that waits in the queue for the remote. Each field is JSON. */
|
|
18
|
+
export interface QueuedOperation {
|
|
19
|
+
/** The kind of the write. */
|
|
20
|
+
type: "set" | "delete" | "setBatch" | "deleteBatch" | "clear";
|
|
21
|
+
/**
|
|
22
|
+
* The arguments of the write: `{ id, value }` for set, `{ id }` for delete,
|
|
23
|
+
* `{ items: [[id, value]] }` for setBatch, `{ ids }` for deleteBatch, `{}` for clear.
|
|
24
|
+
*/
|
|
25
|
+
data: unknown;
|
|
26
|
+
/** The time of the write, in milliseconds since 1970. */
|
|
27
|
+
timestamp: number;
|
|
28
|
+
/**
|
|
29
|
+
* The remote version that the write changed, for each id with a known version. `null`: the
|
|
30
|
+
* remote had no item. Only a storage with `versionOf` writes it. A conflict test after a restart
|
|
31
|
+
* reads it.
|
|
32
|
+
*/
|
|
33
|
+
base?: Array<[string, string | null]>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The store that keeps the queue of a `HybridStorage` (roadmap R28). The storage calls the
|
|
38
|
+
* methods one at a time, in the order of the changes.
|
|
39
|
+
*/
|
|
40
|
+
export interface QueueStore {
|
|
41
|
+
/** This method gives the operations that the store holds, the oldest first. */
|
|
42
|
+
load(): Promise<QueuedOperation[]>;
|
|
43
|
+
/** This method adds an operation at the end. */
|
|
44
|
+
append(operation: QueuedOperation): Promise<void>;
|
|
45
|
+
/** This method puts these operations in the place of all operations. */
|
|
46
|
+
replace(operations: QueuedOperation[]): Promise<void>;
|
|
47
|
+
/** This method removes all operations. */
|
|
48
|
+
clear(): Promise<void>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A queue store in memory: the default. Its queue lives as long as the instance. */
|
|
52
|
+
export class MemoryQueueStore implements QueueStore {
|
|
53
|
+
private operations: QueuedOperation[] = [];
|
|
54
|
+
|
|
55
|
+
load(): Promise<QueuedOperation[]> {
|
|
56
|
+
return Promise.resolve(this.operations.slice());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
append(operation: QueuedOperation): Promise<void> {
|
|
60
|
+
this.operations.push(operation);
|
|
61
|
+
return Promise.resolve();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
replace(operations: QueuedOperation[]): Promise<void> {
|
|
65
|
+
this.operations = operations.slice();
|
|
66
|
+
return Promise.resolve();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
clear(): Promise<void> {
|
|
70
|
+
this.operations = [];
|
|
71
|
+
return Promise.resolve();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The number of operations that the store holds. */
|
|
75
|
+
get size(): number {
|
|
76
|
+
return this.operations.length;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The ids that an operation changes ("*" for clear). */
|
|
81
|
+
export function idsOf(op: QueuedOperation): string[] {
|
|
82
|
+
const data = op.data as { id?: string; ids?: string[]; items?: Array<[string, unknown]> };
|
|
83
|
+
switch (op.type) {
|
|
84
|
+
case "set":
|
|
85
|
+
case "delete":
|
|
86
|
+
return data.id === undefined ? [] : [data.id];
|
|
87
|
+
case "setBatch":
|
|
88
|
+
return (data.items ?? []).map(([id]) => id);
|
|
89
|
+
case "deleteBatch":
|
|
90
|
+
return data.ids ?? [];
|
|
91
|
+
case "clear":
|
|
92
|
+
return ["*"];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The operation without the id, or undefined when nothing stays. A clear cannot lose an id: it
|
|
98
|
+
* stays as it is.
|
|
99
|
+
*/
|
|
100
|
+
export function withoutId(op: QueuedOperation, id: string): QueuedOperation | undefined {
|
|
101
|
+
const data = op.data as { id?: string; ids?: string[]; items?: Array<[string, unknown]> };
|
|
102
|
+
switch (op.type) {
|
|
103
|
+
case "set":
|
|
104
|
+
case "delete":
|
|
105
|
+
return data.id === id ? undefined : op;
|
|
106
|
+
case "setBatch": {
|
|
107
|
+
const items = (data.items ?? []).filter(([itemId]) => itemId !== id);
|
|
108
|
+
return items.length === 0 ? undefined : { ...op, data: { items } };
|
|
109
|
+
}
|
|
110
|
+
case "deleteBatch": {
|
|
111
|
+
const ids = (data.ids ?? []).filter((other) => other !== id);
|
|
112
|
+
return ids.length === 0 ? undefined : { ...op, data: { ids } };
|
|
113
|
+
}
|
|
114
|
+
case "clear":
|
|
115
|
+
return op;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** True when a value has the shape of a queued operation. A loaded file can hold anything. */
|
|
120
|
+
export function isQueuedOperation(value: unknown): value is QueuedOperation {
|
|
121
|
+
if (typeof value !== "object" || value === null) return false;
|
|
122
|
+
const { type, data, timestamp } = value as Partial<QueuedOperation>;
|
|
123
|
+
return (
|
|
124
|
+
(type === "set" || type === "delete" || type === "setBatch" || type === "deleteBatch" || type === "clear") &&
|
|
125
|
+
typeof data === "object" && data !== null &&
|
|
126
|
+
typeof timestamp === "number"
|
|
127
|
+
);
|
|
128
|
+
}
|
|
@@ -21,6 +21,19 @@ import type {
|
|
|
21
21
|
} from "./types.js";
|
|
22
22
|
import { ProcedureStorageAdapter } from "./adapter.js";
|
|
23
23
|
import { serializeProcedure, deserializeProcedure, getSerializedKey } from "./serialization.js";
|
|
24
|
+
import { optionalMs, toMs, type Duration } from "@procedures/client";
|
|
25
|
+
import type { HybridMode } from "./hybrid.js";
|
|
26
|
+
|
|
27
|
+
/** A storage with modes: `HybridStorage` (roadmap R28). */
|
|
28
|
+
interface StorageWithModes {
|
|
29
|
+
getMode(): HybridMode;
|
|
30
|
+
setMode(mode: HybridMode): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function hasModes(storage: unknown): storage is StorageWithModes {
|
|
34
|
+
const candidate = storage as Partial<StorageWithModes> | null;
|
|
35
|
+
return typeof candidate?.setMode === "function" && typeof candidate.getMode === "function";
|
|
36
|
+
}
|
|
24
37
|
|
|
25
38
|
// =============================================================================
|
|
26
39
|
// Extended Registration Options
|
|
@@ -100,7 +113,10 @@ export function getProcedureStore(registry: ProcedureRegistry): SyncedProcedureR
|
|
|
100
113
|
export class SyncedProcedureRegistry {
|
|
101
114
|
private readonly baseRegistry: ProcedureRegistry;
|
|
102
115
|
private readonly adapter: ProcedureStorageAdapter;
|
|
103
|
-
|
|
116
|
+
/** The storage of the records: `setMode` changes its mode */
|
|
117
|
+
private readonly storage: CollectionStorage<SerializedProcedure>;
|
|
118
|
+
/** The options, with the sync interval in milliseconds */
|
|
119
|
+
private readonly options: Omit<SyncedRegistryOptions, "syncInterval"> & { syncInterval: number };
|
|
104
120
|
|
|
105
121
|
/** Pending changes for write-back strategy */
|
|
106
122
|
private pendingChanges: Map<string, AnyProcedure> = new Map();
|
|
@@ -142,13 +158,14 @@ export class SyncedProcedureRegistry {
|
|
|
142
158
|
options: Partial<SyncedRegistryOptions> = {}
|
|
143
159
|
) {
|
|
144
160
|
this.baseRegistry = baseRegistry;
|
|
161
|
+
this.storage = storage;
|
|
145
162
|
this.adapter = new ProcedureStorageAdapter(storage, {
|
|
146
163
|
handlerLoader: options.handlerLoader,
|
|
147
164
|
});
|
|
148
165
|
this.options = {
|
|
149
166
|
writeStrategy: options.writeStrategy ?? "write-through",
|
|
150
167
|
conflictResolution: options.conflictResolution ?? "local",
|
|
151
|
-
syncInterval: options.syncInterval ?? 0,
|
|
168
|
+
syncInterval: optionalMs(options.syncInterval, "syncInterval") ?? 0,
|
|
152
169
|
handlerLoader: options.handlerLoader,
|
|
153
170
|
syncOnInit: options.syncOnInit ?? false,
|
|
154
171
|
};
|
|
@@ -494,9 +511,35 @@ export class SyncedProcedureRegistry {
|
|
|
494
511
|
lastSync: this.lastSyncTime,
|
|
495
512
|
pendingChanges: this.pendingChanges.size + this.pendingDeletes.size,
|
|
496
513
|
syncing: this.syncing,
|
|
514
|
+
mode: this.getMode(),
|
|
497
515
|
};
|
|
498
516
|
}
|
|
499
517
|
|
|
518
|
+
// ===========================================================================
|
|
519
|
+
// The mode of the storage (roadmap R28)
|
|
520
|
+
// ===========================================================================
|
|
521
|
+
|
|
522
|
+
/** The mode of the storage, or undefined when the storage has no modes. */
|
|
523
|
+
getMode(): HybridMode | undefined {
|
|
524
|
+
return hasModes(this.storage) ? this.storage.getMode() : undefined;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* This method changes the mode of the storage while the registry operates (roadmap R28). The registry
|
|
529
|
+
* waits for its write-through stores that run, then `HybridStorage.setMode()` changes the
|
|
530
|
+
* mode: it sends the queued records before the change.
|
|
531
|
+
*
|
|
532
|
+
* @param mode - `local-only`, `write-through`, `write-back` or `remote-only`
|
|
533
|
+
* @throws When the storage has no modes, or when the switch fails (`ModeSwitchError`)
|
|
534
|
+
*/
|
|
535
|
+
async setMode(mode: HybridMode): Promise<void> {
|
|
536
|
+
if (!hasModes(this.storage)) {
|
|
537
|
+
throw new Error('The storage of this registry has no modes: use createSyncedRegistry({ type: "hybrid" })');
|
|
538
|
+
}
|
|
539
|
+
await this.flushWrites();
|
|
540
|
+
await this.storage.setMode(mode);
|
|
541
|
+
}
|
|
542
|
+
|
|
500
543
|
// ===========================================================================
|
|
501
544
|
// Auto-Sync
|
|
502
545
|
// ===========================================================================
|
|
@@ -504,9 +547,11 @@ export class SyncedProcedureRegistry {
|
|
|
504
547
|
/**
|
|
505
548
|
* Start auto-sync at specified interval.
|
|
506
549
|
*
|
|
507
|
-
* @param
|
|
550
|
+
* @param interval - The sync interval
|
|
508
551
|
*/
|
|
509
|
-
startAutoSync(
|
|
552
|
+
startAutoSync(interval: Duration | number): void {
|
|
553
|
+
// A number is the internal form of the option, in milliseconds
|
|
554
|
+
const intervalMs = typeof interval === "number" ? interval : toMs(interval, "interval");
|
|
510
555
|
this.stopAutoSync();
|
|
511
556
|
const handle = setInterval(() => {
|
|
512
557
|
void this.sync("both").catch((err) => {
|
package/src/storage/types.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { ProcedurePath, ProcedureMetadata } from "@procedures/client";
|
|
9
|
+
import type { Duration } from "@procedures/client";
|
|
9
10
|
|
|
10
11
|
// =============================================================================
|
|
11
12
|
// Serialized Procedure
|
|
@@ -136,6 +137,9 @@ export interface SyncStatus {
|
|
|
136
137
|
|
|
137
138
|
/** Whether currently syncing */
|
|
138
139
|
syncing: boolean;
|
|
140
|
+
|
|
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;
|
|
139
143
|
}
|
|
140
144
|
|
|
141
145
|
// =============================================================================
|
|
@@ -152,8 +156,8 @@ export interface SyncedRegistryOptions {
|
|
|
152
156
|
/** How to resolve conflicts when loading from storage */
|
|
153
157
|
conflictResolution: ConflictResolution;
|
|
154
158
|
|
|
155
|
-
/** Auto-sync interval
|
|
156
|
-
syncInterval?:
|
|
159
|
+
/** Auto-sync interval, for example `minutes(1)` (0 to disable) */
|
|
160
|
+
syncInterval?: Duration | undefined;
|
|
157
161
|
|
|
158
162
|
/** Handler loader for restoring handlers from references */
|
|
159
163
|
handlerLoader?: HandlerLoader | undefined;
|