1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-23 15:16:54 -05:00
denoland-deno/cli/tests/unit/kv_test.ts
Heyang Zhou 01a761f1d4
chore(ext/kv): limit total key size in an atomic op to 80 KiB (#20395)
Keys are expensive metadata. We track it for various purposes, e.g.
transaction conflict check, and key expiration.

This patch limits the total key size in an atomic operation to 80 KiB
(81920 bytes). This helps ensure efficiency in implementations.
2023-09-07 15:07:04 +08:00

2073 lines
55 KiB
TypeScript

// 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;
}
// 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);
},
});
}
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<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) {
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)",
);
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 = 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<void>[] = [];
const dequeuedMessages: unknown[] = [];
const listeners: Promise<void>[] = [];
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<void>;
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<void>[] = [];
const dequeuedMessages: unknown[] = [];
const listeners: Promise<void>[] = [];
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);
});
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",
);
let ok = false;
try {
await db.set(["some-key"], 1);
} catch (e) {
if (
e.name === "TypeError" &&
e.message.startsWith("Metadata error: Failed to decode metadata: ")
) {
ok = true;
} else {
throw e;
}
} finally {
db.close();
}
if (!ok) {
throw new Error("did not get expected error");
}
},
});
Deno.test({
name: "remote backend invalid version",
async fn() {
const db = await Deno.openKv(
"http://localhost:4545/kv_remote_authorize_invalid_version",
);
let ok = false;
try {
await db.set(["some-key"], 1);
} catch (e) {
if (
e.name === "TypeError" &&
e.message === "Metadata error: Unsupported metadata version: 2"
) {
ok = true;
} else {
throw e;
}
} finally {
db.close();
}
if (!ok) {
throw new Error("did not get expected error");
}
},
});