@procedures/collections 0.7.0 → 0.8.1
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 +40 -0
- package/README.md +90 -37
- package/dist/.tsbuildinfo +1 -1
- 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/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/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
|
//
|