trpc-uwebsockets
Version:
tRPC adapter for uWebSockets.js server
513 lines • 20.4 kB
JavaScript
;
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
if (value !== null && value !== void 0) {
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
var dispose, inner;
if (async) {
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
dispose = value[Symbol.asyncDispose];
}
if (dispose === void 0) {
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
dispose = value[Symbol.dispose];
if (async) inner = dispose;
}
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
env.stack.push({ value: value, dispose: dispose, async: async });
}
else if (async) {
env.stack.push({ async: true });
}
return value;
};
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
return function (env) {
function fail(e) {
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
env.hasError = true;
}
var r, s = 0;
function next() {
while (r = env.stack.pop()) {
try {
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
if (r.dispose) {
var result = r.dispose.call(r.value);
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
}
else s |= 1;
}
catch (e) {
fail(e);
}
}
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
if (env.hasError) throw env.error;
}
return next();
};
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
});
Object.defineProperty(exports, "__esModule", { value: true });
exports.getWSConnectionHandler = getWSConnectionHandler;
exports.applyWebsocketHandler = applyWebsocketHandler;
exports.handleKeepAlive = handleKeepAlive;
const server_1 = require("@trpc/server");
const http_1 = require("@trpc/server/http");
const observable_1 = require("@trpc/server/observable");
const rpc_1 = require("@trpc/server/rpc");
const unstable_core_do_not_import_1 = require("@trpc/server/unstable-core-do-not-import");
const fetchCompat_1 = require("./fetchCompat");
function createCompleter() {
let resolve;
let reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
return {
promise,
resolve: resolve,
reject: reject,
};
}
function getWSConnectionHandler(opts, allClients) {
const { createContext, router } = opts;
const { transformer } = router._def._config;
function respond(client, untransformedJSON) {
try {
client.send(JSON.stringify((0, server_1.transformTRPCResponse)(router._def._config, untransformedJSON)));
}
catch {
// client.send can throw if connection is already closed.
// happens when client forcefully terminates the connection
// and server is sending keepalive messages
}
}
function getConnectionParams(msgStr) {
let msg;
try {
msg = JSON.parse(msgStr);
if (!(0, unstable_core_do_not_import_1.isObject)(msg)) {
throw new Error('Message was not an object');
}
}
catch (cause) {
throw new server_1.TRPCError({
code: 'PARSE_ERROR',
message: `Malformed TRPCConnectionParamsMessage`,
cause,
});
}
const connectionParams = (0, http_1.parseConnectionParamsFromUnknown)(msg.data);
return connectionParams;
}
async function handleRequest(client, msg, batchIndex) {
const { clientSubscriptions, ctx, req } = client.getUserData();
const { id, jsonrpc } = msg;
/* istanbul ignore next -- @preserve */
if (id === null) {
throw new server_1.TRPCError({
code: 'BAD_REQUEST',
message: '`id` is required',
});
}
if (msg.method === 'subscription.stop') {
clientSubscriptions.get(id)?.abort();
return;
}
const { path, lastEventId } = msg.params;
let { input } = msg.params;
const type = msg.method;
try {
if (lastEventId !== undefined) {
if ((0, unstable_core_do_not_import_1.isObject)(input)) {
input = {
...input,
lastEventId: lastEventId,
};
}
else {
input ?? (input = {
lastEventId: lastEventId,
});
}
}
if (ctx === null) {
throw new Error('assertion: context should never be null');
}
const abortController = new AbortController();
const result = await (0, server_1.callTRPCProcedure)({
router,
path,
getRawInput: async () => input,
ctx,
type,
signal: abortController.signal,
batchIndex,
});
const isIterableResult = (0, unstable_core_do_not_import_1.isAsyncIterable)(result) || (0, observable_1.isObservable)(result);
if (type !== 'subscription') {
if (isIterableResult) {
throw new server_1.TRPCError({
code: 'UNSUPPORTED_MEDIA_TYPE',
message: `Cannot return an async iterable or observable from a ${type} procedure with WebSockets`,
});
}
// send the value as data if the method is not a subscription
respond(client, {
id,
jsonrpc,
result: {
type: 'data',
data: result,
},
});
return;
}
if (!isIterableResult) {
throw new server_1.TRPCError({
message: `Subscription ${path} did not return an observable or a AsyncGenerator`,
code: 'INTERNAL_SERVER_ERROR',
});
}
/* istanbul ignore next -- @preserve */
if (clientSubscriptions.has(id)) {
// duplicate request ids for client
throw new server_1.TRPCError({
message: `Duplicate id ${id}`,
code: 'BAD_REQUEST',
});
}
const iterable = (0, observable_1.isObservable)(result)
? (0, observable_1.observableToAsyncIterable)(result, abortController.signal)
: result;
(0, unstable_core_do_not_import_1.run)(async () => {
const env_1 = { stack: [], error: void 0, hasError: false };
try {
const iterator = __addDisposableResource(env_1, (0, unstable_core_do_not_import_1.iteratorResource)(iterable), true);
const abortPromise = new Promise((resolve) => {
abortController.signal.onabort = () => resolve('abort');
});
// We need those declarations outside the loop for garbage collection reasons. If they
// were declared inside, they would not be freed until the next value is present.
let next;
let result;
while (true) {
next = await unstable_core_do_not_import_1.Unpromise.race([
iterator.next().catch(server_1.getTRPCErrorFromUnknown),
abortPromise,
]);
if (next === 'abort') {
await iterator.return?.();
break;
}
if (next instanceof Error) {
const error = (0, server_1.getTRPCErrorFromUnknown)(next);
opts.onError?.({ error, path, type, ctx, req, input });
respond(client, {
id,
jsonrpc,
error: (0, server_1.getErrorShape)({
config: router._def._config,
error,
type,
path,
input,
ctx,
}),
});
break;
}
if (next.done) {
break;
}
result = {
type: 'data',
data: next.value,
};
if ((0, server_1.isTrackedEnvelope)(next.value)) {
const [id, data] = next.value;
result.id = id;
result.data = {
id,
data,
};
}
respond(client, {
id,
jsonrpc,
result,
});
// free up references for garbage collection
next = null;
result = null;
}
respond(client, {
id,
jsonrpc,
result: {
type: 'stopped',
},
});
clientSubscriptions.delete(id);
}
catch (e_1) {
env_1.error = e_1;
env_1.hasError = true;
}
finally {
const result_1 = __disposeResources(env_1);
if (result_1)
await result_1;
}
}).catch((cause) => {
const error = (0, server_1.getTRPCErrorFromUnknown)(cause);
opts.onError?.({ error, path, type, ctx, req, input });
respond(client, {
id,
jsonrpc,
error: (0, server_1.getErrorShape)({
config: router._def._config,
error,
type,
path,
input,
ctx,
}),
});
abortController.abort();
});
clientSubscriptions.set(id, abortController);
respond(client, {
id,
jsonrpc,
result: {
type: 'started',
},
});
}
catch (cause) /* istanbul ignore next -- @preserve */ {
// procedure threw an error
const error = (0, server_1.getTRPCErrorFromUnknown)(cause);
opts.onError?.({ error, path, type, ctx, req, input });
respond(client, {
id,
jsonrpc,
error: (0, server_1.getErrorShape)({
config: router._def._config,
error,
type,
path,
input,
ctx,
}),
});
}
}
return {
sendPingsAutomatically: opts.uWsBehaviorOptions?.sendPingsAutomatically, // could this be enabled?
closeOnBackpressureLimit: opts.uWsBehaviorOptions?.closeOnBackpressureLimit,
compression: opts.uWsBehaviorOptions?.compression,
maxBackpressure: opts.uWsBehaviorOptions?.maxBackpressure,
maxPayloadLength: opts.uWsBehaviorOptions?.maxPayloadLength,
maxLifetime: opts.uWsBehaviorOptions?.maxLifetime,
idleTimeout: opts.uWsBehaviorOptions?.idleTimeout,
upgrade(res, req, context) {
const resDecorated = (0, fetchCompat_1.decorateHttpResponse)(res);
res.onAborted(() => {
resDecorated.aborted = true;
});
const reqFetch = (0, fetchCompat_1.uWsToRequestNoBody)(req, resDecorated);
const secWebSocketKey = req.getHeader('sec-websocket-key');
const secWebSocketProtocol = req.getHeader('sec-websocket-protocol');
const secWebSocketExtensions = req.getHeader('sec-websocket-extensions');
const clientSubscriptions = new Map();
const abortController = new AbortController();
const data = {
clientSubscriptions,
abortController,
req: reqFetch,
ctx: undefined,
ctxCompleter: null,
keepAlive: null,
url: (0, fetchCompat_1.createURL)(req, resDecorated.sll ? 'wss' : 'ws'),
};
res.upgrade(data, secWebSocketKey, secWebSocketProtocol, secWebSocketExtensions, context);
},
async open(client) {
allClients.add(client);
if (opts.keepAlive?.enabled) {
const { pingMs, pongWaitMs } = opts.keepAlive;
const data = client.getUserData();
data.keepAlive = handleKeepAlive(client, pingMs, pongWaitMs);
}
},
async message(client, rawMsg) {
const data = client.getUserData();
if (data.keepAlive) {
data.keepAlive.onMessage();
}
const msgStr = Buffer.from(rawMsg).toString();
if (msgStr === 'PONG') {
return;
}
if (msgStr === 'PING') {
if (!opts.dangerouslyDisablePong) {
client.send('PONG');
}
return;
}
if (data.ctxCompleter == null) {
data.ctxCompleter = createCompleter();
const useConnectionParams = data.url.searchParams.get('connectionParams') === '1';
try {
data.ctx = await createContext?.({
req: data.req,
res: client,
client: client,
info: {
connectionParams: useConnectionParams
? getConnectionParams(msgStr)
: null,
calls: [],
isBatchCall: false,
accept: null,
type: 'unknown',
signal: data.abortController.signal,
url: data.url,
},
});
data.ctxCompleter.resolve();
}
catch (cause) {
const error = (0, server_1.getTRPCErrorFromUnknown)(cause);
opts.onError?.({
ctx: data.ctx,
error: error,
input: undefined,
path: undefined,
type: 'unknown',
req: data.req,
});
respond(client, {
id: null,
error: (0, server_1.getErrorShape)({
config: router._def._config,
error,
type: 'unknown',
path: undefined,
input: undefined,
ctx: data.ctx,
}),
});
data.ctxCompleter.reject(error);
// close in next tick
(globalThis.setImmediate ?? globalThis.setTimeout)(() => {
client.end(1008);
});
}
if (useConnectionParams) {
// fully consume first message
return;
}
}
try {
await data.ctxCompleter.promise;
}
catch {
// stop execution of pending requests when context could not be resolved
// single error message will be sent
return;
}
try {
const msgJSON = JSON.parse(msgStr);
const msgs = Array.isArray(msgJSON) ? msgJSON : [msgJSON];
const promises = msgs
.map((raw) => (0, rpc_1.parseTRPCMessage)(raw, transformer))
.map((msg, index) => {
return handleRequest(client, msg, index);
});
await Promise.all(promises);
}
catch (cause) {
const error = new server_1.TRPCError({
code: 'PARSE_ERROR',
cause,
});
respond(client, {
id: null,
error: (0, server_1.getErrorShape)({
config: router._def._config,
error,
type: 'unknown',
path: undefined,
input: undefined,
ctx: undefined,
}),
});
}
},
close(client) {
const { clientSubscriptions, abortController, keepAlive } = client.getUserData();
if (keepAlive) {
keepAlive.onClose();
}
for (const sub of clientSubscriptions.values()) {
sub.abort();
}
clientSubscriptions.clear();
abortController.abort();
allClients.delete(client);
},
};
}
function applyWebsocketHandler(app, opts) {
const allClients = new Set();
const behavior = getWSConnectionHandler(opts, allClients);
const prefix = opts.prefix ?? '';
app.ws(prefix, behavior);
return {
broadcastReconnectNotification: () => {
const response = {
id: null,
method: 'reconnect',
};
const data = JSON.stringify(response);
for (const client of allClients) {
client.send(data);
}
},
};
}
function handleKeepAlive(client, pingMs = 30000, pongWaitMs = 5000) {
let timeout = undefined;
let ping = undefined;
const schedulePing = () => {
const scheduleTimeout = () => {
timeout = setTimeout(() => {
client.close();
}, pongWaitMs);
};
ping = setTimeout(() => {
client.send('PING');
scheduleTimeout();
}, pingMs);
};
schedulePing();
return {
onMessage() {
clearTimeout(ping);
clearTimeout(timeout);
schedulePing();
},
onClose() {
clearTimeout(ping);
clearTimeout(timeout);
},
};
}
//# sourceMappingURL=websockets.js.map