Skip to content

Commit

Permalink
feat: initial Cloudflare Worker support
Browse files Browse the repository at this point in the history
  • Loading branch information
kitsonk committed Mar 1, 2024
1 parent f3dc3d5 commit a7f87e5
Show file tree
Hide file tree
Showing 6 changed files with 147 additions and 21 deletions.
46 changes: 42 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,10 @@ resources.
## Usage

### Deno
### Deno CLI and Deno Deploy

oak is available on both [deno.land/x](https://deno.land/x/oak/) and [JSR](https://jsr.io/@oak/oak). To use from `deno.land/x`, import into a module:
oak is available on both [deno.land/x](https://deno.land/x/oak/) and
[JSR](https://jsr.io/@oak/oak). To use from `deno.land/x`, import into a module:

```ts
import { Application } from "https://deno.land/x/oak/mod.ts";
Expand Down Expand Up @@ -75,12 +76,49 @@ npx jsr i @oak/oak@14
And then import into a module:

```js
import { Application } from "@oak/oak";
import { Application } from "@oak/oak/application";
```

> [!NOTE]
> Send, websocket upgrades and serving over TLS/HTTPS are not currently
> supported.
>
> In addition the Cloudflare Worker environment and execution context are not
> currently exposed to middleware.
### Cloudflare Workers

oak is available for [Cloudflare Workers](https://workers.cloudflare.com/) on
[JSR](https://jsr.io/@oak/oak). To use add the package to your Cloudflare Worker
project:

```
npx jsr add @oak/oak@14
```

And then import into a module:

```ts
import { Application } from "@oak/oak/application";
```

Unlike other runtimes, the oak application doesn't listen for incoming requests,
instead it handles worker fetch requests. A minimal example server would be:

```ts
import { Application } from "@oak/oak/application";

const app = new Application();

app.use((ctx) => {
ctx.response.body = "Hello CFW!";
});

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

> [!NOTE]
> Send and websocket upgrades are not currently supported.
### Bun

Expand All @@ -94,7 +132,7 @@ bunx jsr i @oak/oak@14
And then import into a module:

```ts
import { Application } from "@oak/oak";
import { Application } from "@oak/oak/application";
```

> [!NOTE]
Expand Down
86 changes: 86 additions & 0 deletions application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,38 @@ interface HandleMethod {
): Promise<Response | undefined>;
}

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

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>;
}

/** Options which can be specified when listening. */
export type ListenOptions = ListenOptionsTls | ListenOptionsBase;

Expand Down Expand Up @@ -581,6 +613,60 @@ export class Application<AS extends State = Record<string, any>>
super.addEventListener(type, listener, options);
}

/** 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 };
* ```
*/
fetch: CloudflareFetchHandler = async <
Env extends Record<string, string> = Record<string, string>,
>(
request: Request,
_env: Env,
_ctx: CloudflareExecutionContext,
): Promise<Response> => {
if (!this.#middleware.length) {
throw new TypeError("There is no middleware to process requests.");
}
if (!NativeRequestCtor) {
const { NativeRequest } = await import("./http_server_native_request.ts");
NativeRequestCtor = NativeRequest;
}
let remoteAddr: NetAddr | undefined;
const hostname = request.headers.get("CF-Connecting-IP") ?? undefined;
if (hostname) {
remoteAddr = { hostname, port: 0, transport: "tcp" };
}
const contextRequest = new NativeRequestCtor(request, { remoteAddr });
const context = new Context(
this,
contextRequest,
this.#getContextState(),
this.#contextOptions,
);
try {
await this.#getComposed()(context);
const response = await context.response.toDomResponse();
context.response.destroy(false);
return response;
} catch (err) {
this.#handleError(context, err);
throw err;
}
};

/** Handle an individual server request, returning the server response. This
* is similar to `.listen()`, but opening the connection and retrieving
* requests are not the responsibility of the application. If the generated
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/oak",
"version": "14.1.0-alpha.2",
"version": "14.1.0-alpha.5",
"exports": {
".": "./mod.ts",
"./application": "./application.ts",
Expand Down
2 changes: 1 addition & 1 deletion http_server_node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ Deno.test({
const listenOptions = { port: 4508 };

const server = new Server(app, listenOptions);
server.listen();
await server.listen();

const expectedBody = "test-body";

Expand Down
30 changes: 16 additions & 14 deletions http_server_node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

import type { Listener, OakServer, ServerRequest } from "./types.ts";
import { createPromiseWithResolvers } from "./util.ts";
import * as http from "node:http";

// There are quite a few differences between Deno's `std/node/http` and the
// typings for Node.js for `"http"`. Since we develop everything in Deno, but
Expand Down Expand Up @@ -152,32 +151,32 @@ export class Server implements OakServer<NodeRequest> {
#abortController = new AbortController();
#host: string;
#port: number;
#requestStream: ReadableStream<NodeRequest>;
#server!: NodeHttpServer;
#requestStream: ReadableStream<NodeRequest> | undefined;

constructor(
_app: unknown,
options: Deno.ListenOptions | Deno.ListenTlsOptions,
) {
this.#host = options.hostname ?? "127.0.0.1";
this.#port = options.port;
const start: ReadableStreamDefaultControllerCallback<NodeRequest> = (
controller,
) => {
const handler = (req: IncomingMessage, res: ServerResponse) =>
controller.enqueue(new NodeRequest(req, res));
// deno-lint-ignore no-explicit-any
this.#server = http.createServer(handler as any);
};
this.#requestStream = new ReadableStream({ start });
}

close(): void {
this.#abortController.abort();
}

listen(): Listener {
this.#server.listen({
async listen(): Promise<Listener> {
const { createServer } = await import("node:http");
let server: NodeHttpServer;
this.#requestStream = new ReadableStream({
start(controller) {
server = createServer((req, res) => {
// deno-lint-ignore no-explicit-any
controller.enqueue(new NodeRequest(req as any, res as any));
});
},
});
server!.listen({
port: this.#port,
host: this.#host,
signal: this.#abortController.signal,
Expand All @@ -191,6 +190,9 @@ export class Server implements OakServer<NodeRequest> {
}

[Symbol.asyncIterator](): AsyncIterableIterator<NodeRequest> {
if (!this.#requestStream) {
throw new TypeError("stream not properly initialized");
}
return this.#requestStream[Symbol.asyncIterator]();
}

Expand Down
2 changes: 1 addition & 1 deletion util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,7 @@ export function isBun(): boolean {

export function isNode(): boolean {
return "process" in globalThis && "global" in globalThis &&
!("Bun" in globalThis);
!("Bun" in globalThis) && !("WebSocketPair" in globalThis);
}

export function importKey(key: Key): Promise<CryptoKey> {
Expand Down

0 comments on commit a7f87e5

Please sign in to comment.