UNPKG

@trpc/server

Version:

The tRPC server library

688 lines (686 loc) • 20.4 kB
const require_codes = require("./codes-Be-DrYMh.cjs"); const require_getErrorShape = require("./getErrorShape-BAuHhNuh.cjs"); const require_parseTRPCMessage = require("./parseTRPCMessage-BsPr5reh.cjs"); const require_asyncGeneratorDelegate = require("./asyncGeneratorDelegate-CMdmcplC.cjs"); const require_resolveResponse = require("./resolveResponse-CoiQ9diV.cjs"); const require_observable = require("./observable-1zKJIeLl.cjs"); const require_node_http = require("./node-http-vS2eeV-2.cjs"); //#region src/adapters/wsEncoder.ts /** * The default encoder: JSON in text frames. */ const jsonEncoder = { encode: (data) => JSON.stringify(data), decode: (data) => { if (typeof data !== "string") throw new Error("jsonEncoder received binary data. JSON uses text frames. Use a binary encoder for binary data."); return JSON.parse(data); } }; //#endregion //#region src/adapters/ws.ts /** * Importing ws causes a build error * @see https://github.com/trpc/trpc/pull/5279 */ const WEBSOCKET_OPEN = 1; function settle(promise) { return promise.then((value) => ({ ok: true, value }), (error) => ({ ok: false, error })); } /** * What `await using` rejects with when disposal fails after the scope threw: * `error` is the disposal failure and `suppressed` the original error. */ function isSuppressedError(cause) { return require_codes.isObject(cause) && cause["name"] === "SuppressedError" && "error" in cause && "suppressed" in cause; } /** * What the client gets when its error can't be formatted or encoded. It says * nothing about the error, which is what `errorFormatter` would have hidden. */ const fallbackErrorShape = { code: require_codes.TRPC_ERROR_CODES_BY_KEY.INTERNAL_SERVER_ERROR, message: "Internal server error", data: { code: "INTERNAL_SERVER_ERROR", httpStatus: 500 } }; /** * A procedure that gives up because its subscription was cancelled rejects * with an `AbortError`, which is wrapped as an INTERNAL_SERVER_ERROR. */ function isCancelledSetup(error, signal) { return signal.aborted && error.code === "INTERNAL_SERVER_ERROR" && require_resolveResponse.isAbortError(error.cause); } /** * `iterator.next()` for a subscription loop that races it against the * subscription's `signal`, with a rejection turned into a `TRPCError`. * * Once the subscription is stopped, the loop doesn't wait for this call, so a * rejection after the stop, such as a teardown that throws, goes to * `onErrorAfterStop` and resolves `'abort'`. An `AbortError` is how a stopped * subscription is expected to end, and isn't reported. */ function nextUnlessStopped(iterator, signal, onErrorAfterStop) { return iterator.next().catch((cause) => { const error = require_asyncGeneratorDelegate.getTRPCErrorFromUnknown(cause); if (!signal.aborted) return error; if (!require_resolveResponse.isAbortError(error.cause)) onErrorAfterStop(error); return "abort"; }); } /** * Returns a `ws` connection listener that serves a router. `applyWSSHandler` * uses it; call it to attach tRPC to your own WebSocket server. */ function getWSConnectionHandler(opts) { var _opts$experimental_en; const { createContext, router } = opts; const { transformer } = router._def._config; const encoder = (_opts$experimental_en = opts.experimental_encoder) !== null && _opts$experimental_en !== void 0 ? _opts$experimental_en : jsonEncoder; return (client, req) => { var _opts$keepAlive; const clientSubscriptions = /* @__PURE__ */ new Map(); const abortController = new AbortController(); function registerSubscription(id, subscription) { clientSubscriptions.set(id, subscription); return require_resolveResponse.makeResource({}, () => { clientSubscriptions.delete(id); }); } if ((_opts$keepAlive = opts.keepAlive) === null || _opts$keepAlive === void 0 ? void 0 : _opts$keepAlive.enabled) { const { pingMs, pongWaitMs } = opts.keepAlive; handleKeepAlive(client, pingMs, pongWaitMs); } function respond(untransformedJSON) { client.send(encoder.encode(require_asyncGeneratorDelegate.transformTRPCResponse(router._def._config, untransformedJSON))); } /** * `onError` runs in listeners and in promises nothing awaits, where a * throw or a rejection would crash the process, so either goes to * `onFailure` instead. */ function guardOnError(info, onFailure) { try { var _opts$onError; const result = (_opts$onError = opts.onError) === null || _opts$onError === void 0 ? void 0 : _opts$onError.call(opts, info); if (require_resolveResponse.isPromise(result)) result.catch(onFailure); } catch (cause) { onFailure(cause); } } /** Reports an error to `onError`, ignoring an `onError` that fails. */ function callOnError(info) { guardOnError(info, () => {}); } /** * Reports a failure of the adapter's own error handling, such as an * `errorFormatter` that throws or a reply that can't be sent. It's logged * when `onError` is missing or fails, so it's never dropped silently. */ function reportInternalError(info) { const log = () => { console.error("[@trpc/server/adapters/ws]", info.error); }; if (!opts.onError) { log(); return; } guardOnError(info, log); } /** * The error shape the client gets, or a minimal one when `errorFormatter` * throws, so that the request is still answered. */ function errorShape(call) { try { return require_getErrorShape.getErrorShape(require_getErrorShape._objectSpread2({ config: router._def._config, ctx }, call)); } catch (cause) { reportInternalError(require_getErrorShape._objectSpread2(require_getErrorShape._objectSpread2({}, call), {}, { error: require_asyncGeneratorDelegate.getTRPCErrorFromUnknown(cause), ctx, req })); return fallbackErrorShape; } } function reportConnectionError(cause) { reportInternalError({ error: require_asyncGeneratorDelegate.getTRPCErrorFromUnknown(cause), path: void 0, type: "unknown", ctx, req, input: void 0 }); } /** * Sends a response that isn't tied to a request, reporting it if it * can't be built or sent. One that can't be sent is replaced by the * fallback error, so that a client still learns it was rejected. */ function respondOrReport(response) { let message = null; try { message = response(); respond(message); } catch (cause) { reportConnectionError(cause); if (message === null) return; try { respond({ id: message.id, jsonrpc: message.jsonrpc, error: fallbackErrorShape }); } catch (_unused) {} } } async function createCtxPromise(getConnectionParams) { try { return await require_codes.run(async () => { ctx = await (createContext === null || createContext === void 0 ? void 0 : createContext({ req, res: client, info: { connectionParams: getConnectionParams(), calls: [], isBatchCall: false, accept: null, type: "unknown", signal: abortController.signal, url: null } })); return { ok: true, value: ctx }; }); } catch (cause) { var _globalThis$setImmedi; const error = require_asyncGeneratorDelegate.getTRPCErrorFromUnknown(cause); callOnError({ error, path: void 0, type: "unknown", ctx, req, input: void 0 }); respondOrReport(() => ({ id: null, error: errorShape({ error, type: "unknown", path: void 0, input: void 0 }) })); ((_globalThis$setImmedi = globalThis.setImmediate) !== null && _globalThis$setImmedi !== void 0 ? _globalThis$setImmedi : globalThis.setTimeout)(() => { client.close(); }); return { ok: false, error }; } } let ctx = void 0; /** * promise for initializing the context * * - the context promise will be created immediately on connection if no connectionParams are expected * - if connection params are expected, they will be created once received */ let ctxPromise = require_node_http.createURL(req).searchParams.get("connectionParams") === "1" ? null : createCtxPromise(() => null); async function handleRequest(msg, batchIndex, ctxResult) { const { id, jsonrpc } = msg; if (id === null) { const error = require_asyncGeneratorDelegate.getTRPCErrorFromUnknown(new require_asyncGeneratorDelegate.TRPCError({ code: "PARSE_ERROR", message: "`id` is required" })); callOnError({ error, path: void 0, type: "unknown", ctx, req, input: void 0 }); respond({ id, jsonrpc, error: errorShape({ error, type: "unknown", path: void 0, input: void 0 }) }); return; } if (msg.method === "subscription.stop") { var _clientSubscriptions$; (_clientSubscriptions$ = clientSubscriptions.get(id)) === null || _clientSubscriptions$ === void 0 || _clientSubscriptions$.abort(); return; } const { path, lastEventId } = msg.params; let { input } = msg.params; const type = msg.method; if (lastEventId !== void 0) { if (require_codes.isObject(input)) input = require_getErrorShape._objectSpread2(require_getErrorShape._objectSpread2({}, input), {}, { lastEventId }); else { var _input; (_input = input) !== null && _input !== void 0 || (input = { lastEventId }); } } const abortController = new AbortController(); const abort = () => { if (!abortController.signal.aborted) abortController.abort(); }; const reportError = (cause) => { const error = require_asyncGeneratorDelegate.getTRPCErrorFromUnknown(cause); callOnError({ error, path, type, ctx, req, input }); return error; }; const respondWithError = (cause) => { const error = reportError(cause); respond({ id, jsonrpc, error: errorShape({ error, type, path, input }) }); }; if (type === "subscription" && clientSubscriptions.has(id)) { respondWithError(new require_asyncGeneratorDelegate.TRPCError({ message: `Duplicate id ${id}`, code: "BAD_REQUEST" })); return; } const execute = async () => { try { var _usingCtx$1 = require_resolveResponse._usingCtx(); _usingCtx$1.u(type === "subscription" ? registerSubscription(id, abortController) : null); const res = await ctxResult; if (!res.ok) throw res.error; if (abortController.signal.aborted) return; const result = await require_asyncGeneratorDelegate.callProcedure({ router, path, getRawInput: async () => input, ctx, type, signal: abortController.signal, batchIndex }); const isIterableResult = require_codes.isAsyncIterable(result) || require_observable.isObservable(result); if (type !== "subscription") { if (isIterableResult) throw new require_asyncGeneratorDelegate.TRPCError({ code: "UNSUPPORTED_MEDIA_TYPE", message: `Cannot return an async iterable or observable from a ${type} procedure with WebSockets` }); respond({ id, jsonrpc, result: { type: "data", data: result } }); return; } if (!isIterableResult) throw new require_asyncGeneratorDelegate.TRPCError({ message: `Subscription ${path} did not return an observable or a AsyncGenerator`, code: "INTERNAL_SERVER_ERROR" }); const iterable = require_observable.isObservable(result) ? require_observable.observableToAsyncIterable(result, abortController.signal) : result; if (abortController.signal.aborted) { try { try { var _usingCtx3 = require_resolveResponse._usingCtx(); _usingCtx3.a(require_resolveResponse.iteratorResource(iterable)); } catch (_) { _usingCtx3.e = _; } finally { await _usingCtx3.d(); } } catch (cause) { reportError(cause); } return; } respond({ id, jsonrpc, result: { type: "started" } }); let ended = false; try { try { var _usingCtx4 = require_resolveResponse._usingCtx(); const iterator = _usingCtx4.a(require_resolveResponse.iteratorResource(iterable)); let completed = false; _usingCtx4.u(require_resolveResponse.makeResource({}, () => { if (!completed) abort(); })); const abortPromise = new Promise((resolve) => { abortController.signal.onabort = () => resolve("abort"); }); let next; let result; while (true) { next = await require_resolveResponse.Unpromise.race([nextUnlessStopped(iterator, abortController.signal, (error) => callOnError({ error, path, type, ctx, req, input })), abortPromise]); if (next === "abort") break; if (next instanceof Error) throw next; if (next.done) { completed = true; break; } result = { type: "data", data: next.value }; if (require_asyncGeneratorDelegate.isTrackedEnvelope(next.value)) { const [id, data] = next.value; result.id = id; result.data = { id, data }; } respond({ id, jsonrpc, result }); next = null; result = null; } ended = true; } catch (_) { _usingCtx4.e = _; } finally { await _usingCtx4.d(); } } catch (cause) { abort(); if (ended) reportError(cause); else if (isSuppressedError(cause)) { respondWithError(cause.suppressed); reportError(cause.error); } else respondWithError(cause); } } catch (_) { _usingCtx$1.e = _; } finally { _usingCtx$1.d(); } }; const outcome = await settle(execute()); if (!outcome.ok) { const error = require_asyncGeneratorDelegate.getTRPCErrorFromUnknown(outcome.error); if (!isCancelledSetup(error, abortController.signal)) { try { respondWithError(error); } catch (cause) { reportInternalError({ error: require_asyncGeneratorDelegate.getTRPCErrorFromUnknown(cause), path, type, ctx, req, input }); respondOrReport(() => ({ id, jsonrpc, error: fallbackErrorShape })); } return; } } if (type === "subscription") respond({ id, jsonrpc, result: { type: "stopped" } }); } client.on("message", (rawData, isBinary) => { if (!isBinary) { const msgStr = rawData.toString(); if (msgStr === "PONG") return; if (msgStr === "PING") { if (!opts.dangerouslyDisablePong) client.send("PONG"); return; } } if (!Buffer.isBuffer(rawData)) { const error = new require_asyncGeneratorDelegate.TRPCError({ code: "UNPROCESSABLE_CONTENT", message: "Unexpected WebSocket message format" }); respondOrReport(() => ({ id: null, error: errorShape({ error, type: "unknown", path: void 0, input: void 0 }) })); return; } const data = isBinary ? rawData : rawData.toString("utf8"); if (!ctxPromise) { ctxPromise = createCtxPromise(() => { let msg; try { msg = encoder.decode(data); if (!require_codes.isObject(msg)) throw new Error("Message was not an object"); } catch (cause) { throw new require_asyncGeneratorDelegate.TRPCError({ code: "PARSE_ERROR", message: `Malformed TRPCConnectionParamsMessage`, cause }); } return require_resolveResponse.parseConnectionParamsFromUnknown(msg.data); }); return; } const parsedMsgs = require_codes.run(() => { try { const msgJSON = encoder.decode(data); const msgs = Array.isArray(msgJSON) ? msgJSON : [msgJSON]; if (Array.isArray(msgJSON) && typeof opts.maxBatchSize === "number" && msgs.length > opts.maxBatchSize) { const error = new require_asyncGeneratorDelegate.TRPCError({ code: "BAD_REQUEST", message: `Batch call exceeds maximum size` }); callOnError({ error, path: void 0, type: "unknown", ctx, req, input: void 0 }); const shape = errorShape({ error, type: "unknown", path: void 0, input: void 0 }); let responded = false; for (const raw of msgs) if (require_codes.isObject(raw) && raw["method"] !== "subscription.stop" && (typeof raw["id"] === "number" || typeof raw["id"] === "string")) { const id = raw["id"]; const jsonrpc = raw["jsonrpc"] === "2.0" ? "2.0" : void 0; respondOrReport(() => ({ id, jsonrpc, error: shape })); responded = true; } if (!responded) respondOrReport(() => ({ id: null, error: shape })); return []; } return msgs.map((raw) => require_parseTRPCMessage.parseTRPCMessage(raw, transformer)); } catch (cause) { const error = new require_asyncGeneratorDelegate.TRPCError({ code: "PARSE_ERROR", cause }); respondOrReport(() => ({ id: null, error: errorShape({ error, type: "unknown", path: void 0, input: void 0 }) })); return []; } }); for (const [index, msg] of parsedMsgs.entries()) handleRequest(msg, index, ctxPromise).catch(reportConnectionError); }); client.on("error", (cause) => { callOnError({ ctx, error: require_asyncGeneratorDelegate.getTRPCErrorFromUnknown(cause), input: void 0, path: void 0, type: "unknown", req }); }); client.once("close", () => { for (const sub of clientSubscriptions.values()) sub.abort(); clientSubscriptions.clear(); abortController.abort(); }); }; } /** * Pings a client after `pingMs` of quiet, and closes the connection if no * message comes within `pongWaitMs`. */ function handleKeepAlive(client, pingMs = 3e4, pongWaitMs = 5e3) { let timeout = void 0; let ping = void 0; const schedulePing = () => { const scheduleTimeout = () => { timeout = setTimeout(() => { client.terminate(); }, pongWaitMs); }; ping = setTimeout(() => { client.send("PING"); scheduleTimeout(); }, pingMs); }; const onMessage = () => { clearTimeout(ping); clearTimeout(timeout); schedulePing(); }; client.on("message", onMessage); client.on("close", () => { clearTimeout(ping); clearTimeout(timeout); }); schedulePing(); } /** * Serves a router on a `ws` WebSocket server. Returns * `broadcastReconnectNotification()`, which tells every client to * reconnect, for example before a deploy. * @example * ```ts * const wss = new WebSocketServer({ port: 3001 }); * const handler = applyWSSHandler({ wss, router: appRouter, createContext }); * process.on('SIGTERM', () => { * handler.broadcastReconnectNotification(); * wss.close(); * }); * ``` * @see https://trpc.io/docs/server/websockets#creating-a-websocket-server */ function applyWSSHandler(opts) { var _opts$experimental_en2; const encoder = (_opts$experimental_en2 = opts.experimental_encoder) !== null && _opts$experimental_en2 !== void 0 ? _opts$experimental_en2 : jsonEncoder; const onConnection = getWSConnectionHandler(opts); opts.wss.on("connection", (client, req) => { var _req$url; if (opts.prefix && !((_req$url = req.url) === null || _req$url === void 0 ? void 0 : _req$url.startsWith(opts.prefix))) return; onConnection(client, req); }); return { broadcastReconnectNotification: () => { const data = encoder.encode({ id: null, method: "reconnect" }); for (const client of opts.wss.clients) if (client.readyState === WEBSOCKET_OPEN) client.send(data); } }; } //#endregion Object.defineProperty(exports, "applyWSSHandler", { enumerable: true, get: function() { return applyWSSHandler; } }); Object.defineProperty(exports, "getWSConnectionHandler", { enumerable: true, get: function() { return getWSConnectionHandler; } }); Object.defineProperty(exports, "handleKeepAlive", { enumerable: true, get: function() { return handleKeepAlive; } }); Object.defineProperty(exports, "jsonEncoder", { enumerable: true, get: function() { return jsonEncoder; } });