Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 28 additions & 5 deletions docs/content/server/transports.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ weight: 6

By default, Zeta uses `createFetchTransport`. It detects your runtime and provides a minimal transport that supports Bun and Deno.

## Runtime-specific Decorations
## Runtime-specific API Access

The default fetch transport not provide access to runtime-specific APIs. For example, in Bun, you use the `server` arg to setup websockets or get the request IP address:

Expand All @@ -19,21 +19,44 @@ Bun.serve({
});
```

To access the same object, you need to provide a custom `transport` then transport-specific APIs will be available in the request context:
To access the same object, you need to provide a custom `transport` then transport-specific APIs, then use the transports helper functions, likes `getBunServer`:

```ts
import { createApp } from "@aklinker1/zeta";
import { createBunTransport } from "@aklinker1/zeta/transports/bun-transport";
import {
createBunTransport,
getBunServer,
} from "@aklinker1/zeta/transports/bun-transport";

const app = createApp({
transport: createBunTransport(),
}).get(({ request, server }) => {
}).get(({ request }) => {
const server = getBunServer(request);
const ip = server.requestIP(request);
// ...
});
```

You only have to add the transport once, to your top-level app, and any runtime-specific decorations will be made available in all child-apps as well!
Alternatively, you can use the plugin to decorate your context directly:

```ts
import { createApp } from "@aklinker1/zeta";
import {
createBunTransport,
bunServerPlugin,
} from "@aklinker1/zeta/transports/bun-transport";

const app = createApp({
transport: createBunTransport(),
})
.use(bunServerPlugin)
.get(({ server }) => {
const ip = server.requestIP(request);
// ...
});
```

You only have to add the transport once to your top-level app, even if you're using these utils in child-apps.

## Transport Options

Expand Down
13 changes: 9 additions & 4 deletions src/__tests__/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import { createApp } from "../app";
import { HttpStatus } from "../status";
import { createTestAppClient } from "../testing";
import type { AnyDef, GetAppData, Transport } from "../types";
import { createBunTransport } from "../transports/bun-transport";
import {
bunServerPlugin,
createBunTransport,
} from "../transports/bun-transport";

// Silence console.error logs
globalThis.console.error = mock();
Expand Down Expand Up @@ -620,9 +623,11 @@ describe("App", () => {
const app = createApp({
schemaAdapter: zodSchemaAdapter,
transport: createBunTransport(),
}).get("/", (ctx) => {
actual = ctx.server;
});
})
.use(bunServerPlugin)
.get("/", ({ server }) => {
actual = server;
});
await app.build()(new Request("http://localhost:3000"), expected);

expect(actual!).toBe(expected);
Expand Down
57 changes: 49 additions & 8 deletions src/transports/bun-transport.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import type { Transport } from "../types";
import type { RequestContext, Transport } from "../types";
import { createApp } from "../app";

export type BunTransport = Transport<[request: Request, server: Bun.Server]>;
const SERVER_KEY = Symbol("bun-transport.server");

declare module "../types" {
interface RequestContext {
server: Bun.Server;
}
}
export type BunTransport = Transport<[request: Request, server: Bun.Server]>;

export function createBunTransport(
options?: Omit<Bun.ServeFunctionOptions<any, any>, "fetch" | "port">,
Expand All @@ -17,11 +14,55 @@ export function createBunTransport(
};

const decorate: BunTransport["decorate"] = (ctx, _request, server) => {
ctx.server = server;
ctx[SERVER_KEY] = server;
};

return {
listen,
decorate,
};
}

/**
* Given the request context, return Bun's `server` object. Throws an error if the bun transport is not provided on the top-level app.
*
* @example
* ```ts
* const app = createApp({
* transport: createBunTransport(),
* }).get("/", (ctx) => {
* const server = getBunServer(ctx);
* })
* ```
*
* @see `bunServerPlugin` to add the `server` object to request context directly.
*/
export function getBunServer(ctx: RequestContext): Bun.Server {
const server = (ctx as any)[SERVER_KEY];
if (!server)
throw Error(
"Bun server not found. Did you forget to provide the bun transport?",
);

return server;
}

/**
* Plugin that decorates Bun's `server` object in the request context.
*
* @example
* ```ts
* const app = createApp({
* transport: createBunTransport(),
* })
* .use(bunServerPlugin)
* .get("/", ({ server }) => {
* // ...
* })
* ```
*
* @see `getBunServer` for a simple function to return the server
*/
export const bunServerPlugin = createApp()
.onTransform((ctx) => ({ server: getBunServer(ctx) }))
.export();
58 changes: 49 additions & 9 deletions src/transports/deno-transport.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import type { Transport } from "../types";
import { createApp } from "../app";
import type { RequestContext, Transport } from "../types";

const SERVER_KEY = Symbol("deno-transport.server");

export type DenoTransport = Transport<
[request: Request, server: Deno.HttpServer]
>;

declare module "../types" {
interface RequestContext {
// @ts-expect-error: Ignore conflict with bun transport, only one will be imported in production.
server: Deno.HttpServer;
}
}
type ServeOptions = Parameters<typeof Deno.serve>[0];

export function createDenoTransport(
options?: Omit<ServeOptions, "port">,
Expand All @@ -20,7 +18,7 @@ export function createDenoTransport(
};

const decorate: DenoTransport["decorate"] = (ctx, _request, server) => {
ctx.server = server;
ctx[SERVER_KEY] = server;
};

return {
Expand All @@ -29,4 +27,46 @@ export function createDenoTransport(
};
}

type ServeOptions = Parameters<typeof Deno.serve>[0];
/**
* Given the request context, return Deno's `server` object. Throws an error if the Deno transport is not provided on the top-level app.
*
* @example
* ```ts
* const app = createApp({
* transport: createDenoTransport(),
* }).get("/", (ctx) => {
* const server = getDenoServer(ctx);
* })
* ```
*
* @see `denoServerPlugin` to add the `server` object to request context directly.
*/
export function getDenoServer(ctx: RequestContext): Deno.HttpServer {
const server = (ctx as any)[SERVER_KEY];
if (!server)
throw Error(
"Deno server not found. Did you forget to provide the deno transport?",
);

return server;
}

/**
* Plugin that decorates Deno's `server` object in the request context.
*
* @example
* ```ts
* const app = createApp({
* transport: createDenoTransport(),
* })
* .use(denoServerPlugin)
* .get("/", ({ server }) => {
* // ...
* })
* ```
*
* @see `getDenoServer` for a simple function to return the server
*/
export const denoServerPlugin = createApp()
.onTransform((ctx) => ({ server: getDenoServer(ctx) }))
.export();
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true
},
"exclude": ["node_modules", ".git", "dist"]
"exclude": ["./node_modules/**", "./.git/**", "./dist/**"]
}
Loading