Skip to content

Commit ca12a15

Browse files
authored
Merge pull request #242 from dahlia/cloudflare-workers
2 parents 94f077e + d7cb9ef commit ca12a15

23 files changed

Lines changed: 529 additions & 87 deletions

CHANGES.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,24 @@ To be released. Note that 1.6.0 was skipped due to a mistake in the versioning.
4141
- Added `HttpMessageSignaturesSpecDeterminer` interface.
4242
- Added `--first-knock` option to `fedify lookup` command.
4343

44+
- Fedify now supports [Cloudflare Workers]. [[#233]]
45+
46+
- Added `Federation.processQueuedTask()` method. [[#242]]
47+
- Added `Message` type. [[#242]]
48+
- Added `WorkersKvStore` class. [[#241], [#242]]
49+
- Added `WorkersMessageQueue` class. [[#241], [#242]]
50+
4451
- The minimum supported version of Node.js is now 22.0.0.
4552

4653
[RFC 9421]: https://www.rfc-editor.org/rfc/rfc9421
54+
[Cloudflare Workers]: https://workers.cloudflare.com/
4755
[#208]: https://github.com/fedify-dev/fedify/issues/208
4856
[#227]: https://github.com/fedify-dev/fedify/issues/227
57+
[#233]: https://github.com/fedify-dev/fedify/issues/233
4958
[#235]: https://github.com/fedify-dev/fedify/pull/235
5059
[#237]: https://github.com/fedify-dev/fedify/pull/237
60+
[#241]: https://github.com/fedify-dev/fedify/issues/241
61+
[#242]: https://github.com/fedify-dev/fedify/pull/242
5162

5263

5364
Version 1.5.3

docs/.vitepress/config.mts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import deflist from "markdown-it-deflist";
44
import footnote from "markdown-it-footnote";
55
import { jsrRef } from "markdown-it-jsr-ref";
66
import process from "node:process";
7+
import { ModuleKind, ModuleResolutionKind, ScriptTarget } from "typescript";
78
import { defineConfig } from "vitepress";
89
import {
910
groupIconMdPlugin,
@@ -210,12 +211,16 @@ export default withMermaid(defineConfig({
210211
transformerTwoslash({
211212
twoslashOptions: {
212213
compilerOptions: {
214+
moduleResolution: ModuleResolutionKind.Bundler,
215+
module: ModuleKind.ESNext,
216+
target: ScriptTarget.ESNext,
213217
lib: ["dom", "dom.iterable", "esnext"],
214218
types: [
215219
"dom",
216220
"dom.iterable",
217221
"esnext",
218222
"@teidesu/deno-types/full",
223+
"@cloudflare/workers-types/experimental",
219224
],
220225
// @ts-ignore: Although it's typed as string, it's actually an array
221226
jsx: ["react-jsx"],

docs/manual/kv.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,56 @@ const federation = createFederation<void>({
142142
[`PostgresKvStore`]: https://jsr.io/@fedify/postgres/doc/kv/~/PostgresKvStore
143143
[@fedify/postgres]: https://github.com/fedify-dev/postgres
144144

145+
### `WorkersKvStore` (Cloudflare Workers only)
146+
147+
*This API is available since Fedify 1.6.0.*
148+
149+
`WorkersKvStore` is a key–value store implementation for [Cloudflare Workers]
150+
that uses Cloudflare's built-in [Cloudflare Workers KV] API. It provides
151+
persistent storage and good performance for Cloudflare Workers environments.
152+
It's suitable for production use in Cloudflare Workers applications.
153+
154+
Best for
155+
: Production use in Cloudflare Workers environments.
156+
157+
Pros
158+
: Persistent storage, good performance, easy to set up.
159+
160+
Cons
161+
: Only available in Cloudflare Workers runtime.
162+
163+
~~~~ typescript twoslash
164+
// @noErrors: 2345
165+
import type { FederationBuilder } from "@fedify/fedify";
166+
const builder = undefined as unknown as FederationBuilder<void>;
167+
// ---cut-before---
168+
import type { Federation } from "@fedify/fedify";
169+
import { WorkersKvStore } from "@fedify/fedify/x/cfworkers";
170+
171+
export default {
172+
async fetch(request, env, ctx) {
173+
const federation: Federation<void> = await builder.build({
174+
kv: new WorkersKvStore(env.KV_BINDING),
175+
});
176+
return await federation.fetch(request, { contextData: undefined });
177+
}
178+
} satisfies ExportedHandler<{ KV_BINDING: KVNamespace<string> }>;
179+
~~~~
180+
181+
> [!NOTE]
182+
> Since your `KVNamespace` is not bound to a global variable, but rather
183+
> passed as an argument to the `fetch()` method, you need to instantiate
184+
> your `Federation` object inside the `fetch()` method, rather than the top
185+
> level.
186+
>
187+
> For better organization, you probably want to use a builder pattern to
188+
> register your dispatchers and listeners before instantiating the `Federation`
189+
> object. See the [*Builder pattern for structuring*
190+
> section](./federation.md#builder-pattern-for-structuring) for details.
191+
192+
[Cloudflare Workers]: https://workers.cloudflare.com/
193+
[Cloudflare Workers KV]: https://developers.cloudflare.com/kv/
194+
145195

146196
Implementing a custom `KvStore`
147197
-------------------------------

docs/manual/mq.md

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,88 @@ const federation = createFederation({
213213
[@fedify/amqp]: https://github.com/fedify-dev/amqp
214214
[RabbitMQ]: https://www.rabbitmq.com/
215215

216+
### `WorkersMessageQueue` (Cloudflare Workers only)
217+
218+
*This API is available since Fedify 1.6.0.*
219+
220+
`WorkersMessageQueue` is a message queue implementation for [Cloudflare Workers]
221+
that uses Cloudflare's built-in [Cloudflare Queues] API. It provides
222+
scalability and high performance, making it suitable for production use in
223+
Cloudflare Workers environments. It requires a Cloudflare Queues setup and
224+
management.
225+
226+
Best for
227+
: Production use in Cloudflare Workers environments.
228+
229+
Pros
230+
: Persistent, reliable, scalable, easy to set up.
231+
232+
Cons
233+
: Only available in Cloudflare Workers runtime.
234+
235+
~~~~ typescript twoslash
236+
// @noErrors: 2322 2345
237+
import type { FederationBuilder, KvStore } from "@fedify/fedify";
238+
const builder = undefined as unknown as FederationBuilder<void>;
239+
// ---cut-before---
240+
import type { Federation, Message } from "@fedify/fedify";
241+
import { WorkersMessageQueue } from "@fedify/fedify/x/cfworkers";
242+
243+
export default {
244+
async fetch(request, env, ctx) {
245+
const federation: Federation<void> = await builder.build({
246+
// ---cut-start---
247+
kv: undefined as unknown as KvStore,
248+
// ---cut-end---
249+
queue: new WorkersMessageQueue(env.QUEUE_BINDING),
250+
});
251+
// Omit the rest of the code for brevity
252+
},
253+
254+
// Since defining a `queue()` method is the only way to consume messages
255+
// from the queue in Cloudflare Workers, we need to define it so that
256+
// the messages can be manually processed by `Federation.processQueuedTask()`
257+
// method:
258+
async queue(batch, env, ctx) {
259+
const federation: Federation<void> = await builder.build({
260+
// ---cut-start---
261+
kv: undefined as unknown as KvStore,
262+
// ---cut-end---
263+
queue: new WorkersMessageQueue(env.QUEUE_BINDING),
264+
});
265+
for (const msg of batch.messages) {
266+
await federation.processQueuedTask(
267+
undefined, // You need to pass your context data here
268+
msg.body as Message, // You need to cast the message body to `Message`
269+
);
270+
}
271+
}
272+
} satisfies ExportedHandler<{ QUEUE_BINDING: Queue }>;
273+
~~~~
274+
275+
> [!NOTE]
276+
> Since your `Queue` is not bound to a global variable, but rather passed as
277+
> an argument to the `fetch()` and `queue()` methods, you need to instantiate
278+
> your `Federation` object inside these methods, rather than at the top level.
279+
>
280+
> For better organization, you probably want to use a builder pattern to
281+
> register your dispatchers and listeners before instantiating the `Federation`
282+
> object. See the [*Builder pattern for structuring*
283+
> section](./federation.md#builder-pattern-for-structuring) for details.
284+
285+
> [!NOTE]
286+
> The [Cloudflare Queues] API does not provide a way to poll messages from
287+
> the queue, so `WorkersMessageQueue.listen()` method always throws
288+
> a `TypeError` when invoked. Instead, you should define a `queue()` method
289+
> in your Cloudflare worker, which will be called by the Cloudflare Queues
290+
> API when new messages are available in the queue. Inside the `queue()`
291+
> method, you need to call `Federation.processQueuedTask()` method to manually
292+
> process the messages. The `queue()` method is the only way to consume
293+
> messages from the queue in Cloudflare Workers.
294+
295+
[Cloudflare Workers]: https://workers.cloudflare.com/
296+
[Cloudflare Queues]: https://developers.cloudflare.com/queues/
297+
216298

217299
Implementing a custom `MessageQueue`
218300
------------------------------------

docs/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
{
22
"devDependencies": {
33
"@braintree/sanitize-url": "^7.1.1",
4+
"@cloudflare/workers-types": "4.20250529.0",
45
"@deno/kv": "^0.8.4",
56
"@fedify/amqp": "^0.2.0",
6-
"@fedify/fedify": "^1.6.1-dev.828",
7+
"@fedify/fedify": "1.6.1-pr.242.863",
78
"@fedify/postgres": "^0.3.0",
89
"@fedify/redis": "^0.4.0",
910
"@hono/node-server": "^1.13.7",
@@ -30,6 +31,7 @@
3031
"mermaid": "^11.4.1",
3132
"postgres": "^3.4.5",
3233
"stringify-entities": "^4.0.4",
34+
"typescript": "^5.8.3",
3335
"vitepress": "^1.5.0",
3436
"vitepress-plugin-group-icons": "^1.3.5",
3537
"vitepress-plugin-llms": "^1.1.0",

docs/pnpm-lock.yaml

Lines changed: 16 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

fedify/cfworkers/client.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ const mf = new Miniflare({
1111
modules: [
1212
{ type: "ESModule", path: join(import.meta.dirname ?? ".", "server.js") },
1313
],
14+
kvNamespaces: ["KV1", "KV2", "KV3"],
15+
queueProducers: ["Q1"],
16+
queueConsumers: { Q1: { maxBatchSize: 1 } },
1417
async outboundService(request: Request) {
1518
const url = new URL(request.url);
1619
if (url.hostname.endsWith(".test")) {

fedify/cfworkers/server.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {
2-
ansiColorFormatter,
3-
configure,
4-
type LogRecord,
2+
ansiColorFormatter,
3+
configure,
4+
type LogRecord,
55
} from "@logtape/logtape";
66
import { AsyncLocalStorage } from "node:async_hooks";
77
// @ts-ignore: The following code is generated
@@ -21,6 +21,7 @@ interface TestDefinition {
2121
// @ts-ignore: testDefinitions is untyped
2222
const tests: TestDefinition[] = testDefinitions;
2323
const logs: LogRecord[] = [];
24+
const messageBatches: MessageBatch[] = [];
2425

2526
await configure({
2627
sinks: {
@@ -33,7 +34,7 @@ await configure({
3334
});
3435

3536
export default {
36-
async fetch(request: Request): Promise<Response> {
37+
async fetch(request: Request, env: unknown): Promise<Response> {
3738
if (request.method === "GET") {
3839
return new Response(
3940
JSON.stringify(tests.map(({ name }) => name)),
@@ -98,7 +99,7 @@ export default {
9899
}
99100
logs.splice(0, logs.length); // Clear logs
100101
try {
101-
await fn({ name, origin: "", step });
102+
await fn({ name, origin: "", step, env, messageBatches });
102103
} catch (e) {
103104
failed ??= e;
104105
}
@@ -130,4 +131,11 @@ export default {
130131
},
131132
);
132133
},
134+
async queue(
135+
batch: MessageBatch,
136+
env: unknown,
137+
ctx: ExecutionContext
138+
): Promise<void> {
139+
messageBatches.push(batch);
140+
}
133141
};

fedify/codegen/schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ export async function loadSchemaFiles(
275275
const errors: SchemaError[] = [];
276276
for await (const relPath of readDirRecursive(dir)) {
277277
if (!relPath.match(/\.ya?ml$/i)) continue;
278+
if (relPath.match(/(^|[/\\])schema.yaml$/i)) continue;
278279
const path = join(dir, relPath);
279280
let schema: TypeSchema;
280281
try {

fedify/deno.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@
1111
"./sig": "./sig/mod.ts",
1212
"./vocab": "./vocab/mod.ts",
1313
"./webfinger": "./webfinger/mod.ts",
14+
"./x/cfworkers": "./x/cfworkers.ts",
1415
"./x/denokv": "./x/denokv.ts",
1516
"./x/fresh": "./x/fresh.ts",
1617
"./x/hono": "./x/hono.ts",
1718
"./x/sveltekit": "./x/sveltekit.ts"
1819
},
1920
"imports": {
2021
"@cfworker/json-schema": "npm:@cfworker/json-schema@^4.1.1",
22+
"@cloudflare/workers-types": "npm:@cloudflare/workers-types@^4.20250529.0",
2123
"@es-toolkit/es-toolkit": "jsr:@es-toolkit/es-toolkit@^1.38.0",
2224
"@hongminhee/deno-mock-fetch": "jsr:@hongminhee/deno-mock-fetch@^0.3.2",
2325
"@hugoalh/http-header-link": "jsr:@hugoalh/http-header-link@^1.0.2",

0 commit comments

Comments
 (0)