@procedures/collections 0.8.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.
@@ -5,10 +5,40 @@
5
5
  * All operations return Promises for interface consistency.
6
6
  */
7
7
 
8
- import type { CollectionStorage, StorageChanges, StorageMetadata } from "./interface.js";
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
- this.data.set(id, this.copy(value));
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) this.noteDelete(id);
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]>): Promise<void> {
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
- let deleted = 0;
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
- return deleted;
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
  //