UNPKG

rpc-ws

Version:

JSON-RPC 2.0 implementation with websockets

486 lines (482 loc) 14.1 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/main.ts var main_exports = {}; __export(main_exports, { Client: () => Client, Server: () => Server }); module.exports = __toCommonJS(main_exports); // lib/client.ts var import_ws = __toESM(require("ws")); var import_node_stream = __toESM(require("stream")); async function Client(endpoint, opts) { let call_id = 0; let connected = false; const timeout = opts?.timeout || 1e4; const version = "2.0"; const ws = new import_ws.default(endpoint); const events = /* @__PURE__ */ new Map(); const emitter = new import_node_stream.default.EventEmitter(); const queue = /* @__PURE__ */ new Map(); await setup(); function assertConnection() { if (!connected) throw new Error("WebSocket connection not estabilished"); } async function setup() { connected = await Promise.race([ new Promise((resolve) => ws.on("open", async () => resolve(true))), new Promise((_, reject) => setTimeout(() => reject(false), timeout)) ]); assertConnection(); ws.on("message", (data) => { try { if (data instanceof ArrayBuffer) { data = Buffer.from(data); } const payload = JSON.parse(data.toString()); if (payload.notification && events.has(payload.notification)) { const cb = events.get(payload.notification); cb(Array.isArray(payload.params) ? payload.params : [payload.params]); return; } if (!payload.id) return; const event = queue.get(payload.id); if (!event) return; if (payload.error) { queue.set(payload.id, { ...event, error: payload.error }); } else { queue.set(payload.id, { ...event, result: payload.result }); } emitter.emit(String(payload.id)); } catch (error) { console.error(error); } }); } function _send(request) { return new Promise((resolve, reject) => { const callTimeout = setTimeout( () => emitter.emit(String(request.id), new Error("Request timed out")), timeout ); ws.send(JSON.stringify(request), (socketErr) => { if (socketErr) { console.error(socketErr); return reject({ id: request.id, jsonrpc: request.jsonrpc, error: { code: -32700, message: "Parse error" } }); } emitter.on(String(request.id), (error) => { clearTimeout(callTimeout); if (error) { return reject({ id: request.id, jsonrpc: request.jsonrpc, error: { code: -32e3, messsage: error.message } }); } const response = queue.get(request.id); queue.delete(request.id); return resolve({ id: request.id, jsonrpc: request.jsonrpc, result: response.result, error: response.error }); }); }); }); } function subscribe(namespace, cb) { assertConnection(); const request = { jsonrpc: version, method: "rpc.on", params: [namespace], id: ++call_id }; events.set(namespace, cb); queue.set(request.id, { type: "notification" }); return _send(request); } function unsubscribe(namespace) { assertConnection(); const request = { jsonrpc: version, method: "rpc.off", params: [namespace], id: ++call_id }; events.delete(namespace); queue.set(request.id, { type: "notification" }); return _send(request); } function send(method, ...params) { assertConnection(); const request = { jsonrpc: version, method, params, id: ++call_id }; queue.set(request.id, { type: "request" }); return _send(request); } function close() { ws.close(); } const base = { subscribe, unsubscribe, close }; return new Proxy(base, { get(target, prop) { if (prop === "then") { return void 0; } if (prop in base) { return Reflect.get(target, prop); } return (...args) => send(prop, ...args); } }); } // lib/server.ts var import_node_stream2 = __toESM(require("stream")); var import_node_crypto = require("crypto"); var import_ws2 = require("ws"); function Server(opts) { let listening = false; const version = "2.0"; const ws = new import_ws2.WebSocketServer(opts); const emitter = new import_node_stream2.default.EventEmitter(); const namespaces = /* @__PURE__ */ new Map(); const internalMethods = /* @__PURE__ */ new Map([ ["rpc.on", subscribe], ["rpc.off", unsubscribe] ]); function createJSONResponse(data) { return JSON.stringify({ jsonrpc: version, id: data?.id, notification: data?.notification, result: data?.result, params: data?.params, error: data?.error }); } function setup() { generateNamespace(); ws.on("listening", () => { listening = true; emitter.emit("listening"); ws.on("connection", (socket, req) => { const ns = req.url || "/"; const validNs = /^\/\w*$/g.test(ns); if (!validNs) { return socket.close(); } if (!namespaces.has(ns)) { return socket.close(); } const socketId = (0, import_node_crypto.randomUUID)(); const targetNs = namespaces.get(ns); targetNs.clients.set(socketId, socket); emitter.emit("connection", socket, socketId); handleRPC(socket, socketId, ns); socket.on( "error", (error) => emitter.emit("socket-error", socketId, error) ); socket.on("close", () => { targetNs.clients.delete(socketId); emitter.emit("disconnection", socketId); }); }); ws.on("error", (error) => emitter.emit("error", error)); }); } function validateRequest(payload) { return !!payload.params && Array.isArray(payload.params) && payload.params.length > 0; } function handleRPC(socket, socketId, ns = "/") { socket.on("message", async (data) => { const socketOpts = {}; try { if (data instanceof ArrayBuffer) { socketOpts.binary = true; data = Buffer.from(data).toString(); } } catch (error) { return socket.send( createJSONResponse({ id: null, error: { code: -32700, message: "Parse error" } }) ); } let payload; try { payload = JSON.parse(data); } catch (error) { return socket.send( createJSONResponse({ id: null, error: { code: -32700, message: "Parse error" } }) ); } try { const targetNs = namespaces.get(ns); if (internalMethods.has(payload.method)) { const internalMethod = internalMethods.get(payload.method); return socket.send( internalMethod(targetNs, payload, socketId), socketOpts ); } if (!targetNs.methods.has(payload.method)) { return socket.send( createJSONResponse({ id: payload.id, error: { code: -32601, message: "Method not found" } }), socketOpts ); } try { const fn = targetNs.methods.get(payload.method); const response = await fn(payload.params, socketId); return socket.send( createJSONResponse({ id: payload.id, result: response || void 0 }), socketOpts ); } catch (error) { return socket.send( createJSONResponse({ id: payload.id, error: { code: -32e3, message: error?.message || "Internal error", data: error?.data } }), socketOpts ); } } catch (error) { return socket.send( createJSONResponse({ id: payload?.id || null, error: { code: -32603, message: "Internal error" } }), socketOpts ); } }); } function generateNamespace(ns = "/") { namespaces.set(ns, { clients: /* @__PURE__ */ new Map(), events: /* @__PURE__ */ new Map(), methods: /* @__PURE__ */ new Map() }); } function notify(name, socketIds, ns = "/", ...params) { const targetNs = namespaces.get(ns); const sockets = [...targetNs.clients.entries()].filter(([socketId]) => socketIds.has(socketId)).map(([_, socket]) => socket); for (const socket of sockets) { socket.send( createJSONResponse({ notification: name, params }) ); } } function subscribe(targetNs, payload, socketId) { if (!validateRequest(payload)) { return createJSONResponse({ id: payload.id, error: { code: -32602, message: "Invalid params" } }); } if (!targetNs.events.has(payload.params[0])) { return createJSONResponse({ id: payload.id, error: { code: -32602, message: "Invalid params" } }); } const eventName = payload.params[0]; const eventSubscriptions = targetNs.events.get(eventName); eventSubscriptions.add(socketId); return createJSONResponse({ id: payload.id, result: { [eventName]: true } }); } function unsubscribe(targetNs, payload, socketId) { if (!validateRequest(payload)) { return createJSONResponse({ id: payload.id, error: { code: -32602, message: "Invalid params" } }); } if (!targetNs.events.has(payload.params[0])) { return createJSONResponse({ id: payload.id, error: { code: -32602, message: "Invalid params" } }); } const eventName = payload.params[0]; const eventSubscriptions = targetNs.events.get(eventName); eventSubscriptions.delete(socketId); return createJSONResponse({ id: payload.id, result: { [eventName]: false } }); } function register(method, fn, ns = "/") { if (!namespaces.has(ns)) generateNamespace(ns); const targetNs = namespaces.get(ns); targetNs.methods.set(method, fn); } function on(event2, cb) { emitter.on(event2, cb); } function event(name, ns = "/") { if (!namespaces.has(ns)) generateNamespace(ns); const targetNs = namespaces.get(ns); if (targetNs.events.has(name)) throw new Error("Event already exists"); targetNs.events.set(name, /* @__PURE__ */ new Set()); } function emit(name, ns = "/", ...params) { if (!namespaces.has(ns)) return; const targetNs = namespaces.get(ns); if (!targetNs.events.has(name)) return; const eventSubscriptions = targetNs.events.get(name); notify(name, eventSubscriptions, ns, ...params); } function of(ns) { if (!ns) throw new Error("Namespace is required"); if (!namespaces.has(ns)) generateNamespace(ns); return { emit: (name, ...params) => emit(name, ns, ...params), clients: () => namespaces.get(ns)?.clients || /* @__PURE__ */ new Map(), register: (method, fn) => register(method, fn, ns), event: (name) => event(name, ns) }; } async function handleUpgrade(req, socket, upgradeHead, callback) { ws.handleUpgrade(req, socket, upgradeHead, (socket2) => { if (!listening) ws.emit("listening"); ws.emit("connection", socket2, req); if (callback) callback(socket2, req); }); } function close() { return new Promise((resolve, reject) => { try { ws.close(); emitter.emit("close"); resolve(null); } catch (err) { reject(err); } }); } setup(); return { on, of, event: (e) => event(e), handleUpgrade, clients: () => namespaces.get("/")?.clients || /* @__PURE__ */ new Map(), register: (method, fn) => register(method, fn), emit: (name, ...params) => emit(name, "/", ...params), close }; } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { Client, Server });