diff options
author | Matt Mastracci <matthew@mastracci.com> | 2024-02-10 13:22:13 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-02-10 20:22:13 +0000 |
commit | f5e46c9bf2f50d66a953fa133161fc829cecff06 (patch) | |
tree | 8faf2f5831c1c7b11d842cd9908d141082c869a5 /tests/unit/kv_test.ts | |
parent | d2477f780630a812bfd65e3987b70c0d309385bb (diff) |
chore: move cli/tests/ -> tests/ (#22369)
This looks like a massive PR, but it's only a move from cli/tests ->
tests, and updates of relative paths for files.
This is the first step towards aggregate all of the integration test
files under tests/, which will lead to a set of integration tests that
can run without the CLI binary being built.
While we could leave these tests under `cli`, it would require us to
keep a more complex directory structure for the various test runners. In
addition, we have a lot of complexity to ignore various test files in
the `cli` project itself (cargo publish exclusion rules, autotests =
false, etc).
And finally, the `tests/` folder will eventually house the `test_ffi`,
`test_napi` and other testing code, reducing the size of the root repo
directory.
For easier review, the extremely large and noisy "move" is in the first
commit (with no changes -- just a move), while the remainder of the
changes to actual files is in the second commit.
Diffstat (limited to 'tests/unit/kv_test.ts')
-rw-r--r-- | tests/unit/kv_test.ts | 2321 |
1 files changed, 2321 insertions, 0 deletions
diff --git a/tests/unit/kv_test.ts b/tests/unit/kv_test.ts new file mode 100644 index 000000000..5780d9900 --- /dev/null +++ b/tests/unit/kv_test.ts @@ -0,0 +1,2321 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +import { + assert, + assertEquals, + AssertionError, + assertNotEquals, + assertRejects, + assertThrows, +} from "./test_util.ts"; +import { assertType, IsExact } from "@test_util/std/testing/types.ts"; + +const sleep = (time: number) => new Promise((r) => setTimeout(r, time)); + +let isCI: boolean; +try { + isCI = Deno.env.get("CI") !== undefined; +} catch { + isCI = true; +} + +// Defined in test_util/src/lib.rs +Deno.env.set("DENO_KV_ACCESS_TOKEN", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + +Deno.test({ + name: "openKv :memory: no permissions", + permissions: {}, + async fn() { + const db = await Deno.openKv(":memory:"); + await db.close(); + }, +}); + +Deno.test({ + name: "openKv invalid filenames", + permissions: {}, + async fn() { + await assertRejects( + async () => await Deno.openKv(""), + TypeError, + "Filename cannot be empty", + ); + await assertRejects( + async () => await Deno.openKv(":foo"), + TypeError, + "Filename cannot start with ':' unless prefixed with './'", + ); + }, +}); + +function dbTest(name: string, fn: (db: Deno.Kv) => Promise<void> | void) { + Deno.test({ + name, + // https://github.com/denoland/deno/issues/18363 + ignore: Deno.build.os === "darwin" && isCI, + async fn() { + const db: Deno.Kv = await Deno.openKv(":memory:"); + try { + await fn(db); + } finally { + db.close(); + } + }, + }); +} + +function queueTest(name: string, fn: (db: Deno.Kv) => Promise<void>) { + Deno.test({ + name, + // https://github.com/denoland/deno/issues/18363 + ignore: Deno.build.os === "darwin" && isCI, + async fn() { + const db: Deno.Kv = await Deno.openKv(":memory:"); + await fn(db); + }, + }); +} + +const ZERO_VERSIONSTAMP = "00000000000000000000"; + +dbTest("basic read-write-delete and versionstamps", async (db) => { + const result1 = await db.get(["a"]); + assertEquals(result1.key, ["a"]); + assertEquals(result1.value, null); + assertEquals(result1.versionstamp, null); + + const setRes = await db.set(["a"], "b"); + assert(setRes.ok); + assert(setRes.versionstamp > ZERO_VERSIONSTAMP); + const result2 = await db.get(["a"]); + assertEquals(result2.key, ["a"]); + assertEquals(result2.value, "b"); + assertEquals(result2.versionstamp, setRes.versionstamp); + + const setRes2 = await db.set(["a"], "c"); + assert(setRes2.ok); + assert(setRes2.versionstamp > setRes.versionstamp); + const result3 = await db.get(["a"]); + assertEquals(result3.key, ["a"]); + assertEquals(result3.value, "c"); + assertEquals(result3.versionstamp, setRes2.versionstamp); + + await db.delete(["a"]); + const result4 = await db.get(["a"]); + assertEquals(result4.key, ["a"]); + assertEquals(result4.value, null); + assertEquals(result4.versionstamp, null); +}); + +const VALUE_CASES = [ + { name: "string", value: "hello" }, + { name: "number", value: 42 }, + { name: "bigint", value: 42n }, + { name: "boolean", value: true }, + { name: "null", value: null }, + { name: "undefined", value: undefined }, + { name: "Date", value: new Date(0) }, + { name: "Uint8Array", value: new Uint8Array([1, 2, 3]) }, + { name: "ArrayBuffer", value: new ArrayBuffer(3) }, + { name: "array", value: [1, 2, 3] }, + { name: "object", value: { a: 1, b: 2 } }, + { name: "nested array", value: [[1, 2], [3, 4]] }, + { name: "nested object", value: { a: { b: 1 } } }, +]; + +for (const { name, value } of VALUE_CASES) { + dbTest(`set and get ${name} value`, async (db) => { + await db.set(["a"], value); + const result = await db.get(["a"]); + assertEquals(result.key, ["a"]); + assertEquals(result.value, value); + }); +} + +dbTest("set and get recursive object", async (db) => { + // deno-lint-ignore no-explicit-any + const value: any = { a: undefined }; + value.a = value; + await db.set(["a"], value); + const result = await db.get(["a"]); + assertEquals(result.key, ["a"]); + // deno-lint-ignore no-explicit-any + const resultValue: any = result.value; + assert(resultValue.a === resultValue); +}); + +// invalid values (as per structured clone algorithm with _for storage_, NOT JSON) +const INVALID_VALUE_CASES = [ + { name: "function", value: () => {} }, + { name: "symbol", value: Symbol() }, + { name: "WeakMap", value: new WeakMap() }, + { name: "WeakSet", value: new WeakSet() }, + { + name: "WebAssembly.Module", + value: new WebAssembly.Module( + new Uint8Array([0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00]), + ), + }, + { + name: "SharedArrayBuffer", + value: new SharedArrayBuffer(3), + }, +]; + +for (const { name, value } of INVALID_VALUE_CASES) { + dbTest(`set and get ${name} value (invalid)`, async (db) => { + await assertRejects( + async () => await db.set(["a"], value), + Error, + ); + const res = await db.get(["a"]); + assertEquals(res.key, ["a"]); + assertEquals(res.value, null); + }); +} + +const keys = [ + ["a"], + ["a", "b"], + ["a", "b", "c"], + [1], + ["a", 1], + ["a", 1, "b"], + [1n], + ["a", 1n], + ["a", 1n, "b"], + [true], + ["a", true], + ["a", true, "b"], + [new Uint8Array([1, 2, 3])], + ["a", new Uint8Array([1, 2, 3])], + ["a", new Uint8Array([1, 2, 3]), "b"], + [1, 1n, true, new Uint8Array([1, 2, 3]), "a"], +]; + +for (const key of keys) { + dbTest(`set and get ${Deno.inspect(key)} key`, async (db) => { + await db.set(key, "b"); + const result = await db.get(key); + assertEquals(result.key, key); + assertEquals(result.value, "b"); + }); +} + +const INVALID_KEYS = [ + [null], + [undefined], + [], + [{}], + [new Date()], + [new ArrayBuffer(3)], + [new Uint8Array([1, 2, 3]).buffer], + [["a", "b"]], +]; + +for (const key of INVALID_KEYS) { + dbTest(`set and get invalid key ${Deno.inspect(key)}`, async (db) => { + await assertRejects( + async () => { + // @ts-ignore - we are testing invalid keys + await db.set(key, "b"); + }, + Error, + ); + }); +} + +dbTest("compare and mutate", async (db) => { + await db.set(["t"], "1"); + + const currentValue = await db.get(["t"]); + assert(currentValue.versionstamp); + assert(currentValue.versionstamp > ZERO_VERSIONSTAMP); + + let res = await db.atomic() + .check({ key: ["t"], versionstamp: currentValue.versionstamp }) + .set(currentValue.key, "2") + .commit(); + assert(res.ok); + assert(res.versionstamp > currentValue.versionstamp); + + const newValue = await db.get(["t"]); + assertEquals(newValue.versionstamp, res.versionstamp); + assertEquals(newValue.value, "2"); + + res = await db.atomic() + .check({ key: ["t"], versionstamp: currentValue.versionstamp }) + .set(currentValue.key, "3") + .commit(); + assert(!res.ok); + + const newValue2 = await db.get(["t"]); + assertEquals(newValue2.versionstamp, newValue.versionstamp); + assertEquals(newValue2.value, "2"); +}); + +dbTest("compare and mutate not exists", async (db) => { + let res = await db.atomic() + .check({ key: ["t"], versionstamp: null }) + .set(["t"], "1") + .commit(); + assert(res.ok); + assert(res.versionstamp > ZERO_VERSIONSTAMP); + + const newValue = await db.get(["t"]); + assertEquals(newValue.versionstamp, res.versionstamp); + assertEquals(newValue.value, "1"); + + res = await db.atomic() + .check({ key: ["t"], versionstamp: null }) + .set(["t"], "2") + .commit(); + assert(!res.ok); +}); + +dbTest("atomic mutation helper (sum)", async (db) => { + await db.set(["t"], new Deno.KvU64(42n)); + assertEquals((await db.get(["t"])).value, new Deno.KvU64(42n)); + + await db.atomic().sum(["t"], 1n).commit(); + assertEquals((await db.get(["t"])).value, new Deno.KvU64(43n)); +}); + +dbTest("atomic mutation helper (min)", async (db) => { + await db.set(["t"], new Deno.KvU64(42n)); + assertEquals((await db.get(["t"])).value, new Deno.KvU64(42n)); + + await db.atomic().min(["t"], 1n).commit(); + assertEquals((await db.get(["t"])).value, new Deno.KvU64(1n)); + + await db.atomic().min(["t"], 2n).commit(); + assertEquals((await db.get(["t"])).value, new Deno.KvU64(1n)); +}); + +dbTest("atomic mutation helper (max)", async (db) => { + await db.set(["t"], new Deno.KvU64(42n)); + assertEquals((await db.get(["t"])).value, new Deno.KvU64(42n)); + + await db.atomic().max(["t"], 41n).commit(); + assertEquals((await db.get(["t"])).value, new Deno.KvU64(42n)); + + await db.atomic().max(["t"], 43n).commit(); + assertEquals((await db.get(["t"])).value, new Deno.KvU64(43n)); +}); + +dbTest("compare multiple and mutate", async (db) => { + const setRes1 = await db.set(["t1"], "1"); + const setRes2 = await db.set(["t2"], "2"); + assert(setRes1.ok); + assert(setRes1.versionstamp > ZERO_VERSIONSTAMP); + assert(setRes2.ok); + assert(setRes2.versionstamp > ZERO_VERSIONSTAMP); + + const currentValue1 = await db.get(["t1"]); + assertEquals(currentValue1.versionstamp, setRes1.versionstamp); + const currentValue2 = await db.get(["t2"]); + assertEquals(currentValue2.versionstamp, setRes2.versionstamp); + + const res = await db.atomic() + .check({ key: ["t1"], versionstamp: currentValue1.versionstamp }) + .check({ key: ["t2"], versionstamp: currentValue2.versionstamp }) + .set(currentValue1.key, "3") + .set(currentValue2.key, "4") + .commit(); + assert(res.ok); + assert(res.versionstamp > setRes2.versionstamp); + + const newValue1 = await db.get(["t1"]); + assertEquals(newValue1.versionstamp, res.versionstamp); + assertEquals(newValue1.value, "3"); + const newValue2 = await db.get(["t2"]); + assertEquals(newValue2.versionstamp, res.versionstamp); + assertEquals(newValue2.value, "4"); + + // just one of the two checks failed + const res2 = await db.atomic() + .check({ key: ["t1"], versionstamp: newValue1.versionstamp }) + .check({ key: ["t2"], versionstamp: null }) + .set(newValue1.key, "5") + .set(newValue2.key, "6") + .commit(); + assert(!res2.ok); + + const newValue3 = await db.get(["t1"]); + assertEquals(newValue3.versionstamp, res.versionstamp); + assertEquals(newValue3.value, "3"); + const newValue4 = await db.get(["t2"]); + assertEquals(newValue4.versionstamp, res.versionstamp); + assertEquals(newValue4.value, "4"); +}); + +dbTest("atomic mutation ordering (set before delete)", async (db) => { + await db.set(["a"], "1"); + const res = await db.atomic() + .set(["a"], "2") + .delete(["a"]) + .commit(); + assert(res.ok); + const result = await db.get(["a"]); + assertEquals(result.value, null); +}); + +dbTest("atomic mutation ordering (delete before set)", async (db) => { + await db.set(["a"], "1"); + const res = await db.atomic() + .delete(["a"]) + .set(["a"], "2") + .commit(); + assert(res.ok); + const result = await db.get(["a"]); + assertEquals(result.value, "2"); +}); + +dbTest("atomic mutation type=set", async (db) => { + const res = await db.atomic() + .mutate({ key: ["a"], value: "1", type: "set" }) + .commit(); + assert(res.ok); + const result = await db.get(["a"]); + assertEquals(result.value, "1"); +}); + +dbTest("atomic mutation type=set overwrite", async (db) => { + await db.set(["a"], "1"); + const res = await db.atomic() + .mutate({ key: ["a"], value: "2", type: "set" }) + .commit(); + assert(res.ok); + const result = await db.get(["a"]); + assertEquals(result.value, "2"); +}); + +dbTest("atomic mutation type=delete", async (db) => { + await db.set(["a"], "1"); + const res = await db.atomic() + .mutate({ key: ["a"], type: "delete" }) + .commit(); + assert(res.ok); + const result = await db.get(["a"]); + assertEquals(result.value, null); +}); + +dbTest("atomic mutation type=delete no exists", async (db) => { + const res = await db.atomic() + .mutate({ key: ["a"], type: "delete" }) + .commit(); + assert(res.ok); + const result = await db.get(["a"]); + assertEquals(result.value, null); +}); + +dbTest("atomic mutation type=sum", async (db) => { + await db.set(["a"], new Deno.KvU64(10n)); + const res = await db.atomic() + .mutate({ key: ["a"], value: new Deno.KvU64(1n), type: "sum" }) + .commit(); + assert(res.ok); + const result = await db.get(["a"]); + assertEquals(result.value, new Deno.KvU64(11n)); +}); + +dbTest("atomic mutation type=sum no exists", async (db) => { + const res = await db.atomic() + .mutate({ key: ["a"], value: new Deno.KvU64(1n), type: "sum" }) + .commit(); + assert(res.ok); + const result = await db.get(["a"]); + assert(result.value); + assertEquals(result.value, new Deno.KvU64(1n)); +}); + +dbTest("atomic mutation type=sum wrap around", async (db) => { + await db.set(["a"], new Deno.KvU64(0xffffffffffffffffn)); + const res = await db.atomic() + .mutate({ key: ["a"], value: new Deno.KvU64(10n), type: "sum" }) + .commit(); + assert(res.ok); + const result = await db.get(["a"]); + assertEquals(result.value, new Deno.KvU64(9n)); + + const res2 = await db.atomic() + .mutate({ + key: ["a"], + value: new Deno.KvU64(0xffffffffffffffffn), + type: "sum", + }) + .commit(); + assert(res2); + const result2 = await db.get(["a"]); + assertEquals(result2.value, new Deno.KvU64(8n)); +}); + +dbTest("atomic mutation type=sum wrong type in db", async (db) => { + await db.set(["a"], 1); + await assertRejects( + async () => { + await db.atomic() + .mutate({ key: ["a"], value: new Deno.KvU64(1n), type: "sum" }) + .commit(); + }, + TypeError, + "Failed to perform 'sum' mutation on a non-U64 value in the database", + ); +}); + +dbTest("atomic mutation type=sum wrong type in mutation", async (db) => { + await db.set(["a"], new Deno.KvU64(1n)); + await assertRejects( + async () => { + await db.atomic() + // @ts-expect-error wrong type is intentional + .mutate({ key: ["a"], value: 1, type: "sum" }) + .commit(); + }, + TypeError, + "Failed to perform 'sum' mutation on a non-U64 operand", + ); +}); + +dbTest("atomic mutation type=min", async (db) => { + await db.set(["a"], new Deno.KvU64(10n)); + const res = await db.atomic() + .mutate({ key: ["a"], value: new Deno.KvU64(5n), type: "min" }) + .commit(); + assert(res.ok); + const result = await db.get(["a"]); + assertEquals(result.value, new Deno.KvU64(5n)); + + const res2 = await db.atomic() + .mutate({ key: ["a"], value: new Deno.KvU64(15n), type: "min" }) + .commit(); + assert(res2); + const result2 = await db.get(["a"]); + assertEquals(result2.value, new Deno.KvU64(5n)); +}); + +dbTest("atomic mutation type=min no exists", async (db) => { + const res = await db.atomic() + .mutate({ key: ["a"], value: new Deno.KvU64(1n), type: "min" }) + .commit(); + assert(res.ok); + const result = await db.get(["a"]); + assert(result.value); + assertEquals(result.value, new Deno.KvU64(1n)); +}); + +dbTest("atomic mutation type=min wrong type in db", async (db) => { + await db.set(["a"], 1); + await assertRejects( + async () => { + await db.atomic() + .mutate({ key: ["a"], value: new Deno.KvU64(1n), type: "min" }) + .commit(); + }, + TypeError, + "Failed to perform 'min' mutation on a non-U64 value in the database", + ); +}); + +dbTest("atomic mutation type=min wrong type in mutation", async (db) => { + await db.set(["a"], new Deno.KvU64(1n)); + await assertRejects( + async () => { + await db.atomic() + // @ts-expect-error wrong type is intentional + .mutate({ key: ["a"], value: 1, type: "min" }) + .commit(); + }, + TypeError, + "Failed to perform 'min' mutation on a non-U64 operand", + ); +}); + +dbTest("atomic mutation type=max", async (db) => { + await db.set(["a"], new Deno.KvU64(10n)); + const res = await db.atomic() + .mutate({ key: ["a"], value: new Deno.KvU64(5n), type: "max" }) + .commit(); + assert(res.ok); + const result = await db.get(["a"]); + assertEquals(result.value, new Deno.KvU64(10n)); + + const res2 = await db.atomic() + .mutate({ key: ["a"], value: new Deno.KvU64(15n), type: "max" }) + .commit(); + assert(res2); + const result2 = await db.get(["a"]); + assertEquals(result2.value, new Deno.KvU64(15n)); +}); + +dbTest("atomic mutation type=max no exists", async (db) => { + const res = await db.atomic() + .mutate({ key: ["a"], value: new Deno.KvU64(1n), type: "max" }) + .commit(); + assert(res.ok); + const result = await db.get(["a"]); + assert(result.value); + assertEquals(result.value, new Deno.KvU64(1n)); +}); + +dbTest("atomic mutation type=max wrong type in db", async (db) => { + await db.set(["a"], 1); + await assertRejects( + async () => { + await db.atomic() + .mutate({ key: ["a"], value: new Deno.KvU64(1n), type: "max" }) + .commit(); + }, + TypeError, + "Failed to perform 'max' mutation on a non-U64 value in the database", + ); +}); + +dbTest("atomic mutation type=max wrong type in mutation", async (db) => { + await db.set(["a"], new Deno.KvU64(1n)); + await assertRejects( + async () => { + await db.atomic() + // @ts-expect-error wrong type is intentional + .mutate({ key: ["a"], value: 1, type: "max" }) + .commit(); + }, + TypeError, + "Failed to perform 'max' mutation on a non-U64 operand", + ); +}); + +Deno.test("KvU64 comparison", () => { + const a = new Deno.KvU64(1n); + const b = new Deno.KvU64(1n); + assertEquals(a, b); + assertThrows(() => { + assertEquals(a, new Deno.KvU64(2n)); + }, AssertionError); +}); + +Deno.test("KvU64 overflow", () => { + assertThrows(() => { + new Deno.KvU64(2n ** 64n); + }, RangeError); +}); + +Deno.test("KvU64 underflow", () => { + assertThrows(() => { + new Deno.KvU64(-1n); + }, RangeError); +}); + +Deno.test("KvU64 unbox", () => { + const a = new Deno.KvU64(1n); + assertEquals(a.value, 1n); +}); + +Deno.test("KvU64 unbox with valueOf", () => { + const a = new Deno.KvU64(1n); + assertEquals(a.valueOf(), 1n); +}); + +Deno.test("KvU64 auto-unbox", () => { + const a = new Deno.KvU64(1n); + assertEquals(a as unknown as bigint + 1n, 2n); +}); + +Deno.test("KvU64 toString", () => { + const a = new Deno.KvU64(1n); + assertEquals(a.toString(), "1"); +}); + +Deno.test("KvU64 inspect", () => { + const a = new Deno.KvU64(1n); + assertEquals(Deno.inspect(a), "[Deno.KvU64: 1n]"); +}); + +async function collect<T>( + iter: Deno.KvListIterator<T>, +): Promise<Deno.KvEntry<T>[]> { + const entries: Deno.KvEntry<T>[] = []; + for await (const entry of iter) { + entries.push(entry); + } + return entries; +} + +async function setupData(db: Deno.Kv): Promise<string> { + const res = await db.atomic() + .set(["a"], -1) + .set(["a", "a"], 0) + .set(["a", "b"], 1) + .set(["a", "c"], 2) + .set(["a", "d"], 3) + .set(["a", "e"], 4) + .set(["b"], 99) + .set(["b", "a"], 100) + .commit(); + assert(res.ok); + return res.versionstamp; +} + +dbTest("get many", async (db) => { + const versionstamp = await setupData(db); + const entries = await db.getMany([["b", "a"], ["a"], ["c"]]); + assertEquals(entries, [ + { key: ["b", "a"], value: 100, versionstamp }, + { key: ["a"], value: -1, versionstamp }, + { key: ["c"], value: null, versionstamp: null }, + ]); +}); + +dbTest("list prefix", async (db) => { + const versionstamp = await setupData(db); + const entries = await collect(db.list({ prefix: ["a"] })); + assertEquals(entries, [ + { key: ["a", "a"], value: 0, versionstamp }, + { key: ["a", "b"], value: 1, versionstamp }, + { key: ["a", "c"], value: 2, versionstamp }, + { key: ["a", "d"], value: 3, versionstamp }, + { key: ["a", "e"], value: 4, versionstamp }, + ]); +}); + +dbTest("list prefix empty", async (db) => { + await setupData(db); + const entries = await collect(db.list({ prefix: ["c"] })); + assertEquals(entries.length, 0); + + const entries2 = await collect(db.list({ prefix: ["a", "f"] })); + assertEquals(entries2.length, 0); +}); + +dbTest("list prefix with start", async (db) => { + const versionstamp = await setupData(db); + const entries = await collect(db.list({ prefix: ["a"], start: ["a", "c"] })); + assertEquals(entries, [ + { key: ["a", "c"], value: 2, versionstamp }, + { key: ["a", "d"], value: 3, versionstamp }, + { key: ["a", "e"], value: 4, versionstamp }, + ]); +}); + +dbTest("list prefix with start empty", async (db) => { + await setupData(db); + const entries = await collect(db.list({ prefix: ["a"], start: ["a", "f"] })); + assertEquals(entries.length, 0); +}); + +dbTest("list prefix with start equal to prefix", async (db) => { + await setupData(db); + await assertRejects( + async () => await collect(db.list({ prefix: ["a"], start: ["a"] })), + TypeError, + "start key is not in the keyspace defined by prefix", + ); +}); + +dbTest("list prefix with start out of bounds", async (db) => { + await setupData(db); + await assertRejects( + async () => await collect(db.list({ prefix: ["b"], start: ["a"] })), + TypeError, + "start key is not in the keyspace defined by prefix", + ); +}); + +dbTest("list prefix with end", async (db) => { + const versionstamp = await setupData(db); + const entries = await collect(db.list({ prefix: ["a"], end: ["a", "c"] })); + assertEquals(entries, [ + { key: ["a", "a"], value: 0, versionstamp }, + { key: ["a", "b"], value: 1, versionstamp }, + ]); +}); + +dbTest("list prefix with end empty", async (db) => { + await setupData(db); + const entries = await collect(db.list({ prefix: ["a"], end: ["a", "a"] })); + assertEquals(entries.length, 0); +}); + +dbTest("list prefix with end equal to prefix", async (db) => { + await setupData(db); + await assertRejects( + async () => await collect(db.list({ prefix: ["a"], end: ["a"] })), + TypeError, + "end key is not in the keyspace defined by prefix", + ); +}); + +dbTest("list prefix with end out of bounds", async (db) => { + await setupData(db); + await assertRejects( + async () => await collect(db.list({ prefix: ["a"], end: ["b"] })), + TypeError, + "end key is not in the keyspace defined by prefix", + ); +}); + +dbTest("list prefix with empty prefix", async (db) => { + const res = await db.set(["a"], 1); + const entries = await collect(db.list({ prefix: [] })); + assertEquals(entries, [ + { key: ["a"], value: 1, versionstamp: res.versionstamp }, + ]); +}); + +dbTest("list prefix reverse", async (db) => { + const versionstamp = await setupData(db); + const entries = await collect(db.list({ prefix: ["a"] }, { reverse: true })); + assertEquals(entries, [ + { key: ["a", "e"], value: 4, versionstamp }, + { key: ["a", "d"], value: 3, versionstamp }, + { key: ["a", "c"], value: 2, versionstamp }, + { key: ["a", "b"], value: 1, versionstamp }, + { key: ["a", "a"], value: 0, versionstamp }, + ]); +}); + +dbTest("list prefix reverse with start", async (db) => { + const versionstamp = await setupData(db); + const entries = await collect( + db.list({ prefix: ["a"], start: ["a", "c"] }, { reverse: true }), + ); + assertEquals(entries, [ + { key: ["a", "e"], value: 4, versionstamp }, + { key: ["a", "d"], value: 3, versionstamp }, + { key: ["a", "c"], value: 2, versionstamp }, + ]); +}); + +dbTest("list prefix reverse with start empty", async (db) => { + await setupData(db); + const entries = await collect( + db.list({ prefix: ["a"], start: ["a", "f"] }, { reverse: true }), + ); + assertEquals(entries.length, 0); +}); + +dbTest("list prefix reverse with end", async (db) => { + const versionstamp = await setupData(db); + const entries = await collect( + db.list({ prefix: ["a"], end: ["a", "c"] }, { reverse: true }), + ); + assertEquals(entries, [ + { key: ["a", "b"], value: 1, versionstamp }, + { key: ["a", "a"], value: 0, versionstamp }, + ]); +}); + +dbTest("list prefix reverse with end empty", async (db) => { + await setupData(db); + const entries = await collect( + db.list({ prefix: ["a"], end: ["a", "a"] }, { reverse: true }), + ); + assertEquals(entries.length, 0); +}); + +dbTest("list prefix limit", async (db) => { + const versionstamp = await setupData(db); + const entries = await collect(db.list({ prefix: ["a"] }, { limit: 2 })); + assertEquals(entries, [ + { key: ["a", "a"], value: 0, versionstamp }, + { key: ["a", "b"], value: 1, versionstamp }, + ]); +}); + +dbTest("list prefix limit reverse", async (db) => { + const versionstamp = await setupData(db); + const entries = await collect( + db.list({ prefix: ["a"] }, { limit: 2, reverse: true }), + ); + assertEquals(entries, [ + { key: ["a", "e"], value: 4, versionstamp }, + { key: ["a", "d"], value: 3, versionstamp }, + ]); +}); + +dbTest("list prefix with small batch size", async (db) => { + const versionstamp = await setupData(db); + const entries = await collect(db.list({ prefix: ["a"] }, { batchSize: 2 })); + assertEquals(entries, [ + { key: ["a", "a"], value: 0, versionstamp }, + { key: ["a", "b"], value: 1, versionstamp }, + { key: ["a", "c"], value: 2, versionstamp }, + { key: ["a", "d"], value: 3, versionstamp }, + { key: ["a", "e"], value: 4, versionstamp }, + ]); +}); + +dbTest("list prefix with small batch size reverse", async (db) => { + const versionstamp = await setupData(db); + const entries = await collect( + db.list({ prefix: ["a"] }, { batchSize: 2, reverse: true }), + ); + assertEquals(entries, [ + { key: ["a", "e"], value: 4, versionstamp }, + { key: ["a", "d"], value: 3, versionstamp }, + { key: ["a", "c"], value: 2, versionstamp }, + { key: ["a", "b"], value: 1, versionstamp }, + { key: ["a", "a"], value: 0, versionstamp }, + ]); +}); + +dbTest("list prefix with small batch size and limit", async (db) => { + const versionstamp = await setupData(db); + const entries = await collect( + db.list({ prefix: ["a"] }, { batchSize: 2, limit: 3 }), + ); + assertEquals(entries, [ + { key: ["a", "a"], value: 0, versionstamp }, + { key: ["a", "b"], value: 1, versionstamp }, + { key: ["a", "c"], value: 2, versionstamp }, + ]); +}); + +dbTest("list prefix with small batch size and limit reverse", async (db) => { + const versionstamp = await setupData(db); + const entries = await collect( + db.list({ prefix: ["a"] }, { batchSize: 2, limit: 3, reverse: true }), + ); + assertEquals(entries, [ + { key: ["a", "e"], value: 4, versionstamp }, + { key: ["a", "d"], value: 3, versionstamp }, + { key: ["a", "c"], value: 2, versionstamp }, + ]); +}); + +dbTest("list prefix with manual cursor", async (db) => { + const versionstamp = await setupData(db); + const iterator = db.list({ prefix: ["a"] }, { limit: 2 }); + const values = await collect(iterator); + assertEquals(values, [ + { key: ["a", "a"], value: 0, versionstamp }, + { key: ["a", "b"], value: 1, versionstamp }, + ]); + + const cursor = iterator.cursor; + assertEquals(cursor, "AmIA"); + + const iterator2 = db.list({ prefix: ["a"] }, { cursor }); + const values2 = await collect(iterator2); + assertEquals(values2, [ + { key: ["a", "c"], value: 2, versionstamp }, + { key: ["a", "d"], value: 3, versionstamp }, + { key: ["a", "e"], value: 4, versionstamp }, + ]); +}); + +dbTest("list prefix with manual cursor reverse", async (db) => { + const versionstamp = await setupData(db); + + const iterator = db.list({ prefix: ["a"] }, { limit: 2, reverse: true }); + const values = await collect(iterator); + assertEquals(values, [ + { key: ["a", "e"], value: 4, versionstamp }, + { key: ["a", "d"], value: 3, versionstamp }, + ]); + + const cursor = iterator.cursor; + assertEquals(cursor, "AmQA"); + + const iterator2 = db.list({ prefix: ["a"] }, { cursor, reverse: true }); + const values2 = await collect(iterator2); + assertEquals(values2, [ + { key: ["a", "c"], value: 2, versionstamp }, + { key: ["a", "b"], value: 1, versionstamp }, + { key: ["a", "a"], value: 0, versionstamp }, + ]); +}); + +dbTest("list range", async (db) => { + const versionstamp = await setupData(db); + + const entries = await collect( + db.list({ start: ["a", "a"], end: ["a", "z"] }), + ); + assertEquals(entries, [ + { key: ["a", "a"], value: 0, versionstamp }, + { key: ["a", "b"], value: 1, versionstamp }, + { key: ["a", "c"], value: 2, versionstamp }, + { key: ["a", "d"], value: 3, versionstamp }, + { key: ["a", "e"], value: 4, versionstamp }, + ]); +}); + +dbTest("list range reverse", async (db) => { + const versionstamp = await setupData(db); + + const entries = await collect( + db.list({ start: ["a", "a"], end: ["a", "z"] }, { reverse: true }), + ); + assertEquals(entries, [ + { key: ["a", "e"], value: 4, versionstamp }, + { key: ["a", "d"], value: 3, versionstamp }, + { key: ["a", "c"], value: 2, versionstamp }, + { key: ["a", "b"], value: 1, versionstamp }, + { key: ["a", "a"], value: 0, versionstamp }, + ]); +}); + +dbTest("list range with limit", async (db) => { + const versionstamp = await setupData(db); + + const entries = await collect( + db.list({ start: ["a", "a"], end: ["a", "z"] }, { limit: 3 }), + ); + assertEquals(entries, [ + { key: ["a", "a"], value: 0, versionstamp }, + { key: ["a", "b"], value: 1, versionstamp }, + { key: ["a", "c"], value: 2, versionstamp }, + ]); +}); + +dbTest("list range with limit reverse", async (db) => { + const versionstamp = await setupData(db); + + const entries = await collect( + db.list({ start: ["a", "a"], end: ["a", "z"] }, { + limit: 3, + reverse: true, + }), + ); + assertEquals(entries, [ + { key: ["a", "e"], value: 4, versionstamp }, + { key: ["a", "d"], value: 3, versionstamp }, + { key: ["a", "c"], value: 2, versionstamp }, + ]); +}); + +dbTest("list range nesting", async (db) => { + const versionstamp = await setupData(db); + + const entries = await collect(db.list({ start: ["a"], end: ["a", "d"] })); + assertEquals(entries, [ + { key: ["a"], value: -1, versionstamp }, + { key: ["a", "a"], value: 0, versionstamp }, + { key: ["a", "b"], value: 1, versionstamp }, + { key: ["a", "c"], value: 2, versionstamp }, + ]); +}); + +dbTest("list range short", async (db) => { + const versionstamp = await setupData(db); + + const entries = await collect( + db.list({ start: ["a", "b"], end: ["a", "d"] }), + ); + assertEquals(entries, [ + { key: ["a", "b"], value: 1, versionstamp }, + { key: ["a", "c"], value: 2, versionstamp }, + ]); +}); + +dbTest("list range with manual cursor", async (db) => { + const versionstamp = await setupData(db); + + const iterator = db.list({ start: ["a", "b"], end: ["a", "z"] }, { + limit: 2, + }); + const entries = await collect(iterator); + assertEquals(entries, [ + { key: ["a", "b"], value: 1, versionstamp }, + { key: ["a", "c"], value: 2, versionstamp }, + ]); + + const cursor = iterator.cursor; + const iterator2 = db.list({ start: ["a", "b"], end: ["a", "z"] }, { + cursor, + }); + const entries2 = await collect(iterator2); + assertEquals(entries2, [ + { key: ["a", "d"], value: 3, versionstamp }, + { key: ["a", "e"], value: 4, versionstamp }, + ]); +}); + +dbTest("list range with manual cursor reverse", async (db) => { + const versionstamp = await setupData(db); + + const iterator = db.list({ start: ["a", "b"], end: ["a", "z"] }, { + limit: 2, + reverse: true, + }); + const entries = await collect(iterator); + assertEquals(entries, [ + { key: ["a", "e"], value: 4, versionstamp }, + { key: ["a", "d"], value: 3, versionstamp }, + ]); + + const cursor = iterator.cursor; + const iterator2 = db.list({ start: ["a", "b"], end: ["a", "z"] }, { + cursor, + reverse: true, + }); + const entries2 = await collect(iterator2); + assertEquals(entries2, [ + { key: ["a", "c"], value: 2, versionstamp }, + { key: ["a", "b"], value: 1, versionstamp }, + ]); +}); + +dbTest("list range with start greater than end", async (db) => { + await setupData(db); + await assertRejects( + async () => await collect(db.list({ start: ["b"], end: ["a"] })), + TypeError, + "start key is greater than end key", + ); +}); + +dbTest("list range with start equal to end", async (db) => { + await setupData(db); + const entries = await collect(db.list({ start: ["a"], end: ["a"] })); + assertEquals(entries.length, 0); +}); + +dbTest("list invalid selector", async (db) => { + await setupData(db); + + await assertRejects(async () => { + await collect( + db.list({ prefix: ["a"], start: ["a", "b"], end: ["a", "c"] }), + ); + }, TypeError); + + await assertRejects(async () => { + await collect( + // @ts-expect-error missing end + db.list({ start: ["a", "b"] }), + ); + }, TypeError); + + await assertRejects(async () => { + await collect( + // @ts-expect-error missing start + db.list({ end: ["a", "b"] }), + ); + }, TypeError); +}); + +dbTest("invalid versionstamp in atomic check rejects", async (db) => { + await assertRejects(async () => { + await db.atomic().check({ key: ["a"], versionstamp: "" }).commit(); + }, TypeError); + + await assertRejects(async () => { + await db.atomic().check({ key: ["a"], versionstamp: "xx".repeat(10) }) + .commit(); + }, TypeError); + + await assertRejects(async () => { + await db.atomic().check({ key: ["a"], versionstamp: "aa".repeat(11) }) + .commit(); + }, TypeError); +}); + +dbTest("invalid mutation type rejects", async (db) => { + await assertRejects(async () => { + await db.atomic() + // @ts-expect-error invalid type + value combo + .mutate({ key: ["a"], type: "set" }) + .commit(); + }, TypeError); + + await assertRejects(async () => { + await db.atomic() + // @ts-expect-error invalid type + value combo + .mutate({ key: ["a"], type: "delete", value: "123" }) + .commit(); + }, TypeError); + + await assertRejects(async () => { + await db.atomic() + // @ts-expect-error invalid type + .mutate({ key: ["a"], type: "foobar" }) + .commit(); + }, TypeError); + + await assertRejects(async () => { + await db.atomic() + // @ts-expect-error invalid type + .mutate({ key: ["a"], type: "foobar", value: "123" }) + .commit(); + }, TypeError); +}); + +dbTest("key ordering", async (db) => { + await db.atomic() + .set([new Uint8Array(0x1)], 0) + .set(["a"], 0) + .set([1n], 0) + .set([3.14], 0) + .set([false], 0) + .set([true], 0) + .commit(); + + assertEquals((await collect(db.list({ prefix: [] }))).map((x) => x.key), [ + [new Uint8Array(0x1)], + ["a"], + [1n], + [3.14], + [false], + [true], + ]); +}); + +dbTest("key size limit", async (db) => { + // 1 byte prefix + 1 byte suffix + 2045 bytes key + const lastValidKey = new Uint8Array(2046).fill(1); + const firstInvalidKey = new Uint8Array(2047).fill(1); + + const res = await db.set([lastValidKey], 1); + + assertEquals(await db.get([lastValidKey]), { + key: [lastValidKey], + value: 1, + versionstamp: res.versionstamp, + }); + + await assertRejects( + async () => await db.set([firstInvalidKey], 1), + TypeError, + "key too large for write (max 2048 bytes)", + ); + + await assertRejects( + async () => await db.get([firstInvalidKey]), + TypeError, + "key too large for read (max 2049 bytes)", + ); +}); + +dbTest("value size limit", async (db) => { + const lastValidValue = new Uint8Array(65536); + const firstInvalidValue = new Uint8Array(65537); + + const res = await db.set(["a"], lastValidValue); + assertEquals(await db.get(["a"]), { + key: ["a"], + value: lastValidValue, + versionstamp: res.versionstamp, + }); + + await assertRejects( + async () => await db.set(["b"], firstInvalidValue), + TypeError, + "value too large (max 65536 bytes)", + ); +}); + +dbTest("operation size limit", async (db) => { + const lastValidKeys: Deno.KvKey[] = new Array(10).fill(0).map(( + _, + i, + ) => ["a", i]); + const firstInvalidKeys: Deno.KvKey[] = new Array(11).fill(0).map(( + _, + i, + ) => ["a", i]); + const invalidCheckKeys: Deno.KvKey[] = new Array(101).fill(0).map(( + _, + i, + ) => ["a", i]); + + const res = await db.getMany(lastValidKeys); + assertEquals(res.length, 10); + + await assertRejects( + async () => await db.getMany(firstInvalidKeys), + TypeError, + "too many ranges (max 10)", + ); + + const res2 = await collect(db.list({ prefix: ["a"] }, { batchSize: 1000 })); + assertEquals(res2.length, 0); + + await assertRejects( + async () => await collect(db.list({ prefix: ["a"] }, { batchSize: 1001 })), + TypeError, + "too many entries (max 1000)", + ); + + // when batchSize is not specified, limit is used but is clamped to 500 + assertEquals( + (await collect(db.list({ prefix: ["a"] }, { limit: 1001 }))).length, + 0, + ); + + const res3 = await db.atomic() + .check(...lastValidKeys.map((key) => ({ + key, + versionstamp: null, + }))) + .mutate(...lastValidKeys.map((key) => ({ + key, + type: "set", + value: 1, + } satisfies Deno.KvMutation))) + .commit(); + assert(res3); + + await assertRejects( + async () => { + await db.atomic() + .check(...invalidCheckKeys.map((key) => ({ + key, + versionstamp: null, + }))) + .mutate(...lastValidKeys.map((key) => ({ + key, + type: "set", + value: 1, + } satisfies Deno.KvMutation))) + .commit(); + }, + TypeError, + "too many checks (max 100)", + ); + + const validMutateKeys: Deno.KvKey[] = new Array(1000).fill(0).map(( + _, + i, + ) => ["a", i]); + const invalidMutateKeys: Deno.KvKey[] = new Array(1001).fill(0).map(( + _, + i, + ) => ["a", i]); + + const res4 = await db.atomic() + .check(...lastValidKeys.map((key) => ({ + key, + versionstamp: null, + }))) + .mutate(...validMutateKeys.map((key) => ({ + key, + type: "set", + value: 1, + } satisfies Deno.KvMutation))) + .commit(); + assert(res4); + + await assertRejects( + async () => { + await db.atomic() + .check(...lastValidKeys.map((key) => ({ + key, + versionstamp: null, + }))) + .mutate(...invalidMutateKeys.map((key) => ({ + key, + type: "set", + value: 1, + } satisfies Deno.KvMutation))) + .commit(); + }, + TypeError, + "too many mutations (max 1000)", + ); +}); + +dbTest("total mutation size limit", async (db) => { + const keys: Deno.KvKey[] = new Array(1000).fill(0).map(( + _, + i, + ) => ["a", i]); + + const atomic = db.atomic(); + for (const key of keys) { + atomic.set(key, "foo"); + } + const res = await atomic.commit(); + assert(res); + + // Use bigger values to trigger "total mutation size too large" error + await assertRejects( + async () => { + const value = new Array(3000).fill("a").join(""); + const atomic = db.atomic(); + for (const key of keys) { + atomic.set(key, value); + } + await atomic.commit(); + }, + TypeError, + "total mutation size too large (max 819200 bytes)", + ); +}); + +dbTest("total key size limit", async (db) => { + const longString = new Array(1100).fill("a").join(""); + const keys: Deno.KvKey[] = new Array(80).fill(0).map(() => [longString]); + + const atomic = db.atomic(); + for (const key of keys) { + atomic.set(key, "foo"); + } + await assertRejects( + () => atomic.commit(), + TypeError, + "total key size too large (max 81920 bytes)", + ); +}); + +dbTest("keys must be arrays", async (db) => { + await assertRejects( + // @ts-expect-error invalid type + async () => await db.get("a"), + TypeError, + ); + + await assertRejects( + // @ts-expect-error invalid type + async () => await db.getMany(["a"]), + TypeError, + ); + + await assertRejects( + // @ts-expect-error invalid type + async () => await db.set("a", 1), + TypeError, + ); + + await assertRejects( + // @ts-expect-error invalid type + async () => await db.delete("a"), + TypeError, + ); + + await assertRejects( + async () => + await db.atomic() + // @ts-expect-error invalid type + .mutate({ key: "a", type: "set", value: 1 } satisfies Deno.KvMutation) + .commit(), + TypeError, + ); + + await assertRejects( + async () => + await db.atomic() + // @ts-expect-error invalid type + .check({ key: "a", versionstamp: null }) + .set(["a"], 1) + .commit(), + TypeError, + ); +}); + +Deno.test("Deno.Kv constructor throws", () => { + assertThrows(() => { + new Deno.Kv(); + }); +}); + +// This function is never called, it is just used to check that all the types +// are behaving as expected. +async function _typeCheckingTests() { + const kv = new Deno.Kv(); + + const a = await kv.get(["a"]); + assertType<IsExact<typeof a, Deno.KvEntryMaybe<unknown>>>(true); + + const b = await kv.get<string>(["b"]); + assertType<IsExact<typeof b, Deno.KvEntryMaybe<string>>>(true); + + const c = await kv.getMany([["a"], ["b"]]); + assertType< + IsExact<typeof c, [Deno.KvEntryMaybe<unknown>, Deno.KvEntryMaybe<unknown>]> + >(true); + + const d = await kv.getMany([["a"], ["b"]] as const); + assertType< + IsExact<typeof d, [Deno.KvEntryMaybe<unknown>, Deno.KvEntryMaybe<unknown>]> + >(true); + + const e = await kv.getMany<[string, number]>([["a"], ["b"]]); + assertType< + IsExact<typeof e, [Deno.KvEntryMaybe<string>, Deno.KvEntryMaybe<number>]> + >(true); + + const keys: Deno.KvKey[] = [["a"], ["b"]]; + const f = await kv.getMany(keys); + assertType<IsExact<typeof f, Deno.KvEntryMaybe<unknown>[]>>(true); + + const g = kv.list({ prefix: ["a"] }); + assertType<IsExact<typeof g, Deno.KvListIterator<unknown>>>(true); + const h = await g.next(); + assert(!h.done); + assertType<IsExact<typeof h.value, Deno.KvEntry<unknown>>>(true); + + const i = kv.list<string>({ prefix: ["a"] }); + assertType<IsExact<typeof i, Deno.KvListIterator<string>>>(true); + const j = await i.next(); + assert(!j.done); + assertType<IsExact<typeof j.value, Deno.KvEntry<string>>>(true); +} + +queueTest("basic listenQueue and enqueue", async (db) => { + const { promise, resolve } = Promise.withResolvers<void>(); + let dequeuedMessage: unknown = null; + const listener = db.listenQueue((msg) => { + dequeuedMessage = msg; + resolve(); + }); + try { + const res = await db.enqueue("test"); + assert(res.ok); + assertNotEquals(res.versionstamp, null); + await promise; + assertEquals(dequeuedMessage, "test"); + } finally { + db.close(); + await listener; + } +}); + +for (const { name, value } of VALUE_CASES) { + queueTest(`listenQueue and enqueue ${name}`, async (db) => { + const numEnqueues = 10; + let count = 0; + const deferreds: ReturnType<typeof Promise.withResolvers<unknown>>[] = []; + const listeners: Promise<void>[] = []; + listeners.push(db.listenQueue((msg: unknown) => { + deferreds[count++].resolve(msg); + })); + try { + for (let i = 0; i < numEnqueues; i++) { + deferreds.push(Promise.withResolvers<unknown>()); + await db.enqueue(value); + } + const dequeuedMessages = await Promise.all( + deferreds.map(({ promise }) => promise), + ); + for (let i = 0; i < numEnqueues; i++) { + assertEquals(dequeuedMessages[i], value); + } + } finally { + db.close(); + for (const listener of listeners) { + await listener; + } + } + }); +} + +queueTest("queue mixed types", async (db) => { + let deferred: ReturnType<typeof Promise.withResolvers<void>>; + let dequeuedMessage: unknown = null; + const listener = db.listenQueue((msg: unknown) => { + dequeuedMessage = msg; + deferred.resolve(); + }); + try { + for (const item of VALUE_CASES) { + deferred = Promise.withResolvers<void>(); + await db.enqueue(item.value); + await deferred.promise; + assertEquals(dequeuedMessage, item.value); + } + } finally { + db.close(); + await listener; + } +}); + +queueTest("queue delay", async (db) => { + let dequeueTime: number | undefined; + const { promise, resolve } = Promise.withResolvers<void>(); + let dequeuedMessage: unknown = null; + const listener = db.listenQueue((msg) => { + dequeueTime = Date.now(); + dequeuedMessage = msg; + resolve(); + }); + try { + const enqueueTime = Date.now(); + await db.enqueue("test", { delay: 1000 }); + await promise; + assertEquals(dequeuedMessage, "test"); + assert(dequeueTime !== undefined); + assert(dequeueTime - enqueueTime >= 1000); + } finally { + db.close(); + await listener; + } +}); + +queueTest("queue delay with atomic", async (db) => { + let dequeueTime: number | undefined; + const { promise, resolve } = Promise.withResolvers<void>(); + let dequeuedMessage: unknown = null; + const listener = db.listenQueue((msg) => { + dequeueTime = Date.now(); + dequeuedMessage = msg; + resolve(); + }); + try { + const enqueueTime = Date.now(); + const res = await db.atomic() + .enqueue("test", { delay: 1000 }) + .commit(); + assert(res.ok); + + await promise; + assertEquals(dequeuedMessage, "test"); + assert(dequeueTime !== undefined); + assert(dequeueTime - enqueueTime >= 1000); + } finally { + db.close(); + await listener; + } +}); + +queueTest("queue delay and now", async (db) => { + let count = 0; + let dequeueTime: number | undefined; + const { promise, resolve } = Promise.withResolvers<void>(); + let dequeuedMessage: unknown = null; + const listener = db.listenQueue((msg) => { + count += 1; + if (count == 2) { + dequeueTime = Date.now(); + dequeuedMessage = msg; + resolve(); + } + }); + try { + const enqueueTime = Date.now(); + await db.enqueue("test-1000", { delay: 1000 }); + await db.enqueue("test"); + await promise; + assertEquals(dequeuedMessage, "test-1000"); + assert(dequeueTime !== undefined); + assert(dequeueTime - enqueueTime >= 1000); + } finally { + db.close(); + await listener; + } +}); + +dbTest("queue negative delay", async (db) => { + await assertRejects(async () => { + await db.enqueue("test", { delay: -100 }); + }, TypeError); +}); + +dbTest("queue nan delay", async (db) => { + await assertRejects(async () => { + await db.enqueue("test", { delay: Number.NaN }); + }, TypeError); +}); + +dbTest("queue large delay", async (db) => { + await db.enqueue("test", { delay: 30 * 24 * 60 * 60 * 1000 }); + await assertRejects(async () => { + await db.enqueue("test", { delay: 30 * 24 * 60 * 60 * 1000 + 1 }); + }, TypeError); +}); + +queueTest("listenQueue with async callback", async (db) => { + const { promise, resolve } = Promise.withResolvers<void>(); + let dequeuedMessage: unknown = null; + const listener = db.listenQueue(async (msg) => { + dequeuedMessage = msg; + await sleep(100); + resolve(); + }); + try { + await db.enqueue("test"); + await promise; + assertEquals(dequeuedMessage, "test"); + } finally { + db.close(); + await listener; + } +}); + +queueTest("queue retries", async (db) => { + let count = 0; + const listener = db.listenQueue(async (_msg) => { + count += 1; + await sleep(10); + throw new TypeError("dequeue error"); + }); + try { + await db.enqueue("test"); + await sleep(10000); + } finally { + db.close(); + await listener; + } + + // There should have been 1 attempt + 3 retries in the 10 seconds + assertEquals(4, count); +}); + +queueTest("queue retries with backoffSchedule", async (db) => { + let count = 0; + const listener = db.listenQueue((_msg) => { + count += 1; + throw new TypeError("dequeue error"); + }); + try { + await db.enqueue("test", { backoffSchedule: [1] }); + await sleep(2000); + } finally { + db.close(); + await listener; + } + + // There should have been 1 attempt + 1 retry + assertEquals(2, count); +}); + +queueTest("multiple listenQueues", async (db) => { + const numListens = 10; + let count = 0; + const deferreds: ReturnType<typeof Promise.withResolvers<void>>[] = []; + const dequeuedMessages: unknown[] = []; + const listeners: Promise<void>[] = []; + for (let i = 0; i < numListens; i++) { + listeners.push(db.listenQueue((msg) => { + dequeuedMessages.push(msg); + deferreds[count++].resolve(); + })); + } + try { + for (let i = 0; i < numListens; i++) { + deferreds.push(Promise.withResolvers<void>()); + await db.enqueue("msg_" + i); + await deferreds[i].promise; + const msg = dequeuedMessages[i]; + assertEquals("msg_" + i, msg); + } + } finally { + db.close(); + for (let i = 0; i < numListens; i++) { + await listeners[i]; + } + } +}); + +queueTest("enqueue with atomic", async (db) => { + const { promise, resolve } = Promise.withResolvers<void>(); + let dequeuedMessage: unknown = null; + const listener = db.listenQueue((msg) => { + dequeuedMessage = msg; + resolve(); + }); + + try { + await db.set(["t"], "1"); + + let currentValue = await db.get(["t"]); + assertEquals("1", currentValue.value); + + const res = await db.atomic() + .check(currentValue) + .set(currentValue.key, "2") + .enqueue("test") + .commit(); + assert(res.ok); + + await promise; + assertEquals("test", dequeuedMessage); + + currentValue = await db.get(["t"]); + assertEquals("2", currentValue.value); + } finally { + db.close(); + await listener; + } +}); + +queueTest("enqueue with atomic nonce", async (db) => { + const { promise, resolve } = Promise.withResolvers<void>(); + let dequeuedMessage: unknown = null; + + const nonce = crypto.randomUUID(); + + const listener = db.listenQueue(async (val) => { + const message = val as { msg: string; nonce: string }; + const nonce = message.nonce; + const nonceValue = await db.get(["nonces", nonce]); + if (nonceValue.versionstamp === null) { + dequeuedMessage = message.msg; + resolve(); + return; + } + + assertNotEquals(nonceValue.versionstamp, null); + const res = await db.atomic() + .check(nonceValue) + .delete(["nonces", nonce]) + .set(["a", "b"], message.msg) + .commit(); + if (res.ok) { + // Simulate an error so that the message has to be redelivered + throw new Error("injected error"); + } + }); + + try { + const res = await db.atomic() + .check({ key: ["nonces", nonce], versionstamp: null }) + .set(["nonces", nonce], true) + .enqueue({ msg: "test", nonce }) + .commit(); + assert(res.ok); + + await promise; + assertEquals("test", dequeuedMessage); + + const currentValue = await db.get(["a", "b"]); + assertEquals("test", currentValue.value); + + const nonceValue = await db.get(["nonces", nonce]); + assertEquals(nonceValue.versionstamp, null); + } finally { + db.close(); + await listener; + } +}); + +Deno.test({ + name: "queue persistence with inflight messages", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + const filename = await Deno.makeTempFile({ prefix: "queue_db" }); + try { + let db: Deno.Kv = await Deno.openKv(filename); + + let count = 0; + let deferred = Promise.withResolvers<void>(); + + // Register long-running handler. + let listener = db.listenQueue(async (_msg) => { + count += 1; + if (count == 3) { + deferred.resolve(); + } + await new Promise(() => {}); + }); + + // Enqueue 3 messages. + await db.enqueue("msg0"); + await db.enqueue("msg1"); + await db.enqueue("msg2"); + await deferred.promise; + + // Close the database and wait for the listener to finish. + db.close(); + await listener; + + // Wait at least MESSAGE_DEADLINE_TIMEOUT before reopening the database. + // This ensures that inflight messages are requeued immediately after + // the database is reopened. + // https://github.com/denoland/denokv/blob/efb98a1357d37291a225ed5cf1fc4ecc7c737fab/sqlite/backend.rs#L120 + await sleep(6000); + + // Now reopen the database. + db = await Deno.openKv(filename); + + count = 0; + deferred = Promise.withResolvers<void>(); + + // Register a handler that will complete quickly. + listener = db.listenQueue((_msg) => { + count += 1; + if (count == 3) { + deferred.resolve(); + } + }); + + // Wait for the handlers to finish. + await deferred.promise; + assertEquals(3, count); + db.close(); + await listener; + } finally { + try { + await Deno.remove(filename); + } catch { + // pass + } + } + }, +}); + +Deno.test({ + name: "queue persistence with delay messages", + async fn() { + const filename = await Deno.makeTempFile({ prefix: "queue_db" }); + try { + await Deno.remove(filename); + } catch { + // pass + } + try { + let db: Deno.Kv = await Deno.openKv(filename); + + let count = 0; + let deferred = Promise.withResolvers<void>(); + + // Register long-running handler. + let listener = db.listenQueue((_msg) => {}); + + // Enqueue 3 messages into the future. + await db.enqueue("msg0", { delay: 10000 }); + await db.enqueue("msg1", { delay: 10000 }); + await db.enqueue("msg2", { delay: 10000 }); + + // Close the database and wait for the listener to finish. + db.close(); + await listener; + + // Now reopen the database. + db = await Deno.openKv(filename); + + count = 0; + deferred = Promise.withResolvers<void>(); + + // Register a handler that will complete quickly. + listener = db.listenQueue((_msg) => { + count += 1; + if (count == 3) { + deferred.resolve(); + } + }); + + // Wait for the handlers to finish. + await deferred.promise; + assertEquals(3, count); + db.close(); + await listener; + } finally { + try { + await Deno.remove(filename); + } catch { + // pass + } + } + }, +}); + +Deno.test({ + name: "different kv instances for enqueue and queueListen", + async fn() { + const filename = await Deno.makeTempFile({ prefix: "queue_db" }); + try { + const db0 = await Deno.openKv(filename); + const db1 = await Deno.openKv(filename); + const { promise, resolve } = Promise.withResolvers<void>(); + let dequeuedMessage: unknown = null; + const listener = db0.listenQueue((msg) => { + dequeuedMessage = msg; + resolve(); + }); + try { + const res = await db1.enqueue("test"); + assert(res.ok); + assertNotEquals(res.versionstamp, null); + await promise; + assertEquals(dequeuedMessage, "test"); + } finally { + db0.close(); + await listener; + db1.close(); + } + } finally { + try { + await Deno.remove(filename); + } catch { + // pass + } + } + }, +}); + +Deno.test({ + name: "queue graceful close", + async fn() { + const db: Deno.Kv = await Deno.openKv(":memory:"); + const listener = db.listenQueue((_msg) => {}); + db.close(); + await listener; + }, +}); + +dbTest("invalid backoffSchedule", async (db) => { + await assertRejects( + async () => { + await db.enqueue("foo", { backoffSchedule: [1, 1, 1, 1, 1, 1] }); + }, + TypeError, + "invalid backoffSchedule", + ); + await assertRejects( + async () => { + await db.enqueue("foo", { backoffSchedule: [3600001] }); + }, + TypeError, + "invalid backoffSchedule", + ); +}); + +dbTest("atomic operation is exposed", (db) => { + assert(Deno.AtomicOperation); + const ao = db.atomic(); + assert(ao instanceof Deno.AtomicOperation); +}); + +Deno.test({ + name: "racy open", + async fn() { + for (let i = 0; i < 100; i++) { + const filename = await Deno.makeTempFile({ prefix: "racy_open_db" }); + try { + const [db1, db2, db3] = await Promise.all([ + Deno.openKv(filename), + Deno.openKv(filename), + Deno.openKv(filename), + ]); + db1.close(); + db2.close(); + db3.close(); + } finally { + await Deno.remove(filename); + } + } + }, +}); + +Deno.test({ + name: "racy write", + async fn() { + const filename = await Deno.makeTempFile({ prefix: "racy_write_db" }); + const concurrency = 20; + const iterations = 5; + try { + const dbs = await Promise.all( + Array(concurrency).fill(0).map(() => Deno.openKv(filename)), + ); + try { + for (let i = 0; i < iterations; i++) { + await Promise.all( + dbs.map((db) => db.atomic().sum(["counter"], 1n).commit()), + ); + } + assertEquals( + ((await dbs[0].get(["counter"])).value as Deno.KvU64).value, + BigInt(concurrency * iterations), + ); + } finally { + dbs.forEach((db) => db.close()); + } + } finally { + await Deno.remove(filename); + } + }, +}); + +Deno.test({ + name: "kv expiration", + async fn() { + const filename = await Deno.makeTempFile({ prefix: "kv_expiration_db" }); + try { + await Deno.remove(filename); + } catch { + // pass + } + let db: Deno.Kv | null = null; + + try { + db = await Deno.openKv(filename); + + await db.set(["a"], 1, { expireIn: 1000 }); + await db.set(["b"], 2, { expireIn: 1000 }); + assertEquals((await db.get(["a"])).value, 1); + assertEquals((await db.get(["b"])).value, 2); + + // Value overwrite should also reset expiration + await db.set(["b"], 2, { expireIn: 3600 * 1000 }); + + // Wait for expiration + await sleep(1000); + + // Re-open to trigger immediate cleanup + db.close(); + db = null; + db = await Deno.openKv(filename); + + let ok = false; + for (let i = 0; i < 50; i++) { + await sleep(100); + if ( + JSON.stringify( + (await db.getMany([["a"], ["b"]])).map((x) => x.value), + ) === "[null,2]" + ) { + ok = true; + break; + } + } + + if (!ok) { + throw new Error("Values did not expire"); + } + } finally { + if (db) { + try { + db.close(); + } catch { + // pass + } + } + try { + await Deno.remove(filename); + } catch { + // pass + } + } + }, +}); + +Deno.test({ + name: "kv expiration with atomic", + async fn() { + const filename = await Deno.makeTempFile({ prefix: "kv_expiration_db" }); + try { + await Deno.remove(filename); + } catch { + // pass + } + let db: Deno.Kv | null = null; + + try { + db = await Deno.openKv(filename); + + await db.atomic().set(["a"], 1, { expireIn: 1000 }).set(["b"], 2, { + expireIn: 1000, + }).commit(); + assertEquals((await db.getMany([["a"], ["b"]])).map((x) => x.value), [ + 1, + 2, + ]); + + // Wait for expiration + await sleep(1000); + + // Re-open to trigger immediate cleanup + db.close(); + db = null; + db = await Deno.openKv(filename); + + let ok = false; + for (let i = 0; i < 50; i++) { + await sleep(100); + if ( + JSON.stringify( + (await db.getMany([["a"], ["b"]])).map((x) => x.value), + ) === "[null,null]" + ) { + ok = true; + break; + } + } + + if (!ok) { + throw new Error("Values did not expire"); + } + } finally { + if (db) { + try { + db.close(); + } catch { + // pass + } + } + try { + await Deno.remove(filename); + } catch { + // pass + } + } + }, +}); + +Deno.test({ + name: "remote backend", + async fn() { + const db = await Deno.openKv("http://localhost:4545/kv_remote_authorize"); + try { + await db.set(["some-key"], 1); + const entry = await db.get(["some-key"]); + assertEquals(entry.value, null); + assertEquals(entry.versionstamp, null); + } finally { + db.close(); + } + }, +}); + +Deno.test({ + name: "remote backend invalid format", + async fn() { + const db = await Deno.openKv( + "http://localhost:4545/kv_remote_authorize_invalid_format", + ); + + await assertRejects( + async () => { + await db.set(["some-key"], 1); + }, + Error, + "Failed to parse metadata: ", + ); + + db.close(); + }, +}); + +Deno.test({ + name: "remote backend invalid version", + async fn() { + const db = await Deno.openKv( + "http://localhost:4545/kv_remote_authorize_invalid_version", + ); + + await assertRejects( + async () => { + await db.set(["some-key"], 1); + }, + Error, + "Failed to parse metadata: unsupported metadata version: 1000", + ); + + db.close(); + }, +}); + +Deno.test( + { permissions: { read: true } }, + async function kvExplicitResourceManagement() { + let kv2: Deno.Kv; + + { + using kv = await Deno.openKv(":memory:"); + kv2 = kv; + + const res = await kv.get(["a"]); + assertEquals(res.versionstamp, null); + } + + await assertRejects(() => kv2.get(["a"]), Deno.errors.BadResource); + }, +); + +Deno.test( + { permissions: { read: true } }, + async function kvExplicitResourceManagementManualClose() { + using kv = await Deno.openKv(":memory:"); + kv.close(); + await assertRejects(() => kv.get(["a"]), Deno.errors.BadResource); + // calling [Symbol.dispose] after manual close is a no-op + }, +); + +dbTest("key watch", async (db) => { + const changeHistory: Deno.KvEntryMaybe<number>[] = []; + const watcher: ReadableStream<Deno.KvEntryMaybe<number>[]> = db.watch< + number[] + >([["key"]]); + + const reader = watcher.getReader(); + const expectedChanges = 2; + + const work = (async () => { + for (let i = 0; i < expectedChanges; i++) { + const message = await reader.read(); + if (message.done) { + throw new Error("Unexpected end of stream"); + } + changeHistory.push(message.value[0]); + } + + await reader.cancel(); + })(); + + while (changeHistory.length !== 1) { + await sleep(100); + } + assertEquals(changeHistory[0], { + key: ["key"], + value: null, + versionstamp: null, + }); + + const { versionstamp } = await db.set(["key"], 1); + while (changeHistory.length as number !== 2) { + await sleep(100); + } + assertEquals(changeHistory[1], { + key: ["key"], + value: 1, + versionstamp, + }); + + await work; + await reader.cancel(); +}); + +dbTest("set with key versionstamp suffix", async (db) => { + const result1 = await Array.fromAsync(db.list({ prefix: ["a"] })); + assertEquals(result1, []); + + const setRes1 = await db.set(["a", db.commitVersionstamp()], "b"); + assert(setRes1.ok); + assert(setRes1.versionstamp > ZERO_VERSIONSTAMP); + + const result2 = await Array.fromAsync(db.list({ prefix: ["a"] })); + assertEquals(result2.length, 1); + assertEquals(result2[0].key[1], setRes1.versionstamp); + assertEquals(result2[0].value, "b"); + assertEquals(result2[0].versionstamp, setRes1.versionstamp); + + const setRes2 = await db.atomic().set(["a", db.commitVersionstamp()], "c") + .commit(); + assert(setRes2.ok); + assert(setRes2.versionstamp > setRes1.versionstamp); + + const result3 = await Array.fromAsync(db.list({ prefix: ["a"] })); + assertEquals(result3.length, 2); + assertEquals(result3[1].key[1], setRes2.versionstamp); + assertEquals(result3[1].value, "c"); + assertEquals(result3[1].versionstamp, setRes2.versionstamp); + + await assertRejects( + async () => await db.set(["a", db.commitVersionstamp(), "a"], "x"), + TypeError, + "expected string, number, bigint, ArrayBufferView, boolean", + ); +}); + +Deno.test({ + name: "watch should stop when db closed", + async fn() { + const db = await Deno.openKv(":memory:"); + + const watch = db.watch([["a"]]); + const completion = (async () => { + for await (const _item of watch) { + // pass + } + })(); + + setTimeout(() => { + db.close(); + }, 100); + + await completion; + }, +}); |