@procedures/collections 0.55.2 → 0.57.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 +28 -0
- package/README.md +1 -1
- package/dist/testing/index.d.ts +0 -8
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +0 -8
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/storage-contract.d.ts +3 -2
- package/dist/testing/storage-contract.d.ts.map +1 -1
- package/dist/testing/storage-contract.js +162 -155
- package/dist/testing/storage-contract.js.map +1 -1
- package/package.json +15 -4
- package/src/testing/index.ts +0 -9
- package/src/testing/storage-contract.ts +163 -155
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* `@procedures/sqlite`. Before, each package copied the cases of `InMemoryStorage`.
|
|
7
7
|
*
|
|
8
8
|
* The suite is in the test-only entry `@procedures/collections/testing`. It imports no test
|
|
9
|
-
* framework: the caller gives `describe` and `it`, and the checks throw `
|
|
10
|
-
* storage package imports the suite without a cycle, and a runtime entry
|
|
9
|
+
* framework: the caller gives `describe` and `it`, and the checks throw the `AssertionError` of
|
|
10
|
+
* `node:assert`. So a storage package imports the suite without a cycle, and a runtime entry does
|
|
11
|
+
* not load vitest.
|
|
11
12
|
*
|
|
12
13
|
* ```ts
|
|
13
14
|
* import { describe, it } from "vitest";
|
|
@@ -32,7 +33,13 @@
|
|
|
32
33
|
* conflicts and their current items, and two writes at the same time.
|
|
33
34
|
*/
|
|
34
35
|
import { QUERY_OPERATORS, queryEntries, versionConflictsOf, } from "@procedures/client/query";
|
|
35
|
-
import
|
|
36
|
+
import assert from "node:assert/strict";
|
|
37
|
+
import { inspect } from "node:util";
|
|
38
|
+
import { rejects } from "@mark1russell7/rigor-testkit";
|
|
39
|
+
/** This function gives the text of a value for the message of a failed check, on one line. */
|
|
40
|
+
function show(value) {
|
|
41
|
+
return inspect(value, { breakLength: Infinity, depth: null });
|
|
42
|
+
}
|
|
36
43
|
/**
|
|
37
44
|
* The version of an item of the suite: `v` and the field `n`. A storage of `createWithVersions`
|
|
38
45
|
* gives it as its `versionOf` (roadmap CLI-16).
|
|
@@ -247,7 +254,7 @@ export function collectionStorageContract(options) {
|
|
|
247
254
|
["getBatch", storage.getBatch(["a"])],
|
|
248
255
|
];
|
|
249
256
|
for (const [name, result] of results)
|
|
250
|
-
|
|
257
|
+
assert.ok(isPromise(result), `${name}() did not give a promise`);
|
|
251
258
|
await Promise.all(results.map(([, result]) => result));
|
|
252
259
|
for (const [name, start] of [
|
|
253
260
|
["set", () => storage.set("a", item("a", 1))],
|
|
@@ -257,67 +264,67 @@ export function collectionStorageContract(options) {
|
|
|
257
264
|
["clear", () => storage.clear()],
|
|
258
265
|
]) {
|
|
259
266
|
const result = start();
|
|
260
|
-
|
|
267
|
+
assert.ok(isPromise(result), `${name}() did not give a promise`);
|
|
261
268
|
await result;
|
|
262
269
|
}
|
|
263
270
|
}));
|
|
264
271
|
add("an empty storage has no item", () => withStorage(create, async (storage) => {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
272
|
+
assert.deepStrictEqual(await storage.size(), 0, "size() of an empty storage");
|
|
273
|
+
assert.deepStrictEqual(await storage.getAll(), [], "getAll() of an empty storage");
|
|
274
|
+
assert.deepStrictEqual(await storage.get("a"), undefined, "get() of a missing id");
|
|
275
|
+
assert.deepStrictEqual(await storage.has("a"), false, "has() of a missing id");
|
|
276
|
+
assert.deepStrictEqual(await storage.getBatch(["a", "b"]), new Map(), "getBatch() of missing ids");
|
|
277
|
+
assert.deepStrictEqual(await storage.find(() => true), [], "find() in an empty storage");
|
|
278
|
+
assert.deepStrictEqual(await storage.delete("a"), false, "delete() of a missing id");
|
|
279
|
+
assert.deepStrictEqual(await storage.deleteBatch(["a", "b"]), 0, "deleteBatch() of missing ids");
|
|
273
280
|
if (storage.getEntries)
|
|
274
|
-
|
|
281
|
+
assert.deepStrictEqual(await storage.getEntries(), [], "getEntries() of an empty storage");
|
|
275
282
|
if (storage.getIds)
|
|
276
|
-
|
|
283
|
+
assert.deepStrictEqual(await storage.getIds(), [], "getIds() of an empty storage");
|
|
277
284
|
}));
|
|
278
285
|
add("set() stores an item, and a second set() of the id replaces it", () => withStorage(create, async (storage) => {
|
|
279
286
|
await storage.set("a", item("a", 1, ["x"]));
|
|
280
287
|
await storage.set("b", item("b", 2));
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
288
|
+
assert.deepStrictEqual(await storage.get("a"), item("a", 1, ["x"]), "get() after set()");
|
|
289
|
+
assert.deepStrictEqual(await storage.has("a"), true, "has() after set()");
|
|
290
|
+
assert.deepStrictEqual(await storage.size(), 2, "size() after two set() calls");
|
|
284
291
|
await storage.set("a", item("a", 10));
|
|
285
|
-
|
|
286
|
-
|
|
292
|
+
assert.deepStrictEqual(await storage.get("a"), item("a", 10), "get() after a second set() of the id");
|
|
293
|
+
assert.deepStrictEqual(await storage.size(), 2, "size() after a second set() of an id");
|
|
287
294
|
}));
|
|
288
295
|
add("delete() gives true for an item that existed, and deleteBatch() counts the items that existed", () => withStorage(create, async (storage) => {
|
|
289
296
|
await storage.setBatch([["a", item("a", 1)], ["b", item("b", 2)], ["c", item("c", 3)]]);
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
297
|
+
assert.deepStrictEqual(await storage.delete("a"), true, "delete() of an item");
|
|
298
|
+
assert.deepStrictEqual(await storage.delete("a"), false, "a second delete() of the id");
|
|
299
|
+
assert.deepStrictEqual(await storage.has("a"), false, "has() after delete()");
|
|
300
|
+
assert.deepStrictEqual(await storage.get("a"), undefined, "get() after delete()");
|
|
301
|
+
assert.deepStrictEqual(await storage.deleteBatch(["b", "missing", "b"]), 1, "deleteBatch() with a missing id and a repeated id");
|
|
302
|
+
assert.deepStrictEqual(ids(await storage.getAll()), ["c"], "getAll() after the deletes");
|
|
296
303
|
}));
|
|
297
304
|
add("setBatch() and getBatch(): the last value of an id wins, a missing id is not in the map, and empty batches change nothing", () => withStorage(create, async (storage) => {
|
|
298
305
|
await storage.setBatch([["a", item("a", 1)], ["b", item("b", 2)], ["a", item("a", 3)]]);
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
306
|
+
assert.deepStrictEqual(await storage.get("a"), item("a", 3), "get() of an id that setBatch() set two times");
|
|
307
|
+
assert.deepStrictEqual(await storage.size(), 2, "size() after setBatch() with a repeated id");
|
|
308
|
+
assert.deepStrictEqual(await storage.getBatch(["b", "missing", "a"]), new Map([["b", item("b", 2)], ["a", item("a", 3)]]), "getBatch() with a missing id");
|
|
302
309
|
await storage.setBatch([]);
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
310
|
+
assert.deepStrictEqual(await storage.deleteBatch([]), 0, "deleteBatch() of no id");
|
|
311
|
+
assert.deepStrictEqual(await storage.getBatch([]), new Map(), "getBatch() of no id");
|
|
312
|
+
assert.deepStrictEqual(await storage.size(), 2, "size() after the empty batches");
|
|
306
313
|
}));
|
|
307
314
|
add("the entries keep the order of the first set() of each id, as a Map does", () => withStorage(create, async (storage) => {
|
|
308
315
|
await storage.setBatch([["c", item("c", 3)], ["a", item("a", 1)], ["b", item("b", 2)]]);
|
|
309
316
|
await storage.set("a", item("a", 10));
|
|
310
|
-
|
|
317
|
+
assert.deepStrictEqual(ids(await storage.getAll()), ["c", "a", "b"], "getAll() after a second set() of an id: the id keeps its place");
|
|
311
318
|
// A delete, then a set: the id goes to the end
|
|
312
319
|
await storage.delete("c");
|
|
313
320
|
await storage.set("c", item("c", 30));
|
|
314
321
|
const expected = [["a", item("a", 10)], ["b", item("b", 2)], ["c", item("c", 30)]];
|
|
315
|
-
|
|
316
|
-
|
|
322
|
+
assert.deepStrictEqual(await storage.getAll(), expected.map(([, value]) => value), "getAll() after a delete and a new set() of an id");
|
|
323
|
+
assert.deepStrictEqual(ids(await storage.find(() => true)), ["a", "b", "c"], "find() keeps the order of the entries");
|
|
317
324
|
if (storage.getEntries)
|
|
318
|
-
|
|
325
|
+
assert.deepStrictEqual(await storage.getEntries(), expected, "getEntries() keeps the order of the entries");
|
|
319
326
|
if (storage.getIds)
|
|
320
|
-
|
|
327
|
+
assert.deepStrictEqual(await storage.getIds(), ["a", "b", "c"], "getIds() keeps the order of the entries");
|
|
321
328
|
}));
|
|
322
329
|
add("getIds() gives the ids of the entries in their order, as a new array", (context) => withStorage(create, async (storage) => {
|
|
323
330
|
if (lacks(storage, "getIds", context))
|
|
@@ -326,32 +333,32 @@ export function collectionStorageContract(options) {
|
|
|
326
333
|
await storage.delete("a");
|
|
327
334
|
await storage.set("a", item("a", 10));
|
|
328
335
|
const first = await storage.getIds();
|
|
329
|
-
|
|
336
|
+
assert.deepStrictEqual(first, ["c", "b", "a"], "getIds() after a delete and a new set() of an id");
|
|
330
337
|
first.splice(0);
|
|
331
|
-
|
|
338
|
+
assert.deepStrictEqual(await storage.getIds(), ["c", "b", "a"], "getIds() after a change of an earlier result");
|
|
332
339
|
if (storage.getEntries)
|
|
333
|
-
|
|
340
|
+
assert.deepStrictEqual(await storage.getIds(), (await storage.getEntries()).map(([id]) => id), "getIds() and the ids of getEntries()");
|
|
334
341
|
await storage.clear();
|
|
335
|
-
|
|
342
|
+
assert.deepStrictEqual(await storage.getIds(), [], "getIds() after clear()");
|
|
336
343
|
}));
|
|
337
344
|
add("find() gives the matching items in order, and an error of the predicate rejects the promise", () => withStorage(create, async (storage) => {
|
|
338
345
|
await storage.setBatch([["a", item("a", 1)], ["b", item("b", 2)], ["c", item("c", 3)], ["d", item("d", 4)]]);
|
|
339
|
-
|
|
340
|
-
|
|
346
|
+
assert.deepStrictEqual(ids(await storage.find((value) => value.n % 2 === 0)), ["b", "d"], "find() of the even items");
|
|
347
|
+
assert.deepStrictEqual(await storage.find(() => false), [], "find() without a match");
|
|
341
348
|
const error = await rejects(() => storage.find(() => {
|
|
342
349
|
throw new Error("the predicate failed");
|
|
343
350
|
}), "find() with a predicate that throws");
|
|
344
|
-
|
|
351
|
+
assert.ok(error instanceof Error && error.message.includes("the predicate failed"), `find() must reject with the error of the predicate, not ${show(error instanceof Error ? error.message : error)}`);
|
|
345
352
|
}));
|
|
346
353
|
add("clear() deletes every item, and the storage takes new items after it", () => withStorage(create, async (storage) => {
|
|
347
354
|
await storage.setBatch([["a", item("a", 1)], ["b", item("b", 2)], ["c", item("c", 3)]]);
|
|
348
355
|
await storage.clear();
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
356
|
+
assert.deepStrictEqual(await storage.size(), 0, "size() after clear()");
|
|
357
|
+
assert.deepStrictEqual(await storage.getAll(), [], "getAll() after clear()");
|
|
358
|
+
assert.deepStrictEqual(await storage.has("a"), false, "has() after clear()");
|
|
352
359
|
await storage.clear();
|
|
353
360
|
await storage.set("d", item("d", 4));
|
|
354
|
-
|
|
361
|
+
assert.deepStrictEqual(await storage.getAll(), [item("d", 4)], "getAll() after clear() and set()");
|
|
355
362
|
}));
|
|
356
363
|
add("each result is a new array or map: a change of a result does not change the storage", () => withStorage(create, async (storage) => {
|
|
357
364
|
await storage.setBatch([["a", item("a", 1)], ["b", item("b", 2)]]);
|
|
@@ -365,9 +372,9 @@ export function collectionStorageContract(options) {
|
|
|
365
372
|
batch.set("z", item("z", 0));
|
|
366
373
|
if (storage.getEntries)
|
|
367
374
|
(await storage.getEntries()).splice(0);
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
375
|
+
assert.deepStrictEqual(ids(await storage.getAll()), ["a", "b"], "getAll() after changes of the results");
|
|
376
|
+
assert.deepStrictEqual(await storage.has("z"), false, "has() of an id that only a result got");
|
|
377
|
+
assert.deepStrictEqual(await storage.getBatch(["a"]), new Map([["a", item("a", 1)]]), "getBatch() after a change of a result");
|
|
371
378
|
}));
|
|
372
379
|
add("the storage keeps copies: a change of a written or a read value does not change the item", () => withStorage(create, async (storage) => {
|
|
373
380
|
const value = item("a", 1, ["x"]);
|
|
@@ -378,8 +385,8 @@ export function collectionStorageContract(options) {
|
|
|
378
385
|
await storage.setBatch([["b", batch]]);
|
|
379
386
|
batch.tags.push("changed after setBatch");
|
|
380
387
|
const read = (await storage.get("a"));
|
|
381
|
-
|
|
382
|
-
|
|
388
|
+
assert.deepStrictEqual(read, item("a", 1, ["x"]), "get() after a change of the value that set() took");
|
|
389
|
+
assert.deepStrictEqual(await storage.get("b"), item("b", 2, ["y"]), "get() after a change of the value that setBatch() took");
|
|
383
390
|
read.n = 98;
|
|
384
391
|
read.tags.push("changed after get");
|
|
385
392
|
(await storage.getAll())[0].n = 97;
|
|
@@ -409,10 +416,10 @@ export function collectionStorageContract(options) {
|
|
|
409
416
|
});
|
|
410
417
|
await storage.set("c", item("c", 3, ["z"]));
|
|
411
418
|
stop();
|
|
412
|
-
|
|
419
|
+
assert.deepStrictEqual(await storage.get("c"), item("c", 3, ["z"]), "get() after an observer changed the value of a change");
|
|
413
420
|
}
|
|
414
|
-
|
|
415
|
-
|
|
421
|
+
assert.deepStrictEqual(await storage.get("a"), item("a", 1, ["x"]), "get() after changes of the read values");
|
|
422
|
+
assert.deepStrictEqual(await storage.get("b"), item("b", 2, ["y"]), "get() of the other item after changes of the read values");
|
|
416
423
|
}));
|
|
417
424
|
add("the storage keeps JSON values, also null", () => withStorage(create, async (typed) => {
|
|
418
425
|
const storage = typed;
|
|
@@ -428,20 +435,20 @@ export function collectionStorageContract(options) {
|
|
|
428
435
|
];
|
|
429
436
|
await storage.setBatch(values);
|
|
430
437
|
for (const [id, value] of values)
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
438
|
+
assert.deepStrictEqual(await storage.get(id), value, `get("${id}")`);
|
|
439
|
+
assert.deepStrictEqual(await storage.getAll(), values.map(([, value]) => value), "getAll() of the JSON values");
|
|
440
|
+
assert.deepStrictEqual(await storage.getBatch(values.map(([id]) => id)), new Map(values), "getBatch() of the JSON values");
|
|
441
|
+
assert.deepStrictEqual(await storage.size(), values.length, "size() with the JSON values");
|
|
442
|
+
assert.deepStrictEqual(await storage.has("null"), true, "has() of an id whose value is null");
|
|
436
443
|
await storage.set("single", null);
|
|
437
|
-
|
|
438
|
-
|
|
444
|
+
assert.deepStrictEqual(await storage.get("single"), null, "get() of an id that set() gave the value null");
|
|
445
|
+
assert.deepStrictEqual(await storage.get("missing"), undefined, "get() of a missing id, next to a stored null");
|
|
439
446
|
}));
|
|
440
447
|
add("writes that run at the same time keep each write", () => withStorage(create, async (storage) => {
|
|
441
448
|
const count = 20;
|
|
442
449
|
await Promise.all(Array.from({ length: count }, (_, n) => storage.set(`k${n}`, item(`k${n}`, n))));
|
|
443
|
-
|
|
444
|
-
|
|
450
|
+
assert.deepStrictEqual(await storage.size(), count, `size() after ${count} writes at the same time`);
|
|
451
|
+
assert.deepStrictEqual(ids(await storage.getAll()).sort(), Array.from({ length: count }, (_, n) => `k${n}`).sort(), "the ids after the writes at the same time");
|
|
445
452
|
}));
|
|
446
453
|
add("getChangesSince() without a version gives every item, in the order of the entries", (context) => withStorage(create, async (storage) => {
|
|
447
454
|
if (lacks(storage, "getChangesSince", context))
|
|
@@ -451,10 +458,10 @@ export function collectionStorageContract(options) {
|
|
|
451
458
|
await storage.setBatch(entries);
|
|
452
459
|
await storage.set("c", item("c", 30));
|
|
453
460
|
const first = await changes();
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
461
|
+
assert.ok(typeof first.version === "string" && first.version !== "", `the version must be a text that is not empty, not ${show(first.version)}`);
|
|
462
|
+
assert.deepStrictEqual(first.full, true, "full of the first call");
|
|
463
|
+
assert.deepStrictEqual(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");
|
|
464
|
+
assert.deepStrictEqual(first.deleted, [], "deleted of a full result");
|
|
458
465
|
}));
|
|
459
466
|
add("getChangesSince(version) gives the items that changed after the version and the ids that were deleted", (context) => withStorage(create, async (storage) => {
|
|
460
467
|
if (lacks(storage, "getChangesSince", context))
|
|
@@ -467,10 +474,10 @@ export function collectionStorageContract(options) {
|
|
|
467
474
|
await storage.delete("missing");
|
|
468
475
|
await storage.set("d", item("d", 4));
|
|
469
476
|
const delta = await changes(start.version);
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
477
|
+
assert.deepStrictEqual(delta.full, false, "full of a delta");
|
|
478
|
+
assert.deepStrictEqual(delta.changed, [["a", item("a", 10)], ["d", item("d", 4)]], "changed of a delta");
|
|
479
|
+
assert.deepStrictEqual(delta.deleted, ["b"], "deleted of a delta");
|
|
480
|
+
assert.ok(delta.version !== start.version, "the version must change after a write");
|
|
474
481
|
}));
|
|
475
482
|
add("getChangesSince(version) gives the changes in the order of the last change of each id", (context) => withStorage(create, async (storage) => {
|
|
476
483
|
if (lacks(storage, "getChangesSince", context))
|
|
@@ -485,13 +492,13 @@ export function collectionStorageContract(options) {
|
|
|
485
492
|
await storage.delete("c");
|
|
486
493
|
await storage.setBatch([["e", item("e", 5)], ["b", item("b", 21)]]);
|
|
487
494
|
const delta = await changes(start.version);
|
|
488
|
-
|
|
495
|
+
assert.deepStrictEqual({ changed: delta.changed, deleted: delta.deleted }, { changed: [["a", item("a", 10)], ["e", item("e", 5)], ["b", item("b", 21)]], deleted: ["d", "c"] }, "the order of a delta: an id written again after another id comes after it");
|
|
489
496
|
// A delete of d again is not a change. A new set of c moves it from deleted to changed.
|
|
490
497
|
await storage.delete("d");
|
|
491
498
|
await storage.set("c", item("c", 30));
|
|
492
499
|
await storage.deleteBatch(["e", "a"]);
|
|
493
500
|
const next = await changes(delta.version);
|
|
494
|
-
|
|
501
|
+
assert.deepStrictEqual({ changed: next.changed, deleted: next.deleted }, { changed: [["c", item("c", 30)]], deleted: ["e", "a"] }, "the order of the next delta");
|
|
495
502
|
}));
|
|
496
503
|
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) => withStorage(create, async (storage) => {
|
|
497
504
|
if (lacks(storage, "getChangesSince", context))
|
|
@@ -502,13 +509,13 @@ export function collectionStorageContract(options) {
|
|
|
502
509
|
await storage.delete("b");
|
|
503
510
|
await storage.set("b", item("b", 20));
|
|
504
511
|
const back = await changes(start.version);
|
|
505
|
-
|
|
512
|
+
assert.deepStrictEqual({ 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");
|
|
506
513
|
// The entries are a, c, b now: b went to the end at its new set
|
|
507
514
|
await storage.clear();
|
|
508
515
|
const cleared = await changes(back.version);
|
|
509
|
-
|
|
516
|
+
assert.deepStrictEqual({ full: cleared.full, changed: cleared.changed, deleted: cleared.deleted }, { full: false, changed: [], deleted: ["a", "c", "b"] }, "the changes of clear()");
|
|
510
517
|
const quiet = await changes(cleared.version);
|
|
511
|
-
|
|
518
|
+
assert.deepStrictEqual({ full: quiet.full, changed: quiet.changed, deleted: quiet.deleted }, { full: false, changed: [], deleted: [] }, "the changes without a write");
|
|
512
519
|
}));
|
|
513
520
|
add("getChangesSince() gives every item for an unknown version and for a version of another storage", (context) => withStorage(create, async (storage) => {
|
|
514
521
|
if (lacks(storage, "getChangesSince", context))
|
|
@@ -518,12 +525,12 @@ export function collectionStorageContract(options) {
|
|
|
518
525
|
await changes();
|
|
519
526
|
for (const unknown of ["not a version", ""]) {
|
|
520
527
|
const result = await changes(unknown);
|
|
521
|
-
|
|
528
|
+
assert.deepStrictEqual({ 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)}`);
|
|
522
529
|
}
|
|
523
530
|
await withStorage(create, async (other) => {
|
|
524
531
|
await other.set("x", item("x", 9));
|
|
525
532
|
const foreign = (await changesOf(other)()).version;
|
|
526
|
-
|
|
533
|
+
assert.deepStrictEqual((await changes(foreign)).full, true, "full for a version of another storage");
|
|
527
534
|
});
|
|
528
535
|
}));
|
|
529
536
|
if (options.createWithMaxTombstones) {
|
|
@@ -537,12 +544,12 @@ export function collectionStorageContract(options) {
|
|
|
537
544
|
// Two deletes fit in the log
|
|
538
545
|
await storage.deleteBatch(["a", "b"]);
|
|
539
546
|
const two = await changes(start.version);
|
|
540
|
-
|
|
547
|
+
assert.deepStrictEqual({ full: two.full, changed: two.changed, deleted: two.deleted }, { full: false, changed: [], deleted: ["a", "b"] }, "two deletes in a log of two");
|
|
541
548
|
// The third delete drops the oldest one: the start version gives all items now
|
|
542
549
|
await storage.delete("c");
|
|
543
550
|
const after = await changes(start.version);
|
|
544
|
-
|
|
545
|
-
|
|
551
|
+
assert.deepStrictEqual({ full: after.full, changed: after.changed, deleted: after.deleted }, { full: true, changed: [["d", item("d", 4)]], deleted: [] }, "a version older than a dropped delete");
|
|
552
|
+
assert.deepStrictEqual((await changes(after.version)).full, false, "a version after the dropped deletes still gives a delta");
|
|
546
553
|
}));
|
|
547
554
|
}
|
|
548
555
|
add("a random sequence of writes agrees with a Map, and each version gives the right changes in the right order", () => withStorage(create, async (storage) => {
|
|
@@ -576,7 +583,7 @@ export function collectionStorageContract(options) {
|
|
|
576
583
|
const existed = model.delete(id);
|
|
577
584
|
if (existed)
|
|
578
585
|
note(id, true);
|
|
579
|
-
|
|
586
|
+
assert.deepStrictEqual(await storage.delete(id), existed, `the result of step ${step}: ${label}`);
|
|
580
587
|
}
|
|
581
588
|
else if (kind < 15) {
|
|
582
589
|
const batch = Array.from({ length: 1 + random(3) }, (_, index) => {
|
|
@@ -599,7 +606,7 @@ export function collectionStorageContract(options) {
|
|
|
599
606
|
note(id, true);
|
|
600
607
|
return existed;
|
|
601
608
|
}).length;
|
|
602
|
-
|
|
609
|
+
assert.deepStrictEqual(await storage.deleteBatch(batch), expected, `the result of step ${step}: ${label}`);
|
|
603
610
|
}
|
|
604
611
|
else if (kind < 19) {
|
|
605
612
|
label = "clear()";
|
|
@@ -613,20 +620,20 @@ export function collectionStorageContract(options) {
|
|
|
613
620
|
if (hasChanges)
|
|
614
621
|
versions.push({ version: (await storage.getChangesSince()).version, entries: new Map(model), at: clock });
|
|
615
622
|
}
|
|
616
|
-
|
|
623
|
+
assert.deepStrictEqual(await storage.getAll(), [...model.values()], `getAll() after step ${step}: ${label}`);
|
|
617
624
|
}
|
|
618
|
-
|
|
625
|
+
assert.deepStrictEqual(await storage.size(), model.size, "size() after the sequence");
|
|
619
626
|
if (storage.getEntries)
|
|
620
|
-
|
|
627
|
+
assert.deepStrictEqual(await storage.getEntries(), [...model.entries()], "getEntries() after the sequence");
|
|
621
628
|
if (storage.getIds)
|
|
622
|
-
|
|
629
|
+
assert.deepStrictEqual(await storage.getIds(), [...model.keys()], "getIds() after the sequence");
|
|
623
630
|
if (!hasChanges)
|
|
624
631
|
return;
|
|
625
|
-
|
|
632
|
+
assert.ok(versions.length > 0, "the sequence saved no version: change the seed");
|
|
626
633
|
for (const [index, saved] of versions.entries()) {
|
|
627
634
|
const result = await storage.getChangesSince(saved.version);
|
|
628
635
|
if (result.full) {
|
|
629
|
-
|
|
636
|
+
assert.deepStrictEqual(result.changed, [...model.entries()], `the full changes since version ${index}: every item, in the order of the entries`);
|
|
630
637
|
continue;
|
|
631
638
|
}
|
|
632
639
|
// A delta: the changes after the version, in the order of the last change of each id
|
|
@@ -635,13 +642,13 @@ export function collectionStorageContract(options) {
|
|
|
635
642
|
changed: after.filter(([, change]) => !change.deleted).map(([id]) => [id, model.get(id)]),
|
|
636
643
|
deleted: after.filter(([, change]) => change.deleted).map(([id]) => id),
|
|
637
644
|
};
|
|
638
|
-
|
|
645
|
+
assert.deepStrictEqual({ changed: result.changed, deleted: result.deleted }, expected, `the changes since version ${index}`);
|
|
639
646
|
const rebuilt = new Map(saved.entries);
|
|
640
647
|
for (const id of result.deleted)
|
|
641
648
|
rebuilt.delete(id);
|
|
642
649
|
for (const [id, value] of result.changed)
|
|
643
650
|
rebuilt.set(id, value);
|
|
644
|
-
|
|
651
|
+
assert.deepStrictEqual(rebuilt, new Map(model), `the items of version ${index} with its changes`);
|
|
645
652
|
}
|
|
646
653
|
}));
|
|
647
654
|
//
|
|
@@ -654,8 +661,8 @@ export function collectionStorageContract(options) {
|
|
|
654
661
|
await storage.setBatch([...DOCUMENTS]);
|
|
655
662
|
for (const [selector, expected] of SELECTOR_CASES) {
|
|
656
663
|
const result = await storage.query(selector);
|
|
657
|
-
|
|
658
|
-
|
|
664
|
+
assert.deepStrictEqual(entryIds(result), expected, `the ids of query(${show(selector)})`);
|
|
665
|
+
assert.deepStrictEqual(result, queryEntries(DOCUMENTS, selector), `the entries of query(${show(selector)})`);
|
|
659
666
|
}
|
|
660
667
|
}));
|
|
661
668
|
add("query() sorts, then skips, then limits, and items with equal keys keep the order of the entries", (context) => withStorage(create, async (typed) => {
|
|
@@ -664,9 +671,9 @@ export function collectionStorageContract(options) {
|
|
|
664
671
|
const storage = typed;
|
|
665
672
|
await storage.setBatch([...DOCUMENTS]);
|
|
666
673
|
for (const [options, expected] of OPTION_CASES) {
|
|
667
|
-
|
|
674
|
+
assert.deepStrictEqual(entryIds(await storage.query({}, options)), expected, `the ids of query({}, ${show(options)})`);
|
|
668
675
|
}
|
|
669
|
-
|
|
676
|
+
assert.deepStrictEqual(entryIds(await storage.query({ n: { $exists: true } }, { sort: { n: -1 }, limit: 2 })), ["e", "d"], "a selector with a sort and a limit");
|
|
670
677
|
}));
|
|
671
678
|
add("query() rejects a selector or options that are not correct", (context) => withStorage(create, async (typed) => {
|
|
672
679
|
if (lacks(typed, "query", context))
|
|
@@ -714,7 +721,7 @@ export function collectionStorageContract(options) {
|
|
|
714
721
|
if (expected === undefined)
|
|
715
722
|
await rejects(() => storage.query(selector, options), label);
|
|
716
723
|
else
|
|
717
|
-
|
|
724
|
+
assert.deepStrictEqual(await storage.query(selector, options), expected, label);
|
|
718
725
|
}
|
|
719
726
|
}));
|
|
720
727
|
add("query() refuses $regex without a policy that permits it, and a pattern that repeats a repetition", (context) => withStorage(create, async (typed) => {
|
|
@@ -734,8 +741,8 @@ export function collectionStorageContract(options) {
|
|
|
734
741
|
await storage.setBatch([...DOCUMENTS]);
|
|
735
742
|
for (const [selector, expected] of REGEX_CASES) {
|
|
736
743
|
const result = await storage.query(selector);
|
|
737
|
-
|
|
738
|
-
|
|
744
|
+
assert.deepStrictEqual(entryIds(result), expected, `the ids of query(${show(selector)})`);
|
|
745
|
+
assert.deepStrictEqual(result, queryEntries(DOCUMENTS, selector, {}, REGEX_POLICY), `the entries of query(${show(selector)})`);
|
|
739
746
|
}
|
|
740
747
|
await rejects(() => storage.query(REDOS), `query(${show(REDOS)}) under a policy with $regex`);
|
|
741
748
|
}));
|
|
@@ -776,7 +783,7 @@ export function collectionStorageContract(options) {
|
|
|
776
783
|
[{}, { skip: 5 }, ["f", "g"]],
|
|
777
784
|
];
|
|
778
785
|
for (const [selector, query, expected] of permitted) {
|
|
779
|
-
|
|
786
|
+
assert.deepStrictEqual(entryIds(await storage.query(selector, query)), expected, `the ids of query(${show(selector)}, ${show(query)})`);
|
|
780
787
|
}
|
|
781
788
|
}));
|
|
782
789
|
}
|
|
@@ -788,27 +795,27 @@ export function collectionStorageContract(options) {
|
|
|
788
795
|
return;
|
|
789
796
|
const storage = typed;
|
|
790
797
|
await storage.set("a", { id: "a", n: 1, tags: ["x"], meta: { kind: "k1", score: 10 } });
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
+
assert.deepStrictEqual(await storage.patch("a", { n: 2 }), { id: "a", n: 2, tags: ["x"], meta: { kind: "k1", score: 10 } }, "patch() of one field");
|
|
799
|
+
assert.deepStrictEqual(await storage.patch("a", { meta: { score: 11, extra: true }, tags: null }), { id: "a", n: 2, meta: { kind: "k1", score: 11, extra: true } }, "patch() of an object field and a null field");
|
|
800
|
+
assert.deepStrictEqual(await storage.patch("a", { tags: ["y", "z"], meta: { extra: null } }), { id: "a", n: 2, meta: { kind: "k1", score: 11 }, tags: ["y", "z"] }, "patch() of an array field and a nested null");
|
|
801
|
+
assert.deepStrictEqual(await storage.get("a"), { id: "a", n: 2, meta: { kind: "k1", score: 11 }, tags: ["y", "z"] }, "get() after the patches");
|
|
802
|
+
assert.deepStrictEqual(await storage.patch("missing", { n: 1 }), undefined, "patch() of a missing id");
|
|
803
|
+
assert.deepStrictEqual(await storage.has("missing"), false, "has() of a missing id after patch()");
|
|
804
|
+
assert.deepStrictEqual(await storage.size(), 1, "size() after patch() of a missing id");
|
|
798
805
|
await storage.set("plain", 5);
|
|
799
|
-
|
|
806
|
+
assert.deepStrictEqual(await storage.patch("plain", { n: 1 }), { n: 1 }, "patch() of an item that is not an object");
|
|
800
807
|
for (const fields of [null, [1], "x", 3]) {
|
|
801
808
|
await rejects(() => storage.patch("a", fields), `patch() with the fields ${show(fields)}`);
|
|
802
809
|
}
|
|
803
|
-
|
|
810
|
+
assert.deepStrictEqual(await storage.get("a"), { id: "a", n: 2, meta: { kind: "k1", score: 11 }, tags: ["y", "z"] }, "get() after the patches that were not correct");
|
|
804
811
|
}));
|
|
805
812
|
add("patch() with the operations $set and $unset sets null and nested paths, and a patch of two forms rejects", (context) => withStorage(create, async (typed) => {
|
|
806
813
|
if (lacks(typed, "patch", context))
|
|
807
814
|
return;
|
|
808
815
|
const storage = typed;
|
|
809
816
|
await storage.set("a", { id: "a", n: 1, tags: ["x", "y"], meta: { kind: "k1", score: 10 } });
|
|
810
|
-
|
|
811
|
-
|
|
817
|
+
assert.deepStrictEqual(await storage.patch("a", { $set: { "meta.kind": "k2", note: null, "tags.1": "z", "deep.new.path": 1 }, $unset: ["meta.score"] }), { id: "a", n: 1, tags: ["x", "z"], meta: { kind: "k2" }, note: null, deep: { new: { path: 1 } } }, "patch() with $set and $unset");
|
|
818
|
+
assert.deepStrictEqual(await storage.patch("a", { $unset: ["tags.0", "missing.path", "deep"], $set: { "tags.2": "w" } }), { id: "a", n: 1, tags: [null, "z", "w"], meta: { kind: "k2" }, note: null }, "patch() that removes an element of an array, a missing path and a field, and adds an element");
|
|
812
819
|
const after = { id: "a", n: 1, tags: [null, "z", "w"], meta: { kind: "k2" }, note: null };
|
|
813
820
|
const wrong = [
|
|
814
821
|
["a patch of two forms", { $set: { n: 2 }, n: 3 }],
|
|
@@ -821,17 +828,17 @@ export function collectionStorageContract(options) {
|
|
|
821
828
|
];
|
|
822
829
|
for (const [what, fields] of wrong)
|
|
823
830
|
await rejects(() => storage.patch("a", fields), `patch() with ${what}`);
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
831
|
+
assert.deepStrictEqual(await storage.get("a"), after, "get() after the patches that were not correct");
|
|
832
|
+
assert.deepStrictEqual(await storage.patch("missing", { $set: { n: 1 } }), undefined, "patch() with $set of a missing id");
|
|
833
|
+
assert.deepStrictEqual(await storage.has("missing"), false, "has() of a missing id after patch() with $set");
|
|
827
834
|
}));
|
|
828
835
|
add("patch() with $remove and $insert moves the elements of an array, and each index refers to the item before the patch", (context) => withStorage(create, async (typed) => {
|
|
829
836
|
if (lacks(typed, "patch", context))
|
|
830
837
|
return;
|
|
831
838
|
const storage = typed;
|
|
832
839
|
await storage.set("a", { id: "a", tags: ["a", "b", "c", "d"], rows: [{ n: 1, keys: ["x"] }, { n: 2, keys: ["y"] }], meta: { kind: "k1" } });
|
|
833
|
-
|
|
834
|
-
|
|
840
|
+
assert.deepStrictEqual(await storage.patch("a", { $remove: ["tags.1", "tags.3", "meta.kind"], $insert: { "rows.1.keys.0": "w" } }), { id: "a", tags: ["a", "c"], rows: [{ n: 1, keys: ["x"] }, { n: 2, keys: ["w", "y"] }], meta: {} }, "patch() with $remove and $insert");
|
|
841
|
+
assert.deepStrictEqual(await storage.patch("a", { $insert: { "tags.0": "z", "tags.2": "e" }, $remove: ["rows.0", "missing.0"], $set: { "rows.1.n": 3 } }), { id: "a", tags: ["z", "a", "c", "e"], rows: [{ n: 3, keys: ["w", "y"] }], meta: {} }, "patch() that inserts at the start and at the end, and removes a missing path");
|
|
835
842
|
const after = { id: "a", tags: ["z", "a", "c", "e"], rows: [{ n: 3, keys: ["w", "y"] }], meta: {} };
|
|
836
843
|
const wrong = [
|
|
837
844
|
["a $remove that is not a list of paths", { $remove: "tags" }],
|
|
@@ -843,7 +850,7 @@ export function collectionStorageContract(options) {
|
|
|
843
850
|
];
|
|
844
851
|
for (const [what, fields] of wrong)
|
|
845
852
|
await rejects(() => storage.patch("a", fields), `patch() with ${what}`);
|
|
846
|
-
|
|
853
|
+
assert.deepStrictEqual(await storage.get("a"), after, "get() after the patches that were not correct");
|
|
847
854
|
}));
|
|
848
855
|
add("patch() runs in one step: patches of one item at the same time keep each field", (context) => withStorage(create, async (typed) => {
|
|
849
856
|
if (lacks(typed, "patch", context))
|
|
@@ -855,7 +862,7 @@ export function collectionStorageContract(options) {
|
|
|
855
862
|
const expected = { id: "a" };
|
|
856
863
|
for (let n = 0; n < count; n++)
|
|
857
864
|
expected[`f${n}`] = n;
|
|
858
|
-
|
|
865
|
+
assert.deepStrictEqual(await storage.get("a"), expected, `get() after ${count} patches at the same time`);
|
|
859
866
|
}));
|
|
860
867
|
add("patch() is a change: getChangesSince() gives the patched item", (context) => withStorage(create, async (storage) => {
|
|
861
868
|
if (lacks(storage, "patch", context) || lacks(storage, "getChangesSince", context))
|
|
@@ -865,7 +872,7 @@ export function collectionStorageContract(options) {
|
|
|
865
872
|
await storage.patch("a", { n: 10 });
|
|
866
873
|
await storage.patch("missing", { n: 3 });
|
|
867
874
|
const delta = await storage.getChangesSince(start.version);
|
|
868
|
-
|
|
875
|
+
assert.deepStrictEqual({ full: delta.full, changed: delta.changed, deleted: delta.deleted }, { full: false, changed: [["a", item("a", 10)]], deleted: [] }, "the changes after a patch");
|
|
869
876
|
}));
|
|
870
877
|
//
|
|
871
878
|
// ═══ observe() ═══
|
|
@@ -876,7 +883,7 @@ export function collectionStorageContract(options) {
|
|
|
876
883
|
const batches = [];
|
|
877
884
|
const stop = storage.observe((batch) => batches.push(plainBatch(batch)));
|
|
878
885
|
await storage.set("a", item("a", 1));
|
|
879
|
-
|
|
886
|
+
assert.deepStrictEqual(batches.length, 1, "the batches when set() resolves");
|
|
880
887
|
await storage.setBatch([["b", item("b", 2)], ["c", item("c", 3)]]);
|
|
881
888
|
await storage.delete("a");
|
|
882
889
|
await storage.deleteBatch(["b", "missing"]);
|
|
@@ -884,7 +891,7 @@ export function collectionStorageContract(options) {
|
|
|
884
891
|
await storage.patch("c", { n: 30 });
|
|
885
892
|
await storage.clear();
|
|
886
893
|
stop();
|
|
887
|
-
|
|
894
|
+
assert.deepStrictEqual(batches, [
|
|
888
895
|
[{ type: "set", id: "a", value: item("a", 1) }],
|
|
889
896
|
[
|
|
890
897
|
{ type: "set", id: "b", value: item("b", 2) },
|
|
@@ -908,7 +915,7 @@ export function collectionStorageContract(options) {
|
|
|
908
915
|
await storage.clear();
|
|
909
916
|
if (storage.patch)
|
|
910
917
|
await storage.patch("missing", { n: 1 });
|
|
911
|
-
|
|
918
|
+
assert.deepStrictEqual(batches, [], "the batches of the writes that change nothing");
|
|
912
919
|
await storage.set("a", item("a", 1), { origin: "sync" });
|
|
913
920
|
await storage.setBatch([["b", item("b", 2)]], { origin: "app" });
|
|
914
921
|
await storage.delete("a", { origin: "sync" });
|
|
@@ -916,7 +923,7 @@ export function collectionStorageContract(options) {
|
|
|
916
923
|
await storage.set("c", item("c", 3), {});
|
|
917
924
|
await storage.clear({ origin: "reset" });
|
|
918
925
|
stop();
|
|
919
|
-
|
|
926
|
+
assert.deepStrictEqual(batches, [
|
|
920
927
|
[{ type: "set", id: "a", value: item("a", 1), origin: "sync" }],
|
|
921
928
|
[{ type: "set", id: "b", value: item("b", 2), origin: "app" }],
|
|
922
929
|
[{ type: "delete", id: "a", origin: "sync" }],
|
|
@@ -938,8 +945,8 @@ export function collectionStorageContract(options) {
|
|
|
938
945
|
await storage.delete("a");
|
|
939
946
|
stopSecond();
|
|
940
947
|
await storage.set("b", item("b", 2));
|
|
941
|
-
|
|
942
|
-
|
|
948
|
+
assert.deepStrictEqual(first, ["set"], "the changes of the first observer");
|
|
949
|
+
assert.deepStrictEqual(second, ["set", "delete"], "the changes of the second observer");
|
|
943
950
|
}));
|
|
944
951
|
add("flush() resolves, and the items stay", (context) => withStorage(create, async (storage) => {
|
|
945
952
|
if (lacks(storage, "flush", context))
|
|
@@ -947,15 +954,15 @@ export function collectionStorageContract(options) {
|
|
|
947
954
|
await storage.flush();
|
|
948
955
|
await storage.set("a", item("a", 1));
|
|
949
956
|
await storage.flush();
|
|
950
|
-
|
|
957
|
+
assert.deepStrictEqual(await storage.get("a"), item("a", 1), "get() after flush()");
|
|
951
958
|
}));
|
|
952
959
|
add("getMetadata() gives a known type and the size", (context) => withStorage(create, async (storage) => {
|
|
953
960
|
if (lacks(storage, "getMetadata", context))
|
|
954
961
|
return;
|
|
955
962
|
await storage.setBatch([["a", item("a", 1)], ["b", item("b", 2)]]);
|
|
956
963
|
const metadata = await storage.getMetadata();
|
|
957
|
-
|
|
958
|
-
|
|
964
|
+
assert.ok(["memory", "api", "hybrid", "synced", "custom"].includes(metadata.type), `getMetadata() gave the unknown type ${show(metadata.type)}`);
|
|
965
|
+
assert.deepStrictEqual(metadata.size, 2, "the size of getMetadata()");
|
|
959
966
|
}));
|
|
960
967
|
add("after close(), each operation rejects with an error that says that the storage is closed, and a second close() does nothing", () => withStorage(create, async (storage) => {
|
|
961
968
|
await storage.set("a", item("a", 1));
|
|
@@ -993,7 +1000,7 @@ export function collectionStorageContract(options) {
|
|
|
993
1000
|
const check = async (when) => {
|
|
994
1001
|
for (const [name, start] of operations) {
|
|
995
1002
|
const error = await rejects(start, `${name}() ${when}`);
|
|
996
|
-
|
|
1003
|
+
assert.ok(error instanceof Error && /closed/i.test(error.message), `${name}() ${when} must reject with an Error whose message says "closed", not ${show(error instanceof Error ? error.message : error)}`);
|
|
997
1004
|
}
|
|
998
1005
|
};
|
|
999
1006
|
const checkObserve = (when) => {
|
|
@@ -1006,7 +1013,7 @@ export function collectionStorageContract(options) {
|
|
|
1006
1013
|
catch (thrown) {
|
|
1007
1014
|
error = thrown;
|
|
1008
1015
|
}
|
|
1009
|
-
|
|
1016
|
+
assert.ok(error instanceof Error && /closed/i.test(error.message), `observe() ${when} must throw an Error whose message says "closed", not ${show(error instanceof Error ? error.message : error)}`);
|
|
1010
1017
|
};
|
|
1011
1018
|
await check("after close()");
|
|
1012
1019
|
checkObserve("after close()");
|
|
@@ -1024,9 +1031,9 @@ export function collectionStorageContract(options) {
|
|
|
1024
1031
|
*/
|
|
1025
1032
|
function conflictsOfRejection(error, what) {
|
|
1026
1033
|
const code = error?.code;
|
|
1027
|
-
|
|
1034
|
+
assert.ok(code === "VERSION_CONFLICT", `${what}: the error must have the code VERSION_CONFLICT, not ${show(code)} (${show(error instanceof Error ? error.message : error)})`);
|
|
1028
1035
|
const conflicts = versionConflictsOf(error);
|
|
1029
|
-
|
|
1036
|
+
assert.ok(conflicts !== undefined, `${what}: the error must have its conflicts`);
|
|
1030
1037
|
return conflicts;
|
|
1031
1038
|
}
|
|
1032
1039
|
/** The cases of the preconditions of the writes (roadmap CLI-16). */
|
|
@@ -1036,10 +1043,10 @@ function addPreconditionCases(add, create, createWithVersions) {
|
|
|
1036
1043
|
await rejects(() => storage.set("a", item("a", 2), { expectedVersion: "v1" }), "set() with expectedVersion on a storage without versions");
|
|
1037
1044
|
await rejects(() => storage.delete("a", { expectedVersion: "v1" }), "delete() with expectedVersion on a storage without versions");
|
|
1038
1045
|
await rejects(() => storage.setBatch([["a", item("a", 3)]], { expectedVersions: [["a", "v1"]] }), "setBatch() with expectedVersions on a storage without versions");
|
|
1039
|
-
|
|
1046
|
+
assert.deepStrictEqual(await storage.get("a"), item("a", 1), "the item after the refused writes");
|
|
1040
1047
|
// A write without a precondition is the write of before
|
|
1041
1048
|
await storage.set("a", item("a", 4));
|
|
1042
|
-
|
|
1049
|
+
assert.deepStrictEqual(await storage.get("a"), item("a", 4), "a write without expectedVersion");
|
|
1043
1050
|
}));
|
|
1044
1051
|
if (!createWithVersions)
|
|
1045
1052
|
return;
|
|
@@ -1047,11 +1054,11 @@ function addPreconditionCases(add, create, createWithVersions) {
|
|
|
1047
1054
|
add("a write with the expected version of the current item writes, and a write without one writes as before", () => withStorage(versioned, async (storage) => {
|
|
1048
1055
|
await storage.set("a", item("a", 1));
|
|
1049
1056
|
await storage.set("a", item("a", 2), { expectedVersion: "v1" });
|
|
1050
|
-
|
|
1057
|
+
assert.deepStrictEqual(await storage.get("a"), item("a", 2), "the item after set() with the expected version");
|
|
1051
1058
|
await storage.set("a", item("a", 3));
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1059
|
+
assert.deepStrictEqual(await storage.get("a"), item("a", 3), "the item after set() without expectedVersion");
|
|
1060
|
+
assert.deepStrictEqual(await storage.delete("a", { expectedVersion: "v3" }), true, "delete() with the expected version");
|
|
1061
|
+
assert.deepStrictEqual(await storage.get("a"), undefined, "the item after delete() with the expected version");
|
|
1055
1062
|
}));
|
|
1056
1063
|
add("a write with another expected version changes nothing, and rejects with VERSION_CONFLICT and the current item", () => withStorage(versioned, async (storage) => {
|
|
1057
1064
|
await storage.set("a", item("a", 1));
|
|
@@ -1062,35 +1069,35 @@ function addPreconditionCases(add, create, createWithVersions) {
|
|
|
1062
1069
|
const before = storage.getChangesSince ? (await storage.getChangesSince()).version : undefined;
|
|
1063
1070
|
const expected = { id: "a", expected: "v2", found: true, value: item("a", 1), version: "v1" };
|
|
1064
1071
|
const setError = await rejects(() => storage.set("a", item("a", 3), { expectedVersion: "v2" }), "set() with a stale version");
|
|
1065
|
-
|
|
1072
|
+
assert.deepStrictEqual(conflictsOfRejection(setError, "set() with a stale version"), [expected], "the conflicts of set() with a stale version");
|
|
1066
1073
|
const deleteError = await rejects(() => storage.delete("a", { expectedVersion: "v2" }), "delete() with a stale version");
|
|
1067
|
-
|
|
1068
|
-
|
|
1074
|
+
assert.deepStrictEqual(conflictsOfRejection(deleteError, "delete() with a stale version"), [expected], "the conflicts of delete() with a stale version");
|
|
1075
|
+
assert.deepStrictEqual(await storage.get("a"), item("a", 1), "the item after the refused writes");
|
|
1069
1076
|
stop?.();
|
|
1070
|
-
|
|
1077
|
+
assert.deepStrictEqual(batches, 0, "the batches of observe() for the refused writes");
|
|
1071
1078
|
if (storage.getChangesSince && before !== undefined) {
|
|
1072
1079
|
const after = await storage.getChangesSince(before);
|
|
1073
|
-
|
|
1080
|
+
assert.deepStrictEqual({ changed: after.changed, deleted: after.deleted }, { changed: [], deleted: [] }, "the changes after the refused writes");
|
|
1074
1081
|
}
|
|
1075
1082
|
}));
|
|
1076
1083
|
add("the expected version null expects no item", () => withStorage(versioned, async (storage) => {
|
|
1077
1084
|
await storage.set("b", item("b", 1), { expectedVersion: null });
|
|
1078
|
-
|
|
1085
|
+
assert.deepStrictEqual(await storage.get("b"), item("b", 1), "set() with expectedVersion null of a missing id");
|
|
1079
1086
|
const exists = await rejects(() => storage.set("b", item("b", 2), { expectedVersion: null }), "set() with expectedVersion null of an existing id");
|
|
1080
|
-
|
|
1087
|
+
assert.deepStrictEqual(conflictsOfRejection(exists, "set() with expectedVersion null of an existing id"), [{ id: "b", expected: null, found: true, value: item("b", 1), version: "v1" }], "the conflict of an existing id");
|
|
1081
1088
|
const missing = await rejects(() => storage.set("c", item("c", 1), { expectedVersion: "v1" }), "set() with a version of a missing id");
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1089
|
+
assert.deepStrictEqual(conflictsOfRejection(missing, "set() with a version of a missing id"), [{ id: "c", expected: "v1", found: false, version: null }], "the conflict of a missing id");
|
|
1090
|
+
assert.deepStrictEqual(await storage.has("c"), false, "the missing id after the refused set()");
|
|
1091
|
+
assert.deepStrictEqual(await storage.delete("x", { expectedVersion: null }), false, "delete() with expectedVersion null of a missing id");
|
|
1085
1092
|
}));
|
|
1086
1093
|
add("setBatch() tests the expected version of each id, and a conflict changes no item", () => withStorage(versioned, async (storage) => {
|
|
1087
1094
|
await storage.setBatch([["a", item("a", 1)], ["b", item("b", 1)]]);
|
|
1088
1095
|
const error = await rejects(() => storage.setBatch([["a", item("a", 2)], ["b", item("b", 2)], ["c", item("c", 1)]], { expectedVersions: [["a", "v1"], ["b", "v9"]] }), "setBatch() with one stale version");
|
|
1089
|
-
|
|
1090
|
-
|
|
1096
|
+
assert.deepStrictEqual(conflictsOfRejection(error, "setBatch() with one stale version"), [{ id: "b", expected: "v9", found: true, value: item("b", 1), version: "v1" }], "the conflicts of setBatch()");
|
|
1097
|
+
assert.deepStrictEqual(await storage.getBatch(["a", "b", "c"]), new Map([["a", item("a", 1)], ["b", item("b", 1)]]), "the items after the refused batch");
|
|
1091
1098
|
// An id without an expected version has no precondition
|
|
1092
1099
|
await storage.setBatch([["a", item("a", 2)], ["b", item("b", 2)], ["c", item("c", 1)]], { expectedVersions: [["a", "v1"], ["b", "v1"]] });
|
|
1093
|
-
|
|
1100
|
+
assert.deepStrictEqual(await storage.getBatch(["a", "b", "c"]), new Map([["a", item("a", 2)], ["b", item("b", 2)], ["c", item("c", 1)]]), "the items after the batch");
|
|
1094
1101
|
}));
|
|
1095
1102
|
add("two writes with the same expected version at the same time: one writes, and the other gets VERSION_CONFLICT", () => withStorage(versioned, async (storage) => {
|
|
1096
1103
|
await storage.set("a", item("a", 1));
|
|
@@ -1099,11 +1106,11 @@ function addPreconditionCases(add, create, createWithVersions) {
|
|
|
1099
1106
|
storage.set("a", item("a", 3), { expectedVersion: "v1" }),
|
|
1100
1107
|
]);
|
|
1101
1108
|
const won = results.flatMap((result, index) => (result.status === "fulfilled" ? [index] : []));
|
|
1102
|
-
|
|
1109
|
+
assert.deepStrictEqual(won.length, 1, "the number of writes that won");
|
|
1103
1110
|
const lost = results.find((result) => result.status === "rejected");
|
|
1104
1111
|
const conflicts = conflictsOfRejection(lost.reason, "the write that lost");
|
|
1105
|
-
|
|
1106
|
-
|
|
1112
|
+
assert.deepStrictEqual(conflicts.map((conflict) => conflict.id), ["a"], "the conflict of the write that lost");
|
|
1113
|
+
assert.deepStrictEqual(await storage.get("a"), item("a", won[0] === 0 ? 2 : 3), "the item of the write that won");
|
|
1107
1114
|
}));
|
|
1108
1115
|
}
|
|
1109
1116
|
/**
|