rpc-ws
Version:
JSON-RPC 2.0 implementation with websockets
486 lines (482 loc) • 14.1 kB
JavaScript
;
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
});