UNPKG

websocket-rpc-protocol

Version:

A JSON RPC protocol for working over websockets. Sheds the weight of JSON RPC to simplify argument names and adds features.

131 lines (111 loc) 4.16 kB
import { ForErrors, Signal, Unsubscriber } from 'easy-signal'; export type APIMethod = (...args: any[]) => any; export type API = {[key: string]: APIMethod | API}; export type APIFactory = (socket: ServerAPI) => API | Promise<API>; export interface ServerAPI { send: (data: any) => void; push: (data: any, forRequest?: number) => void; close: () => void; } // Exposes an API to a websocket endpoint using the protocol described in PROTOCOL.md // Use server to handle 1 API per socket for state, use server-single if one server can handle multiple sockets export default async function createServer(socket: WebSocket, version: string, apiFactory: APIFactory) { const thisApi = { send, push, close }; let api: API; let preMessages: string[] = []; const streamingRequests = new Map<number, (aborted?: boolean) => boolean>(); socket.addEventListener('message', onMessage); socket.addEventListener('close', close); try { api = await apiFactory(thisApi); send({ ts: Date.now(), v: version }); preMessages.forEach(processMessage); preMessages = null; } catch (err) { console.error(err); send({ err: err.message }); close(); } return thisApi; function send(data: any) { socket.send(JSON.stringify(data)); } function push(data: any, forRequest?: number) { send({ p: forRequest || 1, d: data }); } function close() { socket.removeEventListener('message', onMessage); socket.removeEventListener('close', close); try { socket.close(); } catch(err) {} } function onMessage(event: MessageEvent) { api ? processMessage('' + event.data) : preMessages.push('' + event.data); } async function processMessage(message: string) { if (typeof message !== 'string') { return send({ err: 'Incorrect message format, expecting valid JSON' }); } let data: any; try { data = JSON.parse(message); } catch (err) { return send({ err: 'Incorrect JSON format' }); } if (!data || typeof data.a !== 'string' || typeof data.r !== 'number' || (data.d && !Array.isArray(data.d))) { return send({ err: 'Invalid message protocol' }); } const { a, r, d = [] } = data as { a: string, r: number, d?: any[] }; const path = a.split('.'); const method = path.pop() as string; let namespace: API = api; path.forEach(name => namespace = namespace && (namespace as any)[name]); const apiFunction = namespace?.[method] as APIMethod; function sendError(err: Error | string) { const message = typeof err === 'string' ? err : err.message; return send({ r, err: message }); } if (a === '_abort') { const otherR = d[0]; const success = streamingRequests.get(otherR)?.(true) || false; if (success) send({ r: otherR }); return send({ r, d: success }); } if (a[0] === '_' || typeof apiFunction !== 'function') { return sendError('Unknown action'); } try { const result = await apiFunction.call(namespace, ...d); if (typeof result?.signal === 'function') { // result is an object with method named signal with the library easy-signal, used to stream multiple results. // Send an undefined result to end the stream and an error to end the stream with an error. const { signal, abort } = result as { signal: Signal, abort: Signal }; const unsubscribers: Unsubscriber[] = []; unsubscribers.push(signal((d: any) => { if (d === undefined) { unsubscribe(); send({ r }); } else { send({ r, s: 1, d }); } })); unsubscribers.push(signal((err: Error) => { sendError(err); unsubscribe(); }, ForErrors)); const unsubscribe = (aborted?: boolean) => { if (!streamingRequests.delete(r)) return false; if (aborted && abort) abort(); unsubscribers.forEach(u => u()); return true; }; streamingRequests.set(r, unsubscribe); } else { send({ r, d: result }); } } catch (err: any) { sendError(err); } } }