diff --git a/.vscode/settings.json b/.vscode/settings.json index dbc6d017..f1b9d881 100755 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -37,6 +37,7 @@ "cfworker", "codegen", "Deno", + "denokv", "dereferenceable", "discoverability", "docloader", diff --git a/CHANGES.md b/CHANGES.md index 0188ee0a..9e5ec6a6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -20,7 +20,7 @@ To be released. - Added `KvStore` interface. - Added `KvStoreSetOptions` interface. - Added `KvKey` type. - - Added `MemoryKvStore` class. + - Added `DenoKvStore` class. - `KvCacheParameters.kv` option now accepts a `KvStore` instead of `Deno.Kv`. - `KvCacheParameters.prefix` option now accepts a `KvKey` instead of @@ -39,6 +39,12 @@ To be released. - Added `InProcessMessageQueue` class. - Added `FederationParameters.queue` option. + - Added `@fedify/fedify/x/denokv` module for adapting `Deno.Kv` to `KvStore` + and `MessageQueue`. It is only available in Deno runtime. + + - Added `DenoKvStore` class. + - Added `DenoKvMessageQueue` class. + - Removed dependency on *jose*. - Added `exportSpki()` function. diff --git a/deno.json b/deno.json index 62253af9..f19776db 100644 --- a/deno.json +++ b/deno.json @@ -9,6 +9,7 @@ "./runtime": "./runtime/mod.ts", "./vocab": "./vocab/mod.ts", "./webfinger": "./webfinger/mod.ts", + "./x/denokv": "./x/denokv.ts", "./x/fresh": "./x/fresh.ts" }, "imports": { diff --git a/x/denokv.test.ts b/x/denokv.test.ts new file mode 100644 index 00000000..3e6d00e9 --- /dev/null +++ b/x/denokv.test.ts @@ -0,0 +1,77 @@ +import { Temporal } from "@js-temporal/polyfill"; +import { assertEquals, assertGreater } from "@std/assert"; +import { delay } from "@std/async/delay"; +import { DenoKvMessageQueue, DenoKvStore } from "./denokv.ts"; + +Deno.test("DenoKvStore", async (t) => { + const kv = await Deno.openKv(":memory:"); + const store = new DenoKvStore(kv); + + await t.step("get()", async () => { + await kv.set(["foo", "bar"], "foobar"); + assertEquals(await store.get(["foo", "bar"]), "foobar"); + }); + + await t.step("set()", async () => { + await store.set(["foo", "baz"], "baz"); + assertEquals((await kv.get(["foo", "baz"])).value, "baz"); + }); + + await t.step("delete()", async () => { + await store.delete(["foo", "baz"]); + assertEquals((await kv.get(["foo", "baz"])).value, null); + }); + + kv.close(); +}); + +Deno.test("DenoKvMessageQueue", async (t) => { + const kv = await Deno.openKv(":memory:"); + const mq = new DenoKvMessageQueue(kv); + + const messages: string[] = []; + mq.listen((message: string) => { + messages.push(message); + }); + + await t.step("enqueue()", async () => { + await mq.enqueue("Hello, world!"); + }); + + await waitFor(() => messages.length > 0, 15_000); + + await t.step("listen()", () => { + assertEquals(messages, ["Hello, world!"]); + }); + + let started = 0; + await t.step("enqueue() with delay", async () => { + started = Date.now(); + await mq.enqueue( + "Delayed message", + { delay: Temporal.Duration.from({ seconds: 3 }) }, + ); + }); + + await waitFor(() => messages.length > 1, 15_000); + + await t.step("listen() with delay", () => { + assertEquals(messages, ["Hello, world!", "Delayed message"]); + assertGreater(Date.now() - started, 3_000); + }); + + kv.close(); +}); + +async function waitFor( + predicate: () => boolean, + timeoutMs: number, +): Promise { + const started = Date.now(); + while (!predicate()) { + await delay(500); + if (Date.now() - started > timeoutMs) { + throw new Error("Timeout"); + } + } +} diff --git a/x/denokv.ts b/x/denokv.ts new file mode 100644 index 00000000..049656a1 --- /dev/null +++ b/x/denokv.ts @@ -0,0 +1,104 @@ +/** + * `KvStore` & `MessageQueue` adapters for Deno's KV store + * ======================================================= + * + * This module provides `KvStore` and `MessageQueue` implementations that use + * Deno's KV store. The `DenoKvStore` class implements the `KvStore` interface + * using Deno's KV store, and the `DenoKvMessageQueue` class implements the + * `MessageQueue` interface using Deno's KV store. + * + * @module + * @since 0.5.0 + */ +import type { KvKey, KvStore, KvStoreSetOptions } from "../federation/kv.ts"; +import type { + MessageQueue, + MessageQueueEnqueueOptions, +} from "../federation/mq.ts"; + +/** + * Represents a key-value store implementation using Deno's KV store. + */ +export class DenoKvStore implements KvStore { + #kv: Deno.Kv; + + /** + * Constructs a new {@link DenoKvStore} adapter with the given Deno KV store. + * @param kv The Deno KV store to use. + */ + constructor(kv: Deno.Kv) { + this.#kv = kv; + } + + /** + * {@inheritDoc KvStore.set} + */ + async get(key: KvKey): Promise { + const entry = await this.#kv.get(key); + return entry == null || entry.value == null ? undefined : entry.value; + } + + /** + * {@inheritDoc KvStore.set} + */ + async set( + key: KvKey, + value: unknown, + options?: KvStoreSetOptions, + ): Promise { + await this.#kv.set( + key, + value, + options?.ttl == null ? undefined : { + expireIn: options.ttl.total("millisecond"), + }, + ); + } + + /** + * {@inheritDoc KvStore.delete} + */ + delete(key: KvKey): Promise { + return this.#kv.delete(key); + } +} + +/** + * Represents a message queue adapter that uses Deno KV store. + */ +export class DenoKvMessageQueue implements MessageQueue { + #kv: Deno.Kv; + + /** + * Constructs a new {@link DenoKvMessageQueue} adapter with the given Deno KV + * store. + * @param kv The Deno KV store to use. + */ + constructor(kv: Deno.Kv) { + this.#kv = kv; + } + + /** + * {@inheritDoc MessageQueue.enqueue} + */ + async enqueue( + // deno-lint-ignore no-explicit-any + message: any, + options?: MessageQueueEnqueueOptions | undefined, + ): Promise { + await this.#kv.enqueue( + message, + options?.delay == null ? undefined : { + delay: options.delay.total("millisecond"), + }, + ); + } + + /** + * {@inheritDoc MessageQueue.listen} + */ + // deno-lint-ignore no-explicit-any + listen(handler: (message: any) => void | Promise): void { + this.#kv.listenQueue(handler); + } +}