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.
94 lines • 3.74 kB
JavaScript
import { ForErrors } from 'easy-signal';
;
// Exposes an API to a websocket endpoint using the protocol described in PROTOCOL.md
// use server-single if one server can handle multiple clients without state, otherwise use server
export default function createServer(version, api) {
const thisApi = { send, push, onConnect, onMessage };
const streamingRequests = new Map();
return thisApi;
function onConnect(socket) {
send(socket, { ts: Date.now(), v: version });
}
async function onMessage(socket, event) {
await processMessage(socket, '' + event.data);
}
function send(socket, data) {
socket.send(JSON.stringify(data));
}
function push(socket, data, forRequest) {
send(socket, { p: forRequest || 1, d: data });
}
async function processMessage(socket, message) {
if (typeof message !== 'string') {
return send(socket, { err: 'Incorrect message format, expecting valid JSON' });
}
let data;
try {
data = JSON.parse(message);
}
catch (err) {
return send(socket, { err: 'Incorrect JSON format' });
}
if (!data || typeof data.a !== 'string' || typeof data.r !== 'number' || (data.d && !Array.isArray(data.d))) {
return send(socket, { 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(socket, { r, err: message });
}
if (a === '_abort') {
const otherR = d[0];
const success = streamingRequests.get(otherR)?.(true) || false;
if (success)
send(socket, { r: otherR });
return send(socket, { r, d: success });
}
if (a[0] === '_' || typeof apiFunction !== 'function') {
return sendError('Unknown action');
}
try {
const result = await apiFunction.call(namespace, socket, ...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(socket, { r });
}
else {
send(socket, { 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(socket, { r, d: result });
}
}
catch (err) {
sendError(err);
}
}
}
//# sourceMappingURL=server-single.js.map