├── index.js ├── .travis.yml ├── LICENSE ├── test ├── test-package.js ├── test-qrpc.js ├── test-qrpc-message.js ├── test-qrpc-server.js ├── test-qrpc-client.js ├── test-calls.js └── benchmark.js ├── package.json ├── lib ├── qrpc.js ├── qrpc-response.js ├── qrpc-message.js ├── qrpc-client.js └── qrpc-server.js ├── ChangeLog.md └── Readme.md /index.js: -------------------------------------------------------------------------------- 1 | var qrpc = require('./lib/qrpc.js') 2 | 3 | module.exports = qrpc 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | - 4 5 | - 6 6 | - 8 7 | - 10 8 | - 12 9 | - 13 10 | - 14 11 | - 15 12 | after_success: 13 | - if [ `node -p 'process.version.slice(0, 3)'` != "v8." ]; then exit; fi 14 | - npm install -g nyc@8.4.0 codecov coveralls 15 | - nyc --reporter lcov npm test && codecov 16 | - nyc report -r text-lcov | coveralls 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015-2022 Andras Radics 2 | andras at andrasq dot com 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | -------------------------------------------------------------------------------- /test/test-package.js: -------------------------------------------------------------------------------- 1 | /** 2 | * quick little rpc package 3 | * 4 | * Copyright (C) 2015-2017 Andras Radics 5 | * Licensed under the Apache License, Version 2.0 6 | */ 7 | 8 | 'use strict' 9 | 10 | var assert = require('assert') 11 | 12 | module.exports = { 13 | 'package should parse': function(t) { 14 | require('../package.json') 15 | t.done() 16 | }, 17 | 18 | 'package should load': function(t) { 19 | var rpc = require('../index.js') 20 | t.done() 21 | }, 22 | 23 | 'package should export expected functions': function(t) { 24 | var qrpc = require('../index.js') 25 | assert.equal(typeof qrpc.createServer, 'function') 26 | assert.equal(typeof qrpc.connect, 'function') 27 | t.done() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qrpc", 3 | "version": "1.2.0", 4 | "description": "very fast nodejs remote procedure call", 5 | "main": "index.js", 6 | "author": "Andras", 7 | "license": "Apache-2.0", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/andrasq/node-qrpc" 11 | }, 12 | "keywords": [ 13 | "quick", 14 | "very fast", 15 | "fast", 16 | "rpc", 17 | "remote", 18 | "procedure", 19 | "call" 20 | ], 21 | "scripts": { 22 | "test": "qnit test", 23 | "coverage": "nyc --reporter text --reporter lcov qnit test/test-*", 24 | "clean": "rm -rf .nyc_output coverage/" 25 | }, 26 | "bugs": { 27 | "url": "https://github.com/andrasq/node-qrpc/issues" 28 | }, 29 | "homepage": "https://github.com/andrasq/node-qrpc", 30 | "dependencies": { 31 | "qibl": "1.22.3" 32 | }, 33 | "devDependencies": { 34 | "qnit": "0.15.1" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/test-qrpc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * quick little rpc package 3 | * 4 | * Copyright (C) 2015-2017 Andras Radics 5 | * Licensed under the Apache License, Version 2.0 6 | */ 7 | 8 | 'use strict' 9 | 10 | var assert = require('assert') 11 | var QrpcServer = require('../lib/qrpc-server.js') 12 | var QrpcClient = require('../lib/qrpc-client.js') 13 | 14 | var qrpc = require('../lib/qrpc.js') 15 | 16 | module.exports = { 17 | 'should export createServer and connect': function(t) { 18 | var qrpc = require('../lib/qrpc.js') 19 | assert.equal(typeof qrpc.createServer, 'function') 20 | assert.equal(typeof qrpc.connect, 'function') 21 | t.done() 22 | }, 23 | 24 | 'createServer': { 25 | 'setUp': function(done) { 26 | this.server = qrpc.createServer() 27 | done() 28 | }, 29 | 30 | 'should return a QrpcServer having the expected methods': function(t) { 31 | var server = qrpc.createServer() 32 | assert(server instanceof QrpcServer) 33 | var expectedMethods = ['listen', 'addHandler', 'close', 'setSource'] 34 | for (var i in expectedMethods) { 35 | assert.equal(typeof this.server[expectedMethods[i]], 'function') 36 | } 37 | t.done() 38 | }, 39 | }, 40 | 41 | 'connect': { 42 | 'setUp': function(done) { 43 | this.client = qrpc.connect(80, 'localhost') 44 | done() 45 | }, 46 | 47 | 'should return a QrpcClient having the expected methods': function(t) { 48 | var client = qrpc.connect(80, 'localhost') 49 | assert(client instanceof QrpcClient) 50 | var expectedMethods = ['call', 'close', 'setTarget'] 51 | for (var i in expectedMethods) { 52 | assert.equal(typeof this.client[expectedMethods[i]], 'function') 53 | } 54 | t.done() 55 | }, 56 | }, 57 | } 58 | 59 | var util = require('util') 60 | var EventEmitter = require('events').EventEmitter 61 | function MockSocket( ) { 62 | EventEmitter.call(this) 63 | var self = this 64 | this._written = [] 65 | this.write = function(s) { self._written.push(s) } 66 | this.pause = function() { } 67 | this.resume = function() { } 68 | return this 69 | } 70 | util.inherits(MockSocket, EventEmitter) 71 | 72 | function createReplyChunk( written, reply ) { 73 | var msg = JSON.parse(written) 74 | var data = {v: 1, id: msg.id, m: reply} 75 | return JSON.stringify(data) + "\n" 76 | } 77 | -------------------------------------------------------------------------------- /lib/qrpc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * quick little rpc package 3 | * 4 | * Copyright (C) 2015-2017 Andras Radics 5 | * Licensed under the Apache License, Version 2.0 6 | */ 7 | 8 | 'use strict' 9 | 10 | var net = require('net') 11 | 12 | var QrpcMessage = require('./qrpc-message.js') 13 | var QrpcServer = require('./qrpc-server.js') 14 | var QrpcClient = require('./qrpc-client.js') 15 | var QrpcResponse = require('./qrpc-response.js') 16 | 17 | /* 18 | * the qrpc package 19 | */ 20 | module.exports = { 21 | MSG_REPLY: QrpcMessage.MSG_REPLY, 22 | MSG_LAST: QrpcMessage.MSG_LAST, 23 | MSG_ERROR: QrpcMessage.MSG_ERROR, 24 | 25 | QrpcServer: QrpcServer, 26 | QrpcClient: QrpcClient, 27 | 28 | /* 29 | * build an rpc server 30 | */ 31 | createServer: 32 | function createServer( options, onConnection ) { 33 | options = options || {} 34 | if (!onConnection && typeof options === 'function') { 35 | onConnection = options 36 | options = {} 37 | } 38 | // TODO: if caller closes the socket, still write all responses 39 | // TODO: must track whether closed, and end()-ing it after all responses have been sent 40 | // if (options.allowHalfOpen === undefined) options.allowHalfOpen = true 41 | var server = new this.QrpcServer(options) 42 | var netServer = net.createServer(options, function(socket) { 43 | // pipe data to the server for processing, writing responses back to socket 44 | server.setSource(socket, socket) 45 | // return the socket to the caller for socket config and tuning 46 | if (onConnection) onConnection(socket) 47 | }) 48 | server.setListenFunc(function(port, cb) { netServer.listen(port, cb) }) 49 | server.setCloseFunc(function() { netServer.close() }) 50 | netServer.on('error', function(err) { 51 | throw err 52 | }) 53 | return server 54 | }, 55 | 56 | /* 57 | * build an rpc client 58 | */ 59 | connect: 60 | function connect( port, host, callback ) { 61 | if (!callback && typeof host === 'function') { 62 | callback = host 63 | host = undefined 64 | } 65 | var options = (typeof port === 'object') ? port : { port: port, host: host } 66 | // try to handle pending replies before acting on server disconnect 67 | var socket = net.connect(options) 68 | var client = new this.QrpcClient(options) 69 | .setTarget(socket, socket) 70 | .setCloseFunc(function() { socket.end() }) 71 | // return the socket to the caller for socket config and tuning 72 | if (callback) socket.once('connect', function() { 73 | callback(socket) 74 | }) 75 | return client 76 | }, 77 | } 78 | -------------------------------------------------------------------------------- /ChangeLog.md: -------------------------------------------------------------------------------- 1 | 1.2.0 2 | - switch to using qibl for invoke, varargs and makeGetId. The internal id format is still 3 | monotonically increasing but is now shorter and with a finer time resolution. 4 | 5 | 1.1.8 6 | - avoid Buffer deprecation warnings 7 | 8 | 1.1.7 9 | - upgrade to latest mongoid-js@1.1.3 and qinvoke@0.11.3 10 | - test with more versions of node 11 | 12 | 1.1.6 13 | - optimize response 14 | 15 | 1.1.5 16 | - remove dead code (http_build_query et al) and related tests 17 | 18 | 1.1.4 19 | - fix utf8 (Buffer) input and output using StringDecoder 20 | - clean up `client.call`, require string handlerName 21 | - use `qinvoke.interceptCall` to `client.wrap` methods 22 | - streamline message encoding 23 | - streamline message decoding, optimize for m-at-endt messages, use scanInt to avoid a slice 24 | - benchmark 1K json objects 25 | - more unit tests 26 | 27 | 1.1.3 28 | - upgrade to qinvoke 0.11.0 for _copyError and _extractError 29 | so now only Errors are delivered as Error objects, others are just objects 30 | 31 | 1.1.2 32 | - unit test with qnit 33 | - move `qinvoke` into its own package 34 | - npm script targets `coverage` and `clean` 35 | - upgrade to mongoid-1.1.0 36 | 37 | 1.1.1 38 | - allow full utf8 strings in rpc method names 39 | - fix client.wrap() for multiple methods 40 | - benchmark wrapped method calls 41 | 42 | 1.1.0 43 | - client.wrap() method 44 | - server.wrap() method 45 | - retain Error object non-enumerable property status 46 | 47 | 1.0.4 48 | - fix the return of null errors 49 | - fix the passing and returning of falsy values 50 | 51 | 1.0.3 52 | - return non-object errors as-is 53 | 54 | 1.0.2 55 | - missing copyright notices 56 | 57 | 1.0.1 58 | - update docs to show Buffer usage 59 | - document json_encode / json_decode options 60 | - hold off on allowHalfOpen mode 61 | 62 | 1.0.0 63 | - updated readme 64 | 65 | 0.10.5 66 | - json_encode / json_decode options to server and client 67 | - bring over changes from "blobs" branch 68 | - make server handle calls coming in Buffers 69 | 70 | 0.10.3 71 | - speed up Buffer passing 72 | 73 | 0.10.0 74 | - send data Buffers in a binary-safe manner 75 | - benchmark 1k buffers 76 | 77 | 0.9.5 78 | - benchmark data retrieval 79 | 80 | 0.9.4 81 | - use utf8 encoding if possible to avoid splitting multi-byte chars 82 | - improved error detection, more unit tests 83 | 84 | 0.9.3 85 | - make server and client remove their listeners on close 86 | 87 | 0.9.2 88 | - next() in a handler returns (null, undefined) and invokes the client-side callback. 89 | next(undefined, undefined) is the same as end(), and just cleans up without the callback. 90 | - return error to client if no handler defined for call 91 | 92 | 0.9.1 93 | - normalize close() handling: closeFunc() does not take a callback, close() do 94 | - client.setCloseFunc method 95 | 96 | 0.9.0 97 | - addEndpoint renamed addHandlerNoResponse 98 | 99 | 0.8.0 100 | - addEndpoint method 101 | -------------------------------------------------------------------------------- /lib/qrpc-response.js: -------------------------------------------------------------------------------- 1 | /** 2 | * quick little rpc package 3 | * 4 | * Copyright (C) 2015-2017 Andras Radics 5 | * Licensed under the Apache License, Version 2.0 6 | */ 7 | 8 | 'use strict' 9 | 10 | var QrpcMessage = require('./qrpc-message.js') 11 | 12 | var MSG_REPLY = QrpcMessage.MSG_REPLY 13 | var MSG_ERROR = QrpcMessage.MSG_ERROR 14 | var MSG_LAST = QrpcMessage.MSG_LAST 15 | 16 | /* 17 | Qrpc message format: 18 | v: 1, // 1: json bundles 19 | b: 0000 // length of bson string w/o quotes at end, else omitted 20 | id: id, // unique call id to tag replies 21 | n: name, // call name, request only 22 | e: error // response only 23 | s: status // ok (write), end (end), err (server error; means end) 24 | m: message // call payload, request and response 25 | b: buffer // a binary blob (Buffer) 26 | */ 27 | 28 | function QrpcResponse( version, id, socket, message) { 29 | // ignore the version for now, has only ever been v:1 30 | //this.v = version // protocol version, always 1 31 | //this.v = 1 32 | this.id = id // request id. A request may get multiple responses 33 | this.socket = socket // any writable 34 | this.message = message 35 | this.ended = false 36 | } 37 | 38 | QrpcResponse.prototype = { 39 | _reportError: function(e) { console.log(e) }, 40 | 41 | configure: 42 | function configure( options ) { 43 | this._reportError = options.reportError 44 | return this 45 | }, 46 | 47 | write: 48 | function write( data, callback ) { 49 | if (data == null) return // send no null/undefined responses unless end() 50 | return this._send(MSG_REPLY, undefined, data, callback) 51 | }, 52 | 53 | end: 54 | function end( data, callback ) { 55 | if (!callback && typeof data === 'function') { 56 | var ret = this._send(MSG_LAST, undefined, undefined, data) 57 | } else { 58 | var ret = this._send(MSG_LAST, undefined, data, callback) 59 | } 60 | this.ended = true 61 | return ret 62 | }, 63 | 64 | // TODO: maybe use for a _sendmb method to send both m and b 65 | 66 | _send: 67 | function _send( status, error, data, callback ) { 68 | if (this.ended) { 69 | return this._reportSendAfterEnd(status, error, data, callback) 70 | } 71 | var reply = (data instanceof Buffer) 72 | ? { v: 1, id: this.id, n: undefined, e: error, s: status, m: undefined, b: data } 73 | : { v: 1, id: this.id, n: undefined, e: error, s: status, m: data, b: undefined } 74 | return this.socket.write(this.message.encode(reply) + "\n", callback) 75 | // TODO: throttle the stream if write returned false, until drained 76 | }, 77 | 78 | _reportSendAfterEnd: 79 | function _reportSendAfterEnd( status, error, data, callback ) { 80 | if ((status !== MSG_LAST || data !== undefined || error) && this._reportError) { 81 | // qrpc ignores it, but calling write/end after end() is a calling error, someone should know 82 | this._reportError(new Error("qrpc: handler tried to send after end()").stack) 83 | } 84 | }, 85 | 86 | // TODO: merge QrpcResponse into QrpcMessage, use message in both server and client 87 | 88 | } 89 | 90 | module.exports = QrpcResponse 91 | -------------------------------------------------------------------------------- /lib/qrpc-message.js: -------------------------------------------------------------------------------- 1 | /** 2 | * quick little rpc package 3 | * 4 | * Copyright (C) 2015-2017,2022 Andras Radics 5 | * Licensed under the Apache License, Version 2.0 6 | */ 7 | 8 | 'use strict' 9 | 10 | var qibl = require('qibl'); 11 | 12 | // fromBuf adapted from `qibl`. Wrapped in eval() for test coverage. 13 | var nodeMajor = parseInt(process.versions.node); 14 | var fromBuf = eval('nodeMajor >= 6 ? Buffer.from : Buffer'); 15 | 16 | /** 17 | * qrpc message coder 18 | * Options: 19 | * - json_encode - json stringify 20 | * - json_decode - json parse 21 | */ 22 | function QrpcMessage( options ) { 23 | options = options || {} 24 | if (options.json_encode) this.json_encode = options.json_encode 25 | if (options.json_decode) this.json_decode = options.json_decode 26 | 27 | //this.v = options.v || 1 28 | this.v = 1 29 | this.encode = this.encodeV1 30 | this.decode = this.decodeV1 31 | } 32 | 33 | // fast json stringification, based on `json-simple` 34 | function json_string(s) { 35 | if (s.length > 75) return JSON.stringify(s).slice(1, -1); 36 | for (var i=0; i= 127 || code === 0x5c || code === 0x22) return JSON.stringify(s).slice(1, -1); 39 | } 40 | return s; 41 | } 42 | 43 | QrpcMessage.prototype = { 44 | v: null, 45 | id: null, 46 | 47 | encode: null, 48 | decode: null, 49 | // json-simple is 5-10% faster for some use cases 50 | json_encode: JSON.stringify, 51 | json_decode: JSON.parse, 52 | 53 | // convert the error with its non-enumerable fields into a serializable object 54 | _copyError: function _copyError( err ) { 55 | var ret = err instanceof Error ? qibl.merge(qibl.errorToObject(err), {_isError__: 1}) : err; 56 | return ret; 57 | }, 58 | 59 | // convert the error object back into an Error instance 60 | _extractError: function _extractError( obj ) { 61 | var err = obj._isError__ ? qibl.objectToError(obj) : obj; 62 | delete err._isError__; 63 | return err; 64 | }, 65 | 66 | // faster to string concat than to json encode a temp object 67 | // Buffer passing is 50% faster with this hand-crafted bundle 68 | // blob base64 string length is sent early, with blob as the last field 69 | // blob itself is sent as the very last field 70 | // must deliver `null` errors for correct last-response detection 71 | encodeV1: 72 | function encodeV1( obj ) { 73 | var e, b 74 | if (obj.e !== undefined) e = this.json_encode(this._copyError(obj.e)) 75 | if (obj.b) b = obj.b.toString('base64') 76 | 77 | var s = '{"v":1' 78 | 79 | if (b) s += ',"b":' + b.length 80 | if (obj.id !== undefined) s += ',"id":"' + obj.id + '"' 81 | if (obj.n !== undefined) s += ',"n":"' + json_string(obj.n) + '"' 82 | if (e) s += ',"e":' + e 83 | if (obj.s) s += ',"s":"' + obj.s + '"' 84 | if (obj.m !== undefined) s += ',"m":' + this.json_encode(obj.m) 85 | if (b) s += ',"b":"' + b + '"' 86 | s += '}' 87 | 88 | return s 89 | }, 90 | 91 | decodeV1: 92 | function decodeV1( str ) { 93 | var ret, b, m 94 | // faster to detach the blob before json decoding 95 | if (str.slice(0, 11) === '{"v":1,"b":') { 96 | // json strings containing blobs start as {"v":1,"b":NNNNN... } 97 | // blob is at the very end of the json string 98 | var blobLength = scanInt(str, 11) // extract NN from '{"v":1,"b":NN' 99 | var blobStart = str.length - 2 - blobLength // blob always at end of bundle 100 | var end = blobStart - 6 // blob always prefaced with ',"b":"' 101 | if (end > 0) { 102 | b = str.slice(blobStart, blobStart + blobLength) 103 | str = str.slice(0, end) + '}' 104 | } 105 | } 106 | else { 107 | // opportunistically decode m as if it were the last field 108 | // slightly faster to decode the message separately from the envelope (parallel 3% faster, series 1.5% slower) 109 | var mStart = str.indexOf(',"m":') 110 | if (mStart > 0) { 111 | m = this._decodeJson(str.slice(mStart + 5, -1)) 112 | // if error decoding, try again as part of the bundle 113 | if (m instanceof Error) m = undefined 114 | else str = str.slice(0, mStart) + "}" 115 | } 116 | } 117 | ret = this._decodeJson(str) 118 | if (ret instanceof Error) return ret 119 | if (ret.e) ret.e = this._extractError(ret.e) 120 | if (m !== undefined) ret.m = m 121 | if (b) ret.b = b 122 | if (ret.b) ret.b = fromBuf(ret.b, 'base64') 123 | return ret 124 | }, 125 | 126 | _decodeJson: 127 | function _decodeJson( str ) { 128 | try { return this.json_decode(str) } 129 | catch (err) { 130 | if (str.indexOf(',"m":undefined') > 0) { 131 | // JSON cannot pass undefined values, fix them here 132 | str = str.replace(',"m":undefined', '') 133 | return this._decodeJson(str) 134 | } 135 | err.message = "qrpc: json parse error: " + err.message 136 | return err 137 | } 138 | }, 139 | } 140 | 141 | function scanInt( str, p ) { 142 | var n = 0, ch 143 | while ((ch = str.charCodeAt(p)) >= 0x30 && ch <= 0x39) { 144 | n = n * 10 + (ch - 0x30) 145 | p++ 146 | } 147 | return n 148 | } 149 | 150 | QrpcMessage.MSG_REPLY = 'ok' 151 | QrpcMessage.MSG_LAST = 'end' 152 | QrpcMessage.MSG_ERROR = 'err' 153 | 154 | module.exports = QrpcMessage 155 | -------------------------------------------------------------------------------- /test/test-qrpc-message.js: -------------------------------------------------------------------------------- 1 | /** 2 | * quick little rpc package 3 | * 4 | * Copyright (C) 2015-2017 Andras Radics 5 | * Licensed under the Apache License, Version 2.0 6 | */ 7 | 8 | 'use strict' 9 | 10 | var QrpcMessage = require('../lib/qrpc-message.js') 11 | 12 | module.exports = { 13 | 'v1': { 14 | setUp: function(done) { 15 | this.data = { a:1, b:2.5, c:"three", d:[4,5,6], e:{a:1,b:2} } 16 | this.blob = new Buffer("blobblob") 17 | this.allFields = { 18 | id: "id-1234", 19 | n: "name", 20 | m: this.data, 21 | e: new Error("test error"), 22 | s: 'test', 23 | b: this.blob, 24 | } 25 | this.message = new QrpcMessage() 26 | done() 27 | }, 28 | 29 | 'constructor': { 30 | 'should use provided json_encode function': function(t) { 31 | var called = false, obj = { m: "foobar", b: "boo" } 32 | function encoder(obj) { called = true; return JSON.stringify(obj) } 33 | var message = new QrpcMessage({ json_encode: encoder }) 34 | var bundle = message.encodeV1(obj) 35 | t.ok(called) 36 | t.done() 37 | }, 38 | 39 | 'should use provided json_decode function': function(t) { 40 | var called = false, obj = { a: 123, m: "foobar" } 41 | function decoder(json) { called = true; return JSON.parse(json) } 42 | var message = new QrpcMessage({ json_decode: decoder }) 43 | var bundle = JSON.stringify(obj) 44 | var json = message.decodeV1(bundle) 45 | t.ok(called) 46 | t.done() 47 | }, 48 | }, 49 | 50 | 'encode': { 51 | 'should return versioned json bundle': function(t) { 52 | var bundle = this.message.encodeV1({m: {a:1, b:2}}) 53 | t.assert(bundle.match(/^{.*}$/)) 54 | t.assert(bundle.match(/"v":1/)) 55 | t.done() 56 | }, 57 | 58 | 'should encode all data types': function(t) { 59 | var bundle = this.message.encodeV1({m: this.data}) 60 | var json = JSON.parse(bundle) 61 | t.deepEqual(json.m, this.data) 62 | t.done() 63 | }, 64 | 65 | 'should encode very long method names': function(t) { 66 | var msg = {v:1, n:"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", m:null} 67 | var bundle = this.message.encodeV1(msg) 68 | var json = JSON.parse(bundle) 69 | t.deepEqual(json, msg) 70 | t.done() 71 | }, 72 | 73 | 'should encode utf8 method names': function(t) { 74 | var msg = {v:1, n:"Hello, \xff\xc8 world", m:null} 75 | var bundle = this.message.encodeV1(msg) 76 | var json = JSON.parse(bundle) 77 | t.deepEqual(json, msg) 78 | t.done() 79 | }, 80 | 81 | 'should include error': function(t) { 82 | var bundle = this.message.encodeV1({e: new Error("test error")}) 83 | t.assert(bundle.match(/"message":"test error"/)) 84 | t.assert(bundle.match(/"stack":.Error: test error\\n/)) 85 | t.done() 86 | }, 87 | 88 | 'should encode blobs as base64': function(t) { 89 | var bundle = this.message.encodeV1({b: new Buffer("blobblob")}) 90 | t.assert(bundle.match(/"b":"YmxvYmJsb2I="/)) 91 | t.done() 92 | }, 93 | 94 | 'should include all fields': function(t) { 95 | var bundle = this.message.encodeV1(this.allFields) 96 | var json = JSON.parse(bundle) 97 | for (var i in this.allFields) { 98 | if (i === 'e') { 99 | t.equal(json.e.message, this.allFields.e.message) 100 | t.equal(json.e.stack.slice(0, 18), "Error: test error\n") 101 | } 102 | else if (i === 'b') { 103 | t.assert(typeof json.b === 'string') 104 | } 105 | else t.deepEqual(json[i], this.allFields[i], "field " + i) 106 | } 107 | t.equal(json.v, 1) 108 | t.equal(json.id, "id-1234") 109 | t.equal(json.n, "name") 110 | t.deepEqual(json.m, this.data) 111 | t.equal(json.e.message, "test error") 112 | t.equal(json.e.stack.slice(0, 18), "Error: test error\n") 113 | t.assert(json.b) 114 | t.done() 115 | }, 116 | }, 117 | 118 | 'decode': { 119 | 'should decode all data types': function(t) { 120 | var bundle = this.message.encodeV1({m: this.data}) 121 | var json = this.message.decodeV1(bundle) 122 | t.deepEqual(json.m, this.data) 123 | t.done() 124 | }, 125 | 126 | 'should decode error to Error object': function(t) { 127 | var bundle = this.message.encodeV1({e: new Error("test error")}) 128 | var json = this.message.decodeV1(bundle) 129 | t.assert(json.e instanceof Error) 130 | t.equal(json.e.message, "test error") 131 | t.equal(json.e.stack.slice(0, 18), "Error: test error\n") 132 | t.done() 133 | }, 134 | 135 | 'should decode blobs to Buffers': function(t) { 136 | var bundle = this.message.encodeV1({b: this.blob}) 137 | var obj = this.message.decodeV1(bundle) 138 | t.deepEqual(obj.b, this.blob) 139 | t.done() 140 | }, 141 | 142 | 'should decode all fields': function(t) { 143 | var bundle = this.message.encodeV1(this.allFields) 144 | var json = this.message.decodeV1(bundle) 145 | for (var i in this.allFields) { 146 | if (i === 'e') { 147 | t.equal(json.e.message, this.allFields.e.message) 148 | t.equal(json.e.stack.slice(0, 18), "Error: test error\n") 149 | } 150 | else t.deepEqual(json[i], this.allFields[i]) 151 | } 152 | t.done() 153 | }, 154 | 155 | 'errors': { 156 | 'should decode malformed blob-length': function(t) { 157 | var bundle = '{"v":1,"b":999999,"m":{"a":123},"b":"////"}' 158 | var json = this.message.decodeV1(bundle) 159 | t.deepEqual(json.m, {a: 123}) 160 | t.deepEqual(json.b, new Buffer("\xff\xff\xff", 'binary')) 161 | t.done() 162 | }, 163 | 164 | 'should return error on malformed m': function(t) { 165 | var bundle = '{"v":1,"m":{a}}' 166 | var json = this.message.decodeV1(bundle) 167 | t.ok(json instanceof Error) 168 | t.done(); 169 | }, 170 | 171 | 'should tolerate "m":undefined': function(t) { 172 | var json = this.message.decodeV1('{"v":1,"m":undefined,"e":{"err":1}}') 173 | t.deepEqual(json.e, {err:1}) 174 | t.done() 175 | } 176 | } 177 | }, 178 | }, 179 | } 180 | -------------------------------------------------------------------------------- /test/test-qrpc-server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * quick little rpc package 3 | * 4 | * Copyright (C) 2015-2017 Andras Radics 5 | * Licensed under the Apache License, Version 2.0 6 | */ 7 | 8 | 'use strict' 9 | 10 | var QrpcServer = require('../lib/qrpc-server.js') 11 | 12 | module.exports ={ 13 | 'beforeEach': function(done) { 14 | this.server = new QrpcServer() 15 | this.socket = new MockSocket() 16 | done() 17 | }, 18 | 19 | 'listen method': { 20 | 'error handling': { 21 | 'should throw error without listenFunc if no callback': function(t) { 22 | try { this.server.listen(); t.fail() } 23 | catch (e) { t.ok(true) } 24 | t.done() 25 | }, 26 | 27 | 'should return error without listenFunc with callback': function(t) { 28 | t.expect(1) 29 | this.server.listen(null, function(err) { 30 | t.assert(err instanceof Error) 31 | t.done() 32 | }) 33 | }, 34 | 35 | 'should return error if already listening': function(t) { 36 | this.server.setListenFunc(function(port, cb){ return cb() }) 37 | t.expect(2) 38 | var self = this 39 | this.server.listen(0, function(err) { 40 | t.ifError(err) 41 | self.server.listen(0, function(err) { 42 | t.assert(err instanceof Error) 43 | t.done() 44 | }) 45 | }) 46 | }, 47 | 48 | 'should return error if no handler defined': function(t) { 49 | var call = { v: 1, id: 1, n: 'nonesuch' } 50 | var written = null 51 | var source = { read: function(){ return written ? null : JSON.stringify(call) + "\n" } } 52 | var writable = { write: function(s, cb) { written = s; cb && cb() } } 53 | this.server.setSource(source, writable) 54 | var self = this 55 | setTimeout(function() { 56 | t.assert(written) 57 | t.assert(JSON.parse(written).e.message.indexOf('no handler') > 0) 58 | self.server.setCloseFunc(function(){}) 59 | self.server.close() 60 | t.done() 61 | }, 5) 62 | }, 63 | }, 64 | 65 | 'should call listenFunc': function(t) { 66 | var called = false 67 | var listenFunc = function(port, cb) { called = true; cb() } 68 | this.server.setListenFunc(listenFunc) 69 | this.server.listen(0, function(err) { 70 | t.equal(called, true) 71 | t.done() 72 | }) 73 | }, 74 | }, 75 | 76 | 'close method': { 77 | 'should return error without closeFunc': function(t) { 78 | this.server.close(function(err) { 79 | t.assert(err instanceof Error) 80 | t.done() 81 | }) 82 | }, 83 | 84 | 'should call server.close if listening': function(t) { 85 | var closed = false 86 | var closeFunc = function(cb) { closed = true } 87 | var listenFunc = function(port, cb) { cb() } 88 | var server = this.server 89 | server.setListenFunc(listenFunc) 90 | server.setCloseFunc(closeFunc) 91 | server.listen(0, function(err) { 92 | server.close(); 93 | t.equal(closed, true) 94 | t.done() 95 | }) 96 | }, 97 | }, 98 | 99 | 'addHandler method': { 100 | 'should accept name and function': function(t) { 101 | t.equal(this.server.handlers['test1'], undefined) 102 | var fn = function(){} 103 | this.server.addHandler('test1', fn) 104 | t.equal(this.server.handlers['test1'], fn) 105 | t.done() 106 | }, 107 | 108 | 'should throw error if not a function': function(t) { 109 | try { this.server.addHandler('test', 1); t.fail() } 110 | catch (err) { t.ok(true) } 111 | t.done() 112 | }, 113 | 114 | 'should consume newline terminated JSON messages': function(t) { 115 | t.expect(1) 116 | this.server.addHandler('test', function(req, res, next) { 117 | t.deepEqual(req.m, {a:1, b:2}) 118 | t.done() 119 | }) 120 | var msg = JSON.stringify({v: 1, id: 1, n: 'test', m: {a:1, b:2}}) + "\n" 121 | this.server.onData('', msg) 122 | }, 123 | }, 124 | 125 | 'addHandlerNoResponse method': { 126 | 'should accept name and function': function(t) { 127 | t.equal(this.server.handlers['test1'], undefined) 128 | var fn = function(){} 129 | this.server.addHandler('test1', fn) 130 | t.equal(this.server.handlers['test1'], fn) 131 | t.done() 132 | }, 133 | 134 | 'should throw error if not a function': function(t) { 135 | try { this.server.addHandler('test', 1); t.fail() } 136 | catch (err) { t.ok(true) } 137 | t.done() 138 | }, 139 | 140 | 'should consume newline terminated JSON messages': function(t) { 141 | t.expect(1) 142 | this.server.addHandler('test', function(req, res, next) { 143 | t.deepEqual(req.m, {a:1, b:2}) 144 | t.done() 145 | }) 146 | var msg = JSON.stringify({v: 1, id: 1, n: 'test', m: {a:1, b:2}}) + "\n" 147 | this.server.onData('', msg) 148 | }, 149 | 150 | }, 151 | 152 | 'wrap method': { 153 | 'should add handlers': function(t) { 154 | this.server.wrap({test: function(){}}) 155 | t.equal(typeof this.server.handlers['test'], 'function') 156 | t.done() 157 | }, 158 | 159 | 'should prefix handler names': function(t) { 160 | this.server.wrap({test: function(){}}, {prefix: 'xy_'}) 161 | t.equal(typeof this.server.handlers['xy_test'], 'function') 162 | t.done() 163 | }, 164 | 165 | 'should pass all call args to handler and return all response args to caller': function(t) { 166 | var args = null, reply = [] 167 | this.server.wrap({ 168 | test: function() { 169 | args = arguments 170 | arguments[arguments.length - 1](new Error("testErr"), 4, 5, 'six') 171 | } 172 | }) 173 | var msg = JSON.stringify({ v: 1, id: 1, n: 'test', m: [1, 2, 'three']}) + "\n" 174 | this.server.onData('', msg, {write: function(m){ reply.push(m) }}) 175 | setTimeout(function() { 176 | t.equal(typeof args[3], 'function') 177 | t.equal(args[0], 1) 178 | t.equal(args[1], 2) 179 | t.equal(args[2], 'three') 180 | var msg = JSON.parse(reply[0]) 181 | t.equal(msg.m[0].message, "testErr") 182 | t.equal(msg.m[1], 4) 183 | t.equal(msg.m[2], 5) 184 | t.equal(msg.m[3], 'six') 185 | t.done() 186 | }, 5) 187 | }, 188 | }, 189 | } 190 | 191 | var util = require('util') 192 | var EventEmitter = require('events').EventEmitter 193 | function MockSocket( ) { 194 | EventEmitter.call(this) 195 | var self = this 196 | this._written = [] 197 | this.write = function(s) { self._written.push(s) } 198 | this.pause = function() { } 199 | this.resume = function() { } 200 | return this 201 | } 202 | util.inherits(MockSocket, EventEmitter) 203 | 204 | function createReplyChunk( written, reply, error ) { 205 | var msg = JSON.parse(written) 206 | var data = { v: 1, id: msg.id, m: reply, e: error } 207 | return JSON.stringify(data) + "\n" 208 | } 209 | -------------------------------------------------------------------------------- /test/test-qrpc-client.js: -------------------------------------------------------------------------------- 1 | /** 2 | * quick little rpc package 3 | * 4 | * Copyright (C) 2015-2017,2022 Andras Radics 5 | * Licensed under the Apache License, Version 2.0 6 | */ 7 | 8 | 'use strict' 9 | 10 | var QrpcClient = require('../lib/qrpc-client.js') 11 | 12 | module.exports ={ 13 | 'setUp': function(done) { 14 | this.client = new QrpcClient() 15 | this.socket = new MockSocket() 16 | this.client.setTarget(this.socket, this.socket) 17 | done() 18 | }, 19 | 20 | 'methods': function(t) { 21 | t.assert(this.client.setTarget) 22 | t.assert(this.client.setCloseFunc) 23 | t.assert(this.client.close) 24 | t.assert(this.client.call) 25 | t.done() 26 | }, 27 | 28 | 'setTarget': { 29 | 'should return self': function(t) { 30 | var fakeTarget = { write: function(s, cb) { cb() } } 31 | var fakeReadable = { read: function(n, cb) { if (!cb && typeof n === 'function') cb = n; if (cb) cb() } } 32 | var ret = this.client.setTarget(fakeTarget, fakeReadable) 33 | t.equal(ret, this.client) 34 | t.done() 35 | }, 36 | }, 37 | 38 | 'setCloseFunc': { 39 | 'should return self': function(t) { 40 | var ret = this.client.setCloseFunc(function(){}) 41 | t.equal(ret, this.client) 42 | t.done() 43 | }, 44 | }, 45 | 46 | 'call method': { 47 | 'should write newline terminated v1 JSON message to socket': function(t) { 48 | var socket = new MockSocket() 49 | this.client.setTarget(socket, socket) 50 | this.client.call('test', {a:1, b:2}) 51 | t.equal(socket._written[0].slice(-1), "\n") 52 | var msg = JSON.parse(socket._written[0]) 53 | t.assert(msg.id.match(/[0-9a-z]{13}/)) 54 | delete msg.id 55 | t.deepEqual(msg, {v: 1, n: 'test', m: {a:1, b:2}}) 56 | t.equal(msg.v, 1) 57 | t.equal(msg.n, 'test') 58 | t.done() 59 | }, 60 | 61 | 'should invoke callback on response': function(t) { 62 | var socket = new MockSocket() 63 | this.client.setTarget(socket, socket) 64 | this.client.call('test', {a:2}, function(err, reply) { 65 | t.deepEqual(reply, {reply: 'ok'}) 66 | t.done() 67 | }) 68 | socket.emit('data', createReplyChunk(socket._written[0], {reply: 'ok'})) 69 | }, 70 | 71 | 'should return Error on error response': function(t) { 72 | var self = this 73 | var socket = new MockSocket() 74 | this.client.setTarget(socket, socket) 75 | var errorObject = new Error("oops") 76 | 77 | // node v0.8 and v0.10 errors have two undefined own properties set: `arguments` and `type` 78 | // JSON.stringify can not pass undefined values, so we do not receive them. 79 | // Delete them to make this test pass under node-v0.10.42 80 | var errorObjectProperties = Object.getOwnPropertyNames(errorObject); 81 | for (var i=0; i>> 0 184 | this.server.addHandler('ping', function(req, res, next) { 185 | next(null, data) 186 | res.write(1) 187 | res.end(2) 188 | }) 189 | var received = [] 190 | this.client.call('ping', function(err, ret) { 191 | t.ifError(err) 192 | received.push(ret) 193 | if (received.length > 1) t.fail() 194 | }) 195 | setTimeout(function() { t.done() }, 2) 196 | }, 197 | 198 | 'next() should return error': function(t) { 199 | var data = new Error(Math.random() * 0x1000000 >>> 0) 200 | this.server.addHandler('ping', function(req, res, next) { 201 | next(data) 202 | }) 203 | this.client.call('ping', function(err, ret) { 204 | t.assert(err instanceof Error) 205 | // Error objects are not iterable, cannot be compared with deepEqual 206 | t.equal(err.message, data.message) 207 | t.equal(err.stack, data.stack) 208 | t.done() 209 | }) 210 | }, 211 | 212 | 'end() should return data': function(t) { 213 | var data = Math.random() * 0x1000000 >>> 0 214 | this.server.addHandler('ping', function(req, res, next) { 215 | res.end(data) 216 | }) 217 | this.client.call('ping', function(err, ret) { 218 | t.ifError(err) 219 | t.deepEqual(ret, data) 220 | t.done() 221 | }) 222 | }, 223 | 224 | 'end() should return just one data item': function(t) { 225 | var data = Math.random() * 0x1000000 >>> 0 226 | this.server.addHandler('ping', function(req, res, next) { 227 | if (res.configure) res.configure({reportError: false}) // suppress "send after end()" warning 228 | res.end(data) 229 | res.write(1) 230 | res.end(2) 231 | }) 232 | var i, received = [] 233 | this.client.call('ping', function(err, ret) { 234 | t.ifError(err) 235 | t.deepEqual(ret, data) 236 | received.push(ret) 237 | if (received.length > 1) t.fail() 238 | }) 239 | setTimeout(function(){ t.done() }, 2) 240 | }, 241 | 242 | 'write() should return data': function(t) { 243 | var data = Math.random() * 0x1000000 >>> 0 244 | this.server.addHandler('ping', function(req, res, next) { 245 | res.write(data) 246 | res.end() 247 | }) 248 | this.client.call('ping', function(err, ret) { 249 | t.ifError(err) 250 | t.deepEqual(ret, data) 251 | t.done() 252 | }) 253 | }, 254 | 255 | 'write should return multiple data items': function(t) { 256 | var data = Math.random() * 0x1000000 >>> 0 257 | this.server.addHandler('ping', function(req, res, next) { 258 | res.write(data) 259 | res.write(data) 260 | res.write(data) 261 | res.end() 262 | }) 263 | var i, received = [] 264 | this.client.call('ping', function(err, ret) { 265 | t.ifError(err) 266 | t.deepEqual(ret, data) 267 | received.push(ret) 268 | if (received.length === 3) { 269 | t.done() 270 | } 271 | }) 272 | } 273 | }, 274 | } 275 | -------------------------------------------------------------------------------- /lib/qrpc-client.js: -------------------------------------------------------------------------------- 1 | /** 2 | * quick little rpc package 3 | * 4 | * Copyright (C) 2015-2017,2022 Andras Radics 5 | * Licensed under the Apache License, Version 2.0 6 | */ 7 | 8 | 'use strict' 9 | 10 | var net = require('net') 11 | var qibl = require('qibl'); 12 | var StringDecoder = require('string_decoder').StringDecoder 13 | var EventEmitter = require('events').EventEmitter 14 | var QrpcMessage = require('./qrpc-message.js') 15 | 16 | var setImmediate = global.setImmediate || process.nextTick 17 | var getQuickId = qibl.makeGetId(); 18 | var invoke = qibl.invoke; 19 | 20 | /** 21 | * Create a qrpc client. 22 | * Options: 23 | * - json_encode - json stringify 24 | * - json_decode - json parse 25 | */ 26 | function QrpcClient( options ) { 27 | options = options || {} 28 | this.chunks = new Array() 29 | this.callbacks = {} 30 | this.message = new QrpcMessage({ 31 | v: options.v || 1, 32 | json_encode: options.json_encode, 33 | json_decode: options.json_decode, 34 | }) 35 | if (options.socket) this.setTarget(socket, socket) 36 | } 37 | 38 | QrpcClient.prototype = { 39 | target: null, 40 | readable: null, 41 | chunks: null, 42 | callbacks: null, 43 | message: null, 44 | _connected: false, 45 | _closeFunc: null, 46 | 47 | setTarget: 48 | function setTarget( target, readable ) { 49 | var self = this 50 | var decoder = new StringDecoder() 51 | if (!target || !readable) throw new Error("setTarget requires both target and readable") 52 | this.target = target 53 | this.readable = readable 54 | this._connected = true 55 | // TODO: handle pipables with a pipe, not with pause / resume 56 | if (typeof readable.setEncoding === 'function') readable.setEncoding('utf8') 57 | if (readable instanceof EventEmitter) { 58 | var onData = function(chunk) { 59 | if (typeof chunk !== 'string') chunk = decoder.write(chunk) 60 | self.chunks.push(chunk) 61 | if (self.chunks.length > 5) readable.pause() 62 | self._deliverResponses(function() { 63 | readable.resume() 64 | }) 65 | } 66 | readable.on('data', onData) 67 | 68 | var onEnd = function() { 69 | // remote sent a FIN packet 70 | self._abortAllCalls(new Error("unexpected end on response channel")) 71 | // FIXME: this is an error, no more replies can be received 72 | } 73 | readable.on('end', onEnd) 74 | 75 | var onError = function(err) { 76 | // socket error, close is called immediately after 77 | self._abortAllCalls(err) 78 | } 79 | readable.on('error', onError) 80 | 81 | var onClose = function() { 82 | if (self._connected) self._abortAllCalls(new Error("unexpected close of reply stream")) 83 | readable.emit('qrpcDetach') 84 | // FIXME: this is an error, no more replies can be received 85 | } 86 | readable.on('close', onClose) 87 | 88 | var onQrpcDetach = function() { 89 | readable.removeListener('data', onData) 90 | readable.removeListener('end', onEnd) 91 | readable.removeListener('error', onError) 92 | readable.removeListener('close', onClose) 93 | readable.removeListener('qrpcDetach', onQrpcDetach) 94 | } 95 | readable.on('qrpcDetach', onQrpcDetach) 96 | } 97 | else { 98 | setImmediate(function pollResponses( ) { 99 | var chunk = readable.read(100000) 100 | if (chunk && chunk.length > 0) { 101 | if (typeof chunk !== 'string') chunk = decoder.write(chunk) 102 | self.chunks.push(chunk) 103 | self._deliverResponses(function() { 104 | setImmediate(pollResponses) 105 | }) 106 | } 107 | else if (this._connected) { 108 | var poller = setTimeout(pollResponses, 2) 109 | if (poller.unref) poller.unref() 110 | } 111 | }) 112 | } 113 | if (target instanceof EventEmitter) { 114 | var onError = function(err) { 115 | // sockets do not report write errors in the callback, listen for them 116 | self._abortAllCalls(err) 117 | } 118 | target.on('error', onError) 119 | 120 | var onDrain = function() { 121 | // write buffer empty 122 | // TODO: throttle buffering here, or let socket take care of it? 123 | } 124 | target.on('drain', onDrain) 125 | 126 | var onClose = function() { 127 | target.emit('qrpcDetach') 128 | } 129 | target.on('close', onClose) 130 | 131 | var onQrpcDetach = function() { 132 | target.removeListener('error', onError) 133 | target.removeListener('drain', onDrain) 134 | target.removeListener('close', onClose) 135 | target.removeListener('qrpcDetach', onQrpcDetach) 136 | } 137 | target.on('qrpcDetach', onQrpcDetach) 138 | } 139 | return this 140 | }, 141 | 142 | setCloseFunc: 143 | function setCloseFunc( closeFunc ) { 144 | this._closeFunc = closeFunc 145 | return this 146 | }, 147 | 148 | close: 149 | function close( callback ) { 150 | // TODO: deal only with event emitters 151 | if (this._connected) { 152 | this._connected = false 153 | if (this.target instanceof EventEmitter) this.target.emit('qrpcDetach') 154 | if (this.target.end === 'function') this.target.end() 155 | if (this.readable instanceof EventEmitter) this.readable.emit('qrpcDetach') 156 | if (typeof this.readable.end === 'function') this.readable.end() 157 | 158 | // send a FIN packet 159 | // TODO: check that FIN is sent only after all pending data is written 160 | if (this._closeFunc) this._closeFunc() 161 | } 162 | if (callback) callback() 163 | }, 164 | 165 | call: 166 | function call( handlerName, data, callback ) { 167 | var id = getQuickId() 168 | if (typeof handlerName !== 'string') { 169 | return callback(new Error("handler name must be a string")) 170 | } 171 | if (!callback) { 172 | if (typeof data === 'function') { 173 | callback = data; data = undefined 174 | this.callbacks[id] = callback 175 | } 176 | } else { 177 | if (typeof callback !== 'function') throw new Error("callback must be a function") 178 | this.callbacks[id] = callback 179 | } 180 | var envelope = { v: 1, id: id, n: String(handlerName), m: undefined, b: undefined, e: undefined, s: undefined } 181 | data instanceof Buffer ? envelope.b = data : envelope.m = data 182 | return this.target.write(this.message.encode(envelope) + "\n") 183 | // note: writes are buffered, write/socket errors show up at socket.on('error') and not here 184 | }, 185 | 186 | wrap: 187 | function wrap( object, methods, options ) { 188 | if (options === undefined && methods && !Array.isArray(methods)) { options = methods ; methods = null } 189 | if (typeof object !== 'object') throw new Error("not an object") 190 | if (!options) options = {} 191 | if (!methods) methods = Object.keys(object) 192 | 193 | var self = this, caller = {} 194 | for (var i=0; i 0 && typeof av[av.length-1] === 'function') ? av.pop() : null 201 | self.call(method, av, function(err, args) { 202 | if (cb) err ? cb(err) : invoke(cb, args) 203 | }) 204 | }) 205 | })(prefix + method) 206 | } 207 | return caller 208 | 209 | function hashToStruct( hash ) { 210 | // assigning to a function prototype converts the hash to a struct 211 | // a try-catch block disables optimization, to prevent these statements from being optimized away 212 | // newer V8 optimizes even with try/catch, so pass arguments to a function too 213 | function f () {} 214 | f.prototype = Array.prototype.slice.call(arguments, 0) && hash 215 | try { return f.prototype } catch (err) { } 216 | } 217 | }, 218 | 219 | _deliverResponses: 220 | function _deliverResponses( doneDelivering ) { 221 | var start = 0, end = 0 222 | // faster to string concat than to join 223 | // TODO: concat chunks as they arrive, not here 224 | var data = this.chunks.length ? this.chunks[0] : "" 225 | for (var i=1; i= start) { 231 | var line = data.slice(start, end) 232 | start = end + 1 233 | message = this.message.decode(line) 234 | if (message.b) { message.m = message.b; message.b = undefined } 235 | callback = this.callbacks[message.id] 236 | if (message instanceof Error) { 237 | // skip lines that fail the json decode 238 | // TODO: log bad lines 239 | console.log(new Date().toISOString(), "garbled response, could not decode: ", message) 240 | } 241 | else if (callback) { 242 | if (message.s === QrpcMessage.MSG_LAST) { 243 | // end() leaves .m undefined, just close out the request 244 | // a server-side empty cb() passes (null, undefined) to invoke client-side callback 245 | if (message.e !== undefined || message.m !== undefined) callback(message.e, message.m) 246 | delete this.callbacks[message.id] 247 | } 248 | else if (message.s === QrpcMessage.MSG_REPLY) { 249 | callback(message.e, message.m) 250 | } 251 | else /* (message.s === QrpcMessage.MSG_ERROR) */ { 252 | callback(message.e, message.m) 253 | // no more replies after a server error 254 | delete this.callbacks[message.id] 255 | } 256 | } 257 | } 258 | if (start < data.length) this.chunks.unshift(start > 0 ? data.slice(start) : data) 259 | doneDelivering() 260 | }, 261 | 262 | // connection error, send it to all waiting callbacks and clear callbacks 263 | _abortAllCalls: 264 | function _abortAllCalls( err ) { 265 | for (var i in this.callbacks) { 266 | var cb = this.callbacks[i] 267 | delete this.callbacks[i] 268 | cb(err) 269 | } 270 | }, 271 | } 272 | 273 | module.exports = QrpcClient 274 | -------------------------------------------------------------------------------- /lib/qrpc-server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * quick little rpc package 3 | * 4 | * Copyright (C) 2015-2017,2022 Andras Radics 5 | * Licensed under the Apache License, Version 2.0 6 | */ 7 | 8 | 'use strict' 9 | 10 | var net = require('net') 11 | var StringDecoder = require('string_decoder').StringDecoder 12 | var EventEmitter = require('events').EventEmitter 13 | var QrpcResponse = require('./qrpc-response.js') 14 | var QrpcMessage = require('./qrpc-message.js') 15 | var invoke2 = require('qibl').invoke2 16 | 17 | /** 18 | * Create a qrpc server. 19 | * Options: 20 | * - json_encode - json stringify 21 | * - json_decode - json parse 22 | */ 23 | function QrpcServer( options ) { 24 | options = options || {} 25 | this.handlers = {} // handler that send a response 26 | this.endpoints = {} // handlers that do not send a response 27 | this.message = new QrpcMessage({ // message coder 28 | json_encode: options.json_encode, 29 | json_decode: options.json_decode, 30 | }) 31 | this.fakeWritable = { write: function(s, cb) { cb() } } 32 | } 33 | 34 | var setImmediate = global.setImmediate || process.nextTick 35 | 36 | QrpcServer.prototype = { 37 | handlers: null, 38 | endpoints: null, 39 | message: null, 40 | fakeWritable: null, 41 | _port: null, 42 | _listening: false, 43 | _closed: false, // default to not closed, for testing 44 | _listenFunc: null, 45 | _closeFunc: null, 46 | 47 | addHandler: 48 | function addHandler( name, func ) { 49 | if (typeof func !== 'function') throw new Error("handler must be a function") 50 | this.handlers[name] = func 51 | return this 52 | }, 53 | 54 | addHandlerNoResponse: 55 | function addHandlerNoResponse( name, func ) { 56 | if (typeof func !== 'function') throw new Error("handler must be a function") 57 | this.endpoints[name] = func 58 | return this 59 | // FIXME: caller must *not* provide a callback when sending to an endpoint, 60 | // else will leak the callback structure for the call will never be closed 61 | // Would be better to use a different method 62 | }, 63 | 64 | addEndpoint: null, 65 | 66 | removeHandler: 67 | function removeHandler( name ) { 68 | if (this.handlers[name]) delete this.handlers[name] 69 | if (this.endpoints[name]) delete this.endpoints[name] 70 | return this 71 | }, 72 | 73 | onData: 74 | function onData( oldData, chunk, writeStream ) { 75 | var data = oldData ? oldData + chunk : chunk; 76 | if (data) { 77 | var calls = new Array() 78 | data = this._decodeCalls(data, calls) 79 | this._dispatchCalls(calls, writeStream) 80 | } 81 | return data 82 | }, 83 | 84 | setSource: 85 | function setSource( source, output ) { 86 | var self = this 87 | var data = "" 88 | var decoder = new StringDecoder() 89 | 90 | if (output instanceof EventEmitter) { 91 | var onError = function(err) { 92 | console.log(new Date().toISOString(), "qrpc server error writing response:", err.message) 93 | } 94 | output.on('error', onError) 95 | 96 | var onClose = function() { 97 | output.removeListener('error', onError) 98 | output.removeListener('close', onClose) 99 | } 100 | output.on('close', onClose) 101 | } 102 | 103 | if (typeof source.setEncoding === 'function') { 104 | // ask for utf8 chunks that will not split up multi-byte chars 105 | source.setEncoding('utf8') 106 | data = "" 107 | } 108 | if (source instanceof EventEmitter) { 109 | // TODO: if source can pipe then hook to the pipe and process on write() 110 | // (which would also transparently support throttling the source) 111 | var onData = function(chunk) { 112 | if (typeof chunk !== 'string') chunk = decoder.write(chunk) 113 | data = self.onData(data, chunk, output) 114 | } 115 | source.on('data', onData) 116 | 117 | var onError = function(err) { 118 | // TODO: abort/log socket errors 119 | source.emit('qrpcDetach') 120 | } 121 | source.on('error', onError) 122 | 123 | var onClose = function() { 124 | source.emit('qrpcDetach') 125 | } 126 | source.on('close', onClose) 127 | 128 | var onEnd = function() { 129 | self.onData(data, decoder.end(), output) 130 | source.emit('qrpcDetach') 131 | } 132 | source.on('end', onEnd) 133 | 134 | var onQrpcDetach = function() { 135 | source.removeListener('data', onData) 136 | source.removeListener('error', onError) 137 | source.removeListener('close', onClose) 138 | source.removeListener('end', onEnd) 139 | source.removeListener('qrpcDetach', onQrpcDetach) 140 | } 141 | source.on('qrpcDetach', onQrpcDetach) 142 | 143 | } 144 | else if (typeof source.read === 'function') { 145 | setImmediate(function pollSource( ) { 146 | var chunk = source.read(100000) 147 | if (chunk && chunk.length > 0) { 148 | if (typeof chunk !== 'string') chunk = decoder.write(chunk) 149 | data = self.onData(data, chunk, output) 150 | setImmediate(pollSource) 151 | } 152 | else if (!self._closed) { 153 | var poller = setTimeout(pollSource, 2) 154 | if (poller.unref) poller.unref() 155 | } 156 | }) 157 | } 158 | else return this._throwError(new Error("unable to use the source")) 159 | return self 160 | }, 161 | 162 | pipe: 163 | function pipeFromTo( sourceStream, outputStream ) { 164 | // WRITEME: variant of setSource, pipe source to self and write results to output 165 | // since a pipe reads just one source and writes just one output, 166 | // a server cannot use the streams pipe() call and still talk over multiple sockets 167 | this.setSource(sourceStream, outputStream) 168 | }, 169 | 170 | setListenFunc: 171 | function setListenFunc( listenFunc ) { 172 | this._listenFunc = listenFunc 173 | return this 174 | }, 175 | 176 | setCloseFunc: 177 | function setCloseFunc( closeFunc ) { 178 | this._closeFunc = closeFunc 179 | return this 180 | }, 181 | 182 | listen: 183 | function listen( port, callback ) { 184 | // TODO: support full set of net.listen params: port, host, backlog, cb 185 | if (!this._listenFunc) return this._throwError(new Error("call setListenFunc first"), callback) 186 | if (this._listening) return this._throwError(new Error("already listening"), callback) 187 | var self = this 188 | this._listenFunc(port, function() { 189 | self._listening = true 190 | self._port = port 191 | if (callback) callback() 192 | }) 193 | self._closed = false 194 | return this 195 | }, 196 | 197 | close: 198 | function close( callback ) { 199 | if (!this._closeFunc) return this._throwError(new Error("call setCloseFunc first"), callback) 200 | var self = this 201 | if (this._listening) { 202 | this._listening = false 203 | this._closeFunc() 204 | } 205 | self._closed = true 206 | if (callback) callback() 207 | }, 208 | 209 | wrap: 210 | function wrap( object, methods, options ) { 211 | if (options === undefined && methods && !Array.isArray(methods)) { options = methods ; methods = null } 212 | if (typeof object !== 'object') throw new Error("not an object") 213 | if (!options) options = {} 214 | if (!methods) methods = Object.keys(object) 215 | 216 | var self = this, prefix = options.prefix || "" 217 | for (var i=0; i= 10 ? 10 : calls.length 251 | for (i=0; i 0) setImmediate(function() { self._dispatchCalls(calls, writable) }) 283 | }, 284 | 285 | _decodeCalls: 286 | function _decodeCalls( data, calls ) { 287 | // TODO: v:1 is newline terminated lines, others may not be 288 | var start = 0, end, line, call 289 | while ((end = data.indexOf('\n', start)) >= 0) { 290 | line = data.slice(start, end) 291 | call = this.message.decode(line) 292 | if (call.b) { call.m = call.b; call.b = undefined } 293 | if (call instanceof Error) { 294 | // TODO: pass decode errors to a configured error-reporting function 295 | this._logError(call, "error: unable to decode call: " + line) 296 | } 297 | else calls.push(call) 298 | start = end + 1 299 | } 300 | return start < data.length ? data.slice(start) : "" 301 | }, 302 | 303 | _indexOf: 304 | function _indexOf( buf, ch, start ) { 305 | start = start || 0 306 | var i, c = ch.charCodeAt(0) 307 | for (i=start; i 0) return 10 | 11 | VERSION = "v0.8.1"; 12 | 13 | getopt = require('qgetopt'); 14 | options = getopt(process.argv, "j:h(-help)"); 15 | if (options.h || options.help) { 16 | console.log("benchmark.js " + VERSION + " -- run the qrpc benchmarks"); 17 | console.log("usage: node test/benchmark.js [optios]"); 18 | console.log(""); 19 | console.log("options:"); 20 | console.log(" -j N use N threads to serve requests"); 21 | return; 22 | } 23 | var jobsCount = options.j || options.jobs || 1; 24 | 25 | assert = require('assert') 26 | cluster = require('cluster') 27 | qibl = require('qibl'); 28 | qrpc = require('../index') 29 | json = { encode: JSON.stringify, decode: JSON.parse } 30 | try { json = require('json-simple') } catch (err) { } 31 | 32 | setImmediate = global.setImmediate || process.nextTick 33 | 34 | useCluster = true 35 | if (!useCluster) { 36 | isMaster = true 37 | isWorker = true 38 | } 39 | else { 40 | isMaster = cluster.isMaster 41 | isWorker = cluster.isWorker 42 | if (isMaster) { 43 | for (var j=0; j= n) { 308 | assert.deepEqual(ret.a, data) 309 | assert.deepEqual(ret.b, data) 310 | return cb() 311 | } 312 | } 313 | if (0) { 314 | var data2 = {a: data, b: data} 315 | for (var i=0; i= n) { 328 | assert.deepEqual(ret1, data) 329 | assert.deepEqual(ret2, data) 330 | return cb() 331 | } 332 | }) 333 | } 334 | } 335 | 336 | function testData1K( client, n, data, cb ) { 337 | ndone = 0 338 | var t1 = Date.now() 339 | function handleEchoResponse(err, ret) { 340 | if (++ndone === n) { 341 | var t2 = Date.now() 342 | console.log("parallel 1K object: %d calls in %d ms", n, t2 - t1) 343 | assert.deepEqual(ret, data) 344 | return cb() 345 | } 346 | } 347 | for (i=0; i reply from server: 'test ran!' 66 | // => reply from server: { a: 1, b: 'test' } 67 | }) 68 | client.call('echo', new Buffer("test"), function(err, ret) { 69 | console.log("reply from server:", ret) 70 | // => reply from server: 'test ran!' 71 | // => reply from server: 72 | }) 73 | }) 74 | 75 | 76 | Benchmark 77 | --------- 78 | 79 | Qrpc can sustain 200k calls per second. Full end-to-end throughput measured at the 80 | client is around 160k round-trip calls per second. Timings on a 32-bit i7-6700k 81 | Skylake at 4410 MHz running Linux 3.16-amd64. (64-bit kernel with 32-bit apps: 82 | double the memory of a full 64-bit system for free!) 83 | 84 | $ node-v6.10.2 test/benchmark.js 85 | 86 | rpc: listening on 1337 87 | echo data: { a: 1, b: 2, c: 3, d: 4, e: 5 } 88 | ---- 89 | parallel: 50000 calls in 314 ms 90 | series: 20000 calls in 697 ms 91 | send to endpoint: 100000 in 38 ms 92 | retrieved 100000 data in 245 ms 93 | retrieved 20000 1k Buffers in 82 ms 94 | parallel 1k buffers: 20000 in 228 ms 95 | wrapped parallel: 50000 calls in 452 ms 96 | parallel 1K object: 20000 calls in 1032 ms 97 | logged 100000 200B lines in 483 ms syncing every 250 lines 98 | 99 | Here are the original timings on the old AMD 3600 MHz Phenom II running the 100 | same Linux 3.16.0-amd64: 101 | 102 | $ node-v0.10.29 test/benchmark.js 103 | 104 | rpc: listening on 1337 105 | echo data: { a: 1, b: 2, c: 3, d: 4, e: 5 } 106 | parallel: 50000 calls in 780 ms 107 | series: 20000 calls in 1130 ms 108 | send to endpoint: 100000 in 87 ms 109 | retrieved 100000 data in 825 ms 110 | retrieved 20000 1k Buffers in 342 ms 111 | 112 | The parallel rate is call throughput -- the rpc server decodes the calls, 113 | process them, and encodes and send the response. The times shown above include 114 | the client-side formatting and sending of the rpc messages; the server time to 115 | process the 50000 calls (read request, decode, dispatch, process, encode, write 116 | response) is 250 ms; the 780 ms shown above includes the client time to format 117 | and send send the request and to read, decode and deliver the response. 118 | 119 | The series time is handling latency -- it is all-inclusive back-to-back round-trip 120 | time; each call is made only after the previous response has been received. 121 | 122 | The send rate is the number of calls processed by the server, not including 123 | data prep time (but yes including the time to transmit, decode, and dispatch). 124 | Send-only mode is one-way message passing: the data will be acted on by the 125 | server but no acknowledgement, status or error is returned. 126 | 127 | Retrieval is getting multiple responses for one request, for chunked data 128 | fetching. These tests make one call for every 10,000 responses, one data item 129 | (or one Buffer) per response. 130 | 131 | The logging benchmark ships 200-byte log lines to the server with one-way messages, 132 | and every 250 lines makes a round-trip rpc call to sync them, ie to ensure that all 133 | preceding lines have been successfully received and persisted. `qrpc` servers 134 | process calls in transmission order, so for the sync to have been received all 135 | preceding calls must have been received as well. 136 | 137 | Note that the logging test uses `socket.setNoDelay()` to turn off the Nagle algorithm 138 | on the client side. With Nagle enbaled, only 25 sync calls get through per second, ie 139 | the throughput drops from 200k lines/sec to 25 * 250 = 6.25k lines/sec. 140 | 141 | 142 | Qrpc Server 143 | ----------- 144 | 145 | The server listens for incoming messages, processes them, and returns the 146 | responses. 147 | 148 | Calls are tagged with a the handler name string. Each handler appears similar 149 | to a framework middleware step, taking a request, response and next callback. 150 | 151 | The response is returned via the next() callback, or via res.write() and 152 | res.end(). Any number of write() calls may be used, each will send a response 153 | message that will be pass to the client call's callback. end() and next() 154 | both send a final message and close the call. Once the call is closed, no 155 | more responses must be sent. 156 | 157 | write() and end() return data. Errors may be returned with next(). Qrpc 158 | restores error objects so the client callback receives instanceof Error (note: 159 | these errors occurred in the handler code on the server, not on the client) 160 | 161 | Calls may be kept open indefinitely, but each open call uses memory while 162 | holding on to the callback. 163 | 164 | Calls do not time out. Write errors on the server side will not be noticed by 165 | the client. Timeout-based error handling is up to the application. (But see 166 | the Todo list below) 167 | 168 | ### server = qrpc.createServer( [options][, callback(socket)] ) 169 | 170 | Create a new server. Returns the QrpcServer object. 171 | 172 | The callback, if specified, will be invoked on every connection to the rpc 173 | server with the connected socket. This makes it possible for the server to 174 | tune the socket settings. 175 | 176 | Options: 177 | 178 | - `json_encode` - the object serializer function to use (default `JSON.stringify`) 179 | - `json_decode` - the object deserializer to use (default `JSON.parse`) 180 | 181 | ### server.addHandler( handlerName, handlerFunction(req, res, next) ) 182 | 183 | Define the code that will process calls of type `handlerName`. 184 | 185 | A handler receives 3 parameters just like a middleware stack function: the 186 | call object (`req`), the response object (`res`), and a callback `next` that 187 | can be used to return errors and/or data. 188 | 189 | The `req` object has a field `.m` that contains the object passed to the call, if 190 | any, and a field `.id` that is the unique caller-side id of the call. 191 | 192 | The `res` object has methods `write(data [,cb])` and `end([data] [,cb])` that reply to 193 | the caller with the provided data. Each reply will be delivered to the client 194 | callback. Buffers are sent and received as base64 Buffers, other objects as 195 | JSON serialized strings. End() will send the reply, if any, then close the 196 | call. After the call is closed, no more replies can be sent. 197 | 198 | ### server.addHandlerNoResponse( handlerName, handlerFunction(req, res) ) 199 | 200 | Define the code that will handle messages of type `handlerName`. 201 | 202 | NoResponse handlers are endpoints for one-way messages, not full RPC calls. They process 203 | messages, but do not return a response to the caller. One-way message passing 204 | is a much more efficient way to push data for eg reporting or stats delivery. 205 | The handler function should not declare the `next` argument as a reminder that 206 | no response will be returned to the caller. A no-op `next` function is passed 207 | to the handler, though, just in case. 208 | 209 | Calling endpoints is one-way message passing: the data will be acted on by the 210 | server but no acknowledgement, status or error is returned. This offers a very 211 | fast pipelined path to ship data. 212 | 213 | NOTE: The client _must_ _not_ pass a callback function when calling a message 214 | endoint, because this would leak memory. Endpoint calls will never be closed by 215 | the sender, so the callback context would never be freed. 216 | 217 | ### server.listen( port, [whenListening()] ) 218 | 219 | Start listening for calls. Incoming calls will invoke the appropritae 220 | handlers. 221 | 222 | If the whenListening callback is provided, it will be invoked once the server 223 | is listening for incoming calls. 224 | 225 | ### server.close( ) 226 | 227 | Stop listening for calls. 228 | 229 | var server = qrpc.createServer() 230 | server.addHandler('echo', function(req, res, next) { 231 | // echo server, return our arguments 232 | next(null, req.m) 233 | }) 234 | server.listen(1337) 235 | 236 | ### server.wrap( object [,methods] [,options] ) 237 | 238 | Make the methods of the object callable by rpc. Adds handlers for the methods 239 | whose names are in the `methods` array (all function properties by default). The 240 | handlers will expect the method arguments list in `req.m` and will return a list 241 | with all arguments returned by the method callback. 242 | 243 | To wrap functions, pass an object with the functions as named properties. 244 | 245 | Options: 246 | 247 | - `prefix` - build the rpc handler name by prepending `prefix` to the method name. The default is just the method name. 248 | 249 | 250 | Qrpc Client 251 | ----------- 252 | 253 | The client makes calls to the server, and consumes the responses. A single 254 | request can result in more than one response; qrpc sends all requests and 255 | responses over a single socket (multiplexes) and steers each response to its 256 | correct destination. 257 | 258 | ### client = qrpc.connect( port|options, [host,] whenConnected(clientSocket) ) 259 | 260 | Connect to the qrpc server listening on host:port (or 'localhost':port if host 261 | is not specified). Returns the QrpcClient object. 262 | 263 | If provided, the newly created net.socket will be passed to the whenConnected 264 | callback for socket configuration and tuning. 265 | 266 | Once connected, calls may be made with client.call() 267 | 268 | Instead of port (or port and host) an options object can be passed which is 269 | then passed to `net.connect()`. 270 | 271 | Options: 272 | 273 | - `port` - port to connect to (required, no default) 274 | - `host` - host to connect to (default 'localhost') 275 | - plus all other valid `net.connect()` options 276 | 277 | In addition, QrpcClient recognizes: 278 | 279 | - `json_encode` - the object serializer function to use (default JSON.stringify) 280 | - `json_decode` - the object deserializer to use (default JSON.parse) 281 | 282 | ### client.call( handlerName, [data,] [callback(err, replyData)] ) 283 | 284 | Invoke the handler named _handlerName_, and return the server reply via the 285 | callback. Handlers are registered on the server with addHandler(). Data is 286 | optional; if any data is specified, it is passed in the call to the server in 287 | `req.m` unless it is a `Buffer`, which is passed in `req.b`. 288 | 289 | Omitting the callback sends a one-way message to the server. Any response 290 | received from the server will be discarded. 291 | 292 | Note: `Buffers` maybe be sent only standalone. Sending an object that has as a 293 | Buffer as a property will arrive as a JSON.stringified Buffer `string`and not as an 294 | `instanceof Buffer`. 295 | 296 | ### client.close( ) 297 | 298 | Disconnect from the qrpc server. Any subsequent calls will return a "write 299 | after end" error to their callback. 300 | 301 | client = qrpc.connect(1337, 'localhost', function whenConnected() { 302 | client.call('echo', {i: 123, t: 'test'}, function(err, ret) { 303 | console.log("echo =>", err, ret) 304 | }) 305 | } 306 | 307 | // produces "echo => null, { i: 123, t: 'test' }" 308 | 309 | ### client.wrap( object [,methods] [,options] ) 310 | 311 | Return an object with methods names as in the `methods` array (or all function 312 | properties on `object`) that will invoke the corresponding handlers on the 313 | qrpc server. 314 | 315 | To wrap functions, pass an object with the functions as named properties. 316 | 317 | Options: 318 | 319 | - `prefix` - build the rpc handlerName by prepending `prefix` to the method name 320 | 321 | 322 | Under The Hood 323 | -------------- 324 | 325 | Qrpc is implemented as streaming message passing with call multiplexing. 326 | Each message is a newline terminated string sent to the server. Messages 327 | are batched for efficiency, and arrive at the server in large chunks. The 328 | server splits the stream, decodes each message, and invokes each handler. 329 | 330 | Messages are sent to named handlers; the handlers are registered with the 331 | server using the `addHandler` method. Every handler takes the same arguments, 332 | a middleware stack-like `(req, res, next)`: the request bundle (the message 333 | itself), the response object, and a callback that can return a response and is 334 | used to return errors. 335 | 336 | The response object can be used to send replies back to the caller. The 337 | `write` method sends a reply message and keeps the call open for more 338 | replies later. The `end` method sends an optional final reply and closes 339 | the call. End without an argument just closes the call by sending a special 340 | empty message back to the client. Calling the handler callback `cb(err, 341 | data)` is equivalent to calling `end(data)`, except it will return the error 342 | too, if any. A single call can result in more than once response; 343 | coordinating the responses is up to the application. 344 | 345 | Responses arrive at the caller in the same newline terminated text format, 346 | also in batches, over the same bidirectional connection. The calling 347 | library splits the batches, decodes the responses, demultiplexes the replies 348 | to match them to their originating call, and invokes the call's callback 349 | function with the error and data from reply message. 350 | 351 | Calls are multiplexed, and may complete out of order. Each message is 352 | tagged with a unique call id to identify which call's callback is to process 353 | the reply. 354 | 355 | The RPC service is implemented using the `QrpcServer` and `QrpcClient` classes. 356 | They communicate over any bidirectional EventEmitter 357 | stream or object with a `write()` method. A customized RPC can be built over 358 | non-socket non-socket streams, which is how the unit tests work. 359 | 360 | To build an rpc service using QrpcServer and QrpcClient 361 | on top of net sockets the way `qrpc` does: 362 | 363 | // create qrpc server 364 | var server = new qrpc.QrpcServer() 365 | var netServer = net.createServer(function(socket) { 366 | // have the server read rpc calls from the first arg 367 | // and write responses to the second arg 368 | server.setSource(socket, socket) 369 | }) 370 | server.setListenFunc(function(port, cb){ netServer.listen(port, cb) }) 371 | server.setCloseFunc(function(){ netServer.close() }) 372 | 373 | 374 | // create qrpc client to talk to the server 375 | var client = new qrpc.QrpcClient() 376 | var socket = net.connect(1337, 'localhost') 377 | client.setTarget(socket, socket) 378 | 379 | ### Message Format 380 | 381 | Qrpc requests and responses are sent as simple serialized json objects, 382 | one per line: 383 | 384 | { 385 | v: 1, // protocol version, 1: json bundle 386 | id: id, // unique call id to match replies to calls 387 | n: name, // call name string, in request only 388 | e: error // returned error, in response only 389 | s: status // response status, one of 390 | // 'ok' (on write()), 391 | // 'end' (on end()), 392 | // 'err' (server error; means end) 393 | m: message // object payload, in call or response 394 | b: blob // base64 encoded buffer payload 395 | } 396 | 397 | 398 | Related Work 399 | ------------ 400 | 401 | - [qrpc](https://npmjs.com/package/qrpc) - 60k calls / sec round-trip, 1m messages / sec dispatched 402 | - [rpc-stream](https://npmjs.com/package/rpc-stream) - 16k calls / sec 403 | - [dnode](https://npmjs.com/package/dnode) - 14k calls / sec light load, throughput drops sharply with load 404 | - [fast](https://npmjs.com/package/fast) - 12k calls / sec 405 | - X [mrpc](https://www.npmjs.com/package/mrpc) - npm install failed (C++ compile errors) 406 | - X [kamote](https://www.npmjs.com/package/kamote) - hangs on concurrent calls (v0.0.2) 407 | - X [fast-rpc](https://www.npmjs.com/package/fast-rpc) - just a placeholder since 2013 (v0.0.0) 408 | 409 | 410 | Todo 411 | ---- 412 | 413 | - more unit tests 414 | - server should periodically yield to the event loop 415 | - support call timeouts for more convenient error detection and cleanup (client-side and server-side both) 416 | Make server track running calls with timestamp, set done calls = undefined, periodically copy over object to gc properties, 417 | only the "olds" object could have timed-out calls. 418 | - think about how to gc or time out callbacks that have been abandoned by the server (call not closed) 419 | - maybe make the the client and server pipable event emitters 420 | - provide a `client.send()` method to send to an endpoint without a callback 421 | - ? allow pre- and post-handler functions to be registered, for shared processing 422 | (eg for authentication and stats logging) 423 | - ? allow multi-step handlers (ie an array of functions, each taking req-res-next) 424 | - option to auto-reconnect if connection drops 425 | - way to test whether connection was dropped since last test (counter? or connection id?) 426 | to check whether datagrams got there ok 427 | - Use Cases readme section to discuss rpc, send datagrams to server (addHandlerNoResponse), 428 | data retrieval (multiple replies from server) 429 | - support `qrpc.*` out-of-band calls for client-server metadata, setup/config/etc 430 | - support request cancellation (`qrpc.cancel(id)` call, processed asap out-of-band) 431 | --------------------------------------------------------------------------------