@procedures/collections 0.4.0 → 0.6.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 +20 -0
- package/README.md +28 -10
- package/dist/.tsbuildinfo +1 -1
- package/dist/storage/interface.d.ts +25 -4
- package/dist/storage/interface.d.ts.map +1 -1
- package/dist/storage/interface.js.map +1 -1
- package/dist/storage/memory.d.ts +38 -7
- package/dist/storage/memory.d.ts.map +1 -1
- package/dist/storage/memory.js +79 -25
- package/dist/storage/memory.js.map +1 -1
- package/dist/testing/check.d.ts +0 -2
- package/dist/testing/check.d.ts.map +1 -1
- package/dist/testing/check.js +0 -4
- package/dist/testing/check.js.map +1 -1
- package/dist/testing/storage-contract.d.ts +11 -15
- package/dist/testing/storage-contract.d.ts.map +1 -1
- package/dist/testing/storage-contract.js +167 -45
- package/dist/testing/storage-contract.js.map +1 -1
- package/package.json +1 -1
- package/src/storage/interface.ts +26 -4
- package/src/storage/memory.ts +95 -26
- package/src/testing/check.ts +0 -5
- package/src/testing/storage-contract.ts +174 -60
|
@@ -15,10 +15,14 @@
|
|
|
15
15
|
*
|
|
16
16
|
* describeCollectionStorageContract("MyStorage", { create: () => new MyStorage() }, { describe, it });
|
|
17
17
|
* ```
|
|
18
|
+
*
|
|
19
|
+
* The suite has no option that hides a difference of a storage. Each storage keeps copies, keeps
|
|
20
|
+
* a stored `null`, gives a delta in the order of the last change of each id, and rejects each
|
|
21
|
+
* operation after `close()`.
|
|
18
22
|
*/
|
|
19
23
|
|
|
20
24
|
import type { CollectionStorage, StorageChanges } from "../storage/interface.js";
|
|
21
|
-
import {
|
|
25
|
+
import { holds, rejects, same, show } from "./check.js";
|
|
22
26
|
|
|
23
27
|
/** The item of the suite. Its `id` is its key, so a `HybridStorage` can sync it. */
|
|
24
28
|
export interface ContractItem {
|
|
@@ -30,26 +34,18 @@ export interface ContractItem {
|
|
|
30
34
|
/** A storage of the suite. */
|
|
31
35
|
export type ContractStorage = CollectionStorage<ContractItem>;
|
|
32
36
|
|
|
33
|
-
/** The options of the suite: the
|
|
37
|
+
/** The options of the suite: the factories of the storage. */
|
|
34
38
|
export interface CollectionStorageContractOptions {
|
|
35
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* This function makes a new, empty storage for each case. The case closes it at its end, also
|
|
41
|
+
* when the case closed it already.
|
|
42
|
+
*/
|
|
36
43
|
create(): ContractStorage | Promise<ContractStorage>;
|
|
37
44
|
/**
|
|
38
45
|
* This function makes a new, empty storage whose change log keeps at most `max` deletes. With it,
|
|
39
46
|
* the suite tests the drop of the old deletes. A storage without such a limit does not give it.
|
|
40
47
|
*/
|
|
41
48
|
createWithMaxTombstones?: ((max: number) => ContractStorage | Promise<ContractStorage>) | undefined;
|
|
42
|
-
/**
|
|
43
|
-
* True when the storage keeps a copy of each value: a change of a value after `set`, or of a read
|
|
44
|
-
* value, does not change the stored item. The interface does not ask for copies, so the default
|
|
45
|
-
* is false, and the suite then does not test them.
|
|
46
|
-
*/
|
|
47
|
-
copies?: boolean | undefined;
|
|
48
|
-
/**
|
|
49
|
-
* False when a stored `null` reads back as `undefined`. A remote storage can lose the difference
|
|
50
|
-
* on the wire. The default is true.
|
|
51
|
-
*/
|
|
52
|
-
nullValues?: boolean | undefined;
|
|
53
49
|
}
|
|
54
50
|
|
|
55
51
|
/** The context that a test framework gives to a case. Vitest gives `skip`. */
|
|
@@ -93,7 +89,7 @@ function changesOf(storage: ContractStorage): (since?: string) => Promise<Storag
|
|
|
93
89
|
}
|
|
94
90
|
|
|
95
91
|
/** This function skips a case for a storage without an optional method, and gives true. */
|
|
96
|
-
function lacks(storage: ContractStorage, method: "getEntries" | "getChangesSince" | "flush" | "getMetadata", context: ContractTestContext | undefined): boolean {
|
|
92
|
+
function lacks(storage: ContractStorage, method: "getEntries" | "getIds" | "getChangesSince" | "flush" | "getMetadata", context: ContractTestContext | undefined): boolean {
|
|
97
93
|
if (typeof storage[method] === "function") return false;
|
|
98
94
|
context?.skip?.(`The storage has no ${method}()`);
|
|
99
95
|
return true;
|
|
@@ -110,12 +106,11 @@ function seeded(seed: number): (below: number) => number {
|
|
|
110
106
|
|
|
111
107
|
/**
|
|
112
108
|
* This function gives the cases of the contract for a storage. Each case makes its own storage
|
|
113
|
-
* with `options.create`. A case for an optional method (`getEntries`, `
|
|
114
|
-
* `getMetadata`) skips a storage without the method.
|
|
109
|
+
* with `options.create`. A case for an optional method (`getEntries`, `getIds`, `getChangesSince`,
|
|
110
|
+
* `flush`, `getMetadata`) skips a storage without the method.
|
|
115
111
|
*/
|
|
116
112
|
export function collectionStorageContract(options: CollectionStorageContractOptions): ContractCase[] {
|
|
117
113
|
const create = (): ContractStorage | Promise<ContractStorage> => options.create();
|
|
118
|
-
const nullValues = options.nullValues ?? true;
|
|
119
114
|
const cases: ContractCase[] = [];
|
|
120
115
|
const add = (name: string, run: (context?: ContractTestContext) => Promise<void>): void => {
|
|
121
116
|
cases.push({ name, run });
|
|
@@ -158,6 +153,7 @@ export function collectionStorageContract(options: CollectionStorageContractOpti
|
|
|
158
153
|
same(await storage.delete("a"), false, "delete() of a missing id");
|
|
159
154
|
same(await storage.deleteBatch(["a", "b"]), 0, "deleteBatch() of missing ids");
|
|
160
155
|
if (storage.getEntries) same(await storage.getEntries(), [], "getEntries() of an empty storage");
|
|
156
|
+
if (storage.getIds) same(await storage.getIds(), [], "getIds() of an empty storage");
|
|
161
157
|
}),
|
|
162
158
|
);
|
|
163
159
|
|
|
@@ -215,6 +211,23 @@ export function collectionStorageContract(options: CollectionStorageContractOpti
|
|
|
215
211
|
same(await storage.getAll(), expected.map(([, value]) => value), "getAll() after a delete and a new set() of an id");
|
|
216
212
|
same(ids(await storage.find(() => true)), ["a", "b", "c"], "find() keeps the order of the entries");
|
|
217
213
|
if (storage.getEntries) same(await storage.getEntries(), expected, "getEntries() keeps the order of the entries");
|
|
214
|
+
if (storage.getIds) same(await storage.getIds(), ["a", "b", "c"], "getIds() keeps the order of the entries");
|
|
215
|
+
}),
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
add("getIds() gives the ids of the entries in their order, as a new array", (context) =>
|
|
219
|
+
withStorage(create, async (storage) => {
|
|
220
|
+
if (lacks(storage, "getIds", context)) return;
|
|
221
|
+
await storage.setBatch([["c", item("c", 3)], ["a", item("a", 1)], ["b", item("b", 2)]]);
|
|
222
|
+
await storage.delete("a");
|
|
223
|
+
await storage.set("a", item("a", 10));
|
|
224
|
+
const first = await storage.getIds!();
|
|
225
|
+
same(first, ["c", "b", "a"], "getIds() after a delete and a new set() of an id");
|
|
226
|
+
first.splice(0);
|
|
227
|
+
same(await storage.getIds!(), ["c", "b", "a"], "getIds() after a change of an earlier result");
|
|
228
|
+
if (storage.getEntries) same(await storage.getIds!(), (await storage.getEntries()).map(([id]) => id), "getIds() and the ids of getEntries()");
|
|
229
|
+
await storage.clear();
|
|
230
|
+
same(await storage.getIds!(), [], "getIds() after clear()");
|
|
218
231
|
}),
|
|
219
232
|
);
|
|
220
233
|
|
|
@@ -265,26 +278,34 @@ export function collectionStorageContract(options: CollectionStorageContractOpti
|
|
|
265
278
|
}),
|
|
266
279
|
);
|
|
267
280
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
281
|
+
add("the storage keeps copies: a change of a written or a read value does not change the item", () =>
|
|
282
|
+
withStorage(create, async (storage) => {
|
|
283
|
+
const value = item("a", 1, ["x"]);
|
|
284
|
+
await storage.set("a", value);
|
|
285
|
+
value.n = 99;
|
|
286
|
+
value.tags!.push("changed after set");
|
|
287
|
+
const batch = item("b", 2, ["y"]);
|
|
288
|
+
await storage.setBatch([["b", batch]]);
|
|
289
|
+
batch.tags!.push("changed after setBatch");
|
|
290
|
+
const read = (await storage.get("a"))!;
|
|
291
|
+
same(read, item("a", 1, ["x"]), "get() after a change of the value that set() took");
|
|
292
|
+
same(await storage.get("b"), item("b", 2, ["y"]), "get() after a change of the value that setBatch() took");
|
|
293
|
+
read.n = 98;
|
|
294
|
+
read.tags!.push("changed after get");
|
|
295
|
+
(await storage.getAll())[0]!.n = 97;
|
|
296
|
+
(await storage.getBatch(["a"])).get("a")!.n = 96;
|
|
297
|
+
(await storage.find((found) => {
|
|
298
|
+
found.tags?.push("changed in the predicate");
|
|
299
|
+
return true;
|
|
300
|
+
}))[0]!.n = 95;
|
|
301
|
+
if (storage.getEntries) (await storage.getEntries())[0]![1].n = 94;
|
|
302
|
+
if (storage.getChangesSince) (await storage.getChangesSince()).changed[0]![1].n = 93;
|
|
303
|
+
same(await storage.get("a"), item("a", 1, ["x"]), "get() after changes of the read values");
|
|
304
|
+
same(await storage.get("b"), item("b", 2, ["y"]), "get() of the other item after changes of the read values");
|
|
305
|
+
}),
|
|
306
|
+
);
|
|
286
307
|
|
|
287
|
-
add(
|
|
308
|
+
add("the storage keeps JSON values, also null", () =>
|
|
288
309
|
withStorage(create, async (typed) => {
|
|
289
310
|
const storage = typed as unknown as CollectionStorage<unknown>;
|
|
290
311
|
const values: Array<[string, unknown]> = [
|
|
@@ -295,14 +316,17 @@ export function collectionStorageContract(options: CollectionStorageContractOpti
|
|
|
295
316
|
["negative", -2.25],
|
|
296
317
|
["false", false],
|
|
297
318
|
["list", [1, "two", [3]]],
|
|
319
|
+
["null", null],
|
|
298
320
|
];
|
|
299
|
-
if (nullValues) values.push(["null", null]);
|
|
300
321
|
await storage.setBatch(values);
|
|
301
322
|
for (const [id, value] of values) same(await storage.get(id), value, `get("${id}")`);
|
|
302
323
|
same(await storage.getAll(), values.map(([, value]) => value), "getAll() of the JSON values");
|
|
303
324
|
same(await storage.getBatch(values.map(([id]) => id)), new Map(values), "getBatch() of the JSON values");
|
|
304
325
|
same(await storage.size(), values.length, "size() with the JSON values");
|
|
305
|
-
|
|
326
|
+
same(await storage.has("null"), true, "has() of an id whose value is null");
|
|
327
|
+
await storage.set("single", null);
|
|
328
|
+
same(await storage.get("single"), null, "get() of an id that set() gave the value null");
|
|
329
|
+
same(await storage.get("missing"), undefined, "get() of a missing id, next to a stored null");
|
|
306
330
|
}),
|
|
307
331
|
);
|
|
308
332
|
|
|
@@ -315,16 +339,17 @@ export function collectionStorageContract(options: CollectionStorageContractOpti
|
|
|
315
339
|
}),
|
|
316
340
|
);
|
|
317
341
|
|
|
318
|
-
add("getChangesSince() without a version gives every item", (context) =>
|
|
342
|
+
add("getChangesSince() without a version gives every item, in the order of the entries", (context) =>
|
|
319
343
|
withStorage(create, async (storage) => {
|
|
320
344
|
if (lacks(storage, "getChangesSince", context)) return;
|
|
321
345
|
const changes = changesOf(storage);
|
|
322
|
-
const entries: Array<[string, ContractItem]> = [["
|
|
346
|
+
const entries: Array<[string, ContractItem]> = [["c", item("c", 3)], ["a", item("a", 1)], ["b", item("b", 2)]];
|
|
323
347
|
await storage.setBatch(entries);
|
|
348
|
+
await storage.set("c", item("c", 30));
|
|
324
349
|
const first = await changes();
|
|
325
350
|
holds(typeof first.version === "string" && first.version !== "", `the version must be a text that is not empty, not ${show(first.version)}`);
|
|
326
351
|
same(first.full, true, "full of the first call");
|
|
327
|
-
same(
|
|
352
|
+
same(first.changed, [["c", item("c", 30)], ["a", item("a", 1)], ["b", item("b", 2)]], "changed of the first call: every item, in the order of the entries");
|
|
328
353
|
same(first.deleted, [], "deleted of a full result");
|
|
329
354
|
}),
|
|
330
355
|
);
|
|
@@ -341,13 +366,40 @@ export function collectionStorageContract(options: CollectionStorageContractOpti
|
|
|
341
366
|
await storage.set("d", item("d", 4));
|
|
342
367
|
const delta = await changes(start.version);
|
|
343
368
|
same(delta.full, false, "full of a delta");
|
|
344
|
-
same(
|
|
369
|
+
same(delta.changed, [["a", item("a", 10)], ["d", item("d", 4)]], "changed of a delta");
|
|
345
370
|
same(delta.deleted, ["b"], "deleted of a delta");
|
|
346
371
|
holds(delta.version !== start.version, "the version must change after a write");
|
|
347
372
|
}),
|
|
348
373
|
);
|
|
349
374
|
|
|
350
|
-
add("getChangesSince()
|
|
375
|
+
add("getChangesSince(version) gives the changes in the order of the last change of each id", (context) =>
|
|
376
|
+
withStorage(create, async (storage) => {
|
|
377
|
+
if (lacks(storage, "getChangesSince", context)) return;
|
|
378
|
+
const changes = changesOf(storage);
|
|
379
|
+
await storage.setBatch([["a", item("a", 1)], ["b", item("b", 2)], ["c", item("c", 3)], ["d", item("d", 4)]]);
|
|
380
|
+
const start = await changes();
|
|
381
|
+
// b changes first, then a. Then b changes again: its last change comes after the change of a.
|
|
382
|
+
await storage.set("b", item("b", 20));
|
|
383
|
+
await storage.set("a", item("a", 10));
|
|
384
|
+
await storage.delete("d");
|
|
385
|
+
await storage.delete("c");
|
|
386
|
+
await storage.setBatch([["e", item("e", 5)], ["b", item("b", 21)]]);
|
|
387
|
+
const delta = await changes(start.version);
|
|
388
|
+
same(
|
|
389
|
+
{ changed: delta.changed, deleted: delta.deleted },
|
|
390
|
+
{ changed: [["a", item("a", 10)], ["e", item("e", 5)], ["b", item("b", 21)]], deleted: ["d", "c"] },
|
|
391
|
+
"the order of a delta: an id written again after another id comes after it",
|
|
392
|
+
);
|
|
393
|
+
// A delete of d again is not a change. A new set of c moves it from deleted to changed.
|
|
394
|
+
await storage.delete("d");
|
|
395
|
+
await storage.set("c", item("c", 30));
|
|
396
|
+
await storage.deleteBatch(["e", "a"]);
|
|
397
|
+
const next = await changes(delta.version);
|
|
398
|
+
same({ changed: next.changed, deleted: next.deleted }, { changed: [["c", item("c", 30)]], deleted: ["e", "a"] }, "the order of the next delta");
|
|
399
|
+
}),
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
add("getChangesSince(): a deleted id that comes back is a change, clear() deletes each id in the order of the entries, and no write gives no change", (context) =>
|
|
351
403
|
withStorage(create, async (storage) => {
|
|
352
404
|
if (lacks(storage, "getChangesSince", context)) return;
|
|
353
405
|
const changes = changesOf(storage);
|
|
@@ -356,11 +408,12 @@ export function collectionStorageContract(options: CollectionStorageContractOpti
|
|
|
356
408
|
await storage.delete("b");
|
|
357
409
|
await storage.set("b", item("b", 20));
|
|
358
410
|
const back = await changes(start.version);
|
|
359
|
-
same({ full: back.full, changed:
|
|
411
|
+
same({ full: back.full, changed: back.changed, deleted: back.deleted }, { full: false, changed: [["b", item("b", 20)]], deleted: [] }, "a delete, then a set of the id");
|
|
360
412
|
|
|
413
|
+
// The entries are a, c, b now: b went to the end at its new set
|
|
361
414
|
await storage.clear();
|
|
362
415
|
const cleared = await changes(back.version);
|
|
363
|
-
same({ full: cleared.full, changed: cleared.changed, deleted:
|
|
416
|
+
same({ full: cleared.full, changed: cleared.changed, deleted: cleared.deleted }, { full: false, changed: [], deleted: ["a", "c", "b"] }, "the changes of clear()");
|
|
364
417
|
const quiet = await changes(cleared.version);
|
|
365
418
|
same({ full: quiet.full, changed: quiet.changed, deleted: quiet.deleted }, { full: false, changed: [], deleted: [] }, "the changes without a write");
|
|
366
419
|
}),
|
|
@@ -374,7 +427,7 @@ export function collectionStorageContract(options: CollectionStorageContractOpti
|
|
|
374
427
|
await changes();
|
|
375
428
|
for (const unknown of ["not a version", ""]) {
|
|
376
429
|
const result = await changes(unknown);
|
|
377
|
-
same({ full: result.full, changed:
|
|
430
|
+
same({ full: result.full, changed: result.changed, deleted: result.deleted }, { full: true, changed: [["a", item("a", 1)], ["b", item("b", 2)]], deleted: [] }, `the changes since ${show(unknown)}`);
|
|
378
431
|
}
|
|
379
432
|
await withStorage(create, async (other) => {
|
|
380
433
|
await other.set("x", item("x", 9));
|
|
@@ -395,23 +448,30 @@ export function collectionStorageContract(options: CollectionStorageContractOpti
|
|
|
395
448
|
// Two deletes fit in the log
|
|
396
449
|
await storage.deleteBatch(["a", "b"]);
|
|
397
450
|
const two = await changes(start.version);
|
|
398
|
-
same({ full: two.full, changed: two.changed, deleted:
|
|
451
|
+
same({ full: two.full, changed: two.changed, deleted: two.deleted }, { full: false, changed: [], deleted: ["a", "b"] }, "two deletes in a log of two");
|
|
399
452
|
// The third delete drops the oldest one: the start version gives all items now
|
|
400
453
|
await storage.delete("c");
|
|
401
454
|
const after = await changes(start.version);
|
|
402
|
-
same({ full: after.full, changed:
|
|
455
|
+
same({ full: after.full, changed: after.changed, deleted: after.deleted }, { full: true, changed: [["d", item("d", 4)]], deleted: [] }, "a version older than a dropped delete");
|
|
403
456
|
same((await changes(after.version)).full, false, "a version after the dropped deletes still gives a delta");
|
|
404
457
|
}),
|
|
405
458
|
);
|
|
406
459
|
}
|
|
407
460
|
|
|
408
|
-
add("a random sequence of writes agrees with a Map, and each version gives the right changes", () =>
|
|
461
|
+
add("a random sequence of writes agrees with a Map, and each version gives the right changes in the right order", () =>
|
|
409
462
|
withStorage(create, async (storage) => {
|
|
410
463
|
const random = seeded(45);
|
|
411
464
|
const pool = ["a", "b", "c", "d", "e", "f", "g"];
|
|
412
465
|
const pick = (): string => pool[random(pool.length)]!;
|
|
413
466
|
const model = new Map<string, ContractItem>();
|
|
414
|
-
|
|
467
|
+
// The model of the change log: the number of the last change of each id, oldest first
|
|
468
|
+
const lastChange = new Map<string, { at: number; deleted: boolean }>();
|
|
469
|
+
let clock = 0;
|
|
470
|
+
const note = (id: string, deleted: boolean): void => {
|
|
471
|
+
lastChange.delete(id);
|
|
472
|
+
lastChange.set(id, { at: ++clock, deleted });
|
|
473
|
+
};
|
|
474
|
+
const versions: Array<{ version: string; entries: Map<string, ContractItem>; at: number }> = [];
|
|
415
475
|
const hasChanges = typeof storage.getChangesSince === "function";
|
|
416
476
|
for (let step = 0; step < 80; step++) {
|
|
417
477
|
const kind = random(20);
|
|
@@ -422,10 +482,13 @@ export function collectionStorageContract(options: CollectionStorageContractOpti
|
|
|
422
482
|
label = `set(${id})`;
|
|
423
483
|
await storage.set(id, value);
|
|
424
484
|
model.set(id, value);
|
|
485
|
+
note(id, false);
|
|
425
486
|
} else if (kind < 12) {
|
|
426
487
|
const id = pick();
|
|
427
488
|
label = `delete(${id})`;
|
|
428
|
-
|
|
489
|
+
const existed = model.delete(id);
|
|
490
|
+
if (existed) note(id, true);
|
|
491
|
+
same(await storage.delete(id), existed, `the result of step ${step}: ${label}`);
|
|
429
492
|
} else if (kind < 15) {
|
|
430
493
|
const batch: Array<[string, ContractItem]> = Array.from({ length: 1 + random(3) }, (_, index) => {
|
|
431
494
|
const id = pick();
|
|
@@ -433,34 +496,52 @@ export function collectionStorageContract(options: CollectionStorageContractOpti
|
|
|
433
496
|
});
|
|
434
497
|
label = `setBatch(${batch.map(([id]) => id).join(", ")})`;
|
|
435
498
|
await storage.setBatch(batch);
|
|
436
|
-
for (const [id, value] of batch)
|
|
499
|
+
for (const [id, value] of batch) {
|
|
500
|
+
model.set(id, value);
|
|
501
|
+
note(id, false);
|
|
502
|
+
}
|
|
437
503
|
} else if (kind < 18) {
|
|
438
504
|
const batch = Array.from({ length: 1 + random(3) }, pick);
|
|
439
505
|
label = `deleteBatch(${batch.join(", ")})`;
|
|
440
|
-
const expected = batch.filter((id) =>
|
|
506
|
+
const expected = batch.filter((id) => {
|
|
507
|
+
const existed = model.delete(id);
|
|
508
|
+
if (existed) note(id, true);
|
|
509
|
+
return existed;
|
|
510
|
+
}).length;
|
|
441
511
|
same(await storage.deleteBatch(batch), expected, `the result of step ${step}: ${label}`);
|
|
442
512
|
} else if (kind < 19) {
|
|
443
513
|
label = "clear()";
|
|
444
514
|
await storage.clear();
|
|
515
|
+
for (const id of model.keys()) note(id, true);
|
|
445
516
|
model.clear();
|
|
446
517
|
} else {
|
|
447
518
|
label = "a saved version";
|
|
448
|
-
if (hasChanges) versions.push({ version: (await storage.getChangesSince!()).version, entries: new Map(model) });
|
|
519
|
+
if (hasChanges) versions.push({ version: (await storage.getChangesSince!()).version, entries: new Map(model), at: clock });
|
|
449
520
|
}
|
|
450
521
|
same(await storage.getAll(), [...model.values()], `getAll() after step ${step}: ${label}`);
|
|
451
522
|
}
|
|
452
523
|
same(await storage.size(), model.size, "size() after the sequence");
|
|
453
524
|
if (storage.getEntries) same(await storage.getEntries(), [...model.entries()], "getEntries() after the sequence");
|
|
525
|
+
if (storage.getIds) same(await storage.getIds(), [...model.keys()], "getIds() after the sequence");
|
|
454
526
|
if (!hasChanges) return;
|
|
455
527
|
holds(versions.length > 0, "the sequence saved no version: change the seed");
|
|
456
528
|
for (const [index, saved] of versions.entries()) {
|
|
457
529
|
const result = await storage.getChangesSince!(saved.version);
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
530
|
+
if (result.full) {
|
|
531
|
+
same(result.changed, [...model.entries()], `the full changes since version ${index}: every item, in the order of the entries`);
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
// A delta: the changes after the version, in the order of the last change of each id
|
|
535
|
+
const after = [...lastChange].filter(([, change]) => change.at > saved.at);
|
|
536
|
+
const expected = {
|
|
537
|
+
changed: after.filter(([, change]) => !change.deleted).map(([id]): [string, ContractItem] => [id, model.get(id)!]),
|
|
538
|
+
deleted: after.filter(([, change]) => change.deleted).map(([id]) => id),
|
|
539
|
+
};
|
|
540
|
+
same({ changed: result.changed, deleted: result.deleted }, expected, `the changes since version ${index}`);
|
|
541
|
+
const rebuilt = new Map(saved.entries);
|
|
461
542
|
for (const id of result.deleted) rebuilt.delete(id);
|
|
462
543
|
for (const [id, value] of result.changed) rebuilt.set(id, value);
|
|
463
|
-
same(
|
|
544
|
+
same(rebuilt, new Map(model), `the items of version ${index} with its changes`);
|
|
464
545
|
}
|
|
465
546
|
}),
|
|
466
547
|
);
|
|
@@ -485,11 +566,44 @@ export function collectionStorageContract(options: CollectionStorageContractOpti
|
|
|
485
566
|
}),
|
|
486
567
|
);
|
|
487
568
|
|
|
488
|
-
add("close()
|
|
569
|
+
add("after close(), each operation rejects with an error that says that the storage is closed, and a second close() does nothing", () =>
|
|
489
570
|
withStorage(create, async (storage) => {
|
|
490
571
|
await storage.set("a", item("a", 1));
|
|
491
572
|
await storage.close();
|
|
573
|
+
const operations: Array<[string, () => Promise<unknown>]> = [
|
|
574
|
+
["get", () => storage.get("a")],
|
|
575
|
+
["getAll", () => storage.getAll()],
|
|
576
|
+
["find", () => storage.find(() => true)],
|
|
577
|
+
["has", () => storage.has("a")],
|
|
578
|
+
["size", () => storage.size()],
|
|
579
|
+
["getBatch", () => storage.getBatch(["a"])],
|
|
580
|
+
["getBatch of no id", () => storage.getBatch([])],
|
|
581
|
+
["set", () => storage.set("b", item("b", 2))],
|
|
582
|
+
["delete", () => storage.delete("a")],
|
|
583
|
+
["clear", () => storage.clear()],
|
|
584
|
+
["setBatch", () => storage.setBatch([["b", item("b", 2)]])],
|
|
585
|
+
["setBatch of no item", () => storage.setBatch([])],
|
|
586
|
+
["deleteBatch", () => storage.deleteBatch(["a"])],
|
|
587
|
+
["deleteBatch of no id", () => storage.deleteBatch([])],
|
|
588
|
+
];
|
|
589
|
+
if (storage.getEntries) operations.push(["getEntries", () => storage.getEntries!()]);
|
|
590
|
+
if (storage.getIds) operations.push(["getIds", () => storage.getIds!()]);
|
|
591
|
+
if (storage.getChangesSince) operations.push(["getChangesSince", () => storage.getChangesSince!()]);
|
|
592
|
+
if (storage.flush) operations.push(["flush", () => storage.flush!()]);
|
|
593
|
+
if (storage.getMetadata) operations.push(["getMetadata", () => storage.getMetadata!()]);
|
|
594
|
+
const check = async (when: string): Promise<void> => {
|
|
595
|
+
for (const [name, start] of operations) {
|
|
596
|
+
const error = await rejects(start, `${name}() ${when}`);
|
|
597
|
+
holds(
|
|
598
|
+
error instanceof Error && /closed/i.test(error.message),
|
|
599
|
+
`${name}() ${when} must reject with an Error whose message says "closed", not ${show(error instanceof Error ? error.message : error)}`,
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
await check("after close()");
|
|
604
|
+
// A second close() resolves, and the storage stays closed
|
|
492
605
|
await storage.close();
|
|
606
|
+
await check("after a second close()");
|
|
493
607
|
}),
|
|
494
608
|
);
|
|
495
609
|
|