@trpc/server
Version:
666 lines (664 loc) • 19.1 kB
JavaScript
import { d as isObject, l as isAsyncIterable, m as run, t as TRPC_ERROR_CODES_BY_KEY } from "./codes-nbrm_zWY.mjs";
import { n as _objectSpread2, t as getErrorShape } from "./getErrorShape-B5jPKHJ3.mjs";
import { t as parseTRPCMessage } from "./parseTRPCMessage-BO1-cCsX.mjs";
import { C as getTRPCErrorFromUnknown, b as transformTRPCResponse, d as callProcedure, n as isTrackedEnvelope, x as TRPCError } from "./asyncGeneratorDelegate-JWx0LJUD.mjs";
import { a as isPromise, d as _usingCtx, h as isAbortError, l as iteratorResource, m as Unpromise, p as makeResource, x as parseConnectionParamsFromUnknown } from "./resolveResponse-C3K3zKPn.mjs";
import { r as observableToAsyncIterable, t as isObservable } from "./observable-kpFFzvou.mjs";
import { r as createURL } from "./node-http-Df_b1FfK.mjs";
//#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 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: 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" && 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 = getTRPCErrorFromUnknown(cause);
if (!signal.aborted) return error;
if (!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 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(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 (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 getErrorShape(_objectSpread2({
config: router._def._config,
ctx
}, call));
} catch (cause) {
reportInternalError(_objectSpread2(_objectSpread2({}, call), {}, {
error: getTRPCErrorFromUnknown(cause),
ctx,
req
}));
return fallbackErrorShape;
}
}
function reportConnectionError(cause) {
reportInternalError({
error: 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 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 = 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 = createURL(req).searchParams.get("connectionParams") === "1" ? null : createCtxPromise(() => null);
async function handleRequest(msg, batchIndex, ctxResult) {
const { id, jsonrpc } = msg;
if (id === null) {
const error = getTRPCErrorFromUnknown(new 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 (isObject(input)) input = _objectSpread2(_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 = 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 TRPCError({
message: `Duplicate id ${id}`,
code: "BAD_REQUEST"
}));
return;
}
const execute = async () => {
try {
var _usingCtx$1 = _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 callProcedure({
router,
path,
getRawInput: async () => input,
ctx,
type,
signal: abortController.signal,
batchIndex
});
const isIterableResult = isAsyncIterable(result) || isObservable(result);
if (type !== "subscription") {
if (isIterableResult) throw new 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 TRPCError({
message: `Subscription ${path} did not return an observable or a AsyncGenerator`,
code: "INTERNAL_SERVER_ERROR"
});
const iterable = isObservable(result) ? observableToAsyncIterable(result, abortController.signal) : result;
if (abortController.signal.aborted) {
try {
try {
var _usingCtx3 = _usingCtx();
_usingCtx3.a(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 = _usingCtx();
const iterator = _usingCtx4.a(iteratorResource(iterable));
let completed = false;
_usingCtx4.u(makeResource({}, () => {
if (!completed) abort();
}));
const abortPromise = new Promise((resolve) => {
abortController.signal.onabort = () => resolve("abort");
});
let next;
let result;
while (true) {
next = await 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 (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 = getTRPCErrorFromUnknown(outcome.error);
if (!isCancelledSetup(error, abortController.signal)) {
try {
respondWithError(error);
} catch (cause) {
reportInternalError({
error: 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 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 (!isObject(msg)) throw new Error("Message was not an object");
} catch (cause) {
throw new TRPCError({
code: "PARSE_ERROR",
message: `Malformed TRPCConnectionParamsMessage`,
cause
});
}
return parseConnectionParamsFromUnknown(msg.data);
});
return;
}
const parsedMsgs = 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 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 (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) => parseTRPCMessage(raw, transformer));
} catch (cause) {
const error = new 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: 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
export { jsonEncoder as i, getWSConnectionHandler as n, handleKeepAlive as r, applyWSSHandler as t };
//# sourceMappingURL=ws-BqEmiHOX.mjs.map