@trpc/server
Version:
436 lines (434 loc) • 12.7 kB
JavaScript
const require_getErrorShape = require('./getErrorShape-DKiEF6Zc.cjs');
const require_tracked = require('./tracked-HoF8L_mq.cjs');
const require_utils = require('./utils-BhNVZA-c.cjs');
const require_parseTRPCMessage = require('./parseTRPCMessage-snNQop7N.cjs');
const require_resolveResponse = require('./resolveResponse-CVGbakBm.cjs');
const require_observable = require('./observable-B1Nk6r1H.cjs');
const require_node_http = require('./node-http-kIQEhZUH.cjs');
//#region src/adapters/ws.ts
var import_objectSpread2 = require_getErrorShape.__toESM(require_getErrorShape.require_objectSpread2(), 1);
var import_usingCtx = require_getErrorShape.__toESM(require_resolveResponse.require_usingCtx(), 1);
/**
* Importing ws causes a build error
* @see https://github.com/trpc/trpc/pull/5279
*/
const WEBSOCKET_OPEN = 1;
function getWSConnectionHandler(opts) {
const { createContext, router } = opts;
const { transformer } = router._def._config;
return (client, req) => {
var _opts$keepAlive;
const clientSubscriptions = /* @__PURE__ */ new Map();
const abortController = new AbortController();
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(JSON.stringify(require_tracked.transformTRPCResponse(router._def._config, untransformedJSON)));
}
async function createCtxPromise(getConnectionParams) {
try {
return await require_utils.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 _opts$onError, _globalThis$setImmedi;
const error = require_tracked.getTRPCErrorFromUnknown(cause);
(_opts$onError = opts.onError) === null || _opts$onError === void 0 || _opts$onError.call(opts, {
error,
path: void 0,
type: "unknown",
ctx,
req,
input: void 0
});
respond({
id: null,
error: require_getErrorShape.getErrorShape({
config: router._def._config,
error,
type: "unknown",
path: void 0,
input: void 0,
ctx
})
});
((_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);
function handleRequest(msg) {
const { id, jsonrpc } = msg;
if (id === null) {
var _opts$onError2;
const error = require_tracked.getTRPCErrorFromUnknown(new require_tracked.TRPCError({
code: "PARSE_ERROR",
message: "`id` is required"
}));
(_opts$onError2 = opts.onError) === null || _opts$onError2 === void 0 || _opts$onError2.call(opts, {
error,
path: void 0,
type: "unknown",
ctx,
req,
input: void 0
});
respond({
id,
jsonrpc,
error: require_getErrorShape.getErrorShape({
config: router._def._config,
error,
type: "unknown",
path: void 0,
input: void 0,
ctx
})
});
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_utils.isObject(input)) input = (0, import_objectSpread2.default)((0, import_objectSpread2.default)({}, input), {}, { lastEventId });
else {
var _input;
(_input = input) !== null && _input !== void 0 || (input = { lastEventId });
}
require_utils.run(async () => {
const res = await ctxPromise;
if (!res.ok) throw res.error;
const abortController$1 = new AbortController();
const result = await require_tracked.callProcedure({
router,
path,
getRawInput: async () => input,
ctx,
type,
signal: abortController$1.signal
});
const isIterableResult = require_utils.isAsyncIterable(result) || require_observable.isObservable(result);
if (type !== "subscription") {
if (isIterableResult) throw new require_tracked.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_tracked.TRPCError({
message: `Subscription ${path} did not return an observable or a AsyncGenerator`,
code: "INTERNAL_SERVER_ERROR"
});
/* istanbul ignore next -- @preserve */
if (client.readyState !== WEBSOCKET_OPEN) return;
/* istanbul ignore next -- @preserve */
if (clientSubscriptions.has(id)) throw new require_tracked.TRPCError({
message: `Duplicate id ${id}`,
code: "BAD_REQUEST"
});
const iterable = require_observable.isObservable(result) ? require_observable.observableToAsyncIterable(result, abortController$1.signal) : result;
require_utils.run(async () => {
try {
var _usingCtx = (0, import_usingCtx.default)();
const iterator = _usingCtx.a(require_resolveResponse.iteratorResource(iterable));
const abortPromise = new Promise((resolve) => {
abortController$1.signal.onabort = () => resolve("abort");
});
let next;
let result$1;
while (true) {
next = await require_resolveResponse.Unpromise.race([iterator.next().catch(require_tracked.getTRPCErrorFromUnknown), abortPromise]);
if (next === "abort") {
var _iterator$return;
await ((_iterator$return = iterator.return) === null || _iterator$return === void 0 ? void 0 : _iterator$return.call(iterator));
break;
}
if (next instanceof Error) {
var _opts$onError3;
const error = require_tracked.getTRPCErrorFromUnknown(next);
(_opts$onError3 = opts.onError) === null || _opts$onError3 === void 0 || _opts$onError3.call(opts, {
error,
path,
type,
ctx,
req,
input
});
respond({
id,
jsonrpc,
error: require_getErrorShape.getErrorShape({
config: router._def._config,
error,
type,
path,
input,
ctx
})
});
break;
}
if (next.done) break;
result$1 = {
type: "data",
data: next.value
};
if (require_tracked.isTrackedEnvelope(next.value)) {
const [id$1, data] = next.value;
result$1.id = id$1;
result$1.data = {
id: id$1,
data
};
}
respond({
id,
jsonrpc,
result: result$1
});
next = null;
result$1 = null;
}
respond({
id,
jsonrpc,
result: { type: "stopped" }
});
clientSubscriptions.delete(id);
} catch (_) {
_usingCtx.e = _;
} finally {
await _usingCtx.d();
}
}).catch((cause) => {
var _opts$onError4;
const error = require_tracked.getTRPCErrorFromUnknown(cause);
(_opts$onError4 = opts.onError) === null || _opts$onError4 === void 0 || _opts$onError4.call(opts, {
error,
path,
type,
ctx,
req,
input
});
respond({
id,
jsonrpc,
error: require_getErrorShape.getErrorShape({
config: router._def._config,
error,
type,
path,
input,
ctx
})
});
abortController$1.abort();
});
clientSubscriptions.set(id, abortController$1);
respond({
id,
jsonrpc,
result: { type: "started" }
});
}).catch((cause) => {
var _opts$onError5;
const error = require_tracked.getTRPCErrorFromUnknown(cause);
(_opts$onError5 = opts.onError) === null || _opts$onError5 === void 0 || _opts$onError5.call(opts, {
error,
path,
type,
ctx,
req,
input
});
respond({
id,
jsonrpc,
error: require_getErrorShape.getErrorShape({
config: router._def._config,
error,
type,
path,
input,
ctx
})
});
});
}
client.on("message", (rawData) => {
const msgStr = rawData.toString();
if (msgStr === "PONG") return;
if (msgStr === "PING") {
if (!opts.dangerouslyDisablePong) client.send("PONG");
return;
}
if (!ctxPromise) {
ctxPromise = createCtxPromise(() => {
let msg;
try {
msg = JSON.parse(msgStr);
if (!require_utils.isObject(msg)) throw new Error("Message was not an object");
} catch (cause) {
throw new require_tracked.TRPCError({
code: "PARSE_ERROR",
message: `Malformed TRPCConnectionParamsMessage`,
cause
});
}
const connectionParams = require_resolveResponse.parseConnectionParamsFromUnknown(msg.data);
return connectionParams;
});
return;
}
const parsedMsgs = require_utils.run(() => {
try {
const msgJSON = JSON.parse(msgStr);
const msgs = Array.isArray(msgJSON) ? msgJSON : [msgJSON];
return msgs.map((raw) => require_parseTRPCMessage.parseTRPCMessage(raw, transformer));
} catch (cause) {
const error = new require_tracked.TRPCError({
code: "PARSE_ERROR",
cause
});
respond({
id: null,
error: require_getErrorShape.getErrorShape({
config: router._def._config,
error,
type: "unknown",
path: void 0,
input: void 0,
ctx
})
});
return [];
}
});
parsedMsgs.map(handleRequest);
});
client.on("error", (cause) => {
var _opts$onError6;
(_opts$onError6 = opts.onError) === null || _opts$onError6 === void 0 || _opts$onError6.call(opts, {
ctx,
error: require_tracked.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();
});
};
}
/**
* Handle WebSocket keep-alive messages
*/
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();
}
function applyWSSHandler(opts) {
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 response = {
id: null,
method: "reconnect"
};
const data = JSON.stringify(response);
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;
}
});