Skip to content

Commit

Permalink
Deno KV adapters
Browse files Browse the repository at this point in the history
  • Loading branch information
dahlia committed Mar 31, 2024
1 parent c1aa200 commit 1bd3154
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 2 deletions.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"cfworker",
"codegen",
"Deno",
"denokv",
"dereferenceable",
"discoverability",
"docloader",
Expand Down
8 changes: 7 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
1 change: 1 addition & 0 deletions deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
3 changes: 2 additions & 1 deletion dnt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ await build({
},
outDir: "./npm",
entryPoints: Object.entries(metadata.exports as Record<string, string>)
.map(([name, path]) => ({ name, path })),
.map(([name, path]) => ({ name, path }))
.filter(({ name }) => !name.startsWith("./x/")),
importMap: denoJson,
scriptModule: false,
shims: {
Expand Down
77 changes: 77 additions & 0 deletions x/denokv.test.ts
Original file line number Diff line number Diff line change
@@ -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<string>(["foo", "baz"])).value, "baz");
});

await t.step("delete()", async () => {
await store.delete(["foo", "baz"]);
assertEquals((await kv.get<string>(["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<void> {
const started = Date.now();
while (!predicate()) {
await delay(500);
if (Date.now() - started > timeoutMs) {
throw new Error("Timeout");
}
}
}
104 changes: 104 additions & 0 deletions x/denokv.ts
Original file line number Diff line number Diff line change
@@ -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<T = unknown>(key: KvKey): Promise<T | undefined> {
const entry = await this.#kv.get<T>(key);
return entry == null || entry.value == null ? undefined : entry.value;
}

/**
* {@inheritDoc KvStore.set}
*/
async set(
key: KvKey,
value: unknown,
options?: KvStoreSetOptions,
): Promise<void> {
await this.#kv.set(
key,
value,
options?.ttl == null ? undefined : {
expireIn: options.ttl.total("millisecond"),
},
);
}

/**
* {@inheritDoc KvStore.delete}
*/
delete(key: KvKey): Promise<void> {
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<void> {
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>): void {
this.#kv.listenQueue(handler);
}
}

0 comments on commit 1bd3154

Please sign in to comment.