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.

113 lines 4.19 kB
import { ForErrors } from 'easy-signal'; // 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, version, apiFactory) { const thisApi = { send, push, close }; let api; let preMessages = []; const streamingRequests = new Map(); 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) { socket.send(JSON.stringify(data)); } function push(data, forRequest) { send({ p: forRequest || 1, d: data }); } function close() { socket.removeEventListener('message', onMessage); socket.removeEventListener('close', close); try { socket.close(); } catch (err) { } } function onMessage(event) { api ? processMessage('' + event.data) : preMessages.push('' + event.data); } async function processMessage(message) { if (typeof message !== 'string') { return send({ err: 'Incorrect message format, expecting valid JSON' }); } let data; 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; const path = a.split('.'); const method = path.pop(); let namespace = api; path.forEach(name => namespace = namespace && namespace[name]); const apiFunction = namespace?.[method]; function sendError(err) { 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; const unsubscribers = []; unsubscribers.push(signal((d) => { if (d === undefined) { unsubscribe(); send({ r }); } else { send({ r, s: 1, d }); } })); unsubscribers.push(signal((err) => { sendError(err); unsubscribe(); }, ForErrors)); const unsubscribe = (aborted) => { 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) { sendError(err); } } } //# sourceMappingURL=server.js.map