// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. import { assert, assertEquals, AssertionError, assertNotEquals, assertRejects, assertThrows, Deferred, deferred, } 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; } 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) { 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) { 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); }, }); } 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); assertEquals(setRes.versionstamp, "00000000000000010000"); const result2 = await db.get(["a"]); assertEquals(result2.key, ["a"]); assertEquals(result2.value, "b"); assertEquals(result2.versionstamp, "00000000000000010000"); await db.set(["a"], "c"); const result3 = await db.get(["a"]); assertEquals(result3.key, ["a"]); assertEquals(result3.value, "c"); assertEquals(result3.versionstamp, "00000000000000020000"); 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"]); assertEquals(currentValue.versionstamp, "00000000000000010000"); let res = await db.atomic() .check({ key: ["t"], versionstamp: currentValue.versionstamp }) .set(currentValue.key, "2") .commit(); assert(res.ok); assertEquals(res.versionstamp, "00000000000000020000"); const newValue = await db.get(["t"]); assertEquals(newValue.versionstamp, "00000000000000020000"); 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, "00000000000000020000"); 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); const newValue = await db.get(["t"]); assertEquals(newValue.versionstamp, "00000000000000010000"); 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) => { await db.set(["t1"], "1"); await db.set(["t2"], "2"); const currentValue1 = await db.get(["t1"]); assertEquals(currentValue1.versionstamp, "00000000000000010000"); const currentValue2 = await db.get(["t2"]); assertEquals(currentValue2.versionstamp, "00000000000000020000"); 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); const newValue1 = await db.get(["t1"]); assertEquals(newValue1.versionstamp, "00000000000000030000"); assertEquals(newValue1.value, "3"); const newValue2 = await db.get(["t2"]); assertEquals(newValue2.versionstamp, "00000000000000030000"); 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, "00000000000000030000"); assertEquals(newValue3.value, "3"); const newValue4 = await db.get(["t2"]); assertEquals(newValue4.versionstamp, "00000000000000030000"); 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( iter: Deno.KvListIterator, ): Promise[]> { const entries: Deno.KvEntry[] = []; for await (const entry of iter) { entries.push(entry); } return entries; } async function setupData(db: Deno.Kv) { 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(); } dbTest("get many", async (db) => { await setupData(db); const entries = await db.getMany([["b", "a"], ["a"], ["c"]]); assertEquals(entries, [ { key: ["b", "a"], value: 100, versionstamp: "00000000000000010000" }, { key: ["a"], value: -1, versionstamp: "00000000000000010000" }, { key: ["c"], value: null, versionstamp: null }, ]); }); dbTest("list prefix", async (db) => { await setupData(db); const entries = await collect(db.list({ prefix: ["a"] })); assertEquals(entries, [ { key: ["a", "a"], value: 0, versionstamp: "00000000000000010000" }, { key: ["a", "b"], value: 1, versionstamp: "00000000000000010000" }, { key: ["a", "c"], value: 2, versionstamp: "00000000000000010000" }, { key: ["a", "d"], value: 3, versionstamp: "00000000000000010000" }, { key: ["a", "e"], value: 4, versionstamp: "00000000000000010000" }, ]); }); 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) => { await setupData(db); const entries = await collect(db.list({ prefix: ["a"], start: ["a", "c"] })); assertEquals(entries, [ { key: ["a", "c"], value: 2, versionstamp: "00000000000000010000" }, { key: ["a", "d"], value: 3, versionstamp: "00000000000000010000" }, { key: ["a", "e"], value: 4, versionstamp: "00000000000000010000" }, ]); }); 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 end", async (db) => { await setupData(db); const entries = await collect(db.list({ prefix: ["a"], end: ["a", "c"] })); assertEquals(entries, [ { key: ["a", "a"], value: 0, versionstamp: "00000000000000010000" }, { key: ["a", "b"], value: 1, versionstamp: "00000000000000010000" }, ]); }); 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 empty prefix", async (db) => { await db.set(["a"], 1); const entries = await collect(db.list({ prefix: [] })); assertEquals(entries, [ { key: ["a"], value: 1, versionstamp: "00000000000000010000" }, ]); }); dbTest("list prefix reverse", async (db) => { await setupData(db); const entries = await collect(db.list({ prefix: ["a"] }, { reverse: true })); assertEquals(entries, [ { key: ["a", "e"], value: 4, versionstamp: "00000000000000010000" }, { key: ["a", "d"], value: 3, versionstamp: "00000000000000010000" }, { key: ["a", "c"], value: 2, versionstamp: "00000000000000010000" }, { key: ["a", "b"], value: 1, versionstamp: "00000000000000010000" }, { key: ["a", "a"], value: 0, versionstamp: "00000000000000010000" }, ]); }); dbTest("list prefix reverse with start", async (db) => { await setupData(db); const entries = await collect( db.list({ prefix: ["a"], start: ["a", "c"] }, { reverse: true }), ); assertEquals(entries, [ { key: ["a", "e"], value: 4, versionstamp: "00000000000000010000" }, { key: ["a", "d"], value: 3, versionstamp: "00000000000000010000" }, { key: ["a", "c"], value: 2, versionstamp: "00000000000000010000" }, ]); }); 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) => { await setupData(db); const entries = await collect( db.list({ prefix: ["a"], end: ["a", "c"] }, { reverse: true }), ); assertEquals(entries, [ { key: ["a", "b"], value: 1, versionstamp: "00000000000000010000" }, { key: ["a", "a"], value: 0, versionstamp: "00000000000000010000" }, ]); }); 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) => { await setupData(db); const entries = await collect(db.list({ prefix: ["a"] }, { limit: 2 })); assertEquals(entries, [ { key: ["a", "a"], value: 0, versionstamp: "00000000000000010000" }, { key: ["a", "b"], value: 1, versionstamp: "00000000000000010000" }, ]); }); dbTest("list prefix limit reverse", async (db) => { await setupData(db); const entries = await collect( db.list({ prefix: ["a"] }, { limit: 2, reverse: true }), ); assertEquals(entries, [ { key: ["a", "e"], value: 4, versionstamp: "00000000000000010000" }, { key: ["a", "d"], value: 3, versionstamp: "00000000000000010000" }, ]); }); dbTest("list prefix with small batch size", async (db) => { await setupData(db); const entries = await collect(db.list({ prefix: ["a"] }, { batchSize: 2 })); assertEquals(entries, [ { key: ["a", "a"], value: 0, versionstamp: "00000000000000010000" }, { key: ["a", "b"], value: 1, versionstamp: "00000000000000010000" }, { key: ["a", "c"], value: 2, versionstamp: "00000000000000010000" }, { key: ["a", "d"], value: 3, versionstamp: "00000000000000010000" }, { key: ["a", "e"], value: 4, versionstamp: "00000000000000010000" }, ]); }); dbTest("list prefix with small batch size reverse", async (db) => { await setupData(db); const entries = await collect( db.list({ prefix: ["a"] }, { batchSize: 2, reverse: true }), ); assertEquals(entries, [ { key: ["a", "e"], value: 4, versionstamp: "00000000000000010000" }, { key: ["a", "d"], value: 3, versionstamp: "00000000000000010000" }, { key: ["a", "c"], value: 2, versionstamp: "00000000000000010000" }, { key: ["a", "b"], value: 1, versionstamp: "00000000000000010000" }, { key: ["a", "a"], value: 0, versionstamp: "00000000000000010000" }, ]); }); dbTest("list prefix with small batch size and limit", async (db) => { await setupData(db); const entries = await collect( db.list({ prefix: ["a"] }, { batchSize: 2, limit: 3 }), ); assertEquals(entries, [ { key: ["a", "a"], value: 0, versionstamp: "00000000000000010000" }, { key: ["a", "b"], value: 1, versionstamp: "00000000000000010000" }, { key: ["a", "c"], value: 2, versionstamp: "00000000000000010000" }, ]); }); dbTest("list prefix with small batch size and limit reverse", async (db) => { 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: "00000000000000010000" }, { key: ["a", "d"], value: 3, versionstamp: "00000000000000010000" }, { key: ["a", "c"], value: 2, versionstamp: "00000000000000010000" }, ]); }); dbTest("list prefix with manual cursor", async (db) => { await setupData(db); const iterator = db.list({ prefix: ["a"] }, { limit: 2 }); const values = await collect(iterator); assertEquals(values, [ { key: ["a", "a"], value: 0, versionstamp: "00000000000000010000" }, { key: ["a", "b"], value: 1, versionstamp: "00000000000000010000" }, ]); 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: "00000000000000010000" }, { key: ["a", "d"], value: 3, versionstamp: "00000000000000010000" }, { key: ["a", "e"], value: 4, versionstamp: "00000000000000010000" }, ]); }); dbTest("list prefix with manual cursor reverse", async (db) => { 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: "00000000000000010000" }, { key: ["a", "d"], value: 3, versionstamp: "00000000000000010000" }, ]); 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: "00000000000000010000" }, { key: ["a", "b"], value: 1, versionstamp: "00000000000000010000" }, { key: ["a", "a"], value: 0, versionstamp: "00000000000000010000" }, ]); }); dbTest("list range", async (db) => { await setupData(db); const entries = await collect( db.list({ start: ["a", "a"], end: ["a", "z"] }), ); assertEquals(entries, [ { key: ["a", "a"], value: 0, versionstamp: "00000000000000010000" }, { key: ["a", "b"], value: 1, versionstamp: "00000000000000010000" }, { key: ["a", "c"], value: 2, versionstamp: "00000000000000010000" }, { key: ["a", "d"], value: 3, versionstamp: "00000000000000010000" }, { key: ["a", "e"], value: 4, versionstamp: "00000000000000010000" }, ]); }); dbTest("list range reverse", async (db) => { 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: "00000000000000010000" }, { key: ["a", "d"], value: 3, versionstamp: "00000000000000010000" }, { key: ["a", "c"], value: 2, versionstamp: "00000000000000010000" }, { key: ["a", "b"], value: 1, versionstamp: "00000000000000010000" }, { key: ["a", "a"], value: 0, versionstamp: "00000000000000010000" }, ]); }); dbTest("list range with limit", async (db) => { 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: "00000000000000010000" }, { key: ["a", "b"], value: 1, versionstamp: "00000000000000010000" }, { key: ["a", "c"], value: 2, versionstamp: "00000000000000010000" }, ]); }); dbTest("list range with limit reverse", async (db) => { 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: "00000000000000010000" }, { key: ["a", "d"], value: 3, versionstamp: "00000000000000010000" }, { key: ["a", "c"], value: 2, versionstamp: "00000000000000010000" }, ]); }); dbTest("list range nesting", async (db) => { await setupData(db); const entries = await collect(db.list({ start: ["a"], end: ["a", "d"] })); assertEquals(entries, [ { key: ["a"], value: -1, versionstamp: "00000000000000010000" }, { key: ["a", "a"], value: 0, versionstamp: "00000000000000010000" }, { key: ["a", "b"], value: 1, versionstamp: "00000000000000010000" }, { key: ["a", "c"], value: 2, versionstamp: "00000000000000010000" }, ]); }); dbTest("list range short", async (db) => { await setupData(db); const entries = await collect( db.list({ start: ["a", "b"], end: ["a", "d"] }), ); assertEquals(entries, [ { key: ["a", "b"], value: 1, versionstamp: "00000000000000010000" }, { key: ["a", "c"], value: 2, versionstamp: "00000000000000010000" }, ]); }); dbTest("list range with manual cursor", async (db) => { 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: "00000000000000010000" }, { key: ["a", "c"], value: 2, versionstamp: "00000000000000010000" }, ]); 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: "00000000000000010000" }, { key: ["a", "e"], value: 4, versionstamp: "00000000000000010000" }, ]); }); dbTest("list range with manual cursor reverse", async (db) => { 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: "00000000000000010000" }, { key: ["a", "d"], value: 3, versionstamp: "00000000000000010000" }, ]); 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: "00000000000000010000" }, { key: ["a", "b"], value: 1, versionstamp: "00000000000000010000" }, ]); }); 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); await db.set([lastValidKey], 1); assertEquals(await db.get([lastValidKey]), { key: [lastValidKey], value: 1, versionstamp: "00000000000000010000", }); 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); await db.set(["a"], lastValidValue); assertEquals(await db.get(["a"]), { key: ["a"], value: lastValidValue, versionstamp: "00000000000000010000", }); 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 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(...firstInvalidKeys.map((key) => ({ key, versionstamp: null, }))) .mutate(...lastValidKeys.map((key) => ({ key, type: "set", value: 1, } satisfies Deno.KvMutation))) .commit(); }, TypeError, "too many checks (max 10)", ); await assertRejects( async () => { await db.atomic() .check(...lastValidKeys.map((key) => ({ key, versionstamp: null, }))) .mutate(...firstInvalidKeys.map((key) => ({ key, type: "set", value: 1, } satisfies Deno.KvMutation))) .commit(); }, TypeError, "too many mutations (max 10)", ); }); 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>>(true); const b = await kv.get(["b"]); assertType>>(true); const c = await kv.getMany([["a"], ["b"]]); assertType< IsExact, Deno.KvEntryMaybe]> >(true); const d = await kv.getMany([["a"], ["b"]] as const); assertType< IsExact, Deno.KvEntryMaybe]> >(true); const e = await kv.getMany<[string, number]>([["a"], ["b"]]); assertType< IsExact, Deno.KvEntryMaybe]> >(true); const keys: Deno.KvKey[] = [["a"], ["b"]]; const f = await kv.getMany(keys); assertType[]>>(true); const g = kv.list({ prefix: ["a"] }); assertType>>(true); const h = await g.next(); assert(!h.done); assertType>>(true); const i = kv.list({ prefix: ["a"] }); assertType>>(true); const j = await i.next(); assert(!j.done); assertType>>(true); } queueTest("basic listenQueue and enqueue", async (db) => { const promise = deferred(); let dequeuedMessage: unknown = null; const listener = db.listenQueue((msg) => { dequeuedMessage = msg; promise.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 promises: Deferred[] = []; const dequeuedMessages: unknown[] = []; const listeners: Promise[] = []; listeners.push(db.listenQueue((msg) => { dequeuedMessages.push(msg); promises[count++].resolve(); })); try { for (let i = 0; i < numEnqueues; i++) { promises.push(deferred()); await db.enqueue(value); } for (let i = 0; i < numEnqueues; i++) { await promises[i]; } 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 promise: Deferred; let dequeuedMessage: unknown = null; const listener = db.listenQueue((msg) => { dequeuedMessage = msg; promise.resolve(); }); try { for (const item of VALUE_CASES) { promise = deferred(); await db.enqueue(item.value); await promise; assertEquals(dequeuedMessage, item.value); } } finally { db.close(); await listener; } }); queueTest("queue delay", async (db) => { let dequeueTime: number | undefined; const promise = deferred(); let dequeuedMessage: unknown = null; const listener = db.listenQueue((msg) => { dequeueTime = Date.now(); dequeuedMessage = msg; promise.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 = deferred(); let dequeuedMessage: unknown = null; const listener = db.listenQueue((msg) => { dequeueTime = Date.now(); dequeuedMessage = msg; promise.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 = deferred(); let dequeuedMessage: unknown = null; const listener = db.listenQueue((msg) => { count += 1; if (count == 2) { dequeueTime = Date.now(); dequeuedMessage = msg; promise.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: 7 * 24 * 60 * 60 * 1000 }); await assertRejects(async () => { await db.enqueue("test", { delay: 7 * 24 * 60 * 60 * 1000 + 1 }); }, TypeError); }); queueTest("listenQueue with async callback", async (db) => { const promise = deferred(); let dequeuedMessage: unknown = null; const listener = db.listenQueue(async (msg) => { dequeuedMessage = msg; await sleep(100); promise.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("multiple listenQueues", async (db) => { const numListens = 10; let count = 0; const promises: Deferred[] = []; const dequeuedMessages: unknown[] = []; const listeners: Promise[] = []; for (let i = 0; i < numListens; i++) { listeners.push(db.listenQueue((msg) => { dequeuedMessages.push(msg); promises[count++].resolve(); })); } try { for (let i = 0; i < numListens; i++) { promises.push(deferred()); await db.enqueue("msg_" + i); await promises[i]; 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 = deferred(); let dequeuedMessage: unknown = null; const listener = db.listenQueue((msg) => { dequeuedMessage = msg; promise.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 = deferred(); 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; promise.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 promise = deferred(); // Register long-running handler. let listener = db.listenQueue(async (_msg) => { count += 1; if (count == 3) { promise.resolve(); } await sleep(60000); }); // Enqueue 3 messages. await db.enqueue("msg0"); await db.enqueue("msg1"); await db.enqueue("msg2"); await promise; // 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; promise = deferred(); // Register a handler that will complete quickly. listener = db.listenQueue((_msg) => { count += 1; if (count == 3) { promise.resolve(); } }); // Wait for the handlers to finish. await 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 promise = deferred(); // 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; promise = deferred(); // Register a handler that will complete quickly. listener = db.listenQueue((_msg) => { count += 1; if (count == 3) { promise.resolve(); } }); // Wait for the handlers to finish. await promise; assertEquals(3, count); db.close(); await listener; } finally { try { await Deno.remove(filename); } catch { // pass } } }, }); dbTest("atomic operation is exposed", (db) => { assert(Deno.AtomicOperation); const ao = db.atomic(); assert(ao instanceof Deno.AtomicOperation); });