@procedures/collections 0.8.0 → 0.9.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 +41 -0
- package/README.md +104 -10
- package/dist/.tsbuildinfo +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/overlay/index.d.ts +6 -0
- package/dist/overlay/index.d.ts.map +1 -0
- package/dist/overlay/index.js +6 -0
- package/dist/overlay/index.js.map +1 -0
- package/dist/overlay/overlay.d.ts +55 -0
- package/dist/overlay/overlay.d.ts.map +1 -0
- package/dist/overlay/overlay.js +82 -0
- package/dist/overlay/overlay.js.map +1 -0
- package/dist/overlay/stacks.d.ts +44 -0
- package/dist/overlay/stacks.d.ts.map +1 -0
- package/dist/overlay/stacks.js +43 -0
- package/dist/overlay/stacks.js.map +1 -0
- package/dist/storage/index.d.ts +13 -8
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +13 -7
- package/dist/storage/index.js.map +1 -1
- package/dist/storage/interface.d.ts +105 -13
- package/dist/storage/interface.d.ts.map +1 -1
- package/dist/storage/interface.js +10 -8
- package/dist/storage/interface.js.map +1 -1
- package/dist/storage/listeners.d.ts +39 -0
- package/dist/storage/listeners.d.ts.map +1 -0
- package/dist/storage/listeners.js +65 -0
- package/dist/storage/listeners.js.map +1 -0
- package/dist/storage/memory.d.ts +46 -6
- package/dist/storage/memory.d.ts.map +1 -1
- package/dist/storage/memory.js +81 -10
- package/dist/storage/memory.js.map +1 -1
- package/dist/testing/storage-contract.d.ts +13 -0
- package/dist/testing/storage-contract.d.ts.map +1 -1
- package/dist/testing/storage-contract.js +472 -0
- package/dist/testing/storage-contract.js.map +1 -1
- package/package.json +4 -1
- package/src/index.ts +3 -0
- package/src/overlay/index.ts +15 -0
- package/src/overlay/overlay.ts +119 -0
- package/src/overlay/stacks.ts +76 -0
- package/src/storage/index.ts +54 -8
- package/src/storage/interface.ts +117 -13
- package/src/storage/listeners.ts +72 -0
- package/src/storage/memory.ts +110 -11
- package/src/testing/storage-contract.ts +522 -2
package/src/storage/interface.ts
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The interface of the collection storages. These storages implement it:
|
|
3
|
+
* - `InMemoryStorage` (this package): a fast storage in a `Map`, in memory only.
|
|
4
|
+
* - `ApiStorage` (`@procedures/store`): a remote collection through a `Client`.
|
|
5
|
+
* - `HybridStorage` (`@procedures/store`): a local copy that syncs with a remote, with a
|
|
6
|
+
* conflict policy and a queue of the local writes.
|
|
7
|
+
* - `MongoStorage` (`@procedures/mongo`).
|
|
3
8
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* - ApiStorage: Remote persistence via universal client
|
|
7
|
-
* - HybridStorage: Local cache + remote sync with conflict resolution
|
|
8
|
-
* - SyncedStorage: Periodic background sync with offline support
|
|
9
|
+
* `@procedures/client` declares a copy of this interface with the same shape, so that the core
|
|
10
|
+
* does not depend on this package. `SqliteStorage` (`@procedures/sqlite`) implements that copy.
|
|
9
11
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
+
* Each method gives a `Promise`. Thus the code of a caller cannot tell a local storage from a
|
|
13
|
+
* remote storage.
|
|
12
14
|
*/
|
|
13
15
|
|
|
16
|
+
// The query language is a part of the wire contract of the core: its one copy is there (roadmap CLI-34)
|
|
17
|
+
import type { StoragePatch, StorageQueryOptions, StorageQueryPolicy, StorageSelector } from "@procedures/client/query";
|
|
18
|
+
|
|
14
19
|
/**
|
|
15
20
|
* Storage backend interface for collections.
|
|
16
21
|
*
|
|
@@ -26,6 +31,11 @@
|
|
|
26
31
|
* - A delta of `getChangesSince` follows the order of the last change of each id.
|
|
27
32
|
* - After `close()`, each operation rejects with an error whose message says "closed". A second
|
|
28
33
|
* `close()` does nothing.
|
|
34
|
+
* - `query()` gives the result of `queryEntries()` for the entries of the storage.
|
|
35
|
+
* - `observe()` gives one batch for each write that changes the storage, before the write resolves.
|
|
36
|
+
* - `patch()` changes the item in one step: two patches at the same time keep each field.
|
|
37
|
+
*
|
|
38
|
+
* The optional methods are features: a caller tests for each one before it uses it.
|
|
29
39
|
*/
|
|
30
40
|
export interface CollectionStorage<T> {
|
|
31
41
|
//
|
|
@@ -82,6 +92,37 @@ export interface CollectionStorage<T> {
|
|
|
82
92
|
*/
|
|
83
93
|
find(predicate: (item: T) => boolean): Promise<T[]>;
|
|
84
94
|
|
|
95
|
+
/**
|
|
96
|
+
* The entries whose items match a selector, with an order, a skip and a limit. The selector is
|
|
97
|
+
* JSON, so a remote storage sends it to its server, and a database can run it itself. The result
|
|
98
|
+
* is the result of `queryEntries()` for the entries of the storage. A storage without this method
|
|
99
|
+
* does not run selectors: a caller uses `queryEntries()` on the entries.
|
|
100
|
+
*
|
|
101
|
+
* @param selector - The selector. `{}` matches each item.
|
|
102
|
+
* @param options - The order, the skip and the limit
|
|
103
|
+
* @param policy - The query policy of the caller: the procedure `find` gives the policy of the
|
|
104
|
+
* host. Without it, the storage uses its own policy. The default policy does not permit `$regex`.
|
|
105
|
+
* @returns Promise resolving to the [id, item] pairs, with copies of the items. A selector or
|
|
106
|
+
* options with a problem, or that the policy does not permit, reject with a `TypeError`.
|
|
107
|
+
*/
|
|
108
|
+
query?(selector: StorageSelector, options?: StorageQueryOptions, policy?: StorageQueryPolicy): Promise<Array<[string, T]>>;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* This method starts to send the changes of the storage to an observer. Each write that changes
|
|
112
|
+
* the storage gives one batch, before the write resolves. A write that changes nothing gives no
|
|
113
|
+
* batch. A storage without this method does not tell its changes.
|
|
114
|
+
*
|
|
115
|
+
* - A set gives a `set` change with a copy of the value. A patch gives a `set` change with the new item.
|
|
116
|
+
* - A delete gives a `delete` change for each id that existed.
|
|
117
|
+
* - A clear of a storage with items gives one `clear` change.
|
|
118
|
+
* - Each change has the `origin` of the write options, or no origin.
|
|
119
|
+
*
|
|
120
|
+
* @param observer - The function that gets each batch. Its error does not change the write.
|
|
121
|
+
* @returns The function that stops the batches. A second use of it does nothing.
|
|
122
|
+
* @throws Error after `close()`
|
|
123
|
+
*/
|
|
124
|
+
observe?(observer: StorageObserver<T>): () => void;
|
|
125
|
+
|
|
85
126
|
/**
|
|
86
127
|
* Check if item exists.
|
|
87
128
|
*
|
|
@@ -106,24 +147,42 @@ export interface CollectionStorage<T> {
|
|
|
106
147
|
*
|
|
107
148
|
* @param id - Item identifier
|
|
108
149
|
* @param value - Item to store
|
|
150
|
+
* @param options - The origin of the write, for the observers
|
|
109
151
|
* @returns Promise resolving when operation completes
|
|
110
152
|
*/
|
|
111
|
-
set(id: string, value: T): Promise<void>;
|
|
153
|
+
set(id: string, value: T, options?: StorageWriteOptions): Promise<void>;
|
|
112
154
|
|
|
113
155
|
/**
|
|
114
156
|
* Delete item by ID.
|
|
115
157
|
*
|
|
116
158
|
* @param id - Item identifier
|
|
159
|
+
* @param options - The origin of the write, for the observers
|
|
117
160
|
* @returns Promise resolving to true if item was deleted, false if not found
|
|
118
161
|
*/
|
|
119
|
-
delete(id: string): Promise<boolean>;
|
|
162
|
+
delete(id: string, options?: StorageWriteOptions): Promise<boolean>;
|
|
120
163
|
|
|
121
164
|
/**
|
|
122
165
|
* Clear all items from collection.
|
|
123
166
|
*
|
|
167
|
+
* @param options - The origin of the write, for the observers
|
|
124
168
|
* @returns Promise resolving when operation completes
|
|
125
169
|
*/
|
|
126
|
-
clear(): Promise<void>;
|
|
170
|
+
clear(options?: StorageWriteOptions): Promise<void>;
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* This method changes one item in one step. The fields are a JSON Merge Patch (RFC 7386): each
|
|
174
|
+
* field replaces the field of the item, an object field merges into an object field, and `null`
|
|
175
|
+
* removes the field. Or they are the operations `{ $set, $unset }`: `$set` can set `null` and
|
|
176
|
+
* nested field paths. Two patches of one item at the same time keep the fields of each. A storage
|
|
177
|
+
* without this method cannot change an item in one step.
|
|
178
|
+
*
|
|
179
|
+
* @param id - Item identifier
|
|
180
|
+
* @param fields - The fields: an object of one form. Another value rejects with a `TypeError`.
|
|
181
|
+
* @param options - The origin of the write, for the observers
|
|
182
|
+
* @returns Promise resolving to a copy of the new item, or undefined when the id has no item. A
|
|
183
|
+
* missing id stays missing.
|
|
184
|
+
*/
|
|
185
|
+
patch?(id: string, fields: StoragePatch, options?: StorageWriteOptions): Promise<T | undefined>;
|
|
127
186
|
|
|
128
187
|
//
|
|
129
188
|
// ═══ Bulk Operations ═══
|
|
@@ -133,17 +192,19 @@ export interface CollectionStorage<T> {
|
|
|
133
192
|
* Set multiple items at once (more efficient than individual sets).
|
|
134
193
|
*
|
|
135
194
|
* @param items - Array of [id, value] tuples
|
|
195
|
+
* @param options - The origin of the write, for the observers
|
|
136
196
|
* @returns Promise resolving when operation completes
|
|
137
197
|
*/
|
|
138
|
-
setBatch(items: Array<[string, T]
|
|
198
|
+
setBatch(items: Array<[string, T]>, options?: StorageWriteOptions): Promise<void>;
|
|
139
199
|
|
|
140
200
|
/**
|
|
141
201
|
* Delete multiple items at once.
|
|
142
202
|
*
|
|
143
203
|
* @param ids - Array of item identifiers
|
|
204
|
+
* @param options - The origin of the write, for the observers
|
|
144
205
|
* @returns Promise resolving to number of items actually deleted
|
|
145
206
|
*/
|
|
146
|
-
deleteBatch(ids: string[]): Promise<number>;
|
|
207
|
+
deleteBatch(ids: string[], options?: StorageWriteOptions): Promise<number>;
|
|
147
208
|
|
|
148
209
|
/**
|
|
149
210
|
* Get multiple items by ID at once.
|
|
@@ -183,6 +244,49 @@ export interface CollectionStorage<T> {
|
|
|
183
244
|
getMetadata?(): Promise<StorageMetadata>;
|
|
184
245
|
}
|
|
185
246
|
|
|
247
|
+
/** The options of a write. */
|
|
248
|
+
export interface StorageWriteOptions {
|
|
249
|
+
/**
|
|
250
|
+
* The tag of the writer. Each change that the write gives has it, so an observer knows its own
|
|
251
|
+
* writes: a sync does not send back the changes that it wrote itself.
|
|
252
|
+
*/
|
|
253
|
+
readonly origin?: string | undefined;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** A change of `observe()`: an id got a value. */
|
|
257
|
+
export interface StorageSetChange<T> {
|
|
258
|
+
readonly type: "set";
|
|
259
|
+
readonly id: string;
|
|
260
|
+
/** A copy of the new item. */
|
|
261
|
+
readonly value: T;
|
|
262
|
+
/** The origin of the write, or undefined when the write had none. */
|
|
263
|
+
readonly origin?: string | undefined;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** A change of `observe()`: an id lost its item. */
|
|
267
|
+
export interface StorageDeleteChange {
|
|
268
|
+
readonly type: "delete";
|
|
269
|
+
readonly id: string;
|
|
270
|
+
/** The origin of the write, or undefined when the write had none. */
|
|
271
|
+
readonly origin?: string | undefined;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** A change of `observe()`: the storage lost each item. */
|
|
275
|
+
export interface StorageClearChange {
|
|
276
|
+
readonly type: "clear";
|
|
277
|
+
/** The origin of the write, or undefined when the write had none. */
|
|
278
|
+
readonly origin?: string | undefined;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** One change of `observe()`. */
|
|
282
|
+
export type StorageChangeEvent<T> = StorageSetChange<T> | StorageDeleteChange | StorageClearChange;
|
|
283
|
+
|
|
284
|
+
/** The changes of one write, in the order of the write. The observers share it: do not change it. */
|
|
285
|
+
export type StorageChangeBatch<T> = readonly StorageChangeEvent<T>[];
|
|
286
|
+
|
|
287
|
+
/** The function that gets each batch of `observe()`. */
|
|
288
|
+
export type StorageObserver<T> = (changes: StorageChangeBatch<T>) => void;
|
|
289
|
+
|
|
186
290
|
/**
|
|
187
291
|
* The changes of a storage after a version (roadmap R28).
|
|
188
292
|
*
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A set of listeners for the events of a storage: the change batches of `observe()`, or the status
|
|
3
|
+
* of a sync. A storage package uses it, so each storage treats its listeners in the same way.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** The function that gets the error of a listener. */
|
|
7
|
+
export type ListenerErrorHandler = (error: unknown) => void;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The default handler of the error of a listener. It throws the error again in a microtask, as an
|
|
11
|
+
* `EventTarget` reports the error of a listener. Thus the error is not lost, and the write that
|
|
12
|
+
* sent the event does not fail.
|
|
13
|
+
*/
|
|
14
|
+
export const reportListenerError: ListenerErrorHandler = (error) => {
|
|
15
|
+
queueMicrotask(() => {
|
|
16
|
+
throw error;
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The listeners of one kind of event.
|
|
22
|
+
*
|
|
23
|
+
* - `emit()` gives the event to each listener, in the order of `add()`.
|
|
24
|
+
* - A listener that `add()` gets during an `emit()` gets the next event, not this one.
|
|
25
|
+
* - The error of a listener goes to the error handler. The other listeners get the event.
|
|
26
|
+
* - The same function can be added two times. Each `add()` gives its own function that removes it.
|
|
27
|
+
*/
|
|
28
|
+
export class StorageListeners<E> {
|
|
29
|
+
private readonly listeners = new Set<{ readonly listener: (event: E) => void }>();
|
|
30
|
+
private readonly onError: ListenerErrorHandler;
|
|
31
|
+
|
|
32
|
+
/** @param onError - The function that gets the error of a listener. The default is `reportListenerError`. */
|
|
33
|
+
constructor(onError: ListenerErrorHandler = reportListenerError) {
|
|
34
|
+
this.onError = onError;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The number of listeners. */
|
|
38
|
+
get size(): number {
|
|
39
|
+
return this.listeners.size;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* This method adds a listener.
|
|
44
|
+
*
|
|
45
|
+
* @returns The function that removes the listener. A second use of it does nothing.
|
|
46
|
+
*/
|
|
47
|
+
add(listener: (event: E) => void): () => void {
|
|
48
|
+
const entry = { listener };
|
|
49
|
+
this.listeners.add(entry);
|
|
50
|
+
return () => {
|
|
51
|
+
this.listeners.delete(entry);
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** This method gives the event to each listener. */
|
|
56
|
+
emit(event: E): void {
|
|
57
|
+
for (const entry of [...this.listeners]) {
|
|
58
|
+
// A listener that a listener before it removed gets no event
|
|
59
|
+
if (!this.listeners.has(entry)) continue;
|
|
60
|
+
try {
|
|
61
|
+
entry.listener(event);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
this.onError(error);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** This method removes each listener. */
|
|
69
|
+
clear(): void {
|
|
70
|
+
this.listeners.clear();
|
|
71
|
+
}
|
|
72
|
+
}
|
package/src/storage/memory.ts
CHANGED
|
@@ -5,10 +5,40 @@
|
|
|
5
5
|
* All operations return Promises for interface consistency.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
CollectionStorage,
|
|
10
|
+
StorageChangeBatch,
|
|
11
|
+
StorageChangeEvent,
|
|
12
|
+
StorageChanges,
|
|
13
|
+
StorageMetadata,
|
|
14
|
+
StorageObserver,
|
|
15
|
+
StorageWriteOptions,
|
|
16
|
+
} from "./interface.js";
|
|
17
|
+
import { StorageListeners, type ListenerErrorHandler } from "./listeners.js";
|
|
18
|
+
import {
|
|
19
|
+
applyPatch,
|
|
20
|
+
patchProblems,
|
|
21
|
+
queryEntries,
|
|
22
|
+
type StoragePatch,
|
|
23
|
+
type StorageQueryOptions,
|
|
24
|
+
type StorageQueryPolicy,
|
|
25
|
+
type StorageSelector,
|
|
26
|
+
} from "@procedures/client/query";
|
|
9
27
|
|
|
10
28
|
/** Options of an in-memory storage. */
|
|
11
29
|
export interface InMemoryStorageOptions {
|
|
30
|
+
/**
|
|
31
|
+
* The function that gets the error of an observer of `observe()`. The default throws the error
|
|
32
|
+
* again in a microtask (`reportListenerError`). The write and the other observers do not fail.
|
|
33
|
+
*/
|
|
34
|
+
onObserverError?: ListenerErrorHandler | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* The query policy of `query()` for a call without a policy: the operators and the limits. The
|
|
37
|
+
* procedure `find` gives the policy of its host. A storage that gets selectors of callers that it
|
|
38
|
+
* does not trust keeps the default.
|
|
39
|
+
* @default DEFAULT_QUERY_POLICY: no `$regex`
|
|
40
|
+
*/
|
|
41
|
+
queryPolicy?: StorageQueryPolicy | undefined;
|
|
12
42
|
/**
|
|
13
43
|
* The number of deletes that the change log keeps (roadmap R28). After more deletes, the log
|
|
14
44
|
* drops the oldest ones, and `getChangesSince` of an older version gives all items.
|
|
@@ -37,6 +67,11 @@ type Copy = <V>(value: V) => V;
|
|
|
37
67
|
const same: Copy = (value) => value;
|
|
38
68
|
const clone: Copy = (value) => structuredClone(value);
|
|
39
69
|
|
|
70
|
+
/** A change with the origin of the write, when the write has one. */
|
|
71
|
+
function withOrigin<T>(change: StorageChangeEvent<T>, options: StorageWriteOptions | undefined): StorageChangeEvent<T> {
|
|
72
|
+
return options?.origin === undefined ? change : { ...change, origin: options.origin };
|
|
73
|
+
}
|
|
74
|
+
|
|
40
75
|
/**
|
|
41
76
|
* In-memory storage backed by JavaScript Map.
|
|
42
77
|
*
|
|
@@ -50,6 +85,8 @@ const clone: Copy = (value) => structuredClone(value);
|
|
|
50
85
|
* - Copies: a change of a written or a read value does not change the stored item (the option
|
|
51
86
|
* `copies: false` turns this off)
|
|
52
87
|
* - `getIds()` gives the ids without a copy of the values
|
|
88
|
+
* - `query()` runs a selector, `observe()` tells each change with the origin of its write, and
|
|
89
|
+
* `patch()` merges fields into an item in one step
|
|
53
90
|
* - After `close()`, each operation rejects, and a second `close()` does nothing
|
|
54
91
|
*
|
|
55
92
|
* @example
|
|
@@ -75,10 +112,16 @@ export class InMemoryStorage<T> implements CollectionStorage<T> {
|
|
|
75
112
|
private readonly maxTombstones: number;
|
|
76
113
|
private readonly copy: Copy;
|
|
77
114
|
private closed = false;
|
|
115
|
+
/** The observers of `observe()` */
|
|
116
|
+
private readonly observers: StorageListeners<StorageChangeBatch<T>>;
|
|
117
|
+
/** The query policy of a call without a policy */
|
|
118
|
+
private readonly queryPolicy: StorageQueryPolicy | undefined;
|
|
78
119
|
|
|
79
120
|
constructor(options: InMemoryStorageOptions = {}) {
|
|
80
121
|
this.maxTombstones = Math.max(0, options.maxTombstones ?? 10_000);
|
|
81
122
|
this.copy = options.copies === false ? same : clone;
|
|
123
|
+
this.observers = new StorageListeners<StorageChangeBatch<T>>(options.onObserverError);
|
|
124
|
+
this.queryPolicy = options.queryPolicy;
|
|
82
125
|
}
|
|
83
126
|
|
|
84
127
|
//
|
|
@@ -142,6 +185,26 @@ export class InMemoryStorage<T> implements CollectionStorage<T> {
|
|
|
142
185
|
return results;
|
|
143
186
|
}
|
|
144
187
|
|
|
188
|
+
/**
|
|
189
|
+
* The entries whose items match the selector, with the order, the skip and the limit of
|
|
190
|
+
* `queryEntries()`, under the policy of the call or of the options. The result has copies of the
|
|
191
|
+
* items.
|
|
192
|
+
*/
|
|
193
|
+
async query(selector: StorageSelector, options?: StorageQueryOptions, policy?: StorageQueryPolicy): Promise<Array<[string, T]>> {
|
|
194
|
+
this.checkOpen();
|
|
195
|
+
return queryEntries(this.data, selector, options, policy ?? this.queryPolicy).map(([id, value]): [string, T] => [id, this.copy(value)]);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* This method starts to send the changes of each write to the observer, before the write resolves.
|
|
200
|
+
* The values of the changes are copies, unless the option `copies` is false. The observers share
|
|
201
|
+
* each batch.
|
|
202
|
+
*/
|
|
203
|
+
observe(observer: StorageObserver<T>): () => void {
|
|
204
|
+
this.checkOpen();
|
|
205
|
+
return this.observers.add(observer);
|
|
206
|
+
}
|
|
207
|
+
|
|
145
208
|
async has(id: string): Promise<boolean> {
|
|
146
209
|
this.checkOpen();
|
|
147
210
|
return this.data.has(id);
|
|
@@ -156,23 +219,47 @@ export class InMemoryStorage<T> implements CollectionStorage<T> {
|
|
|
156
219
|
// ═══ Write Operations ═══
|
|
157
220
|
//
|
|
158
221
|
|
|
159
|
-
async set(id: string, value: T): Promise<void> {
|
|
222
|
+
async set(id: string, value: T, options?: StorageWriteOptions): Promise<void> {
|
|
160
223
|
this.checkOpen();
|
|
161
|
-
|
|
224
|
+
const stored = this.copy(value);
|
|
225
|
+
this.data.set(id, stored);
|
|
162
226
|
this.noteSet(id);
|
|
227
|
+
if (this.observers.size > 0) this.observers.emit([this.setChange(id, stored, options)]);
|
|
163
228
|
}
|
|
164
229
|
|
|
165
|
-
async delete(id: string): Promise<boolean> {
|
|
230
|
+
async delete(id: string, options?: StorageWriteOptions): Promise<boolean> {
|
|
166
231
|
this.checkOpen();
|
|
167
232
|
const existed = this.data.delete(id);
|
|
168
|
-
if (existed)
|
|
233
|
+
if (existed) {
|
|
234
|
+
this.noteDelete(id);
|
|
235
|
+
if (this.observers.size > 0) this.observers.emit([withOrigin<T>({ type: "delete", id }, options)]);
|
|
236
|
+
}
|
|
169
237
|
return existed;
|
|
170
238
|
}
|
|
171
239
|
|
|
172
|
-
async clear(): Promise<void> {
|
|
240
|
+
async clear(options?: StorageWriteOptions): Promise<void> {
|
|
173
241
|
this.checkOpen();
|
|
242
|
+
const had = this.data.size > 0;
|
|
174
243
|
for (const id of this.data.keys()) this.noteDelete(id);
|
|
175
244
|
this.data.clear();
|
|
245
|
+
if (had && this.observers.size > 0) this.observers.emit([withOrigin<T>({ type: "clear" }, options)]);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* This method changes the item of the id in one step (a JSON Merge Patch, or the operations
|
|
250
|
+
* `$set` and `$unset`), and gives a copy of the new item. A missing id stays missing, and the
|
|
251
|
+
* result is undefined. A patch that cannot apply to the item changes nothing.
|
|
252
|
+
*/
|
|
253
|
+
async patch(id: string, fields: StoragePatch, options?: StorageWriteOptions): Promise<T | undefined> {
|
|
254
|
+
this.checkOpen();
|
|
255
|
+
const [problem] = patchProblems(fields);
|
|
256
|
+
if (problem !== undefined) throw new TypeError(`The patch of ${id} is not correct: ${problem}`);
|
|
257
|
+
if (!this.data.has(id)) return undefined;
|
|
258
|
+
const stored = this.copy(applyPatch(this.data.get(id), fields) as unknown as T);
|
|
259
|
+
this.data.set(id, stored);
|
|
260
|
+
this.noteSet(id);
|
|
261
|
+
if (this.observers.size > 0) this.observers.emit([this.setChange(id, stored, options)]);
|
|
262
|
+
return this.copy(stored);
|
|
176
263
|
}
|
|
177
264
|
|
|
178
265
|
//
|
|
@@ -180,25 +267,31 @@ export class InMemoryStorage<T> implements CollectionStorage<T> {
|
|
|
180
267
|
//
|
|
181
268
|
|
|
182
269
|
/** This method sets several items. When one value has no copy, nothing changes. */
|
|
183
|
-
async setBatch(items: Array<[string, T]
|
|
270
|
+
async setBatch(items: Array<[string, T]>, options?: StorageWriteOptions): Promise<void> {
|
|
184
271
|
this.checkOpen();
|
|
185
272
|
const copies = items.map(([id, value]): [string, T] => [id, this.copy(value)]);
|
|
186
273
|
for (const [id, value] of copies) {
|
|
187
274
|
this.data.set(id, value);
|
|
188
275
|
this.noteSet(id);
|
|
189
276
|
}
|
|
277
|
+
if (copies.length > 0 && this.observers.size > 0) {
|
|
278
|
+
this.observers.emit(copies.map(([id, value]) => this.setChange(id, value, options)));
|
|
279
|
+
}
|
|
190
280
|
}
|
|
191
281
|
|
|
192
|
-
async deleteBatch(ids: string[]): Promise<number> {
|
|
282
|
+
async deleteBatch(ids: string[], options?: StorageWriteOptions): Promise<number> {
|
|
193
283
|
this.checkOpen();
|
|
194
|
-
|
|
284
|
+
const deleted: string[] = [];
|
|
195
285
|
for (const id of ids) {
|
|
196
286
|
if (this.data.delete(id)) {
|
|
197
287
|
this.noteDelete(id);
|
|
198
|
-
deleted
|
|
288
|
+
deleted.push(id);
|
|
199
289
|
}
|
|
200
290
|
}
|
|
201
|
-
|
|
291
|
+
if (deleted.length > 0 && this.observers.size > 0) {
|
|
292
|
+
this.observers.emit(deleted.map((id) => withOrigin<T>({ type: "delete", id }, options)));
|
|
293
|
+
}
|
|
294
|
+
return deleted.length;
|
|
202
295
|
}
|
|
203
296
|
|
|
204
297
|
async getBatch(ids: string[]): Promise<Map<string, T>> {
|
|
@@ -227,6 +320,7 @@ export class InMemoryStorage<T> implements CollectionStorage<T> {
|
|
|
227
320
|
this.data.clear();
|
|
228
321
|
this.changedAt.clear();
|
|
229
322
|
this.deletedAt.clear();
|
|
323
|
+
this.observers.clear();
|
|
230
324
|
}
|
|
231
325
|
|
|
232
326
|
async getMetadata(): Promise<StorageMetadata> {
|
|
@@ -311,6 +405,11 @@ export class InMemoryStorage<T> implements CollectionStorage<T> {
|
|
|
311
405
|
return Array.from(this.data, ([id, value]): [string, T] => [id, this.copy(value)]);
|
|
312
406
|
}
|
|
313
407
|
|
|
408
|
+
/** The `set` change of a stored value: a copy of it for the observers. */
|
|
409
|
+
private setChange(id: string, stored: T, options: StorageWriteOptions | undefined): StorageChangeEvent<T> {
|
|
410
|
+
return withOrigin<T>({ type: "set", id, value: this.copy(stored) }, options);
|
|
411
|
+
}
|
|
412
|
+
|
|
314
413
|
//
|
|
315
414
|
// ═══ The change log ═══
|
|
316
415
|
//
|