@procedures/collections 0.4.0 → 0.5.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.
@@ -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 { byId, holds, rejects, same, show } from "./check.js";
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 factory of the storage, and what the storage keeps. */
37
+ /** The options of the suite: the factories of the storage. */
34
38
  export interface CollectionStorageContractOptions {
35
- /** This function makes a new, empty storage for each case. The case closes it at its end. */
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`, `getChangesSince`, `flush`,
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
- if (options.copies) {
269
- add("the storage keeps copies: a change of a written or a read value does not change the item", () =>
270
- withStorage(create, async (storage) => {
271
- const value = item("a", 1, ["x"]);
272
- await storage.set("a", value);
273
- value.n = 99;
274
- value.tags!.push("changed after set");
275
- const read = (await storage.get("a"))!;
276
- same(read, item("a", 1, ["x"]), "get() after a change of the value that set() took");
277
- read.n = 98;
278
- read.tags!.push("changed after get");
279
- (await storage.getAll())[0]!.n = 97;
280
- (await storage.getBatch(["a"])).get("a")!.n = 96;
281
- (await storage.find(() => true))[0]!.n = 95;
282
- same(await storage.get("a"), item("a", 1, ["x"]), "get() after changes of the read values");
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(`the storage keeps JSON values${nullValues ? ", also null" : ""}`, () =>
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
- if (nullValues) same(await storage.has("null"), true, "has() of an id whose value is null");
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]> = [["a", item("a", 1)], ["b", item("b", 2)], ["c", item("c", 3)]];
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(byId(first.changed), entries, "changed of the first call: every item");
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(byId(delta.changed), [["a", item("a", 10)], ["d", item("d", 4)]], "changed of a delta");
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(): a deleted id that comes back is a change, clear() deletes each id, and no write gives no change", (context) =>
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: byId(back.changed), deleted: back.deleted }, { full: false, changed: [["b", item("b", 20)]], deleted: [] }, "a delete, then a set of the id");
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: [...cleared.deleted].sort() }, { full: false, changed: [], deleted: ["a", "b", "c"] }, "the changes of clear()");
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: byId(result.changed), deleted: result.deleted }, { full: true, changed: [["a", item("a", 1)], ["b", item("b", 2)]], deleted: [] }, `the changes since ${show(unknown)}`);
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: [...two.deleted].sort() }, { full: false, changed: [], deleted: ["a", "b"] }, "two deletes in a log of two");
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: byId(after.changed), deleted: after.deleted }, { full: true, changed: [["d", item("d", 4)]], deleted: [] }, "a version older than a dropped delete");
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
- const versions: Array<{ version: string; entries: Map<string, ContractItem> }> = [];
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
- same(await storage.delete(id), model.delete(id), `the result of step ${step}: ${label}`);
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) model.set(id, value);
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) => model.delete(id)).length;
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
- const deleted = new Set(result.deleted);
459
- holds(result.changed.every(([id]) => !deleted.has(id)), `the changes since version ${index} have an id in changed and in deleted`);
460
- const rebuilt = result.full ? new Map<string, ContractItem>() : new Map(saved.entries);
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(byId([...rebuilt.entries()]), byId([...model.entries()]), `the items of version ${index} with its changes`);
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() resolves, and a second close() resolves too", () =>
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