Skip to content

Commit

Permalink
feat: support Cloudflare Workers
Browse files Browse the repository at this point in the history
  • Loading branch information
kitsonk committed Jun 6, 2024
1 parent 382350b commit 2eecd21
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 15 deletions.
62 changes: 48 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,51 @@ designed for Deno CLI, Deno Deploy and Bun.

## Usage

### Under Deno CLI or Deploy
### Under Deno runtime or Deploy

You need to import the package into your code:
Add the package to your project:

```
deno add @oak/acorn
```

### Under Node.js and Cloudflare Workers

Install the package using your chosen package manager:

#### npm

```
npx jsr add @oak/acorn
```

#### Yarn

```
yarn dlx jsr add @oak/acorn
```

#### pnpm

```
pnpm dlx jsr add @oak/acorn
```

### Under Bun

```
bunx jsr add @oak/acorn
```

### Listening

For Deno, Node.js, and Bun the router needs to be configured and then start
listening for requests:

```ts
import { Router } from "jsr:@oak/acorn/router";
import { Router } from "@oak/acorn";

const BOOKS: Record<string, { id: number; title: string }> = {
const BOOKS = {
"1": { id: 1, title: "The Hound of the Baskervilles" },
"2": { id: 2, title: "It" },
};
Expand All @@ -27,20 +64,15 @@ router.get("/books/:id", (ctx) => BOOKS[ctx.params.id]);
router.listen({ port: 5000 });
```

### Under Bun

You need to add the package to your project:

```
bunx jsr add @oak/acorn
```
### Fetch handlers

Then you need to import the package into your code:
For Cloudflare Workers the router needs to be configured and the router's fetch
handler needs to be exported:

```ts
import { Router } from "@oak/acorn/router";
import { Router } from "@oak/acorn";

const BOOKS: Record<string, { id: number; title: string }> = {
const BOOKS = {
"1": { id: 1, title: "The Hound of the Baskervilles" },
"2": { id: 2, title: "It" },
};
Expand All @@ -51,6 +83,8 @@ router.get("/", () => ({ hello: "world" }));
router.get("/books/:id", (ctx) => BOOKS[ctx.params.id]);

router.listen({ port: 5000 });

export default { fetch: router.fetch };
```

## Philosophy
Expand Down
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@oak/acorn",
"version": "0.6.0",
"version": "0.7.0-alpha.1",
"exports": {
".": "./mod.ts",
"./context": "./context.ts",
Expand Down
62 changes: 62 additions & 0 deletions request_event_cloudflare.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright 2022-2024 the oak authors. All rights reserved.

import type {
Addr,
CloudflareExecutionContext,
RequestEvent as _RequestEvent,
} from "./types_internal.ts";
import { createPromiseWithResolvers } from "./util.ts";

export class RequestEvent<
Env extends Record<string, string> = Record<string, string>,
> implements _RequestEvent {
#addr?: Addr;
//deno-lint-ignore no-explicit-any
#reject: (reason?: any) => void;
#request: Request;
#resolve: (value: Response | PromiseLike<Response>) => void;
#resolved = false;
#response: Promise<Response>;

get addr(): Addr {
if (!this.#addr) {
const hostname = this.#request.headers.get("CF-Connecting-IP") ??
"localhost";
this.#addr = { hostname, port: 80, transport: "tcp" };
}
return this.#addr;
}

get request(): Request {
return this.#request;
}

get response(): Promise<Response> {
return this.#response;
}

constructor(request: Request, _env: Env, _ctx: CloudflareExecutionContext) {
this.#request = request;
const { resolve, reject, promise } = createPromiseWithResolvers<Response>();
this.#resolve = resolve;
this.#reject = reject;
this.#response = promise;
}

//deno-lint-ignore no-explicit-any
error(reason?: any): void {
if (this.#resolved) {
throw new Error("Request already responded to.");
}
this.#resolved = true;
this.#reject(reason);
}

respond(response: Response | PromiseLike<Response>): void {
if (this.#resolved) {
throw new Error("Request already responded to.");
}
this.#resolved = true;
this.#resolve(response);
}
}
39 changes: 39 additions & 0 deletions router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,12 @@ import {
SecureCookieMap,
Status,
} from "./deps.ts";
import type { RequestEvent as CloudFlareRequestEvent } from "./request_event_cloudflare.ts";
import type { Deserializer, KeyRing, Serializer } from "./types.ts";
import type {
Addr,
CloudflareExecutionContext,
CloudflareFetchHandler,
Destroyable,
Listener,
RequestEvent,
Expand Down Expand Up @@ -184,6 +187,8 @@ const HTTP_VERBS = [

const HANDLE_START = "handle start";

let RequestEventCtor: typeof CloudFlareRequestEvent | undefined;

type HTTPVerbs = typeof HTTP_VERBS[number];

/** A string that represents a range of HTTP response {@linkcode Status} codes:
Expand Down Expand Up @@ -1461,6 +1466,40 @@ export class Router extends EventTarget {
return promise;
}

/**
* A method that is compatible with the Cloudflare Worker
* [Fetch Handler](https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch/)
* and can be exported to handle Cloudflare Worker fetch requests.
*
* @example
*
* ```ts
* import { Router } from "@oak/acorn";
*
* const router = new Router();
* router.get("/", (ctx) => {
* ctx.response.body = "hello world!";
* });
*
* export default { fetch: router.fetch };
* ```
*/
fetch: CloudflareFetchHandler = async <
Env extends Record<string, string> = Record<string, string>,
>(
request: Request,
env: Env,
ctx: CloudflareExecutionContext,
): Promise<Response> => {
if (!RequestEventCtor) {
RequestEventCtor =
(await import("./request_event_cloudflare.ts")).RequestEvent;
}
const requestEvent = new RequestEventCtor(request, env, ctx);
this.#handle(requestEvent);
return requestEvent.response;
};

/** Open a server to listen for requests and handle them by matching against
* registered routes.
*
Expand Down
34 changes: 34 additions & 0 deletions types_internal.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,37 @@
// Copyright 2022-2024 the oak authors. All rights reserved.

export interface CloudflareExecutionContext {
waitUntil(promise: Promise<unknown>): void;
passThroughOnException(): void;
}

export interface CloudflareFetchHandler<
Env extends Record<string, string> = Record<string, string>,
> {
/** A method that is compatible with the Cloudflare Worker
* [Fetch Handler](https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch/)
* and can be exported to handle Cloudflare Worker fetch requests.
*
* # Example
*
* ```ts
* import { Application } from "@oak/oak";
*
* const app = new Application();
* app.use((ctx) => {
* ctx.response.body = "hello world!";
* });
*
* export default { fetch: app.fetch };
* ```
*/
(
request: Request,
env: Env,
ctx: CloudflareExecutionContext,
): Promise<Response>;
}

export interface RequestEvent {
readonly addr: Addr;
readonly request: Request;
Expand Down

0 comments on commit 2eecd21

Please sign in to comment.