diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 9fd8203..0b66e03 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -4,13 +4,11 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 - with: - fetch-depth: 1 + - uses: actions/checkout@v4 - name: Setup Node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v4 with: - node-version: 14.15.1 + node-version: 22 - name: Installing dependencies run: npm install - name: Running tests diff --git a/README.md b/README.md index 4179580..dae430c 100644 --- a/README.md +++ b/README.md @@ -72,17 +72,21 @@ socket.onopen = (event) => { The websocket server could even make *arbitrary calls to the client!* -#### on the server: +#### on the server (using [ws](https://github.com/websockets/ws)): ```javascript +import rawr, { transports } from 'rawr'; + socketServer.on('connection', (socket) => { - const peer = rawr({ - transport: transports.websocket(socket) + const peer = rawr({ + transport: transports.websocket(socket) }); const result = await peer.methods.doSomethingOnClient(); }); ``` +The websocket transport works with both browser WebSocket and Node.js [ws](https://github.com/websockets/ws) library. + ## Handling Notifications Peers can also send each other [notifications](https://www.jsonrpc.org/specification#notification): diff --git a/dist/bundle.js b/dist/bundle.js index 3261941..b3d2b76 100644 --- a/dist/bundle.js +++ b/dist/bundle.js @@ -1,663 +1,483 @@ -(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i { - if (msg.id) { - // handle an RPC request - if (msg.params && methodHandlers[msg.method]) { - methodHandlers[msg.method](msg); - return; +var Rawr = (() => { + 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 __commonJS = (cb, mod) => function __require() { + return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; + }; + 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); + + // node_modules/eventemitter3/index.js + var require_eventemitter3 = __commonJS({ + "node_modules/eventemitter3/index.js"(exports, module) { + "use strict"; + var has = Object.prototype.hasOwnProperty; + var prefix = "~"; + function Events() { } - // handle an RPC result - const promise = pendingCalls[msg.id]; - if (promise) { - if (promise.timeoutId) { - clearTimeout(promise.timeoutId); - } - delete pendingCalls[msg.id]; - if (msg.error) { - promise.reject(msg.error); + if (Object.create) { + Events.prototype = /* @__PURE__ */ Object.create(null); + if (!new Events().__proto__) prefix = false; + } + function EE(fn, context, once) { + this.fn = fn; + this.context = context; + this.once = once || false; + } + function addListener(emitter, event, fn, context, once) { + if (typeof fn !== "function") { + throw new TypeError("The listener must be a function"); } - return promise.resolve(msg.result); + var listener = new EE(fn, context || emitter, once), evt = prefix ? prefix + event : event; + if (!emitter._events[evt]) emitter._events[evt] = listener, emitter._eventsCount++; + else if (!emitter._events[evt].fn) emitter._events[evt].push(listener); + else emitter._events[evt] = [emitter._events[evt], listener]; + return emitter; } - return; - } - // handle a notification - msg.params.unshift(msg.method); - notificationEvents.emit(...msg.params); - }); - - function addHandler(methodName, handler) { - methodHandlers[methodName] = (msg) => { - Promise.resolve() - .then(() => { - return handler.apply(this, msg.params); - }) - .then((result) => { - transport.send({ - id: msg.id, - result - }); - }) - .catch((error) => { - const serializedError = { message: error.message }; - if (error.code) { - serializedError.code = error.code; - } - transport.send({ - id: msg.id, - error: serializedError - }); - }); - }; - } - - Object.keys(methods).forEach((m) => { - addHandler(m, methods[m]); - }); - - function sendMessage(method, params, config) { - const id = idGenerator ? idGenerator() : ++callId; - const msg = { - jsonrpc: '2.0', - method, - params, - id - }; - - let timeoutId; - if (config.timeout || timeout) { - timeoutId = setTimeout(() => { - if (pendingCalls[id]) { - const err = new Error('RPC timeout'); - err.code = 504; - pendingCalls[id].reject(err); - delete pendingCalls[id]; + function clearEvent(emitter, evt) { + if (--emitter._eventsCount === 0) emitter._events = new Events(); + else delete emitter._events[evt]; + } + function EventEmitter2() { + this._events = new Events(); + this._eventsCount = 0; + } + EventEmitter2.prototype.eventNames = function eventNames() { + var names = [], events, name; + if (this._eventsCount === 0) return names; + for (name in events = this._events) { + if (has.call(events, name)) names.push(prefix ? name.slice(1) : name); } - }, config.timeout || timeout); - } - - const response = new Promise((resolve, reject) => { - pendingCalls[id] = { resolve, reject, timeoutId }; - }); - - transport.send(msg, config); - - return response; - } - - const methodsProxy = new Proxy({}, { - get: (target, name) => { - return (...args) => { - return sendMessage(name, args, {}); + if (Object.getOwnPropertySymbols) { + return names.concat(Object.getOwnPropertySymbols(events)); + } + return names; }; - } - }); - - const configurableMethodsProxy = new Proxy({}, { - get: (target, name) => { - return (...args) => { - let config; - if (args.length) { - const testArg = args.pop(); - if (testArg && typeof testArg === 'object' && !Array.isArray(testArg)) { - config = testArg; - } else { - args.push(testArg); + EventEmitter2.prototype.listeners = function listeners(event) { + var evt = prefix ? prefix + event : event, handlers = this._events[evt]; + if (!handlers) return []; + if (handlers.fn) return [handlers.fn]; + for (var i = 0, l = handlers.length, ee = new Array(l); i < l; i++) { + ee[i] = handlers[i].fn; + } + return ee; + }; + EventEmitter2.prototype.listenerCount = function listenerCount(event) { + var evt = prefix ? prefix + event : event, listeners = this._events[evt]; + if (!listeners) return 0; + if (listeners.fn) return 1; + return listeners.length; + }; + EventEmitter2.prototype.emit = function emit(event, a1, a2, a3, a4, a5) { + var evt = prefix ? prefix + event : event; + if (!this._events[evt]) return false; + var listeners = this._events[evt], len = arguments.length, args, i; + if (listeners.fn) { + if (listeners.once) this.removeListener(event, listeners.fn, void 0, true); + switch (len) { + case 1: + return listeners.fn.call(listeners.context), true; + case 2: + return listeners.fn.call(listeners.context, a1), true; + case 3: + return listeners.fn.call(listeners.context, a1, a2), true; + case 4: + return listeners.fn.call(listeners.context, a1, a2, a3), true; + case 5: + return listeners.fn.call(listeners.context, a1, a2, a3, a4), true; + case 6: + return listeners.fn.call(listeners.context, a1, a2, a3, a4, a5), true; + } + for (i = 1, args = new Array(len - 1); i < len; i++) { + args[i - 1] = arguments[i]; + } + listeners.fn.apply(listeners.context, args); + } else { + var length = listeners.length, j; + for (i = 0; i < length; i++) { + if (listeners[i].once) this.removeListener(event, listeners[i].fn, void 0, true); + switch (len) { + case 1: + listeners[i].fn.call(listeners[i].context); + break; + case 2: + listeners[i].fn.call(listeners[i].context, a1); + break; + case 3: + listeners[i].fn.call(listeners[i].context, a1, a2); + break; + case 4: + listeners[i].fn.call(listeners[i].context, a1, a2, a3); + break; + default: + if (!args) for (j = 1, args = new Array(len - 1); j < len; j++) { + args[j - 1] = arguments[j]; + } + listeners[i].fn.apply(listeners[i].context, args); + } } } - return sendMessage(name, args, config || {}); + return true; }; - } - }); - - - const notifiers = new Proxy({}, { - get: (target, name) => { - return (...args) => { - const msg = { - jsonrpc: '2.0', - method: name, - params: args - }; - transport.send(msg); + EventEmitter2.prototype.on = function on(event, fn, context) { + return addListener(this, event, fn, context, false); }; - } - }); - - const configurableNotifiersProxy = new Proxy({}, { - get: (target, name) => { - return (...args) => { - let config; - if (args.length) { - const testArg = args.pop(); - if (testArg && typeof testArg === 'object' && !Array.isArray(testArg)) { - config = testArg; - } else { - args.push(testArg); + EventEmitter2.prototype.once = function once(event, fn, context) { + return addListener(this, event, fn, context, true); + }; + EventEmitter2.prototype.removeListener = function removeListener(event, fn, context, once) { + var evt = prefix ? prefix + event : event; + if (!this._events[evt]) return this; + if (!fn) { + clearEvent(this, evt); + return this; + } + var listeners = this._events[evt]; + if (listeners.fn) { + if (listeners.fn === fn && (!once || listeners.once) && (!context || listeners.context === context)) { + clearEvent(this, evt); + } + } else { + for (var i = 0, events = [], length = listeners.length; i < length; i++) { + if (listeners[i].fn !== fn || once && !listeners[i].once || context && listeners[i].context !== context) { + events.push(listeners[i]); + } } + if (events.length) this._events[evt] = events.length === 1 ? events[0] : events; + else clearEvent(this, evt); } - const msg = { - jsonrpc: '2.0', - method: name, - params: args - }; - transport.send(msg, config || {}); + return this; }; - } - }); - - const notifications = new Proxy({}, { - get: (target, name) => { - return (callback) => { - notificationEvents.on(name.substring(2), (...args) => { - return callback.apply(callback, args); - }); + EventEmitter2.prototype.removeAllListeners = function removeAllListeners(event) { + var evt; + if (event) { + evt = prefix ? prefix + event : event; + if (this._events[evt]) clearEvent(this, evt); + } else { + this._events = new Events(); + this._eventsCount = 0; + } + return this; }; + EventEmitter2.prototype.off = EventEmitter2.prototype.removeListener; + EventEmitter2.prototype.addListener = EventEmitter2.prototype.on; + EventEmitter2.prefixed = prefix; + EventEmitter2.EventEmitter = EventEmitter2; + if ("undefined" !== typeof module) { + module.exports = EventEmitter2; + } } }); - return { - methods: methodsProxy, - methodsExt: configurableMethodsProxy, - addHandler, - notifications, - notifiers, - notifiersExt: configurableNotifiersProxy, - transport, - }; -} - -rawr.transports = transports; - -module.exports = rawr; - -},{"./transports":4,"eventemitter3":3}],3:[function(require,module,exports){ -'use strict'; - -var has = Object.prototype.hasOwnProperty - , prefix = '~'; - -/** - * Constructor to create a storage for our `EE` objects. - * An `Events` instance is a plain object whose properties are event names. - * - * @constructor - * @private - */ -function Events() {} - -// -// We try to not inherit from `Object.prototype`. In some engines creating an -// instance in this way is faster than calling `Object.create(null)` directly. -// If `Object.create(null)` is not supported we prefix the event names with a -// character to make sure that the built-in object properties are not -// overridden or used as an attack vector. -// -if (Object.create) { - Events.prototype = Object.create(null); - - // - // This hack is needed because the `__proto__` property is still inherited in - // some old browsers like Android 4, iPhone 5.1, Opera 11 and Safari 5. - // - if (!new Events().__proto__) prefix = false; -} - -/** - * Representation of a single event listener. - * - * @param {Function} fn The listener function. - * @param {*} context The context to invoke the listener with. - * @param {Boolean} [once=false] Specify if the listener is a one-time listener. - * @constructor - * @private - */ -function EE(fn, context, once) { - this.fn = fn; - this.context = context; - this.once = once || false; -} - -/** - * Add a listener for a given event. - * - * @param {EventEmitter} emitter Reference to the `EventEmitter` instance. - * @param {(String|Symbol)} event The event name. - * @param {Function} fn The listener function. - * @param {*} context The context to invoke the listener with. - * @param {Boolean} once Specify if the listener is a one-time listener. - * @returns {EventEmitter} - * @private - */ -function addListener(emitter, event, fn, context, once) { - if (typeof fn !== 'function') { - throw new TypeError('The listener must be a function'); - } - - var listener = new EE(fn, context || emitter, once) - , evt = prefix ? prefix + event : event; - - if (!emitter._events[evt]) emitter._events[evt] = listener, emitter._eventsCount++; - else if (!emitter._events[evt].fn) emitter._events[evt].push(listener); - else emitter._events[evt] = [emitter._events[evt], listener]; - - return emitter; -} - -/** - * Clear event by name. - * - * @param {EventEmitter} emitter Reference to the `EventEmitter` instance. - * @param {(String|Symbol)} evt The Event name. - * @private - */ -function clearEvent(emitter, evt) { - if (--emitter._eventsCount === 0) emitter._events = new Events(); - else delete emitter._events[evt]; -} - -/** - * Minimal `EventEmitter` interface that is molded against the Node.js - * `EventEmitter` interface. - * - * @constructor - * @public - */ -function EventEmitter() { - this._events = new Events(); - this._eventsCount = 0; -} - -/** - * Return an array listing the events for which the emitter has registered - * listeners. - * - * @returns {Array} - * @public - */ -EventEmitter.prototype.eventNames = function eventNames() { - var names = [] - , events - , name; - - if (this._eventsCount === 0) return names; - - for (name in (events = this._events)) { - if (has.call(events, name)) names.push(prefix ? name.slice(1) : name); - } - - if (Object.getOwnPropertySymbols) { - return names.concat(Object.getOwnPropertySymbols(events)); - } - - return names; -}; - -/** - * Return the listeners registered for a given event. - * - * @param {(String|Symbol)} event The event name. - * @returns {Array} The registered listeners. - * @public - */ -EventEmitter.prototype.listeners = function listeners(event) { - var evt = prefix ? prefix + event : event - , handlers = this._events[evt]; - - if (!handlers) return []; - if (handlers.fn) return [handlers.fn]; - - for (var i = 0, l = handlers.length, ee = new Array(l); i < l; i++) { - ee[i] = handlers[i].fn; - } - - return ee; -}; - -/** - * Return the number of listeners listening to a given event. - * - * @param {(String|Symbol)} event The event name. - * @returns {Number} The number of listeners. - * @public - */ -EventEmitter.prototype.listenerCount = function listenerCount(event) { - var evt = prefix ? prefix + event : event - , listeners = this._events[evt]; - - if (!listeners) return 0; - if (listeners.fn) return 1; - return listeners.length; -}; - -/** - * Calls each of the listeners registered for a given event. - * - * @param {(String|Symbol)} event The event name. - * @returns {Boolean} `true` if the event had listeners, else `false`. - * @public - */ -EventEmitter.prototype.emit = function emit(event, a1, a2, a3, a4, a5) { - var evt = prefix ? prefix + event : event; - - if (!this._events[evt]) return false; - - var listeners = this._events[evt] - , len = arguments.length - , args - , i; - - if (listeners.fn) { - if (listeners.once) this.removeListener(event, listeners.fn, undefined, true); + // global.js + var global_exports = {}; + __export(global_exports, { + default: () => global_default + }); - switch (len) { - case 1: return listeners.fn.call(listeners.context), true; - case 2: return listeners.fn.call(listeners.context, a1), true; - case 3: return listeners.fn.call(listeners.context, a1, a2), true; - case 4: return listeners.fn.call(listeners.context, a1, a2, a3), true; - case 5: return listeners.fn.call(listeners.context, a1, a2, a3, a4), true; - case 6: return listeners.fn.call(listeners.context, a1, a2, a3, a4, a5), true; - } + // node_modules/eventemitter3/index.mjs + var import_index = __toESM(require_eventemitter3(), 1); + var eventemitter3_default = import_index.default; + + // transports/index.js + var transports_exports = {}; + __export(transports_exports, { + mqtt: () => mqtt, + socketio: () => socketio, + websocket: () => websocket, + worker: () => transport + }); - for (i = 1, args = new Array(len -1); i < len; i++) { - args[i - 1] = arguments[i]; + // transports/mqtt/index.js + function mqtt({ connection, subTopic, pubTopic, subscribe = true }) { + const emitter = new eventemitter3_default(); + if (subscribe) { + connection.subscribe(subTopic); } - - listeners.fn.apply(listeners.context, args); - } else { - var length = listeners.length - , j; - - for (i = 0; i < length; i++) { - if (listeners[i].once) this.removeListener(event, listeners[i].fn, undefined, true); - - switch (len) { - case 1: listeners[i].fn.call(listeners[i].context); break; - case 2: listeners[i].fn.call(listeners[i].context, a1); break; - case 3: listeners[i].fn.call(listeners[i].context, a1, a2); break; - case 4: listeners[i].fn.call(listeners[i].context, a1, a2, a3); break; - default: - if (!args) for (j = 1, args = new Array(len -1); j < len; j++) { - args[j - 1] = arguments[j]; + connection.on("message", (topic, message) => { + if (topic === subTopic) { + try { + const msg = JSON.parse(message.toString()); + if (msg.method || msg.id && ("result" in msg || "error" in msg)) { + emitter.emit("rpc", msg); } - - listeners[i].fn.apply(listeners[i].context, args); + } catch (err) { + } } - } + }); + emitter.send = (msg) => { + connection.publish(pubTopic, JSON.stringify(msg)); + }; + return emitter; } - return true; -}; - -/** - * Add a listener for a given event. - * - * @param {(String|Symbol)} event The event name. - * @param {Function} fn The listener function. - * @param {*} [context=this] The context to invoke the listener with. - * @returns {EventEmitter} `this`. - * @public - */ -EventEmitter.prototype.on = function on(event, fn, context) { - return addListener(this, event, fn, context, false); -}; - -/** - * Add a one-time listener for a given event. - * - * @param {(String|Symbol)} event The event name. - * @param {Function} fn The listener function. - * @param {*} [context=this] The context to invoke the listener with. - * @returns {EventEmitter} `this`. - * @public - */ -EventEmitter.prototype.once = function once(event, fn, context) { - return addListener(this, event, fn, context, true); -}; - -/** - * Remove the listeners of a given event. - * - * @param {(String|Symbol)} event The event name. - * @param {Function} fn Only remove the listeners that match this function. - * @param {*} context Only remove the listeners that have this context. - * @param {Boolean} once Only remove one-time listeners. - * @returns {EventEmitter} `this`. - * @public - */ -EventEmitter.prototype.removeListener = function removeListener(event, fn, context, once) { - var evt = prefix ? prefix + event : event; - - if (!this._events[evt]) return this; - if (!fn) { - clearEvent(this, evt); - return this; + // transports/socketio/index.js + function socketio({ connection, subTopic, pubTopic }) { + const emitter = new eventemitter3_default(); + connection.on(subTopic, (msg) => { + if (msg.method || msg.id && ("result" in msg || "error" in msg)) { + emitter.emit("rpc", msg); + } + }); + emitter.send = (msg) => { + connection.emit(pubTopic, msg); + }; + return emitter; } - var listeners = this._events[evt]; - - if (listeners.fn) { - if ( - listeners.fn === fn && - (!once || listeners.once) && - (!context || listeners.context === context) - ) { - clearEvent(this, evt); - } - } else { - for (var i = 0, events = [], length = listeners.length; i < length; i++) { - if ( - listeners[i].fn !== fn || - (once && !listeners[i].once) || - (context && listeners[i].context !== context) - ) { - events.push(listeners[i]); + // transports/websocket/index.js + function websocket(socket, allowBinary = false) { + const emitter = new eventemitter3_default(); + const handleMessage = async (data) => { + let str = data; + if (data && typeof data === "object" && "data" in data) { + str = data.data; + if (allowBinary && str instanceof Blob) { + str = await new Response(str).text().catch(() => null); + } } + if (Buffer && Buffer.isBuffer(str)) { + str = str.toString(); + } + if (typeof str === "string") { + try { + const msg = JSON.parse(str); + if (msg.method || msg.id && ("result" in msg || "error" in msg)) { + emitter.emit("rpc", msg); + } + } catch (err) { + } + } + }; + if (typeof socket.addEventListener === "function") { + socket.addEventListener("message", handleMessage); + } else if (typeof socket.on === "function") { + socket.on("message", handleMessage); } - - // - // Reset the array, or remove it completely if we have no more listeners. - // - if (events.length) this._events[evt] = events.length === 1 ? events[0] : events; - else clearEvent(this, evt); - } - - return this; -}; - -/** - * Remove all listeners, or those of the specified event. - * - * @param {(String|Symbol)} [event] The event name. - * @returns {EventEmitter} `this`. - * @public - */ -EventEmitter.prototype.removeAllListeners = function removeAllListeners(event) { - var evt; - - if (event) { - evt = prefix ? prefix + event : event; - if (this._events[evt]) clearEvent(this, evt); - } else { - this._events = new Events(); - this._eventsCount = 0; + emitter.send = (msg) => { + socket.send(JSON.stringify(msg)); + }; + return emitter; } - return this; -}; - -// -// Alias methods names because people roll like that. -// -EventEmitter.prototype.off = EventEmitter.prototype.removeListener; -EventEmitter.prototype.addListener = EventEmitter.prototype.on; - -// -// Expose the prefix. -// -EventEmitter.prefixed = prefix; - -// -// Allow `EventEmitter` to be imported as module namespace. -// -EventEmitter.EventEmitter = EventEmitter; - -// -// Expose the module. -// -if ('undefined' !== typeof module) { - module.exports = EventEmitter; -} - -},{}],4:[function(require,module,exports){ -const mqtt = require('./mqtt'); -const socketio = require('./socketio'); -const websocket = require('./websocket'); -const worker = require('./worker'); - -module.exports = { - mqtt, - socketio, - websocket, - worker -}; -},{"./mqtt":5,"./socketio":6,"./websocket":7,"./worker":8}],5:[function(require,module,exports){ -const EventEmitter = require('eventemitter3'); - -function transport({ connection, subTopic, pubTopic, subscribe = true }) { - const emitter = new EventEmitter(); - if (subscribe) { - connection.subscribe(subTopic); + // transports/worker/index.js + function dom(webWorker) { + const emitter = new eventemitter3_default(); + webWorker.addEventListener("message", (msg) => { + const { data } = msg; + if (data && (data.method || data.id && ("result" in data || "error" in data))) { + emitter.emit("rpc", data); + } + }); + emitter.send = (msg, config) => { + webWorker.postMessage(msg, config ? config.postMessageOptions : void 0); + }; + return emitter; } - connection.on('message', (topic, message) => { - if (topic === subTopic) { - try { - const msg = JSON.parse(message.toString()); - if (msg.method || (msg.id && ('result' in msg || 'error' in msg))) { - emitter.emit('rpc', msg); - } - } catch (err) { - console.error(err); + function worker() { + const emitter = new eventemitter3_default(); + self.onmessage = (msg) => { + const { data } = msg; + if (data && (data.method || data.id && ("result" in data || "error" in data))) { + emitter.emit("rpc", data); } + }; + emitter.send = (msg, config) => { + self.postMessage(msg, config ? config.postMessageOptions : void 0); + }; + return emitter; + } + function transport(webWorker) { + if (webWorker) { + return dom(webWorker); } - }); - emitter.send = (msg) => { - connection.publish(pubTopic, JSON.stringify(msg)); - }; - return emitter; -} - -module.exports = transport; - -},{"eventemitter3":3}],6:[function(require,module,exports){ -const EventEmitter = require('eventemitter3'); - -function transport({ connection, subTopic, pubTopic }) { - const emitter = new EventEmitter(); - connection.on(subTopic, (msg) => { - if (msg.method || (msg.id && ('result' in msg || 'error' in msg))) { - emitter.emit('rpc', msg); - } - }); - emitter.send = (msg) => { - connection.emit(pubTopic, msg); - }; - return emitter; -} - -module.exports = transport; - -},{"eventemitter3":3}],7:[function(require,module,exports){ -const EventEmitter = require('eventemitter3'); + return worker(); + } -function transport(socket, allowBinary = false) { - const emitter = new EventEmitter(); - socket.addEventListener('message', async (evt) => { - let { data } = evt; - if (allowBinary && data instanceof Blob) { - data = await (new Response(data)).text().catch(() => null); - } - if (typeof evt.data === 'string') { - try { - const msg = JSON.parse(evt.data); - if (msg.method || (msg.id && ('result' in msg || 'error' in msg))) { - emitter.emit('rpc', msg); + // index.js + function rawr({ transport: transport2, timeout = 0, handlers = {}, methods, idGenerator }) { + let callId = 0; + methods = methods || handlers || {}; + const pendingCalls = {}; + const methodHandlers = {}; + const notificationEvents = new eventemitter3_default(); + notificationEvents.on = notificationEvents.on.bind(notificationEvents); + transport2.on("rpc", (msg) => { + if (msg.id) { + if (msg.params && methodHandlers[msg.method]) { + methodHandlers[msg.method](msg); + return; } - } catch (err) { - // wasn't a JSON message + const promise = pendingCalls[msg.id]; + if (promise) { + if (promise.timeoutId) { + clearTimeout(promise.timeoutId); + } + delete pendingCalls[msg.id]; + if (msg.error) { + promise.reject(msg.error); + } + return promise.resolve(msg.result); + } + return; } + msg.params.unshift(msg.method); + notificationEvents.emit(...msg.params); + }); + function addHandler(methodName, handler) { + methodHandlers[methodName] = (msg) => { + Promise.resolve().then(() => { + return handler.apply(this, msg.params); + }).then((result) => { + transport2.send({ + id: msg.id, + result + }); + }).catch((error) => { + const serializedError = { message: error.message }; + if (error.code) { + serializedError.code = error.code; + } + transport2.send({ + id: msg.id, + error: serializedError + }); + }); + }; } - }); - emitter.send = (msg) => { - socket.send(JSON.stringify(msg)); - }; - return emitter; -} - -module.exports = transport; - -},{"eventemitter3":3}],8:[function(require,module,exports){ -const EventEmitter = require('eventemitter3'); - -function dom(webWorker) { - const emitter = new EventEmitter(); - webWorker.addEventListener('message', (msg) => { - const { data } = msg; - if (data && (data.method || (data.id && ('result' in data || 'error' in data)))) { - emitter.emit('rpc', data); - } - }); - emitter.send = (msg, config) => { - webWorker.postMessage(msg, config ? config.postMessageOptions : undefined); - }; - return emitter; -} - -function worker() { - const emitter = new EventEmitter(); - self.onmessage = (msg) => { - const { data } = msg; - if (data && (data.method || (data.id && ('result' in data || 'error' in data)))) { - emitter.emit('rpc', data); + Object.keys(methods).forEach((m) => { + addHandler(m, methods[m]); + }); + function sendMessage(method, params, config) { + const id = idGenerator ? idGenerator() : ++callId; + const msg = { + jsonrpc: "2.0", + method, + params, + id + }; + let timeoutId; + if (config.timeout || timeout) { + timeoutId = setTimeout(() => { + if (pendingCalls[id]) { + const err = new Error("RPC timeout"); + err.code = 504; + pendingCalls[id].reject(err); + delete pendingCalls[id]; + } + }, config.timeout || timeout); + } + const response = new Promise((resolve, reject) => { + pendingCalls[id] = { resolve, reject, timeoutId }; + }); + transport2.send(msg, config); + return response; } - }; - emitter.send = (msg, config) => { - self.postMessage(msg, config ? config.postMessageOptions : undefined); - }; - return emitter; -} - -function transport(webWorker) { - if (webWorker) { - return dom(webWorker); + const methodsProxy = new Proxy({}, { + get: (target, name) => { + return (...args) => { + return sendMessage(name, args, {}); + }; + } + }); + const configurableMethodsProxy = new Proxy({}, { + get: (target, name) => { + return (...args) => { + let config; + if (args.length) { + const testArg = args.pop(); + if (testArg && typeof testArg === "object" && !Array.isArray(testArg)) { + config = testArg; + } else { + args.push(testArg); + } + } + return sendMessage(name, args, config || {}); + }; + } + }); + const notifiers = new Proxy({}, { + get: (target, name) => { + return (...args) => { + const msg = { + jsonrpc: "2.0", + method: name, + params: args + }; + transport2.send(msg); + }; + } + }); + const configurableNotifiersProxy = new Proxy({}, { + get: (target, name) => { + return (...args) => { + let config; + if (args.length) { + const testArg = args.pop(); + if (testArg && typeof testArg === "object" && !Array.isArray(testArg)) { + config = testArg; + } else { + args.push(testArg); + } + } + const msg = { + jsonrpc: "2.0", + method: name, + params: args + }; + transport2.send(msg, config || {}); + }; + } + }); + const notifications = new Proxy({}, { + get: (target, name) => { + return (callback) => { + notificationEvents.on(name.substring(2), (...args) => { + return callback.apply(callback, args); + }); + }; + } + }); + return { + methods: methodsProxy, + methodsExt: configurableMethodsProxy, + addHandler, + notifications, + notifiers, + notifiersExt: configurableNotifiersProxy, + transport: transport2 + }; } - return worker(); -} - -// backwards compat -transport.dom = dom; -transport.worker = worker; - -module.exports = transport; - -},{"eventemitter3":3}]},{},[1]); + rawr.transports = transports_exports; + var index_default = rawr; + + // global.js + index_default.EventEmitter = eventemitter3_default; + index_default.transports = transports_exports; + globalThis.Rawr = index_default; + var global_default = index_default; + return __toCommonJS(global_exports); +})(); diff --git a/dist/bundle.min.js b/dist/bundle.min.js new file mode 100644 index 0000000..d17ca67 --- /dev/null +++ b/dist/bundle.min.js @@ -0,0 +1 @@ +var Rawr=(()=>{var K=Object.create;var L=Object.defineProperty;var Q=Object.getOwnPropertyDescriptor;var U=Object.getOwnPropertyNames;var V=Object.getPrototypeOf,X=Object.prototype.hasOwnProperty;var Y=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),B=(e,t)=>{for(var r in t)L(e,r,{get:t[r],enumerable:!0})},I=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of U(t))!X.call(e,o)&&o!==r&&L(e,o,{get:()=>t[o],enumerable:!(n=Q(t,o))||n.enumerable});return e};var Z=(e,t,r)=>(r=e!=null?K(V(e)):{},I(t||!e||!e.__esModule?L(r,"default",{value:e,enumerable:!0}):r,e)),$=e=>I(L({},"__esModule",{value:!0}),e);var T=Y((se,C)=>{"use strict";var G=Object.prototype.hasOwnProperty,h="~";function _(){}Object.create&&(_.prototype=Object.create(null),new _().__proto__||(h=!1));function W(e,t,r){this.fn=e,this.context=t,this.once=r||!1}function J(e,t,r,n,o){if(typeof r!="function")throw new TypeError("The listener must be a function");var f=new W(r,n||e,o),i=h?h+t:t;return e._events[i]?e._events[i].fn?e._events[i]=[e._events[i],f]:e._events[i].push(f):(e._events[i]=f,e._eventsCount++),e}function A(e,t){--e._eventsCount===0?e._events=new _:delete e._events[t]}function m(){this._events=new _,this._eventsCount=0}m.prototype.eventNames=function(){var t=[],r,n;if(this._eventsCount===0)return t;for(n in r=this._events)G.call(r,n)&&t.push(h?n.slice(1):n);return Object.getOwnPropertySymbols?t.concat(Object.getOwnPropertySymbols(r)):t};m.prototype.listeners=function(t){var r=h?h+t:t,n=this._events[r];if(!n)return[];if(n.fn)return[n.fn];for(var o=0,f=n.length,i=new Array(f);ore});var R=Z(T(),1);var v=R.default;var b={};B(b,{mqtt:()=>S,socketio:()=>M,websocket:()=>N,worker:()=>k});function S({connection:e,subTopic:t,pubTopic:r,subscribe:n=!0}){let o=new v;return n&&e.subscribe(t),e.on("message",(f,i)=>{if(f===t)try{let p=JSON.parse(i.toString());(p.method||p.id&&("result"in p||"error"in p))&&o.emit("rpc",p)}catch{}}),o.send=f=>{e.publish(r,JSON.stringify(f))},o}function M({connection:e,subTopic:t,pubTopic:r}){let n=new v;return e.on(t,o=>{(o.method||o.id&&("result"in o||"error"in o))&&n.emit("rpc",o)}),n.send=o=>{e.emit(r,o)},n}function N(e,t=!1){let r=new v,n=async o=>{let f=o;if(o&&typeof o=="object"&&"data"in o&&(f=o.data,t&&f instanceof Blob&&(f=await new Response(f).text().catch(()=>null))),Buffer&&Buffer.isBuffer(f)&&(f=f.toString()),typeof f=="string")try{let i=JSON.parse(f);(i.method||i.id&&("result"in i||"error"in i))&&r.emit("rpc",i)}catch{}};return typeof e.addEventListener=="function"?e.addEventListener("message",n):typeof e.on=="function"&&e.on("message",n),r.send=o=>{e.send(JSON.stringify(o))},r}function ee(e){let t=new v;return e.addEventListener("message",r=>{let{data:n}=r;n&&(n.method||n.id&&("result"in n||"error"in n))&&t.emit("rpc",n)}),t.send=(r,n)=>{e.postMessage(r,n?n.postMessageOptions:void 0)},t}function te(){let e=new v;return self.onmessage=t=>{let{data:r}=t;r&&(r.method||r.id&&("result"in r||"error"in r))&&e.emit("rpc",r)},e.send=(t,r)=>{self.postMessage(t,r?r.postMessageOptions:void 0)},e}function k(e){return e?ee(e):te()}function q({transport:e,timeout:t=0,handlers:r={},methods:n,idGenerator:o}){let f=0;n=n||r||{};let i={},p={},s=new v;s.on=s.on.bind(s),e.on("rpc",u=>{if(u.id){if(u.params&&p[u.method]){p[u.method](u);return}let d=i[u.id];return d?(d.timeoutId&&clearTimeout(d.timeoutId),delete i[u.id],u.error&&d.reject(u.error),d.resolve(u.result)):void 0}u.params.unshift(u.method),s.emit(...u.params)});function x(u,d){p[u]=c=>{Promise.resolve().then(()=>d.apply(this,c.params)).then(l=>{e.send({id:c.id,result:l})}).catch(l=>{let y={message:l.message};l.code&&(y.code=l.code),e.send({id:c.id,error:y})})}}Object.keys(n).forEach(u=>{x(u,n[u])});function w(u,d,c){let l=o?o():++f,y={jsonrpc:"2.0",method:u,params:d,id:l},E;(c.timeout||t)&&(E=setTimeout(()=>{if(i[l]){let P=new Error("RPC timeout");P.code=504,i[l].reject(P),delete i[l]}},c.timeout||t));let D=new Promise((P,F)=>{i[l]={resolve:P,reject:F,timeoutId:E}});return e.send(y,c),D}let a=new Proxy({},{get:(u,d)=>(...c)=>w(d,c,{})}),j=new Proxy({},{get:(u,d)=>(...c)=>{let l;if(c.length){let y=c.pop();y&&typeof y=="object"&&!Array.isArray(y)?l=y:c.push(y)}return w(d,c,l||{})}}),g=new Proxy({},{get:(u,d)=>(...c)=>{let l={jsonrpc:"2.0",method:d,params:c};e.send(l)}}),H=new Proxy({},{get:(u,d)=>(...c)=>{let l;if(c.length){let E=c.pop();E&&typeof E=="object"&&!Array.isArray(E)?l=E:c.push(E)}let y={jsonrpc:"2.0",method:d,params:c};e.send(y,l||{})}}),z=new Proxy({},{get:(u,d)=>c=>{s.on(d.substring(2),(...l)=>c.apply(c,l))}});return{methods:a,methodsExt:j,addHandler:x,notifications:z,notifiers:g,notifiersExt:H,transport:e}}q.transports=b;var O=q;O.EventEmitter=v;O.transports=b;globalThis.Rawr=O;var re=O;return $(ne);})(); diff --git a/global.js b/global.js index 3f53413..c2301f0 100644 --- a/global.js +++ b/global.js @@ -1,7 +1,9 @@ -const rawr = require('./'); -const EventEmitter = require('eventemitter3'); +import rawr, { transports } from './index.js'; +import EventEmitter from 'eventemitter3'; + rawr.EventEmitter = EventEmitter; +rawr.transports = transports; globalThis.Rawr = rawr; -module.exports = rawr; +export default rawr; diff --git a/index.js b/index.js index 9819ab1..4f8e48e 100644 --- a/index.js +++ b/index.js @@ -1,9 +1,8 @@ -const EventEmitter = require('eventemitter3'); -const transports = require('./transports'); +import EventEmitter from 'eventemitter3'; +import * as transports from './transports/index.js'; -function rawr({ transport, timeout = 0, handlers = {}, methods, idGenerator }) { +export function rawr({ transport, timeout = 0, handlers = {}, methods, idGenerator }) { let callId = 0; - // eslint-disable-next-line no-param-reassign methods = methods || handlers || {}; // backwards compat const pendingCalls = {}; const methodHandlers = {}; @@ -177,6 +176,11 @@ function rawr({ transport, timeout = 0, handlers = {}, methods, idGenerator }) { }; } +// Attach transports to rawr for convenience rawr.transports = transports; -module.exports = rawr; +// Default export for backwards compatibility +export default rawr; + +// Named exports +export { transports }; diff --git a/package.json b/package.json index c2f3a32..7dbb807 100644 --- a/package.json +++ b/package.json @@ -1,31 +1,27 @@ { "name": "rawr", - "version": "0.19.0", + "version": "1.0.0", "description": "JSON-RPC over simple node style event emitters", + "type": "module", + "exports": { + ".": "./index.js", + "./transports": "./transports/index.js", + "./transports/websocket": "./transports/websocket/index.js", + "./transports/worker": "./transports/worker/index.js", + "./transports/mqtt": "./transports/mqtt/index.js", + "./transports/socketio": "./transports/socketio/index.js" + }, "dependencies": { "eventemitter3": "^5.0.1" }, "devDependencies": { - "b64id": "^1.0.1", - "browserify": "^16.2.3", - "chai": "^4.2.0", - "coveralls": "^3.0.3", - "eslint": "^5.15.3", - "eslint-config-airbnb": "^17.1.0", - "eslint-plugin-import": "^2.16.0", - "eslint-plugin-jsx-a11y": "^6.2.1", - "eslint-plugin-react": "^7.12.4", - "istanbul": "^0.4.5", - "mocha": "^10.2.0" + "b64id": "^1.0.0", + "esbuild": "^0.24.0", + "vitest": "^2.0.0" }, - "main": "index.js", "scripts": { - "lint": "eslint ./index.js --ext .js", - "test": "npm run lint && istanbul cover _mocha && npm run check-coverage", - "mocha": "_mocha", - "build": "browserify global.js -o dist/bundle.js", - "check-coverage": "istanbul check-coverage --statements 100 --branches 75 --lines 100 --functions 100", - "coveralls": "cat ./coverage/lcov.info | ./node_modules/.bin/coveralls" + "test": "vitest run", + "build": "esbuild global.js --bundle --format=iife --global-name=Rawr --outfile=dist/bundle.js && esbuild global.js --bundle --format=iife --global-name=Rawr --minify --outfile=dist/bundle.min.js" }, "repository": { "type": "git", @@ -42,7 +38,6 @@ ], "author": "Luis Montes (http://iceddev.com/)", "license": "MIT", - "readmeFilename": "README.md", "bugs": { "url": "https://github.com/iceddev/rawr/issues" } diff --git a/test/index.js b/test/index.test.js similarity index 64% rename from test/index.js rename to test/index.test.js index 1c3e01f..13a3abd 100644 --- a/test/index.js +++ b/test/index.test.js @@ -1,9 +1,7 @@ -const chai = require('chai'); -const EventEmitter = require('eventemitter3'); -const b64id = require('b64id'); -const rawr = require('../'); - -chai.should(); +import { describe, it, expect } from 'vitest'; +import EventEmitter from 'eventemitter3'; +import { generateId } from 'b64id'; +import rawr from '../index.js'; function mockTransports() { const a = new EventEmitter(); @@ -32,7 +30,6 @@ function mockTransports() { return { a, b }; } - function helloTest(name) { return new Promise((resolve, reject) => { if (name === 'bad') { @@ -67,11 +64,10 @@ function hi() { } describe('rawr', () => { - it('should make a client', (done) => { + it('should make a client', () => { const client = rawr({ transport: mockTransports().a }); - client.should.be.a('object'); - client.addHandler.should.be.a('function'); - done(); + expect(client).toBeTypeOf('object'); + expect(client.addHandler).toBeTypeOf('function'); }); it('client should make a successful rpc call to another peer', async () => { @@ -81,108 +77,118 @@ describe('rawr', () => { const resultA = await clientA.methods.subtract(7, 2); const resultB = await clientB.methods.add(1, 2); - resultA.should.equal(5); - resultB.should.equal(3); + expect(resultA).toBe(5); + expect(resultB).toBe(3); }); it('client should make a successful rpc call to another peer with custom id generators', async () => { const { a, b } = mockTransports(); - const clientA = rawr({ transport: a, handlers: { add }, idGenerator: b64id.generateId }); - const clientB = rawr({ transport: b, handlers: { subtract, idGenerator: b64id.generateId } }); + const clientA = rawr({ transport: a, handlers: { add }, idGenerator: generateId }); + const clientB = rawr({ transport: b, handlers: { subtract }, idGenerator: generateId }); const resultA = await clientA.methods.subtract(7, 2); const resultB = await clientB.methods.add(1, 2); - resultA.should.equal(5); - resultB.should.equal(3); + expect(resultA).toBe(5); + expect(resultB).toBe(3); }); it('client should make an unsuccessful rpc call to a peer', async () => { const { a, b } = mockTransports(); - const clientA = rawr({ transport: a, handlers: { helloTest } }); + rawr({ transport: a, handlers: { helloTest } }); const clientB = rawr({ transport: b }); - clientA.should.be.an('object'); try { await clientB.methods.helloTest('bad'); + expect.fail('Should have thrown'); } catch (error) { - error.code.should.equal(9000); + expect(error.code).toBe(9000); } }); it('client handle an rpc under a specified timeout', async () => { const { a, b } = mockTransports(); - const clientA = rawr({ transport: a, handlers: { helloTest } }); + rawr({ transport: a, handlers: { helloTest } }); const clientB = rawr({ transport: b, timeout: 1000 }); - clientA.should.be.an('object'); const result = await clientB.methods.helloTest('luis'); - result.should.equal('hello, luis'); + expect(result).toBe('hello, luis'); }); it('client handle an rpc timeout', async () => { const { a, b } = mockTransports(); - const clientA = rawr({ transport: a, handlers: { helloTest } }); + rawr({ transport: a, handlers: { helloTest } }); const clientB = rawr({ transport: b, timeout: 10 }); - clientA.should.be.an('object'); try { await clientB.methods.helloTest('luis'); + expect.fail('Should have thrown'); } catch (error) { - error.code.should.equal(504); + expect(error.code).toBe(504); } }); - it('client should be able to send a notification to a server', (done) => { + it('client should be able to send a notification to a server', async () => { const { a, b } = mockTransports(); const clientA = rawr({ transport: a }); const clientB = rawr({ transport: b }); - clientA.notifications.ondoSomething((someData) => { - someData.should.equal('testing_notification'); - done(); + const received = new Promise((resolve) => { + clientA.notifications.ondoSomething((someData) => { + resolve(someData); + }); }); clientB.notifiers.doSomething('testing_notification'); + + const result = await received; + expect(result).toBe('testing_notification'); }); it('client should have notifiersExt method', () => { const { a } = mockTransports(); const client = rawr({ transport: a }); - client.should.have.property('notifiersExt'); - (typeof client.notifiersExt).should.equal('object'); + expect(client).toHaveProperty('notifiersExt'); + expect(client.notifiersExt).toBeTypeOf('object'); }); - it('client should be able to send a notification with notifiersExt', (done) => { + it('client should be able to send a notification with notifiersExt', async () => { const { a, b } = mockTransports(); const clientA = rawr({ transport: a }); const clientB = rawr({ transport: b }); - clientA.notifications.ondoSomething((someData) => { - someData.should.equal('testing_notification_ext'); - done(); + const received = new Promise((resolve) => { + clientA.notifications.ondoSomething((someData) => { + resolve(someData); + }); }); clientB.notifiersExt.doSomething('testing_notification_ext'); + + const result = await received; + expect(result).toBe('testing_notification_ext'); }); - it('client should pass config to transport when using notifiersExt', (done) => { + it('client should pass config to transport when using notifiersExt', async () => { const { a, b } = mockTransports(); const clientA = rawr({ transport: a }); const clientB = rawr({ transport: b }); - let receivedConfig = false; + let receivedConfig = null; a.on('config', (config) => { - config.should.deep.equal({ postMessageOptions: { transfer: ['test'] } }); - receivedConfig = true; + receivedConfig = config; }); - clientA.notifications.ondoConfigTest((someData) => { - someData.should.equal('config_test'); - receivedConfig.should.equal(true); - done(); + const received = new Promise((resolve) => { + clientA.notifications.ondoConfigTest((someData) => { + resolve(someData); + }); }); clientB.notifiersExt.doConfigTest('config_test', { postMessageOptions: { transfer: ['test'] } }); + + const result = await received; + expect(result).toBe('config_test'); + expect(receivedConfig).toEqual({ postMessageOptions: { transfer: ['test'] } }); }); it('client should fail on a configured timeout', async () => { @@ -190,23 +196,30 @@ describe('rawr', () => { const clientA = rawr({ transport: a, handlers: { slowFunction, hi } }); const clientB = rawr({ transport: b, handlers: { slowFunction, add } }); + // clientA calls slowFunction on clientB's handlers const resultA = await clientA.methodsExt.slowFunction({ timeout: 1000 }); - resultA.should.equal('slow'); + expect(resultA).toBe('slow'); + + // clientA calls add on clientB's handlers const resultA2 = await clientA.methodsExt.add(1, 2, null); - resultA2.should.equal(3); + expect(resultA2).toBe(3); + + // clientB calls slowFunction on clientA's handlers, but times out try { await clientB.methodsExt.slowFunction({ timeout: 100 }); - + expect.fail('Should have thrown'); } catch (error) { - error.code.should.equal(504); + expect(error.code).toBe(504); } + try { await clientB.methodsExt.slowFunction('useless param', { timeout: 100 }); + expect.fail('Should have thrown'); } catch (error) { - error.code.should.equal(504); + expect(error.code).toBe(504); } - const resultB2 = await clientB.methodsExt.hi(); - resultB2.should.equal('hi'); + const resultB2 = await clientB.methodsExt.hi(); + expect(resultB2).toBe('hi'); }); }); diff --git a/test/mqtt_transport.js b/test/mqtt_transport.test.js similarity index 69% rename from test/mqtt_transport.js rename to test/mqtt_transport.test.js index 290bec3..26280c8 100644 --- a/test/mqtt_transport.js +++ b/test/mqtt_transport.test.js @@ -1,8 +1,6 @@ -const chai = require('chai'); -const EventEmitter = require('eventemitter3'); -const rawr = require('../'); - -chai.should(); +import { describe, it, expect } from 'vitest'; +import EventEmitter from 'eventemitter3'; +import rawr, { transports } from '../index.js'; function mockTransports() { const a = new EventEmitter(); @@ -18,27 +16,27 @@ function mockTransports() { }; b.subscribe = () => {}; - const transportA = rawr.transports.mqtt({ + const transportA = transports.mqtt({ connection: a, pubTopic: 'aPub', subTopic: 'bPub' }); transportA.a = a; - const transportB = rawr.transports.mqtt({ + const transportB = transports.mqtt({ connection: b, pubTopic: 'bPub', subTopic: 'aPub' }); transportB.b = b; - const transportDontSub = rawr.transports.mqtt({ + const transportDontSub = transports.mqtt({ connection: a, pubTopic: 'aPub', subTopic: 'bPub', subscribe: false, }); - const transportBadTopic = rawr.transports.mqtt({ + const transportBadTopic = transports.mqtt({ connection: a, pubTopic: 'somethingElse', subTopic: 'somethingElse', @@ -68,23 +66,21 @@ function subtract(a, b) { return a - b; } -describe('mqtt', () => { - it('should make a client', (done) => { +describe('mqtt transport', () => { + it('should make a client', () => { const { transportA, transportB } = mockTransports(); transportB.b.publish('bPub', 'check bad json'); const client = rawr({ transport: transportA }); - client.should.be.a('object'); - client.addHandler.should.be.a('function'); - done(); + expect(client).toBeTypeOf('object'); + expect(client.addHandler).toBeTypeOf('function'); }); - it('should make a client with an already subscribed transport', (done) => { + it('should make a client with an already subscribed transport', () => { const { transportDontSub, transportB } = mockTransports(); transportB.b.publish('bPub', 'check bad json'); const client = rawr({ transport: transportDontSub }); - client.should.be.a('object'); - client.addHandler.should.be.a('function'); - done(); + expect(client).toBeTypeOf('object'); + expect(client.addHandler).toBeTypeOf('function'); }); it('client should make a successful rpc call to another peer', async () => { @@ -94,79 +90,82 @@ describe('mqtt', () => { const resultA = await clientA.methods.subtract(7, 2); const resultB = await clientB.methods.add(1, 2); - resultA.should.equal(5); - resultB.should.equal(3); + expect(resultA).toBe(5); + expect(resultB).toBe(3); }); it('client should handle bad messages on topic', async () => { const { transportA, transportB } = mockTransports(); const clientA = rawr({ transport: transportA }); - const clientB = rawr({ transport: transportB, handlers: { subtract } }); + rawr({ transport: transportB, handlers: { subtract } }); transportA.a.publish('aPub', `{"something": "bad"}`); const resultA = await clientA.methods.subtract(7, 2); - resultA.should.equal(5); + expect(resultA).toBe(5); }); it('client should make an unsuccessful rpc call to a peer', async () => { const { transportA, transportB } = mockTransports(); - const clientA = rawr({ transport: transportA, handlers: { helloTest } }); + rawr({ transport: transportA, handlers: { helloTest } }); const clientB = rawr({ transport: transportB }); - clientA.should.be.an('object'); try { await clientB.methods.helloTest('bad'); + expect.fail('Should have thrown'); } catch (error) { - error.code.should.equal(9000); + expect(error.code).toBe(9000); } }); it('client handle an rpc under a specified timeout', async () => { const { transportA, transportB } = mockTransports(); - const clientA = rawr({ transport: transportA, handlers: { helloTest } }); + rawr({ transport: transportA, handlers: { helloTest } }); const clientB = rawr({ transport: transportB, timeout: 1000 }); - clientA.should.be.an('object'); const result = await clientB.methods.helloTest('luis'); - result.should.equal('hello, luis'); + expect(result).toBe('hello, luis'); }); it('client handle an rpc timeout', async () => { const { transportA, transportB } = mockTransports(); - const clientA = rawr({ transport: transportA, handlers: { helloTest } }); + rawr({ transport: transportA, handlers: { helloTest } }); const clientB = rawr({ transport: transportB, timeout: 10 }); - clientA.should.be.an('object'); try { await clientB.methods.helloTest('luis'); + expect.fail('Should have thrown'); } catch (error) { - error.code.should.equal(504); + expect(error.code).toBe(504); } }); - it('client handle an rpc timeout becuase topic didnt match', async () => { + it('client handle an rpc timeout because topic didnt match', async () => { const { transportA, transportBadTopic } = mockTransports(); - const clientA = rawr({ transport: transportA, handlers: { helloTest } }); + rawr({ transport: transportA, handlers: { helloTest } }); const clientB = rawr({ transport: transportBadTopic, timeout: 10 }); - clientA.should.be.an('object'); try { await clientB.methods.helloTest('luis'); + expect.fail('Should have thrown'); } catch (error) { - error.code.should.equal(504); + expect(error.code).toBe(504); } }); - it('client should be able to send a notification to a server', (done) => { + it('client should be able to send a notification to a server', async () => { const { transportA, transportB } = mockTransports(); const clientA = rawr({ transport: transportA }); const clientB = rawr({ transport: transportB }); - clientA.notifications.ondoSomething((someData) => { - someData.should.equal('testing_notification'); - done(); + const received = new Promise((resolve) => { + clientA.notifications.ondoSomething((someData) => { + resolve(someData); + }); }); clientB.notifiers.doSomething('testing_notification'); + + const result = await received; + expect(result).toBe('testing_notification'); }); }); diff --git a/test/socketio_transport.js b/test/socketio_transport.test.js similarity index 69% rename from test/socketio_transport.js rename to test/socketio_transport.test.js index 96e609f..b53f809 100644 --- a/test/socketio_transport.js +++ b/test/socketio_transport.test.js @@ -1,8 +1,6 @@ -const chai = require('chai'); -const EventEmitter = require('eventemitter3'); -const rawr = require('../'); - -chai.should(); +import { describe, it, expect } from 'vitest'; +import EventEmitter from 'eventemitter3'; +import rawr, { transports } from '../index.js'; function mockTransports() { const a = new EventEmitter(); @@ -10,7 +8,7 @@ function mockTransports() { a.originalEmit = a.emit; a.emit = (topic, msg) => { - if(topic === 'aPub') { + if (topic === 'aPub') { b.emit(topic, msg); } else { a.originalEmit(topic, msg); @@ -19,19 +17,19 @@ function mockTransports() { b.originalEmit = b.emit; b.emit = (topic, msg) => { - if(topic === 'bPub') { + if (topic === 'bPub') { a.emit(topic, msg); } else { b.originalEmit(topic, msg); } }; - const transportA = rawr.transports.socketio({ + const transportA = transports.socketio({ connection: a, pubTopic: 'aPub', subTopic: 'bPub' }); - const transportB = rawr.transports.socketio({ + const transportB = transports.socketio({ connection: b, pubTopic: 'bPub', subTopic: 'aPub' @@ -61,13 +59,12 @@ function subtract(a, b) { return a - b; } -describe('socketio', () => { - it('should make a client', (done) => { +describe('socketio transport', () => { + it('should make a client', () => { const { transportA } = mockTransports(); const client = rawr({ transport: transportA }); - client.should.be.a('object'); - client.addHandler.should.be.a('function'); - done(); + expect(client).toBeTypeOf('object'); + expect(client.addHandler).toBeTypeOf('function'); }); it('client should make a successful rpc call to another peer', async () => { @@ -77,56 +74,59 @@ describe('socketio', () => { const resultA = await clientA.methods.subtract(7, 2); const resultB = await clientB.methods.add(1, 2); - resultA.should.equal(5); - resultB.should.equal(3); + expect(resultA).toBe(5); + expect(resultB).toBe(3); }); it('client should make an unsuccessful rpc call to a peer', async () => { const { transportA, transportB } = mockTransports(); - const clientA = rawr({ transport: transportA, handlers: { helloTest } }); + rawr({ transport: transportA, handlers: { helloTest } }); const clientB = rawr({ transport: transportB }); - clientA.should.be.an('object'); try { await clientB.methods.helloTest('bad'); + expect.fail('Should have thrown'); } catch (error) { - error.code.should.equal(9000); + expect(error.code).toBe(9000); } }); it('client handle an rpc under a specified timeout', async () => { const { transportA, transportB } = mockTransports(); - const clientA = rawr({ transport: transportA, handlers: { helloTest } }); + rawr({ transport: transportA, handlers: { helloTest } }); const clientB = rawr({ transport: transportB, timeout: 1000 }); - clientA.should.be.an('object'); const result = await clientB.methods.helloTest('luis'); - result.should.equal('hello, luis'); + expect(result).toBe('hello, luis'); }); it('client handle an rpc timeout', async () => { const { transportA, transportB } = mockTransports(); - const clientA = rawr({ transport: transportA, handlers: { helloTest } }); + rawr({ transport: transportA, handlers: { helloTest } }); const clientB = rawr({ transport: transportB, timeout: 10 }); - clientA.should.be.an('object'); try { await clientB.methods.helloTest('luis'); + expect.fail('Should have thrown'); } catch (error) { - error.code.should.equal(504); + expect(error.code).toBe(504); } }); - it('client should be able to send a notification to a server', (done) => { + it('client should be able to send a notification to a server', async () => { const { transportA, transportB } = mockTransports(); const clientA = rawr({ transport: transportA }); const clientB = rawr({ transport: transportB }); - clientA.notifications.ondoSomething((someData) => { - someData.should.equal('testing_notification'); - done(); + const received = new Promise((resolve) => { + clientA.notifications.ondoSomething((someData) => { + resolve(someData); + }); }); clientB.notifiers.doSomething('testing_notification'); + + const result = await received; + expect(result).toBe('testing_notification'); }); }); diff --git a/test/websocket_transport.js b/test/websocket_transport.test.js similarity index 51% rename from test/websocket_transport.js rename to test/websocket_transport.test.js index e4fcf38..303c0bc 100644 --- a/test/websocket_transport.js +++ b/test/websocket_transport.test.js @@ -1,29 +1,27 @@ -const chai = require('chai'); -const EventEmitter = require('eventemitter3'); -const rawr = require('../'); - -chai.should(); +import { describe, it, expect } from 'vitest'; +import EventEmitter from 'eventemitter3'; +import rawr, { transports } from '../index.js'; function mockTransports() { const a = new EventEmitter(); const b = new EventEmitter(); a.send = (msg) => { - b.emit('message', {data: msg}); + b.emit('message', { data: msg }); }; a.addEventListener = (topic, cb) => { a.on(topic, cb); - } + }; b.send = (msg) => { - a.emit('message', {data: msg}); + a.emit('message', { data: msg }); }; b.addEventListener = (topic, cb) => { b.on(topic, cb); - } + }; - const transportA = rawr.transports.websocket(a); - const transportB = rawr.transports.websocket(b); + const transportA = transports.websocket(a); + const transportB = transports.websocket(b); return { transportA, transportB }; } @@ -49,13 +47,12 @@ function subtract(a, b) { return a - b; } -describe('websocket', () => { - it('should make a client', (done) => { +describe('websocket transport', () => { + it('should make a client', () => { const { transportA } = mockTransports(); const client = rawr({ transport: transportA }); - client.should.be.a('object'); - client.addHandler.should.be.a('function'); - done(); + expect(client).toBeTypeOf('object'); + expect(client.addHandler).toBeTypeOf('function'); }); it('client should make a successful rpc call to another peer', async () => { @@ -65,56 +62,92 @@ describe('websocket', () => { const resultA = await clientA.methods.subtract(7, 2); const resultB = await clientB.methods.add(1, 2); - resultA.should.equal(5); - resultB.should.equal(3); + expect(resultA).toBe(5); + expect(resultB).toBe(3); }); it('client should make an unsuccessful rpc call to a peer', async () => { const { transportA, transportB } = mockTransports(); - const clientA = rawr({ transport: transportA, handlers: { helloTest } }); + rawr({ transport: transportA, handlers: { helloTest } }); const clientB = rawr({ transport: transportB }); - clientA.should.be.an('object'); try { await clientB.methods.helloTest('bad'); + expect.fail('Should have thrown'); } catch (error) { - error.code.should.equal(9000); + expect(error.code).toBe(9000); } }); it('client handle an rpc under a specified timeout', async () => { const { transportA, transportB } = mockTransports(); - const clientA = rawr({ transport: transportA, handlers: { helloTest } }); + rawr({ transport: transportA, handlers: { helloTest } }); const clientB = rawr({ transport: transportB, timeout: 1000 }); - clientA.should.be.an('object'); const result = await clientB.methods.helloTest('luis'); - result.should.equal('hello, luis'); + expect(result).toBe('hello, luis'); }); it('client handle an rpc timeout', async () => { const { transportA, transportB } = mockTransports(); - const clientA = rawr({ transport: transportA, handlers: { helloTest } }); + rawr({ transport: transportA, handlers: { helloTest } }); const clientB = rawr({ transport: transportB, timeout: 10 }); - clientA.should.be.an('object'); try { await clientB.methods.helloTest('luis'); + expect.fail('Should have thrown'); } catch (error) { - error.code.should.equal(504); + expect(error.code).toBe(504); } }); - it('client should be able to send a notification to a server', (done) => { + it('client should be able to send a notification to a server', async () => { const { transportA, transportB } = mockTransports(); const clientA = rawr({ transport: transportA }); const clientB = rawr({ transport: transportB }); - clientA.notifications.ondoSomething((someData) => { - someData.should.equal('testing_notification'); - done(); + const received = new Promise((resolve) => { + clientA.notifications.ondoSomething((someData) => { + resolve(someData); + }); }); clientB.notifiers.doSomething('testing_notification'); + + const result = await received; + expect(result).toBe('testing_notification'); + }); +}); + +describe('websocket transport with Node.js ws style (on instead of addEventListener)', () => { + function mockNodeTransports() { + const a = new EventEmitter(); + const b = new EventEmitter(); + + // Node.js ws style - uses 'on' instead of 'addEventListener' + // and passes raw data instead of MessageEvent + a.send = (msg) => { + b.emit('message', msg); + }; + + b.send = (msg) => { + a.emit('message', msg); + }; + + const transportA = transports.websocket(a); + const transportB = transports.websocket(b); + + return { transportA, transportB }; + } + + it('should work with Node.js ws style sockets', async () => { + const { transportA, transportB } = mockNodeTransports(); + const clientA = rawr({ transport: transportA, handlers: { add } }); + const clientB = rawr({ transport: transportB, handlers: { subtract } }); + + const resultA = await clientA.methods.subtract(7, 2); + const resultB = await clientB.methods.add(1, 2); + expect(resultA).toBe(5); + expect(resultB).toBe(3); }); }); diff --git a/test/worker_transport.js b/test/worker_transport.test.js similarity index 64% rename from test/worker_transport.js rename to test/worker_transport.test.js index 24e1a48..3eabe95 100644 --- a/test/worker_transport.js +++ b/test/worker_transport.test.js @@ -1,28 +1,26 @@ -const chai = require('chai'); -const EventEmitter = require('eventemitter3'); -const rawr = require('../'); - -chai.should(); +import { describe, it, expect, beforeEach } from 'vitest'; +import EventEmitter from 'eventemitter3'; +import rawr, { transports } from '../index.js'; function mockTransports() { - const fakeWorkerRef = new EventEmitter(); //in the dom + const fakeWorkerRef = new EventEmitter(); // in the dom const fakeWorkerInstance = new EventEmitter(); // the worker thread fakeWorkerInstance.postMessage = (msg) => { - fakeWorkerRef.emit('message', {data: msg}); + fakeWorkerRef.emit('message', { data: msg }); }; fakeWorkerRef.postMessage = (msg) => { - fakeWorkerInstance.onmessage({data: msg}); + fakeWorkerInstance.onmessage({ data: msg }); }; fakeWorkerRef.addEventListener = (topic, cb) => { fakeWorkerRef.on(topic, cb); }; - const transportA = rawr.transports.worker(fakeWorkerRef); - global.self = fakeWorkerInstance; - const transportB = rawr.transports.worker(); + const transportA = transports.worker(fakeWorkerRef); + globalThis.self = fakeWorkerInstance; + const transportB = transports.worker(); return { transportA, transportB }; } @@ -48,13 +46,12 @@ function subtract(a, b) { return a - b; } -describe('worker', () => { - it('should make a client', (done) => { +describe('worker transport', () => { + it('should make a client', () => { const { transportA } = mockTransports(); const client = rawr({ transport: transportA }); - client.should.be.a('object'); - client.addHandler.should.be.a('function'); - done(); + expect(client).toBeTypeOf('object'); + expect(client.addHandler).toBeTypeOf('function'); }); it('client should make a successful rpc call to another peer', async () => { @@ -64,56 +61,59 @@ describe('worker', () => { const resultA = await clientA.methods.subtract(7, 2); const resultB = await clientB.methods.add(1, 2); - resultA.should.equal(5); - resultB.should.equal(3); + expect(resultA).toBe(5); + expect(resultB).toBe(3); }); it('client should make an unsuccessful rpc call to a peer', async () => { const { transportA, transportB } = mockTransports(); - const clientA = rawr({ transport: transportA, handlers: { helloTest } }); + rawr({ transport: transportA, handlers: { helloTest } }); const clientB = rawr({ transport: transportB }); - clientA.should.be.an('object'); try { await clientB.methods.helloTest('bad'); + expect.fail('Should have thrown'); } catch (error) { - error.code.should.equal(9000); + expect(error.code).toBe(9000); } }); it('client handle an rpc under a specified timeout', async () => { const { transportA, transportB } = mockTransports(); - const clientA = rawr({ transport: transportA, handlers: { helloTest } }); + rawr({ transport: transportA, handlers: { helloTest } }); const clientB = rawr({ transport: transportB, timeout: 1000 }); - clientA.should.be.an('object'); const result = await clientB.methods.helloTest('luis'); - result.should.equal('hello, luis'); + expect(result).toBe('hello, luis'); }); it('client handle an rpc timeout', async () => { const { transportA, transportB } = mockTransports(); - const clientA = rawr({ transport: transportA, handlers: { helloTest } }); + rawr({ transport: transportA, handlers: { helloTest } }); const clientB = rawr({ transport: transportB, timeout: 10 }); - clientA.should.be.an('object'); try { await clientB.methods.helloTest('luis'); + expect.fail('Should have thrown'); } catch (error) { - error.code.should.equal(504); + expect(error.code).toBe(504); } }); - it('client should be able to send a notification to a server', (done) => { + it('client should be able to send a notification to a server', async () => { const { transportA, transportB } = mockTransports(); const clientA = rawr({ transport: transportA }); const clientB = rawr({ transport: transportB }); - clientA.notifications.ondoSomething((someData) => { - someData.should.equal('testing_notification'); - done(); + const received = new Promise((resolve) => { + clientA.notifications.ondoSomething((someData) => { + resolve(someData); + }); }); clientB.notifiers.doSomething('testing_notification'); + + const result = await received; + expect(result).toBe('testing_notification'); }); }); diff --git a/transports/index.js b/transports/index.js index c7c4c06..dea33e3 100644 --- a/transports/index.js +++ b/transports/index.js @@ -1,11 +1,4 @@ -const mqtt = require('./mqtt'); -const socketio = require('./socketio'); -const websocket = require('./websocket'); -const worker = require('./worker'); - -module.exports = { - mqtt, - socketio, - websocket, - worker -}; \ No newline at end of file +export { default as mqtt } from './mqtt/index.js'; +export { default as socketio } from './socketio/index.js'; +export { default as websocket } from './websocket/index.js'; +export { default as worker } from './worker/index.js'; diff --git a/transports/mqtt/index.js b/transports/mqtt/index.js index 2856b23..7479f60 100644 --- a/transports/mqtt/index.js +++ b/transports/mqtt/index.js @@ -1,10 +1,21 @@ -const EventEmitter = require('eventemitter3'); +import EventEmitter from 'eventemitter3'; -function transport({ connection, subTopic, pubTopic, subscribe = true }) { +/** + * MQTT transport for rawr + * @param {object} options + * @param {object} options.connection - MQTT client connection + * @param {string} options.subTopic - Topic to subscribe to for incoming messages + * @param {string} options.pubTopic - Topic to publish outgoing messages to + * @param {boolean} [options.subscribe=true] - Whether to subscribe to subTopic + * @returns {EventEmitter} rawr-compatible transport + */ +export default function mqtt({ connection, subTopic, pubTopic, subscribe = true }) { const emitter = new EventEmitter(); + if (subscribe) { connection.subscribe(subTopic); } + connection.on('message', (topic, message) => { if (topic === subTopic) { try { @@ -13,14 +24,14 @@ function transport({ connection, subTopic, pubTopic, subscribe = true }) { emitter.emit('rpc', msg); } } catch (err) { - console.error(err); + // Not a JSON message, ignore } } }); + emitter.send = (msg) => { connection.publish(pubTopic, JSON.stringify(msg)); }; + return emitter; } - -module.exports = transport; diff --git a/transports/socketio/index.js b/transports/socketio/index.js index 6c72b46..6206c26 100644 --- a/transports/socketio/index.js +++ b/transports/socketio/index.js @@ -1,16 +1,25 @@ -const EventEmitter = require('eventemitter3'); +import EventEmitter from 'eventemitter3'; -function transport({ connection, subTopic, pubTopic }) { +/** + * Socket.IO transport for rawr + * @param {object} options + * @param {object} options.connection - Socket.IO client connection + * @param {string} options.subTopic - Event name to listen for incoming messages + * @param {string} options.pubTopic - Event name to emit outgoing messages + * @returns {EventEmitter} rawr-compatible transport + */ +export default function socketio({ connection, subTopic, pubTopic }) { const emitter = new EventEmitter(); + connection.on(subTopic, (msg) => { if (msg.method || (msg.id && ('result' in msg || 'error' in msg))) { emitter.emit('rpc', msg); } }); + emitter.send = (msg) => { connection.emit(pubTopic, msg); }; + return emitter; } - -module.exports = transport; diff --git a/transports/websocket/index.js b/transports/websocket/index.js index bb57927..5eec3f4 100644 --- a/transports/websocket/index.js +++ b/transports/websocket/index.js @@ -1,27 +1,54 @@ -const EventEmitter = require('eventemitter3'); +import EventEmitter from 'eventemitter3'; -function transport(socket, allowBinary = false) { +/** + * WebSocket transport for rawr + * Works with browser WebSocket API (addEventListener) and Node.js ws library (on) + * @param {WebSocket} socket - WebSocket instance + * @param {boolean} allowBinary - Allow binary messages (browser only) + * @returns {EventEmitter} rawr-compatible transport + */ +export default function websocket(socket, allowBinary = false) { const emitter = new EventEmitter(); - socket.addEventListener('message', async (evt) => { - let { data } = evt; - if (allowBinary && data instanceof Blob) { - data = await (new Response(data)).text().catch(() => null); + + const handleMessage = async (data) => { + // Handle both browser (MessageEvent) and Node.js ws (Buffer/string) formats + let str = data; + + // Browser MessageEvent + if (data && typeof data === 'object' && 'data' in data) { + str = data.data; + if (allowBinary && str instanceof Blob) { + str = await new Response(str).text().catch(() => null); + } } - if (typeof evt.data === 'string') { + + // Node.js ws Buffer + if (Buffer && Buffer.isBuffer(str)) { + str = str.toString(); + } + + if (typeof str === 'string') { try { - const msg = JSON.parse(evt.data); + const msg = JSON.parse(str); if (msg.method || (msg.id && ('result' in msg || 'error' in msg))) { emitter.emit('rpc', msg); } } catch (err) { - // wasn't a JSON message + // Not a JSON message, ignore } } - }); + }; + + // Support both browser WebSocket (addEventListener) and Node.js ws (on) + if (typeof socket.addEventListener === 'function') { + socket.addEventListener('message', handleMessage); + } else if (typeof socket.on === 'function') { + socket.on('message', handleMessage); + } + emitter.send = (msg) => { socket.send(JSON.stringify(msg)); }; + return emitter; } - -module.exports = transport; diff --git a/transports/worker/index.js b/transports/worker/index.js index e1ecb18..15567cb 100644 --- a/transports/worker/index.js +++ b/transports/worker/index.js @@ -1,5 +1,10 @@ -const EventEmitter = require('eventemitter3'); +import EventEmitter from 'eventemitter3'; +/** + * Worker transport for DOM side (main thread talking to worker) + * @param {Worker} webWorker - Web Worker instance + * @returns {EventEmitter} rawr-compatible transport + */ function dom(webWorker) { const emitter = new EventEmitter(); webWorker.addEventListener('message', (msg) => { @@ -14,6 +19,10 @@ function dom(webWorker) { return emitter; } +/** + * Worker transport for worker side (inside the worker) + * @returns {EventEmitter} rawr-compatible transport + */ function worker() { const emitter = new EventEmitter(); self.onmessage = (msg) => { @@ -28,15 +37,17 @@ function worker() { return emitter; } -function transport(webWorker) { +/** + * Worker transport - auto-detects DOM or worker context + * @param {Worker} [webWorker] - Web Worker instance (omit if inside worker) + * @returns {EventEmitter} rawr-compatible transport + */ +export default function transport(webWorker) { if (webWorker) { return dom(webWorker); } return worker(); } -// backwards compat -transport.dom = dom; -transport.worker = worker; - -module.exports = transport; +// Named exports for explicit usage +export { dom, worker };