trpc-bun-adapter
Advanced tools
Comparing version 1.0.0-rc.0 to 1.0.0
@@ -1,45 +0,73 @@ | ||
import * as bun from 'bun'; | ||
import { Server, ServerWebSocket, WebSocketHandler, ServeOptions } from 'bun'; | ||
import { AnyRouter, inferRouterContext } from '@trpc/server'; | ||
import { HTTPBaseHandlerOptions } from '@trpc/server/http'; | ||
import { TRPCClientOutgoingMessage } from '@trpc/server/rpc'; | ||
import { BaseHandlerOptions } from '@trpc/server/src/internals/types'; | ||
import * as bun from "bun"; | ||
import { Server, ServerWebSocket, WebSocketHandler, ServeOptions } from "bun"; | ||
import { AnyRouter, inferRouterContext } from "@trpc/server"; | ||
import { HTTPBaseHandlerOptions } from "@trpc/server/http"; | ||
import { TRPCClientOutgoingMessage } from "@trpc/server/rpc"; | ||
import { BaseHandlerOptions } from "@trpc/server/src/internals/types"; | ||
type BunHttpHandlerOptions<TRouter extends AnyRouter> = HTTPBaseHandlerOptions<TRouter, Request> & { | ||
endpoint?: string; | ||
createContext?: (opts: { | ||
req: Request; | ||
}) => inferRouterContext<TRouter> | Promise<inferRouterContext<TRouter>>; | ||
type BunHttpHandlerOptions<TRouter extends AnyRouter> = HTTPBaseHandlerOptions< | ||
TRouter, | ||
Request | ||
> & { | ||
endpoint?: string; | ||
createContext?: (opts: { | ||
req: Request; | ||
}) => inferRouterContext<TRouter> | Promise<inferRouterContext<TRouter>>; | ||
}; | ||
declare function createBunHttpHandler<TRouter extends AnyRouter>(opts: BunHttpHandlerOptions<TRouter> & { | ||
emitWsUpgrades?: boolean; | ||
}): (request: Request, server: Server) => Response | Promise<Response> | undefined; | ||
declare function createBunHttpHandler<TRouter extends AnyRouter>( | ||
opts: BunHttpHandlerOptions<TRouter> & { | ||
emitWsUpgrades?: boolean; | ||
}, | ||
): ( | ||
request: Request, | ||
server: Server, | ||
) => Response | Promise<Response> | undefined; | ||
type BunWSAdapterOptions<TRouter extends AnyRouter> = BaseHandlerOptions<TRouter, Request> & { | ||
createContext?: (params: { | ||
req: Request; | ||
client: ServerWebSocket<BunWSClientCtx>; | ||
}) => Promise<unknown> | unknown; | ||
type BunWSAdapterOptions<TRouter extends AnyRouter> = BaseHandlerOptions< | ||
TRouter, | ||
Request | ||
> & { | ||
createContext?: (params: { | ||
req: Request; | ||
client: ServerWebSocket<BunWSClientCtx>; | ||
}) => Promise<unknown> | unknown; | ||
}; | ||
type BunWSClientCtx = { | ||
req: Request; | ||
handleRequest: (msg: TRPCClientOutgoingMessage) => Promise<void>; | ||
unsubscribe(): void; | ||
req: Request; | ||
handleRequest: (msg: TRPCClientOutgoingMessage) => Promise<void>; | ||
unsubscribe(): void; | ||
}; | ||
declare function createBunWSHandler<TRouter extends AnyRouter>(opts: BunWSAdapterOptions<TRouter>): WebSocketHandler<BunWSClientCtx>; | ||
declare function createBunWSHandler<TRouter extends AnyRouter>( | ||
opts: BunWSAdapterOptions<TRouter>, | ||
): WebSocketHandler<BunWSClientCtx>; | ||
type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>; | ||
declare function createBunServeHandler<TRouter extends AnyRouter>(opts: BunHttpHandlerOptions<TRouter>, serveOptions?: Optional<ServeOptions, "fetch">): { | ||
fetch(req: Request, server: Server): Promise<Response | undefined>; | ||
websocket: bun.WebSocketHandler<BunWSClientCtx>; | ||
error?: ((this: Server, request: bun.Errorlike) => Response | Promise<Response> | Promise<undefined> | undefined) | undefined; | ||
id?: string | null | undefined; | ||
port?: string | number | undefined; | ||
reusePort?: boolean | undefined; | ||
hostname?: string | undefined; | ||
unix?: undefined; | ||
maxRequestBodySize?: number | undefined; | ||
development?: boolean | undefined; | ||
declare function createBunServeHandler<TRouter extends AnyRouter>( | ||
opts: BunHttpHandlerOptions<TRouter>, | ||
serveOptions?: Optional<ServeOptions, "fetch">, | ||
): { | ||
fetch(req: Request, server: Server): Promise<Response | undefined>; | ||
websocket: bun.WebSocketHandler<BunWSClientCtx>; | ||
error?: | ||
| (( | ||
this: Server, | ||
request: bun.Errorlike, | ||
) => Response | Promise<Response> | Promise<undefined> | undefined) | ||
| undefined; | ||
id?: string | null | undefined; | ||
port?: string | number | undefined; | ||
reusePort?: boolean | undefined; | ||
hostname?: string | undefined; | ||
unix?: undefined; | ||
maxRequestBodySize?: number | undefined; | ||
development?: boolean | undefined; | ||
}; | ||
export { type BunHttpHandlerOptions, type BunWSAdapterOptions, type BunWSClientCtx, createBunHttpHandler, createBunServeHandler, createBunWSHandler }; | ||
export { | ||
type BunHttpHandlerOptions, | ||
type BunWSAdapterOptions, | ||
type BunWSClientCtx, | ||
createBunHttpHandler, | ||
createBunServeHandler, | ||
createBunWSHandler, | ||
}; |
@@ -7,14 +7,18 @@ "use strict"; | ||
var __export = (target, all) => { | ||
for (var name in all) | ||
__defProp(target, name, { get: all[name], enumerable: true }); | ||
for (var name in all) | ||
__defProp(target, name, { get: all[name], enumerable: true }); | ||
}; | ||
var __copyProps = (to, from, except, desc) => { | ||
if (from && typeof from === "object" || typeof from === "function") { | ||
for (let key of __getOwnPropNames(from)) | ||
if (!__hasOwnProp.call(to, key) && key !== except) | ||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); | ||
} | ||
return to; | ||
if ((from && typeof from === "object") || typeof from === "function") { | ||
for (let key of __getOwnPropNames(from)) | ||
if (!__hasOwnProp.call(to, key) && key !== except) | ||
__defProp(to, key, { | ||
get: () => from[key], | ||
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable, | ||
}); | ||
} | ||
return to; | ||
}; | ||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); | ||
var __toCommonJS = (mod) => | ||
__copyProps(__defProp({}, "__esModule", { value: true }), mod); | ||
@@ -24,5 +28,5 @@ // src/index.ts | ||
__export(src_exports, { | ||
createBunHttpHandler: () => createBunHttpHandler, | ||
createBunServeHandler: () => createBunServeHandler, | ||
createBunWSHandler: () => createBunWSHandler | ||
createBunHttpHandler: () => createBunHttpHandler, | ||
createBunServeHandler: () => createBunServeHandler, | ||
createBunWSHandler: () => createBunWSHandler, | ||
}); | ||
@@ -34,16 +38,19 @@ module.exports = __toCommonJS(src_exports); | ||
function createBunHttpHandler(opts) { | ||
return (request, server) => { | ||
const url = new URL(request.url); | ||
if (opts.endpoint && !url.pathname.startsWith(opts.endpoint)) { | ||
return; | ||
} | ||
if (opts.emitWsUpgrades && server.upgrade(request, { data: { req: request } })) { | ||
return new Response(null, { status: 101 }); | ||
} | ||
return (0, import_fetch.fetchRequestHandler)({ | ||
endpoint: opts.endpoint ?? "", | ||
...opts, | ||
req: request | ||
}); | ||
}; | ||
return (request, server) => { | ||
const url = new URL(request.url); | ||
if (opts.endpoint && !url.pathname.startsWith(opts.endpoint)) { | ||
return; | ||
} | ||
if ( | ||
opts.emitWsUpgrades && | ||
server.upgrade(request, { data: { req: request } }) | ||
) { | ||
return new Response(null, { status: 101 }); | ||
} | ||
return (0, import_fetch.fetchRequestHandler)({ | ||
endpoint: opts.endpoint ?? "", | ||
...opts, | ||
req: request, | ||
}); | ||
}; | ||
} | ||
@@ -57,209 +64,219 @@ | ||
function createBunWSHandler(opts) { | ||
const { router, createContext } = opts; | ||
const respond = (client, untransformedJSON) => { | ||
client.send( | ||
JSON.stringify( | ||
(0, import_shared.transformTRPCResponse)(opts.router._def._config, untransformedJSON) | ||
) | ||
); | ||
}; | ||
return { | ||
async open(client) { | ||
const { req } = client.data; | ||
const clientSubscriptions = /* @__PURE__ */ new Map(); | ||
const ctxPromise = createContext?.({ req, client }); | ||
let ctx = void 0; | ||
await (async () => { | ||
try { | ||
ctx = await ctxPromise; | ||
} catch (cause) { | ||
const error = (0, import_server.getTRPCErrorFromUnknown)(cause); | ||
opts.onError?.({ | ||
error, | ||
path: void 0, | ||
type: "unknown", | ||
ctx, | ||
req, | ||
input: void 0 | ||
}); | ||
respond(client, { | ||
id: null, | ||
error: (0, import_shared.getErrorShape)({ | ||
config: router._def._config, | ||
error, | ||
type: "unknown", | ||
path: void 0, | ||
input: void 0, | ||
ctx | ||
}) | ||
}); | ||
setImmediate(() => client.close()); | ||
} | ||
})(); | ||
const stopSubscription = (subscription, { id, jsonrpc }) => { | ||
subscription.unsubscribe(); | ||
respond(client, { | ||
id, | ||
jsonrpc, | ||
result: { | ||
type: "stopped" | ||
} | ||
}); | ||
}; | ||
client.data.handleRequest = async (msg) => { | ||
const { id, jsonrpc } = msg; | ||
if (id === null) { | ||
throw new import_server.TRPCError({ | ||
code: "BAD_REQUEST", | ||
message: "`id` is required" | ||
}); | ||
} | ||
if (msg.method === "subscription.stop") { | ||
const sub = clientSubscriptions.get(id); | ||
if (sub) { | ||
stopSubscription(sub, { id, jsonrpc }); | ||
} | ||
clientSubscriptions.delete(id); | ||
return; | ||
} | ||
const { path, input } = msg.params; | ||
const type = msg.method; | ||
try { | ||
await ctxPromise; | ||
const result = await (0, import_server.callProcedure)({ | ||
procedures: router._def.procedures, | ||
path, | ||
rawInput: input, | ||
// @ts-expect-error: for a newer trpc versions, cuz we use internal API | ||
getRawInput: () => Promise.resolve(input), | ||
ctx, | ||
type | ||
}); | ||
if (type === "subscription") { | ||
if (!(0, import_observable.isObservable)(result)) { | ||
throw new import_server.TRPCError({ | ||
message: `Subscription ${path} did not return an observable`, | ||
code: "INTERNAL_SERVER_ERROR" | ||
}); | ||
} | ||
} else { | ||
respond(client, { | ||
id, | ||
jsonrpc, | ||
result: { | ||
type: "data", | ||
data: result | ||
} | ||
}); | ||
return; | ||
} | ||
const observable = result; | ||
const sub = observable.subscribe({ | ||
next(data) { | ||
respond(client, { | ||
id, | ||
jsonrpc, | ||
result: { | ||
type: "data", | ||
data | ||
} | ||
}); | ||
}, | ||
error(err) { | ||
const error = (0, import_server.getTRPCErrorFromUnknown)(err); | ||
opts.onError?.({ error, path, type, ctx, req, input }); | ||
respond(client, { | ||
id, | ||
jsonrpc, | ||
error: (0, import_shared.getErrorShape)({ | ||
config: router._def._config, | ||
error, | ||
type, | ||
path, | ||
input, | ||
ctx | ||
}) | ||
}); | ||
}, | ||
complete() { | ||
respond(client, { | ||
id, | ||
jsonrpc, | ||
result: { | ||
type: "stopped" | ||
} | ||
}); | ||
} | ||
}); | ||
if (client.readyState !== WebSocket.OPEN) { | ||
sub.unsubscribe(); | ||
return; | ||
} | ||
if (clientSubscriptions.has(id)) { | ||
stopSubscription(sub, { id, jsonrpc }); | ||
throw new import_server.TRPCError({ | ||
message: `Duplicate id ${id}`, | ||
code: "BAD_REQUEST" | ||
}); | ||
} | ||
clientSubscriptions.set(id, sub); | ||
respond(client, { | ||
id, | ||
jsonrpc, | ||
result: { | ||
type: "started" | ||
} | ||
}); | ||
} catch (cause) { | ||
const error = (0, import_server.getTRPCErrorFromUnknown)(cause); | ||
opts.onError?.({ error, path, type, ctx, req, input }); | ||
respond(client, { | ||
id, | ||
jsonrpc, | ||
error: (0, import_shared.getErrorShape)({ | ||
config: router._def._config, | ||
error, | ||
type, | ||
path, | ||
input, | ||
ctx | ||
}) | ||
}); | ||
} | ||
}; | ||
client.data.unsubscribe = () => { | ||
for (const sub of clientSubscriptions.values()) { | ||
sub.unsubscribe(); | ||
} | ||
clientSubscriptions.clear(); | ||
}; | ||
}, | ||
async close(client) { | ||
client.data.unsubscribe?.(); | ||
}, | ||
async message(client, message) { | ||
try { | ||
const msgJSON = JSON.parse(message.toString()); | ||
const msgs = Array.isArray(msgJSON) ? msgJSON : [msgJSON]; | ||
const promises = msgs.map((raw) => (0, import_rpc.parseTRPCMessage)(raw, router._def._config.transformer)).map(client.data.handleRequest); | ||
await Promise.all(promises); | ||
} catch (cause) { | ||
const error = new import_server.TRPCError({ | ||
code: "PARSE_ERROR", | ||
cause | ||
}); | ||
respond(client, { | ||
id: null, | ||
error: (0, import_shared.getErrorShape)({ | ||
config: router._def._config, | ||
error, | ||
type: "unknown", | ||
path: void 0, | ||
input: void 0, | ||
ctx: void 0 | ||
}) | ||
}); | ||
} | ||
} | ||
}; | ||
const { router, createContext } = opts; | ||
const respond = (client, untransformedJSON) => { | ||
client.send( | ||
JSON.stringify( | ||
(0, import_shared.transformTRPCResponse)( | ||
opts.router._def._config, | ||
untransformedJSON, | ||
), | ||
), | ||
); | ||
}; | ||
return { | ||
async open(client) { | ||
const { req } = client.data; | ||
const clientSubscriptions = /* @__PURE__ */ new Map(); | ||
const ctxPromise = createContext?.({ req, client }); | ||
let ctx = void 0; | ||
await (async () => { | ||
try { | ||
ctx = await ctxPromise; | ||
} catch (cause) { | ||
const error = (0, import_server.getTRPCErrorFromUnknown)(cause); | ||
opts.onError?.({ | ||
error, | ||
path: void 0, | ||
type: "unknown", | ||
ctx, | ||
req, | ||
input: void 0, | ||
}); | ||
respond(client, { | ||
id: null, | ||
error: (0, import_shared.getErrorShape)({ | ||
config: router._def._config, | ||
error, | ||
type: "unknown", | ||
path: void 0, | ||
input: void 0, | ||
ctx, | ||
}), | ||
}); | ||
setImmediate(() => client.close()); | ||
} | ||
})(); | ||
const stopSubscription = (subscription, { id, jsonrpc }) => { | ||
subscription.unsubscribe(); | ||
respond(client, { | ||
id, | ||
jsonrpc, | ||
result: { | ||
type: "stopped", | ||
}, | ||
}); | ||
}; | ||
client.data.handleRequest = async (msg) => { | ||
const { id, jsonrpc } = msg; | ||
if (id === null) { | ||
throw new import_server.TRPCError({ | ||
code: "BAD_REQUEST", | ||
message: "`id` is required", | ||
}); | ||
} | ||
if (msg.method === "subscription.stop") { | ||
const sub = clientSubscriptions.get(id); | ||
if (sub) { | ||
stopSubscription(sub, { id, jsonrpc }); | ||
} | ||
clientSubscriptions.delete(id); | ||
return; | ||
} | ||
const { path, input } = msg.params; | ||
const type = msg.method; | ||
try { | ||
await ctxPromise; | ||
const result = await (0, import_server.callProcedure)({ | ||
procedures: router._def.procedures, | ||
path, | ||
rawInput: input, | ||
// @ts-expect-error: for a newer trpc versions, cuz we use internal API | ||
getRawInput: () => Promise.resolve(input), | ||
ctx, | ||
type, | ||
}); | ||
if (type === "subscription") { | ||
if (!(0, import_observable.isObservable)(result)) { | ||
throw new import_server.TRPCError({ | ||
message: `Subscription ${path} did not return an observable`, | ||
code: "INTERNAL_SERVER_ERROR", | ||
}); | ||
} | ||
} else { | ||
respond(client, { | ||
id, | ||
jsonrpc, | ||
result: { | ||
type: "data", | ||
data: result, | ||
}, | ||
}); | ||
return; | ||
} | ||
const observable = result; | ||
const sub = observable.subscribe({ | ||
next(data) { | ||
respond(client, { | ||
id, | ||
jsonrpc, | ||
result: { | ||
type: "data", | ||
data, | ||
}, | ||
}); | ||
}, | ||
error(err) { | ||
const error = (0, import_server.getTRPCErrorFromUnknown)(err); | ||
opts.onError?.({ error, path, type, ctx, req, input }); | ||
respond(client, { | ||
id, | ||
jsonrpc, | ||
error: (0, import_shared.getErrorShape)({ | ||
config: router._def._config, | ||
error, | ||
type, | ||
path, | ||
input, | ||
ctx, | ||
}), | ||
}); | ||
}, | ||
complete() { | ||
respond(client, { | ||
id, | ||
jsonrpc, | ||
result: { | ||
type: "stopped", | ||
}, | ||
}); | ||
}, | ||
}); | ||
if (client.readyState !== WebSocket.OPEN) { | ||
sub.unsubscribe(); | ||
return; | ||
} | ||
if (clientSubscriptions.has(id)) { | ||
stopSubscription(sub, { id, jsonrpc }); | ||
throw new import_server.TRPCError({ | ||
message: `Duplicate id ${id}`, | ||
code: "BAD_REQUEST", | ||
}); | ||
} | ||
clientSubscriptions.set(id, sub); | ||
respond(client, { | ||
id, | ||
jsonrpc, | ||
result: { | ||
type: "started", | ||
}, | ||
}); | ||
} catch (cause) { | ||
const error = (0, import_server.getTRPCErrorFromUnknown)(cause); | ||
opts.onError?.({ error, path, type, ctx, req, input }); | ||
respond(client, { | ||
id, | ||
jsonrpc, | ||
error: (0, import_shared.getErrorShape)({ | ||
config: router._def._config, | ||
error, | ||
type, | ||
path, | ||
input, | ||
ctx, | ||
}), | ||
}); | ||
} | ||
}; | ||
client.data.unsubscribe = () => { | ||
for (const sub of clientSubscriptions.values()) { | ||
sub.unsubscribe(); | ||
} | ||
clientSubscriptions.clear(); | ||
}; | ||
}, | ||
async close(client) { | ||
client.data.unsubscribe?.(); | ||
}, | ||
async message(client, message) { | ||
try { | ||
const msgJSON = JSON.parse(message.toString()); | ||
const msgs = Array.isArray(msgJSON) ? msgJSON : [msgJSON]; | ||
const promises = msgs | ||
.map((raw) => | ||
(0, import_rpc.parseTRPCMessage)( | ||
raw, | ||
router._def._config.transformer, | ||
), | ||
) | ||
.map(client.data.handleRequest); | ||
await Promise.all(promises); | ||
} catch (cause) { | ||
const error = new import_server.TRPCError({ | ||
code: "PARSE_ERROR", | ||
cause, | ||
}); | ||
respond(client, { | ||
id: null, | ||
error: (0, import_shared.getErrorShape)({ | ||
config: router._def._config, | ||
error, | ||
type: "unknown", | ||
path: void 0, | ||
input: void 0, | ||
ctx: void 0, | ||
}), | ||
}); | ||
} | ||
}, | ||
}; | ||
} | ||
@@ -269,25 +286,26 @@ | ||
function createBunServeHandler(opts, serveOptions) { | ||
const trpcHandler = createBunHttpHandler({ | ||
...opts, | ||
emitWsUpgrades: true | ||
}); | ||
return { | ||
...serveOptions, | ||
async fetch(req, server) { | ||
const trpcReponse = trpcHandler(req, server); | ||
if (trpcReponse) { | ||
return trpcReponse; | ||
} | ||
return serveOptions?.fetch?.call(server, req, server); | ||
}, | ||
websocket: createBunWSHandler(opts) | ||
}; | ||
const trpcHandler = createBunHttpHandler({ | ||
...opts, | ||
emitWsUpgrades: true, | ||
}); | ||
return { | ||
...serveOptions, | ||
async fetch(req, server) { | ||
const trpcReponse = trpcHandler(req, server); | ||
if (trpcReponse) { | ||
return trpcReponse; | ||
} | ||
return serveOptions?.fetch?.call(server, req, server); | ||
}, | ||
websocket: createBunWSHandler(opts), | ||
}; | ||
} | ||
// Annotate the CommonJS export names for ESM import in node: | ||
0 && (module.exports = { | ||
createBunHttpHandler, | ||
createBunServeHandler, | ||
createBunWSHandler | ||
}); | ||
0 && | ||
(module.exports = { | ||
createBunHttpHandler, | ||
createBunServeHandler, | ||
createBunWSHandler, | ||
}); | ||
/* istanbul ignore next -- @preserve */ | ||
//# sourceMappingURL=index.js.map | ||
//# sourceMappingURL=index.js.map |
{ | ||
"name": "trpc-bun-adapter", | ||
"version": "1.0.0-rc.0", | ||
"version": "1.0.0", | ||
"description": "TRPC adapter for bun js runtime", | ||
@@ -10,3 +10,3 @@ "main": "dist/index.js", | ||
"test": "bun test", | ||
"fmt": "bunx @biomejs/biome format --write ./src", | ||
"fmt": "bunx @biomejs/biome format --write .", | ||
"build": "npx tsup src/index.ts --format cjs,esm --dts --clean --sourcemap" | ||
@@ -20,3 +20,5 @@ }, | ||
], | ||
"repository": "https://github.com/cah4a/trpc-bun-adapter", | ||
"repository": { | ||
"url": "git+https://github.com/cah4a/trpc-bun-adapter.git" | ||
}, | ||
"bugs": "https://github.com/cah4a/trpc-bun-adapter/issues", | ||
@@ -23,0 +25,0 @@ "author": "Sancha <cah4o3@gmail.com>", |
@@ -1,13 +0,16 @@ | ||
# TRPC Bun Adapter | ||
# tRPC Bun Adapter | ||
[![npm version](https://badge.fury.io/js/trpc-bun-adapter.svg)](https://badge.fury.io/js/trpc-bun-adapter) | ||
[![License](https://img.shields.io/github/license/cah4a/trpc-bun-adapter)](https://opensource.org/licenses/MIT) | ||
## Description | ||
Leverage [TRPC](https://trpc.io/) on [Bun](https://bun.sh/) with ease. | ||
Supports both HTTP and WebSockets transports. | ||
`trpc-bun-adapter` is a [tRPC](https://trpc.io/) adapter for [Bun](https://github.com/OptimalBits/bun). | ||
Start both HTTP and WebSockets transports with ease. | ||
## Quick Start | ||
Install the package: | ||
Install packages: | ||
```bash | ||
@@ -17,26 +20,40 @@ bun install @trpc/server trpc-bun-adapter | ||
Paste the following code into `server.ts`: | ||
Create a server.ts file with the following content: | ||
```ts | ||
import {initTRPC} from '@trpc/server'; | ||
import {createBunServeHandler} from 'trpc-bun-adapter'; | ||
import {router} from './router'; | ||
const t = initTRPC.create(); | ||
export const router = t.router({ | ||
ping: t.procedure.query(() => "pong"), | ||
}); | ||
Bun.serve(createBunServeHandler({ router })); | ||
``` | ||
Run the server with HTTP and WebSocket transports: | ||
To start the server, run: | ||
```bash | ||
bun run server.ts | ||
bun run --watch server.ts # to restart on file changes | ||
``` | ||
Check that it works: | ||
```bash | ||
curl http://localhost:3000/ping | ||
``` | ||
## API Reference | ||
Ensure you have created a `router.ts` file as outlined in the tRPC documentation: [Define Routers](https://trpc.io/docs/server/routers). | ||
### createBunServeHandler | ||
Creates a Bun serve handler for HTTP and WebSocket transports: | ||
Creates a Bun serve handler: | ||
```ts | ||
import {createBunServeHandler} from 'trpc-bun-adapter'; | ||
import {createBunServeHandler, CreateBunContextOptions} from 'trpc-bun-adapter'; | ||
import {router} from './router'; | ||
const createContext = (opts: { req: Request }) => ({ | ||
const createContext = (opts: CreateBunContextOptions) => ({ | ||
user: 1, | ||
@@ -75,15 +92,11 @@ }); | ||
Arguments are: | ||
- `options` - TRPC options | ||
- `bunOptions` - Bun serve options | ||
### createBunHttpHandler | ||
Creates a Bun HTTP handler for HTTP transport: | ||
Creates a Bun HTTP handler for tRPC HTTP requests: | ||
```ts | ||
import {createBunHttpHandler} from 'trpc-bun-adapter'; | ||
import {createBunHttpHandler, CreateBunContextOptions} from 'trpc-bun-adapter'; | ||
import {router} from './router'; | ||
const createContext = (opts: { req: Request }) => ({ | ||
const createContext = (opts: CreateBunContextOptions) => ({ | ||
user: 1, | ||
@@ -119,10 +132,9 @@ }); | ||
Creates a Bun WebSocket handler for WebSocket transport: | ||
Creates a Bun WebSocket handler for tRPC websocket requests: | ||
```ts | ||
import {ServerWebSocket} from "bun"; | ||
import {createBunWSHandler, BunWSClientCtx} from './src'; | ||
import {router} from './router'; | ||
import { createBunWSHandler, CreateBunContextOptions } from './src'; | ||
import { router } from './router'; | ||
const createContext = (opts: { req: Request, client: ServerWebSocket<BunWSClientCtx> }) => ({ | ||
const createContext = (opts: CreateBunContextOptions) => ({ | ||
user: 1, | ||
@@ -153,2 +165,37 @@ }); | ||
### CreateBunContextOptions | ||
To ensure your router recognizes the context type, define a `createContext` function utilizing the `CreateBunContextOptions` type: | ||
```ts | ||
import { initTRPC } from '@trpc/server'; | ||
import type { CreateBunContextOptions } from "src/createBunHttpHandler"; | ||
export const createContext = async (opts: CreateBunContextOptions) => { | ||
return { | ||
authorization: req.headers.get('Authorization') | ||
}; | ||
}; | ||
``` | ||
With `createContext` defined, you can use it in your router to access the context, such as the authorization information: | ||
```ts | ||
const t = initTRPC.context<typeof createContext>().create(); | ||
export const router = t.router({ | ||
session: t.procedure.query(({ ctx }) => ctx.authorization), | ||
}); | ||
``` | ||
Finally, pass your `createContext` function besides `router` to `createBunHttpHandler`. | ||
This integrates your custom context into the HTTP handler setup: | ||
```ts | ||
createBunHttpHandler({ | ||
router, | ||
createContext, | ||
}) | ||
``` | ||
Read more documentation about tRPC contexts here: [Contexts](https://trpc.io/docs/server/context) | ||
## License | ||
@@ -155,0 +202,0 @@ |
@@ -1,34 +0,39 @@ | ||
import {Server} from "bun"; | ||
import {fetchRequestHandler} from "@trpc/server/adapters/fetch"; | ||
import type {AnyRouter, inferRouterContext} from "@trpc/server"; | ||
import type {HTTPBaseHandlerOptions} from "@trpc/server/http"; | ||
import { Server } from "bun"; | ||
import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; | ||
import type { AnyRouter, inferRouterContext } from "@trpc/server"; | ||
import type { HTTPBaseHandlerOptions } from "@trpc/server/http"; | ||
export type CreateBunContextOptions = { req: Request }; | ||
export type BunHttpHandlerOptions<TRouter extends AnyRouter> = | ||
HTTPBaseHandlerOptions<TRouter, Request> & { | ||
endpoint?: string; | ||
createContext?: (opts: { req: Request }) => | ||
| inferRouterContext<TRouter> | ||
| Promise<inferRouterContext<TRouter>>; | ||
}; | ||
HTTPBaseHandlerOptions<TRouter, Request> & { | ||
endpoint?: string; | ||
createContext?: ( | ||
opts: CreateBunContextOptions, | ||
) => inferRouterContext<TRouter> | Promise<inferRouterContext<TRouter>>; | ||
}; | ||
export function createBunHttpHandler<TRouter extends AnyRouter>( | ||
opts: BunHttpHandlerOptions<TRouter> & { emitWsUpgrades?: boolean }, | ||
opts: BunHttpHandlerOptions<TRouter> & { emitWsUpgrades?: boolean }, | ||
) { | ||
return (request: Request, server: Server) => { | ||
const url = new URL(request.url); | ||
return (request: Request, server: Server) => { | ||
const url = new URL(request.url); | ||
if (opts.endpoint && !url.pathname.startsWith(opts.endpoint)) { | ||
return; | ||
} | ||
if (opts.endpoint && !url.pathname.startsWith(opts.endpoint)) { | ||
return; | ||
} | ||
if (opts.emitWsUpgrades && server.upgrade(request, {data: {req: request}})) { | ||
return new Response(null, {status: 101}); | ||
} | ||
if ( | ||
opts.emitWsUpgrades && | ||
server.upgrade(request, { data: { req: request } }) | ||
) { | ||
return new Response(null, { status: 101 }); | ||
} | ||
return fetchRequestHandler({ | ||
endpoint: opts.endpoint ?? "", | ||
...opts, | ||
req: request, | ||
}); | ||
}; | ||
return fetchRequestHandler({ | ||
endpoint: opts.endpoint ?? "", | ||
...opts, | ||
req: request, | ||
}); | ||
}; | ||
} |
@@ -1,306 +0,305 @@ | ||
import {test, describe, beforeAll, afterAll, expect} from "bun:test"; | ||
import {createBunServeHandler} from "./createBunServeHandler"; | ||
import {initTRPC} from "@trpc/server"; | ||
import {observable} from "@trpc/server/observable"; | ||
import {Server} from "bun"; | ||
import { test, describe, beforeAll, afterAll, expect } from "bun:test"; | ||
import { createBunServeHandler } from "./createBunServeHandler"; | ||
import { initTRPC } from "@trpc/server"; | ||
import { observable } from "@trpc/server/observable"; | ||
import { Server } from "bun"; | ||
describe("e2e", () => { | ||
let server: Server; | ||
let server: Server; | ||
const createContext = ({req}: { req: Request }) => { | ||
return { | ||
name: req.headers.get("x-name") ?? "World", | ||
}; | ||
}; | ||
const createContext = ({ req }: { req: Request }) => { | ||
return { | ||
name: req.headers.get("x-name") ?? "World", | ||
}; | ||
}; | ||
const t = initTRPC.context<typeof createContext>().create(); | ||
const t = initTRPC.context<typeof createContext>().create(); | ||
const router = t.router({ | ||
hello: t.procedure.query(({ctx}) => `Hello ${ctx.name}!`), | ||
const router = t.router({ | ||
hello: t.procedure.query(({ ctx }) => `Hello ${ctx.name}!`), | ||
exception: t.procedure.query(() => { | ||
throw new Error("MyError"); | ||
}), | ||
exception: t.procedure.query(() => { | ||
throw new Error("MyError"); | ||
}), | ||
digits: t.procedure.subscription(() => | ||
observable<number>((subscriber) => { | ||
setTimeout(() => { | ||
subscriber.next(0); | ||
subscriber.next(1); | ||
subscriber.next(2); | ||
subscriber.error(new Error("MyError")); | ||
}, 10) | ||
}), | ||
), | ||
}); | ||
digits: t.procedure.subscription(() => | ||
observable<number>((subscriber) => { | ||
setTimeout(() => { | ||
subscriber.next(0); | ||
subscriber.next(1); | ||
subscriber.next(2); | ||
subscriber.error(new Error("MyError")); | ||
}, 10); | ||
}), | ||
), | ||
}); | ||
beforeAll(async () => { | ||
server = Bun.serve( | ||
createBunServeHandler( | ||
{ | ||
router, | ||
endpoint: "/trpc", | ||
createContext, | ||
}, | ||
{ | ||
port: 13123, | ||
fetch(request, server): Response | Promise<Response> { | ||
return new Response("Falling back to fetch"); | ||
}, | ||
}, | ||
), | ||
); | ||
}); | ||
beforeAll(async () => { | ||
server = Bun.serve( | ||
createBunServeHandler( | ||
{ | ||
router, | ||
endpoint: "/trpc", | ||
createContext, | ||
}, | ||
{ | ||
port: 13123, | ||
fetch(request, server): Response | Promise<Response> { | ||
return new Response("Falling back to fetch"); | ||
}, | ||
}, | ||
), | ||
); | ||
}); | ||
afterAll(() => server.stop()); | ||
afterAll(() => server.stop()); | ||
test("http call procedure", async () => { | ||
const response = await fetch("http://localhost:13123/trpc/hello"); | ||
expect(response.ok).toBe(true); | ||
const result = await response.json(); | ||
expect(result).toEqual({result: {data: "Hello World!"}}); | ||
}); | ||
test("http call procedure", async () => { | ||
const response = await fetch("http://localhost:13123/trpc/hello"); | ||
expect(response.ok).toBe(true); | ||
const result = await response.json(); | ||
expect(result).toEqual({ result: { data: "Hello World!" } }); | ||
}); | ||
test("http call procedure +ctx", async () => { | ||
const response = await fetch("http://localhost:13123/trpc/hello", { | ||
headers: { | ||
"x-name": "John", | ||
}, | ||
}); | ||
expect(response.ok).toBe(true); | ||
const result = await response.json(); | ||
expect(result).toEqual({result: {data: "Hello John!"}}); | ||
}); | ||
test("http call procedure +ctx", async () => { | ||
const response = await fetch("http://localhost:13123/trpc/hello", { | ||
headers: { | ||
"x-name": "John", | ||
}, | ||
}); | ||
expect(response.ok).toBe(true); | ||
const result = await response.json(); | ||
expect(result).toEqual({ result: { data: "Hello John!" } }); | ||
}); | ||
test("http call exception", async () => { | ||
const response = await fetch("http://localhost:13123/trpc/exception"); | ||
expect(response.ok).toBe(false); | ||
const result = await response.json(); | ||
expect(result).toEqual({ | ||
error: { | ||
code: -32603, | ||
message: "MyError", | ||
data: { | ||
code: "INTERNAL_SERVER_ERROR", | ||
httpStatus: 500, | ||
path: "exception", | ||
stack: expect.any(String), | ||
}, | ||
}, | ||
}); | ||
}); | ||
test("http call exception", async () => { | ||
const response = await fetch("http://localhost:13123/trpc/exception"); | ||
expect(response.ok).toBe(false); | ||
const result = await response.json(); | ||
expect(result).toEqual({ | ||
error: { | ||
code: -32603, | ||
message: "MyError", | ||
data: { | ||
code: "INTERNAL_SERVER_ERROR", | ||
httpStatus: 500, | ||
path: "exception", | ||
stack: expect.any(String), | ||
}, | ||
}, | ||
}); | ||
}); | ||
test("websocket call procedure", async () => { | ||
const ws = new WebSocket("ws://localhost:13123/trpc"); | ||
const id = Math.random(); | ||
test("websocket call procedure", async () => { | ||
const ws = new WebSocket("ws://localhost:13123/trpc"); | ||
const id = Math.random(); | ||
ws.onopen = () => { | ||
ws.send( | ||
JSON.stringify({ | ||
id, | ||
method: "query", | ||
params: { | ||
path: "hello", | ||
}, | ||
}), | ||
); | ||
}; | ||
ws.onopen = () => { | ||
ws.send( | ||
JSON.stringify({ | ||
id, | ||
method: "query", | ||
params: { | ||
path: "hello", | ||
}, | ||
}), | ||
); | ||
}; | ||
await new Promise((resolve, reject) => { | ||
ws.onmessage = (event) => { | ||
const data = JSON.parse(event.data); | ||
try { | ||
expect(data).toEqual({ | ||
id, | ||
result: { | ||
type: "data", | ||
data: "Hello World!", | ||
}, | ||
}); | ||
} finally { | ||
resolve(true); | ||
} | ||
}; | ||
}); | ||
await new Promise((resolve, reject) => { | ||
ws.onmessage = (event) => { | ||
const data = JSON.parse(event.data); | ||
try { | ||
expect(data).toEqual({ | ||
id, | ||
result: { | ||
type: "data", | ||
data: "Hello World!", | ||
}, | ||
}); | ||
} finally { | ||
resolve(true); | ||
} | ||
}; | ||
}); | ||
ws.close(); | ||
}); | ||
ws.close(); | ||
}); | ||
test("ws error", async () => { | ||
const ws = new WebSocket("ws://localhost:13123/trpc"); | ||
const id = Math.random(); | ||
test("ws error", async () => { | ||
const ws = new WebSocket("ws://localhost:13123/trpc"); | ||
const id = Math.random(); | ||
ws.onopen = () => { | ||
ws.send( | ||
JSON.stringify({ | ||
id, | ||
method: "query", | ||
params: { | ||
path: "unknown", | ||
}, | ||
}), | ||
); | ||
}; | ||
ws.onopen = () => { | ||
ws.send( | ||
JSON.stringify({ | ||
id, | ||
method: "query", | ||
params: { | ||
path: "unknown", | ||
}, | ||
}), | ||
); | ||
}; | ||
await new Promise((resolve, reject) => { | ||
ws.onmessage = (event) => { | ||
const data = JSON.parse(event.data); | ||
try { | ||
expect(data).toEqual({ | ||
id, | ||
error: { | ||
code: -32004, | ||
message: `No "query"-procedure on path "unknown"`, | ||
data: { | ||
code: "NOT_FOUND", | ||
httpStatus: 404, | ||
path: "unknown", | ||
stack: expect.any(String), | ||
} | ||
} | ||
}); | ||
} finally { | ||
resolve(true); | ||
} | ||
}; | ||
}); | ||
await new Promise((resolve, reject) => { | ||
ws.onmessage = (event) => { | ||
const data = JSON.parse(event.data); | ||
try { | ||
expect(data).toEqual({ | ||
id, | ||
error: { | ||
code: -32004, | ||
message: `No "query"-procedure on path "unknown"`, | ||
data: { | ||
code: "NOT_FOUND", | ||
httpStatus: 404, | ||
path: "unknown", | ||
stack: expect.any(String), | ||
}, | ||
}, | ||
}); | ||
} finally { | ||
resolve(true); | ||
} | ||
}; | ||
}); | ||
ws.close(); | ||
}) | ||
ws.close(); | ||
}); | ||
test("ws exception", async () => { | ||
const ws = new WebSocket("ws://localhost:13123/trpc"); | ||
const id = Math.random(); | ||
test("ws exception", async () => { | ||
const ws = new WebSocket("ws://localhost:13123/trpc"); | ||
const id = Math.random(); | ||
ws.onopen = () => { | ||
ws.send( | ||
JSON.stringify({ | ||
id, | ||
method: "query", | ||
params: { | ||
path: "exception", | ||
}, | ||
}), | ||
); | ||
}; | ||
ws.onopen = () => { | ||
ws.send( | ||
JSON.stringify({ | ||
id, | ||
method: "query", | ||
params: { | ||
path: "exception", | ||
}, | ||
}), | ||
); | ||
}; | ||
await new Promise((resolve, reject) => { | ||
ws.onmessage = (event) => { | ||
const data = JSON.parse(event.data); | ||
try { | ||
expect(data).toEqual({ | ||
id, | ||
error: { | ||
code: -32603, | ||
message: "MyError", | ||
data: { | ||
code: "INTERNAL_SERVER_ERROR", | ||
httpStatus: 500, | ||
path: "exception", | ||
stack: expect.any(String), | ||
} | ||
} | ||
}); | ||
} finally { | ||
resolve(true); | ||
} | ||
}; | ||
}); | ||
await new Promise((resolve, reject) => { | ||
ws.onmessage = (event) => { | ||
const data = JSON.parse(event.data); | ||
try { | ||
expect(data).toEqual({ | ||
id, | ||
error: { | ||
code: -32603, | ||
message: "MyError", | ||
data: { | ||
code: "INTERNAL_SERVER_ERROR", | ||
httpStatus: 500, | ||
path: "exception", | ||
stack: expect.any(String), | ||
}, | ||
}, | ||
}); | ||
} finally { | ||
resolve(true); | ||
} | ||
}; | ||
}); | ||
ws.close(); | ||
}) | ||
ws.close(); | ||
}); | ||
test( | ||
"websocket call subscription", | ||
async () => { | ||
const ws = new WebSocket("ws://localhost:13123/trpc"); | ||
test("websocket call subscription", async () => { | ||
const ws = new WebSocket("ws://localhost:13123/trpc"); | ||
const messages: unknown[] = []; | ||
const id = Math.random(); | ||
const messages: unknown[] = []; | ||
const id = Math.random(); | ||
ws.onopen = () => { | ||
ws.send( | ||
JSON.stringify({ | ||
id, | ||
method: "subscription", | ||
params: { | ||
path: "digits", | ||
}, | ||
}), | ||
); | ||
}; | ||
ws.onopen = () => { | ||
ws.send( | ||
JSON.stringify({ | ||
id, | ||
method: "subscription", | ||
params: { | ||
path: "digits", | ||
}, | ||
}), | ||
); | ||
}; | ||
ws.onmessage = (event) => { | ||
const data = JSON.parse(event.data); | ||
messages.push(data); | ||
}; | ||
ws.onmessage = (event) => { | ||
const data = JSON.parse(event.data); | ||
messages.push(data); | ||
}; | ||
await new Promise((resolve) => setTimeout(resolve, 100)); | ||
await new Promise((resolve) => setTimeout(resolve, 100)); | ||
ws.send(JSON.stringify({ | ||
id, | ||
method: "subscription.stop", | ||
})) | ||
ws.send( | ||
JSON.stringify({ | ||
id, | ||
method: "subscription.stop", | ||
}), | ||
); | ||
await new Promise((resolve) => setTimeout(resolve, 100)); | ||
await new Promise((resolve) => setTimeout(resolve, 100)); | ||
ws.close(); | ||
ws.close(); | ||
expect(messages).toEqual([ | ||
{ | ||
id, | ||
result: { | ||
type: "started", | ||
}, | ||
}, | ||
{ | ||
id, | ||
result: { | ||
type: "data", | ||
data: 0, | ||
}, | ||
}, | ||
{ | ||
id, | ||
result: { | ||
type: "data", | ||
data: 1, | ||
}, | ||
}, | ||
{ | ||
id, | ||
result: { | ||
type: "data", | ||
data: 2, | ||
}, | ||
}, | ||
{ | ||
id, | ||
error: { | ||
code: -32603, | ||
message: "MyError", | ||
data: { | ||
code: "INTERNAL_SERVER_ERROR", | ||
httpStatus: 500, | ||
path: "digits", | ||
stack: expect.any(String), | ||
}, | ||
} | ||
}, | ||
{ | ||
id, | ||
result: { | ||
type: "stopped", | ||
}, | ||
}, | ||
]); | ||
}, | ||
); | ||
expect(messages).toEqual([ | ||
{ | ||
id, | ||
result: { | ||
type: "started", | ||
}, | ||
}, | ||
{ | ||
id, | ||
result: { | ||
type: "data", | ||
data: 0, | ||
}, | ||
}, | ||
{ | ||
id, | ||
result: { | ||
type: "data", | ||
data: 1, | ||
}, | ||
}, | ||
{ | ||
id, | ||
result: { | ||
type: "data", | ||
data: 2, | ||
}, | ||
}, | ||
{ | ||
id, | ||
error: { | ||
code: -32603, | ||
message: "MyError", | ||
data: { | ||
code: "INTERNAL_SERVER_ERROR", | ||
httpStatus: 500, | ||
path: "digits", | ||
stack: expect.any(String), | ||
}, | ||
}, | ||
}, | ||
{ | ||
id, | ||
result: { | ||
type: "stopped", | ||
}, | ||
}, | ||
]); | ||
}); | ||
test("fall through to fetch", async () => { | ||
const response = await fetch("http://localhost:13123/other"); | ||
expect(response.ok).toBe(true); | ||
const result = await response.text(); | ||
expect(result).toEqual("Falling back to fetch"); | ||
}); | ||
test("fall through to fetch", async () => { | ||
const response = await fetch("http://localhost:13123/other"); | ||
expect(response.ok).toBe(true); | ||
const result = await response.text(); | ||
expect(result).toEqual("Falling back to fetch"); | ||
}); | ||
}); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
1249
1
204
70042