UNPKG

trpc-uwebsockets

Version:

tRPC adapter for uWebSockets.js server

512 lines 20.3 kB
"use strict"; 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) { 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, }); 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) => { return handleRequest(client, msg); }); 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