├── .gitignore ├── LICENSE ├── README.md ├── example └── tcp │ ├── client.js │ ├── server.js │ └── somefile ├── index.js ├── messages.js ├── package.json ├── rpcify.js ├── schema.proto ├── scripts └── compile-schema.js └── test ├── index.js └── object.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | package-lock.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Franz Heinzmann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hyperpc 2 | 3 | Asynchronous bidirectional RPC in Javascript that works over any binary stream. Supports passing both callbacks and arbitrary streams (both in object and binary mode) to the remote end. An optional promise mode allows to return promises and use the remote API with `async`/`await`. Also supports rpcifying objects and constructors. 4 | 5 | Uses [multiplex](https://github.com/maxogden/multiplex) under the hood to float many streams through a single binary stream. 6 | 7 | In the spirit of [dnode](https://github.com/substack/dnode), [rpc-stream](https://github.com/dominictarr/rpc-stream), [muxrpc](https://github.com/ssbc/muxrpc) and [rpc-multistream](https://github.com/biobricks/rpc-multistream). 8 | 9 | ## Installation 10 | 11 | `npm install hyperpc` 12 | 13 | ## Usage 14 | 15 | ```js 16 | var hyperpc = require('hyperpc') 17 | 18 | var values = ['hello', 'world!'] 19 | var api = { 20 | upper: (str, cb) => cb(null, str.toUpperCase()), 21 | readStream: (str, cb) => { 22 | var rs = new stream.Readable({ 23 | objectMode: true, 24 | read () { this.push(values.length ? values.shift() : null)} 25 | }) 26 | cb(null, rs) 27 | } 28 | } 29 | 30 | var server = hyperpc(api) 31 | var client = hyperpc() 32 | 33 | server.pipe(client).pipe(server) 34 | // usually, you'd do something like: 35 | // server.pipe(serverSideTransportStream).pipe(server) 36 | // clientTransport.pipe(client).pipe(clientTransport) 37 | 38 | client.on('remote', (remote) => { 39 | remote.upper('foo', (err, res) => { 40 | console.log(res) // FOO 41 | }) 42 | 43 | remote.readStream('bar', (err, rs) => { 44 | rs.on('data', (data) => { 45 | console.log(data) 46 | }) 47 | rs.on('end', () => console.log('read stream ended')) 48 | 49 | // prints: 50 | // hello 51 | // world! 52 | // read stream ended 53 | }) 54 | }) 55 | }) 56 | ``` 57 | 58 | More examples are in `test.js` and `examples/`. 59 | 60 | ## API 61 | 62 | ### `var stream = hyperpc([api], [opts])` 63 | 64 | `api` is an object of functions. The functions can be called from the remote site. The implementing side may call any callbacks that are passed. For both the call and the callbacks you may pass streams, callbacks or errors as args. They all work transparently over the remote connection. Supported streams are readable streams, writable streams, duplex streams in both object and binary modes. If a transform stream is passed, it is assumed to be a readable stream if it does not have pipes assigned (i.e. is piped to but not piped from). 65 | 66 | `opts` and their defaults are: 67 | 68 | * `log: false`: Enable debug mode. Log all messages to `console.log` 69 | * `name: null`: Set a name for this end of the connection. Only used in log mode. 70 | * `promise: false`: Support returning promises (experimental) 71 | 72 | ### RPCifying objects 73 | 74 | hyperpc supports passing classes (functions with constructors) or instances (objects with bound function properties) through a helper, `hyperpc.rpcify`, to make transparent calls to the instance on the backend. See `test/object.js` for examples. 75 | 76 | ### Support for promises and `async/await` 77 | 78 | Return values are ignored, unless `{ promise: true }` is set in `opts` AND the return value is a promise. In that case, on the remote end a promise is returned as well and the resolve/reject callbacks are streamed transparently. 79 | 80 | This allows to use `hyperpc` with `async/await`: 81 | 82 | ```js 83 | var api = { 84 | promtest: async function (str) { 85 | if (!str) throw new Error('no arg') 86 | return str.toUpperCase() 87 | } 88 | } 89 | 90 | var server = hyperpc(api, {promise: true}) 91 | var client = hyperpc(null, {promise: true}) 92 | 93 | pump(server, client, server) 94 | 95 | client.on('remote', async (api) => { 96 | var val = 'hello' 97 | try { 98 | var bar = await api.promtest(val) 99 | console.log(bar) 100 | } catch (err) { 101 | console.log(err.message) 102 | } 103 | // prints "HELLO", and would print "no arg" if val were false. 104 | }) 105 | ``` 106 | 107 | 108 | ### Motivation 109 | 110 | There's many RPC-over-streams modules already. Why another one? First, I wanted to learn streams in-depth. Second, hyperpc uses [multiplex](https://github.com/maxogden/multiplex) under the hood, and supports setting up arbitrary binary streams from both ends, so it should be fast to not only exchange RPC messages, but only binary data streams. No benchmarks though, yet. 111 | 112 | Some differences to other great modules in this space: 113 | 114 | * [dnode](https://github.com/substack/dnode): The oldest kid on the block. Does not support streams as arguments natively though. 115 | * [muxrpc](https://github.com/ssbc/muxrpc): The preferred streaming RPC in Scuttlebut land. Uses [pull-streams](https://github.com/pull-stream/pull-stream), which I didn't want to include. Needs a manifest, which hyperpc does not. 116 | * [rpc-multistream](https://github.com/biobricks/rpc-multistream): Similar feature set to *hyperpc*, also uses [multiplex](https://github.com/maxogden/multiplex). hyperpc can be considered a rewrite, with additional suppport for Promises. 117 | -------------------------------------------------------------------------------- /example/tcp/client.js: -------------------------------------------------------------------------------- 1 | var net = require('net') 2 | var hyperpc = require('../..') 3 | 4 | var clientApi = { 5 | updateState: (state) => { 6 | console.log('new client state', state) 7 | } 8 | } 9 | 10 | var rpc = hyperpc(clientApi, {log: true}) 11 | 12 | var client = new net.Socket() 13 | client.connect(1337, '127.0.0.1', () => { 14 | rpc.pipe(client).pipe(rpc) 15 | }) 16 | 17 | client.on('error', (err) => console.log('client socket error: ', err)) 18 | 19 | rpc.on('remote', (api) => { 20 | // setInterval(() => api.upper('foo', () => {}), 500) 21 | api.upper('foo', (err, string) => { 22 | if (err) return 23 | console.log('transformed foo: ', string.toString()) 24 | }) 25 | api.readValues((err, rs) => { 26 | // if (err) return 27 | if (err) console.log(err) 28 | rs.on('data', (data) => console.log('client readValues:', data.toString())) // prints 'read value 1', 'read value 2' 29 | rs.on('error', (err) => console.log('rs error', err)) 30 | rs.on('close', () => console.log('rs close')) 31 | rs.on('end', () => console.log('rs end')) 32 | }) 33 | // api.writeValues((err, ws) => { 34 | // if (err) return 35 | // ws.write('val1') 36 | // ws.write('val2') 37 | // ws.end() 38 | // }) 39 | }) 40 | -------------------------------------------------------------------------------- /example/tcp/server.js: -------------------------------------------------------------------------------- 1 | var hyperpc = require('../..') 2 | var stream = require('stream') 3 | var net = require('net') 4 | var fs = require('fs') 5 | var through = require('through2') 6 | var pump = require('pump') 7 | 8 | var serverApi = { 9 | upper: (string, cb) => cb(null, string.toUpperCase()), 10 | readValues: (cb) => { 11 | var file = fs.createReadStream('./somefile') 12 | var splitAndNumberLines = through(function (chunk, enc, next) { 13 | chunk.toString().split('\n').forEach((line, i) => { 14 | if (line) this.push(Buffer.from(i + ': ' + line)) 15 | }) 16 | next() 17 | }) 18 | // this is needed for hyperpc to detect the pumped stream as readable (and not duplex). 19 | // cb(null, duplexify(null, pump(file, splitAndNumberLines))) 20 | 21 | // EDIT: Fixed with a hacky code in streamType() in index.js 22 | // now this works: 23 | var s = pump(file, splitAndNumberLines) 24 | 25 | cb(null, s) 26 | }, 27 | writeValues: (cb) => { 28 | var ws = new stream.Writable({ 29 | write (chunk, enc, next) { 30 | console.log(chunk.toString()) // prints val1, val2 31 | next() 32 | } 33 | }) 34 | ws.on('finish', () => console.log('write stream finished')) 35 | cb(null, ws) 36 | } 37 | } 38 | 39 | var rpc = hyperpc(serverApi, {name: 'server', log: true}) 40 | 41 | var server = net.createServer((socket) => { 42 | socket.setKeepAlive(true) 43 | socket.on('close', (hadErr) => console.log('client socket closed', hadErr)) 44 | socket.on('error', (err) => console.log('client socket error', err)) 45 | rpc.pipe(socket).pipe(rpc) 46 | }) 47 | 48 | server.listen(1337, '127.0.0.1') 49 | 50 | server.on('error', (err) => console.log('client tcp error', err)) 51 | 52 | rpc.on('remote', (api) => { 53 | api.updateState({ 54 | init: true, 55 | timer: 0 56 | }) 57 | var i = 0 58 | setInterval(() => { 59 | api.updateState({ timer: i++ }) 60 | }, 1000) 61 | }) 62 | -------------------------------------------------------------------------------- /example/tcp/somefile: -------------------------------------------------------------------------------- 1 | somefile firstline 2 | another line 3 | last line of somefile 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var multiplex = require('multiplex') 2 | var duplexify = require('duplexify') 3 | var through = require('through2') 4 | var thunky = require('thunky') 5 | var stream = require('stream') 6 | var pump = require('pump') 7 | // var debug = require('debug') 8 | 9 | var m = require('./messages.js') 10 | var rpcify = require('./rpcify.js') 11 | 12 | var READABLE = 1 // 10 13 | var WRITABLE = 2 // 01 14 | // var DUPLEX = 1 | 2 // 11 15 | 16 | var FUNCTION = 1 17 | var VALUE = 2 18 | var OBJECT = 3 19 | var CONSTRUCTOR = 4 20 | 21 | var SEPERATOR = '.' 22 | 23 | function hyperpc (api, opts) { 24 | var rpc = HypeRPC(api, opts) 25 | return rpc.stream 26 | } 27 | 28 | hyperpc.rpcify = rpcify 29 | 30 | module.exports = hyperpc 31 | 32 | function HypeRPC (api, opts) { 33 | if (!(this instanceof HypeRPC)) return new HypeRPC(api, opts) 34 | var self = this 35 | opts = opts || {} 36 | 37 | this.promise = opts.promise || false 38 | this.prefix = opts.prefix || null 39 | this.name = opts.name || '' 40 | this.debug = opts.debug || false 41 | this.id = opts.id || 0 42 | 43 | this.api = api || [] 44 | 45 | this.remote = null 46 | this.callbacks = {} 47 | this.objects = {} 48 | this.transports = {} 49 | this.incoming = {} 50 | this.promises = {} 51 | this.cnt = 0 52 | this.nonce = Math.round(Math.random() * 10000000) 53 | 54 | this.stream = multiplex({objectMode: false}, this.onstream.bind(this)) 55 | 56 | var rpc = this.stream.createSharedStream('rpc') 57 | this.send = through() 58 | this.recv = through() 59 | 60 | pump(this.send, rpc) 61 | pump(rpc, this.recv) 62 | 63 | this.recv.on('data', this.onData.bind(this)) 64 | 65 | this.sendManifest() 66 | 67 | this.ready = thunky((cb) => self.stream.on('remote', () => cb())) 68 | this.ready() 69 | } 70 | 71 | HypeRPC.prototype.onData = function (data) { 72 | var self = this 73 | var msg = m.Msg.decode(data) 74 | if (this.debug) this.log('in', msg) 75 | 76 | switch (msg.type) { 77 | case m.TYPE.MANIFEST: 78 | this.onManifest(msg.manifest) 79 | break 80 | case m.TYPE.CALL: 81 | this.ready(() => self.onCall(msg.call)) 82 | break 83 | case m.TYPE.RETURN: 84 | this.ready(() => self.onReturn(msg.return)) 85 | break 86 | } 87 | } 88 | 89 | HypeRPC.prototype.send = function (data) { 90 | this.send.write(data) 91 | } 92 | 93 | HypeRPC.prototype.sendMsg = function (msg) { 94 | if (this.debug) this.log('out', msg) 95 | this.send.write(m.Msg.encode(msg)) 96 | } 97 | 98 | HypeRPC.prototype.sendManifest = function () { 99 | this.sendMsg({ 100 | type: m.TYPE.MANIFEST, 101 | manifest: { 102 | manifest: JSON.stringify(this.makeManifest()), 103 | nonce: this.nonce 104 | } 105 | }) 106 | } 107 | 108 | HypeRPC.prototype.makeManifest = function () { 109 | var manifest = reduce(this.api) 110 | return manifest 111 | 112 | function reduce (obj) { 113 | return Object.keys(obj).reduce((manifest, key) => { 114 | if (obj[key] instanceof rpcify) { 115 | manifest[key] = [CONSTRUCTOR, obj[key].toManifest()] 116 | } else if (isObject(obj[key])) { 117 | manifest[key] = [OBJECT, reduce(obj[key])] 118 | } else if (isFunc(obj[key])) { 119 | manifest[key] = [FUNCTION] 120 | } else if (isLiteral(obj[key])) { 121 | manifest[key] = [VALUE, obj[key]] 122 | } 123 | return manifest 124 | }, {}) 125 | } 126 | } 127 | 128 | HypeRPC.prototype.onManifest = function (msg) { 129 | var self = this 130 | var manifest = JSON.parse(msg.manifest) 131 | var remoteNonce = msg.nonce 132 | 133 | if (!this.prefix) this.prefix = calculatePrefix(this.nonce, remoteNonce) 134 | 135 | this.remote = reduce(manifest) 136 | this.stream.emit('remote', this.remote) 137 | 138 | function reduce (manifest, prefixes) { 139 | prefixes = prefixes || [] 140 | return Object.keys(manifest).reduce((remote, name) => { 141 | var path = [...prefixes, name] 142 | var [type, data] = manifest[name] 143 | 144 | if (type === CONSTRUCTOR) { 145 | remote[name] = self.mockConstructor(path, data) 146 | } else if (type === OBJECT) { 147 | remote[name] = reduce(data, path) 148 | } else if (type === FUNCTION) { 149 | remote[name] = self.mockFunction(path) 150 | } else if (type === VALUE) { 151 | remote[name] = data 152 | } 153 | return remote 154 | }, {}) 155 | } 156 | } 157 | 158 | HypeRPC.prototype.mockFunction = function (path, objectid, method) { 159 | var self = this 160 | var name, type 161 | if (path) { 162 | type = m.CALL.API 163 | name = path.join(SEPERATOR) 164 | } else { 165 | type = m.CALL.OBJECT 166 | } 167 | return function () { 168 | var id = self.makeId() 169 | var args = self.prepareArgs(id, Array.from(arguments)) 170 | self.sendMsg({ 171 | type: m.TYPE.CALL, 172 | call: { type, id, name, objectid, method, args } 173 | }) 174 | 175 | if (self.promise) { 176 | return new Promise((resolve, reject) => { 177 | self.promises[id] = [resolve, reject] 178 | }) 179 | } 180 | } 181 | } 182 | 183 | HypeRPC.prototype.mockConstructor = function (path, manifest) { 184 | var self = this 185 | var name = path.join(SEPERATOR) 186 | return function () { 187 | var id = self.makeId() 188 | 189 | var args = self.prepareArgs(id, Array.from(arguments)) 190 | self.sendMsg({ 191 | type: m.TYPE.CALL, 192 | call: { type: m.CALL.API, id, name, args } 193 | }) 194 | 195 | var MockConstructor = function () {} 196 | manifest.methods.forEach((method) => { 197 | MockConstructor.prototype[method] = self.mockFunction(path, id, method) 198 | }) 199 | Object.defineProperty(MockConstructor, 'name', { value: manifest.name }) 200 | return new MockConstructor() 201 | } 202 | } 203 | 204 | HypeRPC.prototype.onCall = function (msg) { 205 | var { type, id, name, objectid, method, args } = msg 206 | 207 | args = this.resolveArgs(id, args) 208 | 209 | var ret 210 | switch (type) { 211 | case m.CALL.OBJECT: 212 | ret = this.objects[objectid].makeCall(method, objectid, args) 213 | break 214 | 215 | case m.CALL.API: 216 | var obj = name.split(SEPERATOR).reduce((api, path) => api[path], this.api) 217 | if (obj instanceof rpcify) { 218 | if (objectid) ret = obj.makeCall(method, objectid, args) 219 | else ret = obj.makeNew(id, args) 220 | } else { 221 | ret = obj.apply(obj, args) 222 | } 223 | break 224 | } 225 | 226 | if (this.promise) { 227 | var promise 228 | if (isPromise(ret)) promise = ret 229 | else promise = new Promise((resolve, reject) => resolve(ret)) 230 | this.preparePromise(id, promise) 231 | } 232 | } 233 | 234 | HypeRPC.prototype.onReturn = function (msg) { 235 | var { id, args, type } = msg 236 | 237 | args = this.resolveArgs(id, args) 238 | 239 | switch (type) { 240 | case m.RETURN.CALLBACK: 241 | var func = this.callbacks[id] 242 | func.apply(func, args) 243 | break 244 | case m.RETURN.PROMISE: 245 | var promise = this.promises[id] 246 | if (!promise) return 247 | var res = msg.promise 248 | promise[res].apply(promise[res], args) 249 | break 250 | } 251 | } 252 | 253 | HypeRPC.prototype.preparePromise = function (id, promise) { 254 | var self = this 255 | promise.then(handle(m.PROMISE.RESOLVE), handle(m.PROMISE.REJECT)) 256 | 257 | function handle (result) { 258 | return function () { 259 | var args = self.prepareArgs(id, Array.from(arguments)) 260 | self.sendMsg({ 261 | type: m.TYPE.RETURN, 262 | return: { 263 | type: m.RETURN.PROMISE, 264 | id, 265 | args, 266 | promise: result 267 | } 268 | }) 269 | } 270 | } 271 | } 272 | 273 | HypeRPC.prototype.resolveArgs = function (id, args) { 274 | return this.convertArgs('resolve', id, args) 275 | } 276 | 277 | HypeRPC.prototype.prepareArgs = function (id, args) { 278 | return this.convertArgs('prepare', id, args) 279 | } 280 | 281 | HypeRPC.prototype.convertArgs = function (step, id, args) { 282 | var self = this 283 | var TYPE = 0 284 | var MATCH = 1 285 | var PREPARE = 2 286 | var RESOLVE = 3 287 | 288 | var STEPS = { 289 | prepare: prepareArg, 290 | resolve: resolveArg 291 | } 292 | 293 | var CONVERSION_MAP = [ 294 | // [ TYPE, MATCH, PREPARE, RESOLVE ] 295 | [m.ARGUMENT.RPCIFIED, isRpcified, this.prepareRpcified, this.resolveRpcified], 296 | [m.ARGUMENT.ERROR, isError, this.prepareError, this.resolveError], 297 | [m.ARGUMENT.CALLBACK, isFunc, this.prepareCallback, this.resolveCallback], 298 | [m.ARGUMENT.STREAM, isStream, this.prepareStream, this.resolveStream], 299 | [m.ARGUMENT.BYTES, isBuffer, this.prepareBuffer, this.resolveBuffer], 300 | [m.ARGUMENT.JSON, () => true, this.prepareJson, this.resolveJson] 301 | ] 302 | 303 | return args.map((arg, i) => STEPS[step](arg, id, i)) 304 | 305 | function prepareArg (arg, id, i) { 306 | return CONVERSION_MAP.reduce((preparedArg, info, type) => { 307 | if (preparedArg === null && info[MATCH](arg)) { 308 | preparedArg = info[PREPARE].apply(self, [arg, joinIds(id, i)]) 309 | preparedArg.type = info[TYPE] 310 | } 311 | return preparedArg 312 | }, null) 313 | } 314 | 315 | function resolveArg (arg, id, i) { 316 | var group = CONVERSION_MAP.filter(group => group[0] === arg.type)[0] 317 | return group[RESOLVE].apply(self, [arg, joinIds(id, i)]) 318 | } 319 | } 320 | 321 | HypeRPC.prototype.prepareJson = function (arg) { 322 | try { 323 | return { json: JSON.stringify(arg) } 324 | } catch (e) { 325 | this.log('JSON encoding error.') 326 | return { json: null } 327 | } 328 | } 329 | 330 | HypeRPC.prototype.resolveJson = function (arg) { 331 | if (!arg.json) return null 332 | try { 333 | return JSON.parse(arg.json) 334 | } catch (e) { 335 | return null 336 | } 337 | } 338 | 339 | HypeRPC.prototype.prepareRpcified = function (arg, id) { 340 | this.objects[id] = arg 341 | return { 342 | rpcified: { 343 | manifest: JSON.stringify(arg.toManifest()) 344 | } 345 | } 346 | } 347 | 348 | HypeRPC.prototype.resolveRpcified = function (arg, id) { 349 | var self = this 350 | var spec = JSON.parse(arg.rpcified.manifest) 351 | var MockConstructor = function () {} 352 | spec.methods.forEach((key) => { 353 | MockConstructor.prototype[key] = self.mockFunction(null, id, key) 354 | }) 355 | Object.defineProperty(MockConstructor, 'name', { value: spec.name }) 356 | return new MockConstructor() 357 | } 358 | 359 | HypeRPC.prototype.prepareError = function (arg) { 360 | return { error: JSON.stringify({ message: arg.toString() }) } 361 | // todo: somehow this does not alway work. 362 | // return Object.getOwnPropertyNames(arg).reduce((spec, name) => { 363 | // spec[name] = arg[name] 364 | // return spec 365 | // }, {}) 366 | } 367 | 368 | HypeRPC.prototype.resolveError = function (arg, id) { 369 | var spec = JSON.parse(arg.error) 370 | var err = new Error() 371 | Object.getOwnPropertyNames(spec).map((name) => { 372 | err[name] = spec[name] 373 | }) 374 | return err 375 | } 376 | 377 | HypeRPC.prototype.prepareCallback = function (arg, id) { 378 | this.callbacks[id] = arg 379 | return { callback: id } 380 | } 381 | 382 | HypeRPC.prototype.resolveCallback = function (arg) { 383 | var self = this 384 | var id = arg.callback 385 | return function () { 386 | var args = self.prepareArgs(id, Array.from(arguments)) 387 | self.sendMsg({ 388 | type: m.TYPE.RETURN, 389 | return: { 390 | type: m.RETURN.CALLBACK, 391 | id, 392 | args 393 | } 394 | }) 395 | } 396 | } 397 | 398 | HypeRPC.prototype.prepareStream = function (stream, id) { 399 | var type = streamType(stream) 400 | var objectMode = isObjectStream(stream) 401 | 402 | if (type & READABLE) { 403 | var rsT = this.getTransportStream(id, READABLE, stream) 404 | pump(stream, maybeConvert(objectMode, false), rsT) 405 | } 406 | if (type & WRITABLE) { 407 | var wsT = this.getTransportStream(id, WRITABLE, stream) 408 | pump(wsT, maybeConvert(false, objectMode), stream) 409 | } 410 | 411 | return { stream: { type, objectMode } } 412 | } 413 | 414 | HypeRPC.prototype.resolveStream = function (arg, id) { 415 | var { type, objectMode } = arg.stream 416 | var ds = objectMode ? duplexify.obj() : duplexify() 417 | 418 | if (type & READABLE) { 419 | var rs = through({objectMode}) 420 | var rsT = this.getTransportStream(id, READABLE, rs) 421 | pump(rsT, maybeConvert(false, objectMode), rs) 422 | ds.setReadable(rs) 423 | } 424 | if (type & WRITABLE) { 425 | var ws = through({objectMode}) 426 | var wsT = this.getTransportStream(id, WRITABLE, ws) 427 | pump(ws, maybeConvert(objectMode, false), wsT) 428 | ds.setWritable(ws) 429 | } 430 | 431 | return ds 432 | } 433 | 434 | HypeRPC.prototype.prepareBuffer = function (arg) { 435 | return { bytes: arg } 436 | } 437 | 438 | HypeRPC.prototype.resolveBuffer = function (arg) { 439 | return arg.bytes 440 | } 441 | 442 | HypeRPC.prototype.onstream = function (sT, name) { 443 | var self = this 444 | // stream names are: ID-TYPE 445 | var match = name.match(/^([a-zA-Z0-9.]+)-([0-3]){1}$/) 446 | 447 | if (!match) return console.error('received unrecognized stream: ' + name) 448 | 449 | var id = match[1] 450 | var type = match[2] 451 | 452 | sT.on('error', (err) => self.log(name, err)) 453 | 454 | this.transports[`${id}-${type}`] = sT 455 | } 456 | 457 | HypeRPC.prototype.getTransportStream = function (id, type, stream) { 458 | var sid = `${id}-${type}` 459 | if (!this.transports[sid]) this.transports[sid] = this.stream.createSharedStream(sid) 460 | return this.transports[sid] 461 | } 462 | 463 | HypeRPC.prototype.makeId = function () { 464 | return joinIds(this.prefix, this.cnt++) 465 | } 466 | 467 | HypeRPC.prototype.toLog = function (name) { 468 | var self = this 469 | return through.obj(function (chunk, enc, next) { 470 | self.log(name, Buffer.isBuffer(chunk) ? chunk.toString() : chunk) 471 | this.push(chunk) 472 | next() 473 | }) 474 | } 475 | 476 | HypeRPC.prototype.log = function (...args) { 477 | if (!this.debug) return 478 | if (!this.__logger) { 479 | try { 480 | this.__logger = require('debug')('hyperpc') 481 | } catch (e) { 482 | this.__logger = console.log 483 | } 484 | } 485 | var s = this.prefix + (this.name ? `=${this.name}` : '') 486 | this.__logger('rpcstream [%s]:', s, ...args) 487 | } 488 | 489 | // Pure helpers. 490 | function joinIds (...ids) { 491 | return ids.join(SEPERATOR) 492 | } 493 | 494 | function calculatePrefix (nonce, remoteNonce) { 495 | if (remoteNonce > nonce) return 'A' 496 | else if (remoteNonce < nonce) return 'B' 497 | else return 'X' + (Math.round(Math.random() * 1000)) 498 | } 499 | 500 | function isFunc (obj) { 501 | return typeof obj === 'function' 502 | } 503 | 504 | function isError (arg) { 505 | return arg instanceof Error 506 | } 507 | 508 | function isRpcified (arg) { 509 | return arg instanceof rpcify 510 | } 511 | 512 | function isPromise (obj) { 513 | return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function' 514 | } 515 | 516 | function isStream (obj) { 517 | return obj instanceof stream.Stream || (isObject(obj) && obj && (obj._readableState || obj._writableState)) 518 | } 519 | 520 | function isReadable (obj) { 521 | return isStream(obj) && typeof obj._read === 'function' && typeof obj._readableState === 'object' 522 | } 523 | 524 | function isWritable (obj) { 525 | return isStream(obj) && typeof obj._write === 'function' && typeof obj._writableState === 'object' 526 | } 527 | 528 | function isTransform (obj) { 529 | return isStream(obj) && typeof obj._transform === 'function' && typeof obj._transformState === 'object' 530 | } 531 | 532 | function isObjectStream (stream) { 533 | if (isWritable(stream)) return stream._writableState.objectMode 534 | if (isReadable(stream)) return stream._readableState.objectMode 535 | } 536 | 537 | function isBuffer (buf) { 538 | return Buffer.isBuffer(buf) 539 | } 540 | 541 | function isObject (obj) { 542 | return (typeof obj === 'object') 543 | } 544 | 545 | function isLiteral (val) { 546 | return (typeof val === 'boolean' || typeof val === 'string' || typeof val === 'number') 547 | } 548 | 549 | function streamType (stream) { 550 | var type = 0 551 | 552 | // Special handling for transform streams. If it has no pipes attached, 553 | // assume its readable. Otherwise, assume its writable. If this leads 554 | // to unexpected behaviors, set up a duplex stream with duplexify and 555 | // use either setReadable() or setWritable() to only set up one end. 556 | if (isTransform(stream)) { 557 | if (typeof stream._readableState === 'object' && !stream._readableState.pipes) { 558 | return READABLE 559 | } else { 560 | return WRITABLE 561 | } 562 | } 563 | 564 | if (isReadable(stream)) type = type | READABLE 565 | if (isWritable(stream)) type = type | WRITABLE 566 | 567 | return type 568 | } 569 | 570 | function pass (objectMode) { 571 | return through({objectMode}) 572 | } 573 | 574 | function toObj () { 575 | return through.obj(function (chunk, enc, next) { 576 | this.push(JSON.parse(chunk)) 577 | next() 578 | }) 579 | } 580 | 581 | function toBin () { 582 | return through.obj(function (chunk, enc, next) { 583 | this.push(JSON.stringify(chunk)) 584 | next() 585 | }) 586 | } 587 | 588 | function maybeConvert (oneInObjMode, twoInObjMode) { 589 | if (oneInObjMode && !twoInObjMode) return toBin() 590 | if (!oneInObjMode && twoInObjMode) return toObj() 591 | if (oneInObjMode && twoInObjMode) return pass(true) 592 | if (!oneInObjMode && !twoInObjMode) return pass(false) 593 | } 594 | -------------------------------------------------------------------------------- /messages.js: -------------------------------------------------------------------------------- 1 | // This file is auto generated by the protocol-buffers compiler 2 | 3 | /* eslint-disable quotes */ 4 | /* eslint-disable indent */ 5 | /* eslint-disable no-redeclare */ 6 | /* eslint-disable camelcase */ 7 | 8 | // Remember to `npm install --save protocol-buffers-encodings` 9 | var encodings = require('protocol-buffers-encodings') 10 | var varint = encodings.varint 11 | var skip = encodings.skip 12 | 13 | exports.TYPE = { 14 | "MANIFEST": 1, 15 | "CALL": 2, 16 | "RETURN": 3 17 | } 18 | 19 | exports.CALL = { 20 | "API": 1, 21 | "OBJECT": 2 22 | } 23 | 24 | exports.RETURN = { 25 | "CALLBACK": 1, 26 | "PROMISE": 2 27 | } 28 | 29 | exports.PROMISE = { 30 | "RESOLVE": 0, 31 | "REJECT": 1 32 | } 33 | 34 | exports.ARGUMENT = { 35 | "BYTES": 1, 36 | "JSON": 2, 37 | "CALLBACK": 3, 38 | "RPCIFIED": 4, 39 | "STREAM": 5, 40 | "ERROR": 6 41 | } 42 | 43 | exports.STREAM = { 44 | "READABLE": 1, 45 | "WRITEABLE": 2, 46 | "DUPLEX": 3 47 | } 48 | 49 | var Arg = exports.Arg = { 50 | buffer: true, 51 | encodingLength: null, 52 | encode: null, 53 | decode: null 54 | } 55 | 56 | var Call = exports.Call = { 57 | buffer: true, 58 | encodingLength: null, 59 | encode: null, 60 | decode: null 61 | } 62 | 63 | var Return = exports.Return = { 64 | buffer: true, 65 | encodingLength: null, 66 | encode: null, 67 | decode: null 68 | } 69 | 70 | var Manifest = exports.Manifest = { 71 | buffer: true, 72 | encodingLength: null, 73 | encode: null, 74 | decode: null 75 | } 76 | 77 | var Msg = exports.Msg = { 78 | buffer: true, 79 | encodingLength: null, 80 | encode: null, 81 | decode: null 82 | } 83 | 84 | defineArg() 85 | defineCall() 86 | defineReturn() 87 | defineManifest() 88 | defineMsg() 89 | 90 | function defineArg () { 91 | var Rpcified = Arg.Rpcified = { 92 | buffer: true, 93 | encodingLength: null, 94 | encode: null, 95 | decode: null 96 | } 97 | 98 | var Stream = Arg.Stream = { 99 | buffer: true, 100 | encodingLength: null, 101 | encode: null, 102 | decode: null 103 | } 104 | 105 | defineRpcified() 106 | defineStream() 107 | 108 | function defineRpcified () { 109 | var enc = [ 110 | encodings.string 111 | ] 112 | 113 | Rpcified.encodingLength = encodingLength 114 | Rpcified.encode = encode 115 | Rpcified.decode = decode 116 | 117 | function encodingLength (obj) { 118 | var length = 0 119 | if (!defined(obj.manifest)) throw new Error("manifest is required") 120 | var len = enc[0].encodingLength(obj.manifest) 121 | length += 1 + len 122 | return length 123 | } 124 | 125 | function encode (obj, buf, offset) { 126 | if (!offset) offset = 0 127 | if (!buf) buf = Buffer.allocUnsafe(encodingLength(obj)) 128 | var oldOffset = offset 129 | if (!defined(obj.manifest)) throw new Error("manifest is required") 130 | buf[offset++] = 10 131 | enc[0].encode(obj.manifest, buf, offset) 132 | offset += enc[0].encode.bytes 133 | encode.bytes = offset - oldOffset 134 | return buf 135 | } 136 | 137 | function decode (buf, offset, end) { 138 | if (!offset) offset = 0 139 | if (!end) end = buf.length 140 | if (!(end <= buf.length && offset <= buf.length)) throw new Error("Decoded message is not valid") 141 | var oldOffset = offset 142 | var obj = { 143 | manifest: "" 144 | } 145 | var found0 = false 146 | while (true) { 147 | if (end <= offset) { 148 | if (!found0) throw new Error("Decoded message is not valid") 149 | decode.bytes = offset - oldOffset 150 | return obj 151 | } 152 | var prefix = varint.decode(buf, offset) 153 | offset += varint.decode.bytes 154 | var tag = prefix >> 3 155 | switch (tag) { 156 | case 1: 157 | obj.manifest = enc[0].decode(buf, offset) 158 | offset += enc[0].decode.bytes 159 | found0 = true 160 | break 161 | default: 162 | offset = skip(prefix & 7, buf, offset) 163 | } 164 | } 165 | } 166 | } 167 | 168 | function defineStream () { 169 | var enc = [ 170 | encodings.enum, 171 | encodings.bool 172 | ] 173 | 174 | Stream.encodingLength = encodingLength 175 | Stream.encode = encode 176 | Stream.decode = decode 177 | 178 | function encodingLength (obj) { 179 | var length = 0 180 | if (!defined(obj.type)) throw new Error("type is required") 181 | var len = enc[0].encodingLength(obj.type) 182 | length += 1 + len 183 | if (!defined(obj.objectMode)) throw new Error("objectMode is required") 184 | var len = enc[1].encodingLength(obj.objectMode) 185 | length += 1 + len 186 | return length 187 | } 188 | 189 | function encode (obj, buf, offset) { 190 | if (!offset) offset = 0 191 | if (!buf) buf = Buffer.allocUnsafe(encodingLength(obj)) 192 | var oldOffset = offset 193 | if (!defined(obj.type)) throw new Error("type is required") 194 | buf[offset++] = 8 195 | enc[0].encode(obj.type, buf, offset) 196 | offset += enc[0].encode.bytes 197 | if (!defined(obj.objectMode)) throw new Error("objectMode is required") 198 | buf[offset++] = 16 199 | enc[1].encode(obj.objectMode, buf, offset) 200 | offset += enc[1].encode.bytes 201 | encode.bytes = offset - oldOffset 202 | return buf 203 | } 204 | 205 | function decode (buf, offset, end) { 206 | if (!offset) offset = 0 207 | if (!end) end = buf.length 208 | if (!(end <= buf.length && offset <= buf.length)) throw new Error("Decoded message is not valid") 209 | var oldOffset = offset 210 | var obj = { 211 | type: 1, 212 | objectMode: false 213 | } 214 | var found0 = false 215 | var found1 = false 216 | while (true) { 217 | if (end <= offset) { 218 | if (!found0 || !found1) throw new Error("Decoded message is not valid") 219 | decode.bytes = offset - oldOffset 220 | return obj 221 | } 222 | var prefix = varint.decode(buf, offset) 223 | offset += varint.decode.bytes 224 | var tag = prefix >> 3 225 | switch (tag) { 226 | case 1: 227 | obj.type = enc[0].decode(buf, offset) 228 | offset += enc[0].decode.bytes 229 | found0 = true 230 | break 231 | case 2: 232 | obj.objectMode = enc[1].decode(buf, offset) 233 | offset += enc[1].decode.bytes 234 | found1 = true 235 | break 236 | default: 237 | offset = skip(prefix & 7, buf, offset) 238 | } 239 | } 240 | } 241 | } 242 | 243 | var enc = [ 244 | encodings.enum, 245 | encodings.bytes, 246 | encodings.string, 247 | Rpcified, 248 | Stream 249 | ] 250 | 251 | Arg.encodingLength = encodingLength 252 | Arg.encode = encode 253 | Arg.decode = decode 254 | 255 | function encodingLength (obj) { 256 | var length = 0 257 | if ((+defined(obj.bytes) + +defined(obj.json) + +defined(obj.callback) + +defined(obj.rpcified) + +defined(obj.stream) + +defined(obj.error)) > 1) throw new Error("only one of the properties defined in oneof payload can be set") 258 | if (!defined(obj.type)) throw new Error("type is required") 259 | var len = enc[0].encodingLength(obj.type) 260 | length += 1 + len 261 | if (defined(obj.bytes)) { 262 | var len = enc[1].encodingLength(obj.bytes) 263 | length += 1 + len 264 | } 265 | if (defined(obj.json)) { 266 | var len = enc[2].encodingLength(obj.json) 267 | length += 1 + len 268 | } 269 | if (defined(obj.callback)) { 270 | var len = enc[2].encodingLength(obj.callback) 271 | length += 1 + len 272 | } 273 | if (defined(obj.rpcified)) { 274 | var len = enc[3].encodingLength(obj.rpcified) 275 | length += varint.encodingLength(len) 276 | length += 1 + len 277 | } 278 | if (defined(obj.stream)) { 279 | var len = enc[4].encodingLength(obj.stream) 280 | length += varint.encodingLength(len) 281 | length += 1 + len 282 | } 283 | if (defined(obj.error)) { 284 | var len = enc[2].encodingLength(obj.error) 285 | length += 1 + len 286 | } 287 | return length 288 | } 289 | 290 | function encode (obj, buf, offset) { 291 | if (!offset) offset = 0 292 | if (!buf) buf = Buffer.allocUnsafe(encodingLength(obj)) 293 | var oldOffset = offset 294 | if ((+defined(obj.bytes) + +defined(obj.json) + +defined(obj.callback) + +defined(obj.rpcified) + +defined(obj.stream) + +defined(obj.error)) > 1) throw new Error("only one of the properties defined in oneof payload can be set") 295 | if (!defined(obj.type)) throw new Error("type is required") 296 | buf[offset++] = 8 297 | enc[0].encode(obj.type, buf, offset) 298 | offset += enc[0].encode.bytes 299 | if (defined(obj.bytes)) { 300 | buf[offset++] = 18 301 | enc[1].encode(obj.bytes, buf, offset) 302 | offset += enc[1].encode.bytes 303 | } 304 | if (defined(obj.json)) { 305 | buf[offset++] = 26 306 | enc[2].encode(obj.json, buf, offset) 307 | offset += enc[2].encode.bytes 308 | } 309 | if (defined(obj.callback)) { 310 | buf[offset++] = 34 311 | enc[2].encode(obj.callback, buf, offset) 312 | offset += enc[2].encode.bytes 313 | } 314 | if (defined(obj.rpcified)) { 315 | buf[offset++] = 42 316 | varint.encode(enc[3].encodingLength(obj.rpcified), buf, offset) 317 | offset += varint.encode.bytes 318 | enc[3].encode(obj.rpcified, buf, offset) 319 | offset += enc[3].encode.bytes 320 | } 321 | if (defined(obj.stream)) { 322 | buf[offset++] = 50 323 | varint.encode(enc[4].encodingLength(obj.stream), buf, offset) 324 | offset += varint.encode.bytes 325 | enc[4].encode(obj.stream, buf, offset) 326 | offset += enc[4].encode.bytes 327 | } 328 | if (defined(obj.error)) { 329 | buf[offset++] = 58 330 | enc[2].encode(obj.error, buf, offset) 331 | offset += enc[2].encode.bytes 332 | } 333 | encode.bytes = offset - oldOffset 334 | return buf 335 | } 336 | 337 | function decode (buf, offset, end) { 338 | if (!offset) offset = 0 339 | if (!end) end = buf.length 340 | if (!(end <= buf.length && offset <= buf.length)) throw new Error("Decoded message is not valid") 341 | var oldOffset = offset 342 | var obj = { 343 | type: 1, 344 | bytes: null, 345 | json: "", 346 | callback: "", 347 | rpcified: null, 348 | stream: null, 349 | error: "" 350 | } 351 | var found0 = false 352 | while (true) { 353 | if (end <= offset) { 354 | if (!found0) throw new Error("Decoded message is not valid") 355 | decode.bytes = offset - oldOffset 356 | return obj 357 | } 358 | var prefix = varint.decode(buf, offset) 359 | offset += varint.decode.bytes 360 | var tag = prefix >> 3 361 | switch (tag) { 362 | case 1: 363 | obj.type = enc[0].decode(buf, offset) 364 | offset += enc[0].decode.bytes 365 | found0 = true 366 | break 367 | case 2: 368 | delete obj.json 369 | delete obj.callback 370 | delete obj.rpcified 371 | delete obj.stream 372 | delete obj.error 373 | obj.bytes = enc[1].decode(buf, offset) 374 | offset += enc[1].decode.bytes 375 | break 376 | case 3: 377 | delete obj.bytes 378 | delete obj.callback 379 | delete obj.rpcified 380 | delete obj.stream 381 | delete obj.error 382 | obj.json = enc[2].decode(buf, offset) 383 | offset += enc[2].decode.bytes 384 | break 385 | case 4: 386 | delete obj.bytes 387 | delete obj.json 388 | delete obj.rpcified 389 | delete obj.stream 390 | delete obj.error 391 | obj.callback = enc[2].decode(buf, offset) 392 | offset += enc[2].decode.bytes 393 | break 394 | case 5: 395 | delete obj.bytes 396 | delete obj.json 397 | delete obj.callback 398 | delete obj.stream 399 | delete obj.error 400 | var len = varint.decode(buf, offset) 401 | offset += varint.decode.bytes 402 | obj.rpcified = enc[3].decode(buf, offset, offset + len) 403 | offset += enc[3].decode.bytes 404 | break 405 | case 6: 406 | delete obj.bytes 407 | delete obj.json 408 | delete obj.callback 409 | delete obj.rpcified 410 | delete obj.error 411 | var len = varint.decode(buf, offset) 412 | offset += varint.decode.bytes 413 | obj.stream = enc[4].decode(buf, offset, offset + len) 414 | offset += enc[4].decode.bytes 415 | break 416 | case 7: 417 | delete obj.bytes 418 | delete obj.json 419 | delete obj.callback 420 | delete obj.rpcified 421 | delete obj.stream 422 | obj.error = enc[2].decode(buf, offset) 423 | offset += enc[2].decode.bytes 424 | break 425 | default: 426 | offset = skip(prefix & 7, buf, offset) 427 | } 428 | } 429 | } 430 | } 431 | 432 | function defineCall () { 433 | var enc = [ 434 | encodings.enum, 435 | encodings.string, 436 | Arg 437 | ] 438 | 439 | Call.encodingLength = encodingLength 440 | Call.encode = encode 441 | Call.decode = decode 442 | 443 | function encodingLength (obj) { 444 | var length = 0 445 | if (!defined(obj.type)) throw new Error("type is required") 446 | var len = enc[0].encodingLength(obj.type) 447 | length += 1 + len 448 | if (!defined(obj.id)) throw new Error("id is required") 449 | var len = enc[1].encodingLength(obj.id) 450 | length += 1 + len 451 | if (defined(obj.name)) { 452 | var len = enc[1].encodingLength(obj.name) 453 | length += 1 + len 454 | } 455 | if (defined(obj.objectid)) { 456 | var len = enc[1].encodingLength(obj.objectid) 457 | length += 1 + len 458 | } 459 | if (defined(obj.method)) { 460 | var len = enc[1].encodingLength(obj.method) 461 | length += 1 + len 462 | } 463 | if (defined(obj.args)) { 464 | for (var i = 0; i < obj.args.length; i++) { 465 | if (!defined(obj.args[i])) continue 466 | var len = enc[2].encodingLength(obj.args[i]) 467 | length += varint.encodingLength(len) 468 | length += 1 + len 469 | } 470 | } 471 | return length 472 | } 473 | 474 | function encode (obj, buf, offset) { 475 | if (!offset) offset = 0 476 | if (!buf) buf = Buffer.allocUnsafe(encodingLength(obj)) 477 | var oldOffset = offset 478 | if (!defined(obj.type)) throw new Error("type is required") 479 | buf[offset++] = 8 480 | enc[0].encode(obj.type, buf, offset) 481 | offset += enc[0].encode.bytes 482 | if (!defined(obj.id)) throw new Error("id is required") 483 | buf[offset++] = 18 484 | enc[1].encode(obj.id, buf, offset) 485 | offset += enc[1].encode.bytes 486 | if (defined(obj.name)) { 487 | buf[offset++] = 26 488 | enc[1].encode(obj.name, buf, offset) 489 | offset += enc[1].encode.bytes 490 | } 491 | if (defined(obj.objectid)) { 492 | buf[offset++] = 34 493 | enc[1].encode(obj.objectid, buf, offset) 494 | offset += enc[1].encode.bytes 495 | } 496 | if (defined(obj.method)) { 497 | buf[offset++] = 42 498 | enc[1].encode(obj.method, buf, offset) 499 | offset += enc[1].encode.bytes 500 | } 501 | if (defined(obj.args)) { 502 | for (var i = 0; i < obj.args.length; i++) { 503 | if (!defined(obj.args[i])) continue 504 | buf[offset++] = 50 505 | varint.encode(enc[2].encodingLength(obj.args[i]), buf, offset) 506 | offset += varint.encode.bytes 507 | enc[2].encode(obj.args[i], buf, offset) 508 | offset += enc[2].encode.bytes 509 | } 510 | } 511 | encode.bytes = offset - oldOffset 512 | return buf 513 | } 514 | 515 | function decode (buf, offset, end) { 516 | if (!offset) offset = 0 517 | if (!end) end = buf.length 518 | if (!(end <= buf.length && offset <= buf.length)) throw new Error("Decoded message is not valid") 519 | var oldOffset = offset 520 | var obj = { 521 | type: 1, 522 | id: "", 523 | name: "", 524 | objectid: "", 525 | method: "", 526 | args: [] 527 | } 528 | var found0 = false 529 | var found1 = false 530 | while (true) { 531 | if (end <= offset) { 532 | if (!found0 || !found1) throw new Error("Decoded message is not valid") 533 | decode.bytes = offset - oldOffset 534 | return obj 535 | } 536 | var prefix = varint.decode(buf, offset) 537 | offset += varint.decode.bytes 538 | var tag = prefix >> 3 539 | switch (tag) { 540 | case 1: 541 | obj.type = enc[0].decode(buf, offset) 542 | offset += enc[0].decode.bytes 543 | found0 = true 544 | break 545 | case 2: 546 | obj.id = enc[1].decode(buf, offset) 547 | offset += enc[1].decode.bytes 548 | found1 = true 549 | break 550 | case 3: 551 | obj.name = enc[1].decode(buf, offset) 552 | offset += enc[1].decode.bytes 553 | break 554 | case 4: 555 | obj.objectid = enc[1].decode(buf, offset) 556 | offset += enc[1].decode.bytes 557 | break 558 | case 5: 559 | obj.method = enc[1].decode(buf, offset) 560 | offset += enc[1].decode.bytes 561 | break 562 | case 6: 563 | var len = varint.decode(buf, offset) 564 | offset += varint.decode.bytes 565 | obj.args.push(enc[2].decode(buf, offset, offset + len)) 566 | offset += enc[2].decode.bytes 567 | break 568 | default: 569 | offset = skip(prefix & 7, buf, offset) 570 | } 571 | } 572 | } 573 | } 574 | 575 | function defineReturn () { 576 | var enc = [ 577 | encodings.enum, 578 | encodings.string, 579 | encodings.enum, 580 | Arg 581 | ] 582 | 583 | Return.encodingLength = encodingLength 584 | Return.encode = encode 585 | Return.decode = decode 586 | 587 | function encodingLength (obj) { 588 | var length = 0 589 | if (!defined(obj.type)) throw new Error("type is required") 590 | var len = enc[0].encodingLength(obj.type) 591 | length += 1 + len 592 | if (!defined(obj.id)) throw new Error("id is required") 593 | var len = enc[1].encodingLength(obj.id) 594 | length += 1 + len 595 | if (defined(obj.promise)) { 596 | var len = enc[2].encodingLength(obj.promise) 597 | length += 1 + len 598 | } 599 | if (defined(obj.args)) { 600 | for (var i = 0; i < obj.args.length; i++) { 601 | if (!defined(obj.args[i])) continue 602 | var len = enc[3].encodingLength(obj.args[i]) 603 | length += varint.encodingLength(len) 604 | length += 1 + len 605 | } 606 | } 607 | return length 608 | } 609 | 610 | function encode (obj, buf, offset) { 611 | if (!offset) offset = 0 612 | if (!buf) buf = Buffer.allocUnsafe(encodingLength(obj)) 613 | var oldOffset = offset 614 | if (!defined(obj.type)) throw new Error("type is required") 615 | buf[offset++] = 8 616 | enc[0].encode(obj.type, buf, offset) 617 | offset += enc[0].encode.bytes 618 | if (!defined(obj.id)) throw new Error("id is required") 619 | buf[offset++] = 18 620 | enc[1].encode(obj.id, buf, offset) 621 | offset += enc[1].encode.bytes 622 | if (defined(obj.promise)) { 623 | buf[offset++] = 24 624 | enc[2].encode(obj.promise, buf, offset) 625 | offset += enc[2].encode.bytes 626 | } 627 | if (defined(obj.args)) { 628 | for (var i = 0; i < obj.args.length; i++) { 629 | if (!defined(obj.args[i])) continue 630 | buf[offset++] = 34 631 | varint.encode(enc[3].encodingLength(obj.args[i]), buf, offset) 632 | offset += varint.encode.bytes 633 | enc[3].encode(obj.args[i], buf, offset) 634 | offset += enc[3].encode.bytes 635 | } 636 | } 637 | encode.bytes = offset - oldOffset 638 | return buf 639 | } 640 | 641 | function decode (buf, offset, end) { 642 | if (!offset) offset = 0 643 | if (!end) end = buf.length 644 | if (!(end <= buf.length && offset <= buf.length)) throw new Error("Decoded message is not valid") 645 | var oldOffset = offset 646 | var obj = { 647 | type: 1, 648 | id: "", 649 | promise: 0, 650 | args: [] 651 | } 652 | var found0 = false 653 | var found1 = false 654 | while (true) { 655 | if (end <= offset) { 656 | if (!found0 || !found1) throw new Error("Decoded message is not valid") 657 | decode.bytes = offset - oldOffset 658 | return obj 659 | } 660 | var prefix = varint.decode(buf, offset) 661 | offset += varint.decode.bytes 662 | var tag = prefix >> 3 663 | switch (tag) { 664 | case 1: 665 | obj.type = enc[0].decode(buf, offset) 666 | offset += enc[0].decode.bytes 667 | found0 = true 668 | break 669 | case 2: 670 | obj.id = enc[1].decode(buf, offset) 671 | offset += enc[1].decode.bytes 672 | found1 = true 673 | break 674 | case 3: 675 | obj.promise = enc[2].decode(buf, offset) 676 | offset += enc[2].decode.bytes 677 | break 678 | case 4: 679 | var len = varint.decode(buf, offset) 680 | offset += varint.decode.bytes 681 | obj.args.push(enc[3].decode(buf, offset, offset + len)) 682 | offset += enc[3].decode.bytes 683 | break 684 | default: 685 | offset = skip(prefix & 7, buf, offset) 686 | } 687 | } 688 | } 689 | } 690 | 691 | function defineManifest () { 692 | var enc = [ 693 | encodings.string, 694 | encodings.varint 695 | ] 696 | 697 | Manifest.encodingLength = encodingLength 698 | Manifest.encode = encode 699 | Manifest.decode = decode 700 | 701 | function encodingLength (obj) { 702 | var length = 0 703 | if (!defined(obj.manifest)) throw new Error("manifest is required") 704 | var len = enc[0].encodingLength(obj.manifest) 705 | length += 1 + len 706 | if (!defined(obj.nonce)) throw new Error("nonce is required") 707 | var len = enc[1].encodingLength(obj.nonce) 708 | length += 1 + len 709 | return length 710 | } 711 | 712 | function encode (obj, buf, offset) { 713 | if (!offset) offset = 0 714 | if (!buf) buf = Buffer.allocUnsafe(encodingLength(obj)) 715 | var oldOffset = offset 716 | if (!defined(obj.manifest)) throw new Error("manifest is required") 717 | buf[offset++] = 10 718 | enc[0].encode(obj.manifest, buf, offset) 719 | offset += enc[0].encode.bytes 720 | if (!defined(obj.nonce)) throw new Error("nonce is required") 721 | buf[offset++] = 16 722 | enc[1].encode(obj.nonce, buf, offset) 723 | offset += enc[1].encode.bytes 724 | encode.bytes = offset - oldOffset 725 | return buf 726 | } 727 | 728 | function decode (buf, offset, end) { 729 | if (!offset) offset = 0 730 | if (!end) end = buf.length 731 | if (!(end <= buf.length && offset <= buf.length)) throw new Error("Decoded message is not valid") 732 | var oldOffset = offset 733 | var obj = { 734 | manifest: "", 735 | nonce: 0 736 | } 737 | var found0 = false 738 | var found1 = false 739 | while (true) { 740 | if (end <= offset) { 741 | if (!found0 || !found1) throw new Error("Decoded message is not valid") 742 | decode.bytes = offset - oldOffset 743 | return obj 744 | } 745 | var prefix = varint.decode(buf, offset) 746 | offset += varint.decode.bytes 747 | var tag = prefix >> 3 748 | switch (tag) { 749 | case 1: 750 | obj.manifest = enc[0].decode(buf, offset) 751 | offset += enc[0].decode.bytes 752 | found0 = true 753 | break 754 | case 2: 755 | obj.nonce = enc[1].decode(buf, offset) 756 | offset += enc[1].decode.bytes 757 | found1 = true 758 | break 759 | default: 760 | offset = skip(prefix & 7, buf, offset) 761 | } 762 | } 763 | } 764 | } 765 | 766 | function defineMsg () { 767 | var enc = [ 768 | encodings.enum, 769 | Manifest, 770 | Call, 771 | Return 772 | ] 773 | 774 | Msg.encodingLength = encodingLength 775 | Msg.encode = encode 776 | Msg.decode = decode 777 | 778 | function encodingLength (obj) { 779 | var length = 0 780 | if ((+defined(obj.manifest) + +defined(obj.call) + +defined(obj.return)) > 1) throw new Error("only one of the properties defined in oneof msg can be set") 781 | if (!defined(obj.type)) throw new Error("type is required") 782 | var len = enc[0].encodingLength(obj.type) 783 | length += 1 + len 784 | if (defined(obj.manifest)) { 785 | var len = enc[1].encodingLength(obj.manifest) 786 | length += varint.encodingLength(len) 787 | length += 1 + len 788 | } 789 | if (defined(obj.call)) { 790 | var len = enc[2].encodingLength(obj.call) 791 | length += varint.encodingLength(len) 792 | length += 1 + len 793 | } 794 | if (defined(obj.return)) { 795 | var len = enc[3].encodingLength(obj.return) 796 | length += varint.encodingLength(len) 797 | length += 1 + len 798 | } 799 | return length 800 | } 801 | 802 | function encode (obj, buf, offset) { 803 | if (!offset) offset = 0 804 | if (!buf) buf = Buffer.allocUnsafe(encodingLength(obj)) 805 | var oldOffset = offset 806 | if ((+defined(obj.manifest) + +defined(obj.call) + +defined(obj.return)) > 1) throw new Error("only one of the properties defined in oneof msg can be set") 807 | if (!defined(obj.type)) throw new Error("type is required") 808 | buf[offset++] = 8 809 | enc[0].encode(obj.type, buf, offset) 810 | offset += enc[0].encode.bytes 811 | if (defined(obj.manifest)) { 812 | buf[offset++] = 18 813 | varint.encode(enc[1].encodingLength(obj.manifest), buf, offset) 814 | offset += varint.encode.bytes 815 | enc[1].encode(obj.manifest, buf, offset) 816 | offset += enc[1].encode.bytes 817 | } 818 | if (defined(obj.call)) { 819 | buf[offset++] = 26 820 | varint.encode(enc[2].encodingLength(obj.call), buf, offset) 821 | offset += varint.encode.bytes 822 | enc[2].encode(obj.call, buf, offset) 823 | offset += enc[2].encode.bytes 824 | } 825 | if (defined(obj.return)) { 826 | buf[offset++] = 34 827 | varint.encode(enc[3].encodingLength(obj.return), buf, offset) 828 | offset += varint.encode.bytes 829 | enc[3].encode(obj.return, buf, offset) 830 | offset += enc[3].encode.bytes 831 | } 832 | encode.bytes = offset - oldOffset 833 | return buf 834 | } 835 | 836 | function decode (buf, offset, end) { 837 | if (!offset) offset = 0 838 | if (!end) end = buf.length 839 | if (!(end <= buf.length && offset <= buf.length)) throw new Error("Decoded message is not valid") 840 | var oldOffset = offset 841 | var obj = { 842 | type: 1, 843 | manifest: null, 844 | call: null, 845 | return: null 846 | } 847 | var found0 = false 848 | while (true) { 849 | if (end <= offset) { 850 | if (!found0) throw new Error("Decoded message is not valid") 851 | decode.bytes = offset - oldOffset 852 | return obj 853 | } 854 | var prefix = varint.decode(buf, offset) 855 | offset += varint.decode.bytes 856 | var tag = prefix >> 3 857 | switch (tag) { 858 | case 1: 859 | obj.type = enc[0].decode(buf, offset) 860 | offset += enc[0].decode.bytes 861 | found0 = true 862 | break 863 | case 2: 864 | delete obj.call 865 | delete obj.return 866 | var len = varint.decode(buf, offset) 867 | offset += varint.decode.bytes 868 | obj.manifest = enc[1].decode(buf, offset, offset + len) 869 | offset += enc[1].decode.bytes 870 | break 871 | case 3: 872 | delete obj.manifest 873 | delete obj.return 874 | var len = varint.decode(buf, offset) 875 | offset += varint.decode.bytes 876 | obj.call = enc[2].decode(buf, offset, offset + len) 877 | offset += enc[2].decode.bytes 878 | break 879 | case 4: 880 | delete obj.manifest 881 | delete obj.call 882 | var len = varint.decode(buf, offset) 883 | offset += varint.decode.bytes 884 | obj.return = enc[3].decode(buf, offset, offset + len) 885 | offset += enc[3].decode.bytes 886 | break 887 | default: 888 | offset = skip(prefix & 7, buf, offset) 889 | } 890 | } 891 | } 892 | } 893 | 894 | function defined (val) { 895 | return val !== null && val !== undefined && (typeof val !== 'number' || !isNaN(val)) 896 | } 897 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperpc", 3 | "version": "2.1.0", 4 | "description": "Bidirectional RPC over any stream with callbacks, streams and promises", 5 | "main": "index.js", 6 | "directories": { 7 | "example": "example" 8 | }, 9 | "scripts": { 10 | "test": "tape test/**", 11 | "compile": "protocol-buffers schema.proto -o messages.js" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/Frando/hyperpc.git" 16 | }, 17 | "keywords": [ 18 | "rpc", 19 | "streams", 20 | "multiplex" 21 | ], 22 | "author": "Franz Heinzmann", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/Frando/hyperpc/issues" 26 | }, 27 | "homepage": "https://github.com/Frando/hyperpc#readme", 28 | "dependencies": { 29 | "duplexify": "^3.6.0", 30 | "multiplex": "^6.7.0", 31 | "protocol-buffers-encodings": "^1.1.0", 32 | "pump": "^3.0.0", 33 | "through2": "^2.0.3", 34 | "thunky": "^1.0.2" 35 | }, 36 | "devDependencies": { 37 | "debug": "^4.0.1", 38 | "protocol-buffers": "^4.1.0", 39 | "tape": "^4.9.1" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /rpcify.js: -------------------------------------------------------------------------------- 1 | // var debug = require('debug')('rpcify') 2 | 3 | function RPCify (obj, opts) { 4 | if (!obj) return null 5 | if (!(this instanceof RPCify)) return new RPCify(obj, opts) 6 | opts = opts || {} 7 | 8 | var defaults = { 9 | skipPrivate: true, 10 | include: null, 11 | exclude: [], 12 | override: {}, 13 | factory: null, 14 | name: null, 15 | access: function () { return true } 16 | } 17 | 18 | if (obj.__hyperpc) { 19 | this.opts = Object.assign(defaults, obj.__hyperpc, opts) 20 | } else if (obj.prototype && obj.prototype.__hyperpc) { 21 | this.opts = Object.assign(defaults, obj.prototype.__hyperpc, opts) 22 | } else if (!obj.prototype && Object.getPrototypeOf(obj) && Object.getPrototypeOf(obj).__hyperpc) { 23 | this.opts = Object.assign(defaults, Object.getPrototypeOf(obj).__hyperpc, opts) 24 | } else { 25 | this.opts = Object.assign(defaults, opts) 26 | } 27 | 28 | this.access = this.opts.access 29 | this.override = this.opts.override 30 | this.cache = {} 31 | 32 | if (obj.prototype) { 33 | // 1. Class (prototype) 34 | 35 | if (opts.factory) this.factory = opts.factory 36 | else this.factory = makeDefaultFactory(obj) 37 | 38 | this.instance = null 39 | this.name = obj.name 40 | this.funcs = getAllFuncs(obj.prototype) 41 | } else { 42 | // 2. Object instance 43 | 44 | this.factory = null 45 | this.instance = obj 46 | this.name = Object.getPrototypeOf(obj).name 47 | this.funcs = getAllFuncs(obj) 48 | } 49 | 50 | var out = ['constructor'] 51 | if (this.opts.exclude) out = out.concat(this.opts.exclude) 52 | 53 | this.filteredFuncs = this.funcs.filter(f => { 54 | if (this.opts.include && this.opts.include.indexOf(f) === -1) return false 55 | if (out.indexOf(f) !== -1) return false 56 | if (this.opts.skipPrivate && f.substr(0, 1) === '_') return false 57 | return true 58 | }) 59 | } 60 | 61 | RPCify.prototype.toManifest = function () { 62 | var ret = { 63 | name: this.name, 64 | methods: this.filteredFuncs 65 | } 66 | // debug('manifest', ret) 67 | 68 | return ret 69 | } 70 | 71 | RPCify.prototype.makeNew = function (id, args) { 72 | // debug('makeNew', id, args) 73 | if (this.instance) return this.instance 74 | else { 75 | var obj = this.factory(...args) 76 | this.cache[id] = obj 77 | } 78 | } 79 | 80 | RPCify.prototype.makeCall = function (method, id, args) { 81 | var instance 82 | if (this.instance) instance = this.instance 83 | else if (id && this.cache[id]) instance = this.cache[id] 84 | else return null 85 | 86 | if (!this.access(instance, method, args)) return null 87 | 88 | if (this.opts.override[method]) { 89 | return this.opts.override[method].apply(instance, args) 90 | } else { 91 | return instance[method].apply(instance, args) 92 | } 93 | // debug('makeCall - call: %O', instance[method]) 94 | } 95 | 96 | function makeDefaultFactory (Obj) { 97 | return function (...args) { 98 | return new Obj(...args) 99 | } 100 | } 101 | 102 | function getAllFuncs (obj) { 103 | var props = [] 104 | var cur = obj 105 | 106 | while (Object.getPrototypeOf(cur)) { 107 | Object.getOwnPropertyNames(cur).forEach(prop => { 108 | if (props.indexOf(prop) === -1) props.push(prop) 109 | }) 110 | cur = Object.getPrototypeOf(cur) 111 | } 112 | return props 113 | } 114 | 115 | module.exports = RPCify 116 | -------------------------------------------------------------------------------- /schema.proto: -------------------------------------------------------------------------------- 1 | enum TYPE { 2 | MANIFEST = 1; 3 | CALL = 2; 4 | RETURN = 3; 5 | } 6 | 7 | enum CALL { 8 | API = 1; 9 | OBJECT = 2; 10 | } 11 | 12 | enum RETURN { 13 | CALLBACK = 1; 14 | PROMISE = 2; 15 | } 16 | 17 | enum PROMISE { 18 | RESOLVE = 0; 19 | REJECT = 1; 20 | } 21 | 22 | enum ARGUMENT { 23 | BYTES = 1; 24 | JSON = 2; 25 | CALLBACK = 3; 26 | RPCIFIED = 4; 27 | STREAM = 5; 28 | ERROR = 6; 29 | } 30 | 31 | enum STREAM { 32 | READABLE = 1; 33 | WRITEABLE = 2; 34 | DUPLEX = 3; 35 | } 36 | 37 | message Arg { 38 | message Rpcified { 39 | required string manifest = 1; 40 | // required string objectid = 2; 41 | } 42 | 43 | message Stream { 44 | required STREAM type = 1; 45 | required bool objectMode = 2; 46 | } 47 | 48 | required ARGUMENT type = 1; 49 | oneof payload { 50 | bytes bytes = 2; 51 | string json = 3; 52 | string callback = 4; 53 | Rpcified rpcified = 5; 54 | Stream stream = 6; 55 | string error = 7; 56 | } 57 | } 58 | 59 | message Call { 60 | required CALL type = 1; 61 | required string id = 2; 62 | optional string name = 3; 63 | optional string objectid = 4; 64 | optional string method = 5; 65 | repeated Arg args = 6; 66 | } 67 | 68 | message Return { 69 | required RETURN type = 1; 70 | required string id = 2; 71 | optional PROMISE promise = 3; 72 | repeated Arg args = 4; 73 | } 74 | 75 | message Manifest { 76 | required string manifest = 1; 77 | required uint64 nonce = 2; 78 | } 79 | 80 | message Msg { 81 | required TYPE type = 1; 82 | oneof msg { 83 | Manifest manifest = 2; 84 | Call call = 3; 85 | Return return = 4; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /scripts/compile-schema.js: -------------------------------------------------------------------------------- 1 | var protobuf = require('protocol-buffers') 2 | var json = require('json-protobuf-encoding') 3 | var p = require('path') 4 | var fs = require('fs') 5 | 6 | var base = p.join(__dirname, '..') 7 | var schema = fs.readFileSync(p.join(base, 'schema.proto')) 8 | var js = protobuf.toJS(schema, { encodings: { 9 | json: json() 10 | }}) 11 | 12 | fs.writeFileSync(p.join(base, 'messages.js'), js) 13 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape') 2 | var rpc = require('..') 3 | var stream = require('stream') 4 | var pump = require('pump') 5 | var duplexify = require('duplexify') 6 | var through = require('through2') 7 | 8 | function opts (way) { 9 | var opts = { debug: true } 10 | if (way === 's') opts.name = 'server' 11 | if (way === 'c') opts.name = 'client' 12 | } 13 | 14 | tape('basic api calling', function (t) { 15 | var api = { 16 | upper: (str, cb) => cb(null, str.toUpperCase()) 17 | } 18 | 19 | var server = rpc(api, {name: 'server', debug: true}) 20 | var client = rpc(null, {name: 'client', debug: true}) 21 | 22 | server.pipe(client).pipe(server) 23 | 24 | client.on('remote', (remote) => { 25 | console.log('got remote', remote) 26 | remote.upper('foo', (err, res) => { 27 | t.equal(err, null) 28 | t.equal(res, 'FOO') 29 | t.end() 30 | }) 31 | }) 32 | }) 33 | 34 | tape('bidirectional api calling', function (t) { 35 | var api1 = { 36 | upper: (str, cb) => cb(null, str.toUpperCase()) 37 | } 38 | var api2 = { 39 | multiply: (num, num2, cb) => cb(null, num * num2) 40 | } 41 | 42 | var server = rpc(api1) 43 | var client = rpc(api2) 44 | 45 | server.pipe(client).pipe(server) 46 | 47 | client.on('remote', (remote) => { 48 | remote.upper('foo', (err, res) => { 49 | t.equal(err, null) 50 | t.equal(res, 'FOO') 51 | }) 52 | }) 53 | server.on('remote', (remote) => { 54 | remote.multiply(3, 4, (err, res) => { 55 | t.equal(err, null) 56 | t.equal(res, 12) 57 | t.end() 58 | }) 59 | }) 60 | }) 61 | 62 | tape('read stream', function (t) { 63 | function reader (str) { 64 | var i = 0 65 | return function () { 66 | if (i < 3) this.push(str + i) 67 | else this.push(null) 68 | i++ 69 | } 70 | } 71 | 72 | var api = { 73 | rs: (str, cb) => { 74 | var rs = new stream.Readable({ 75 | objectMode: true, 76 | read: reader(str) 77 | }) 78 | cb(null, rs) 79 | } 80 | } 81 | 82 | var server = rpc(api, {name: 'server'}) 83 | var client = rpc(null, {name: 'client'}) 84 | pump(server, client, server) 85 | 86 | client.on('remote', (remote) => { 87 | remote.rs('foo', (err, rs) => { 88 | t.equal(err, null) 89 | var i = 0 90 | rs.on('data', (data) => { 91 | t.equal(data, 'foo' + i) 92 | i++ 93 | }) 94 | rs.on('end', () => { 95 | t.equal(i, 3) 96 | t.end() 97 | }) 98 | }) 99 | }) 100 | }) 101 | 102 | tape('write stream', function (t) { 103 | var api = { 104 | ws: (str, cb) => { 105 | t.equal(str, 'foo', 'arg passed') 106 | var buffer = [] 107 | var ws = new stream.Writable({ 108 | objectMode: true, 109 | write (chunk, enc, next) { 110 | buffer.push(chunk) 111 | next() 112 | } 113 | }) 114 | ws.on('finish', () => { 115 | t.deepEqual(buffer, ['bar0', 'bar1', 'bar2'], 'write is correct') 116 | t.end() 117 | }) 118 | cb(null, ws) 119 | } 120 | } 121 | 122 | var server = rpc(api, {name: 'sever', log: true}) 123 | var client = rpc(null, {name: 'client', log: true}) 124 | pump(server, client, server) 125 | 126 | client.on('remote', (remote) => { 127 | remote.ws('foo', (err, ws) => { 128 | t.equal(err, null, 'check err') 129 | for (var i = 0; i < 3; i++) { 130 | ws.write('bar' + i) 131 | } 132 | ws.end() 133 | }) 134 | }) 135 | }) 136 | 137 | tape('read and write back and forth', function (t) { 138 | t.plan(28) 139 | var api = { 140 | ws: (str, cb) => { 141 | var buffer = [] 142 | var ws = new stream.Writable({ 143 | objectMode: true, 144 | write (chunk, enc, next) { 145 | buffer.push(chunk) 146 | next() 147 | } 148 | }) 149 | cb(null, ws) 150 | ws.on('finish', () => { 151 | t.deepEqual(buffer, [str + '0', str + '1', str + '2'], 'ws write is correct') 152 | }) 153 | }, 154 | rs: (str, cb) => { 155 | var i = 0 156 | var rs = new stream.Readable({ 157 | objectMode: true, 158 | read () { 159 | if (i < 3) this.push(str + i) 160 | else this.push(null) 161 | i++ 162 | } 163 | }) 164 | cb(null, rs) 165 | } 166 | } 167 | 168 | var server = rpc(api, {name: 'sever'}) 169 | var client = rpc(api, {name: 'client'}) 170 | pump(server, client, server) 171 | 172 | client.on('remote', (remote) => { 173 | remote.ws('foo', (err, ws) => { 174 | t.equal(err, null, 'err is null') 175 | for (var i = 0; i < 3; i++) { 176 | ws.write('foo' + i) 177 | } 178 | ws.end() 179 | }) 180 | remote.rs('abc', (err, rs) => { 181 | t.equal(err, null, 'err is null') 182 | var i = 0 183 | rs.on('data', (data) => { 184 | t.equal(data, 'abc' + i, 'read 1 is correct') 185 | i++ 186 | }) 187 | rs.on('end', () => { 188 | t.equal(i, 3, 'read 1 ends at right pos') 189 | }) 190 | }) 191 | remote.ws('bar', (err, ws) => { 192 | t.equal(err, null) 193 | for (var i = 0; i < 3; i++) { 194 | ws.write('bar' + i) 195 | } 196 | ws.end() 197 | }) 198 | remote.rs('kkl', (err, rs) => { 199 | t.equal(err, null, 'err is null') 200 | var i = 0 201 | rs.on('data', (data) => { 202 | t.equal(data, 'kkl' + i, 'read 2 is correct') 203 | i++ 204 | }) 205 | rs.on('end', () => { 206 | t.equal(i, 3, 'read 2 ends at right pos') 207 | }) 208 | }) 209 | }) 210 | server.on('remote', (remote) => { 211 | remote.ws('def', (err, ws) => { 212 | t.equal(err, null, 'err is null') 213 | for (var i = 0; i < 3; i++) { 214 | ws.write('def' + i) 215 | } 216 | ws.end() 217 | }) 218 | remote.rs('xyz', (err, rs) => { 219 | t.equal(err, null) 220 | var i = 0 221 | rs.on('data', (data) => { 222 | t.equal(data, 'xyz' + i, 'read 3 is correct') 223 | i++ 224 | }) 225 | rs.on('end', () => { 226 | t.equal(i, 3, 'read 3 ends at right pos') 227 | }) 228 | }) 229 | remote.ws('jkl', (err, ws) => { 230 | t.equal(err, null) 231 | for (var i = 0; i < 3; i++) { 232 | ws.write('jkl' + i) 233 | } 234 | ws.end() 235 | }) 236 | remote.rs('xyz', (err, rs) => { 237 | t.equal(err, null) 238 | var i = 0 239 | rs.on('data', (data) => { 240 | t.equal(data, 'xyz' + i) 241 | i++ 242 | }) 243 | rs.on('end', () => { 244 | t.equal(i, 3) 245 | }) 246 | }) 247 | }) 248 | }) 249 | 250 | tape.skip('duplex stream solo', function (t) { 251 | // var i = 0 252 | var buf = ['precr'] 253 | var ds = new stream.Duplex({ 254 | read () { 255 | if (buf.length) this.push(buf.shift().toUpperCase()) 256 | else setTimeout(this.read.bind(this), 100) 257 | // console.log(i) 258 | // i++ 259 | // if (i < 2) this.push('READ THIS') 260 | // else this.push(null) 261 | }, 262 | write (chunk) { 263 | console.log('ds write', chunk.toString()) 264 | buf.push(chunk.toString()) 265 | } 266 | }) 267 | // var transport1 = through() 268 | // var transport2 = through() 269 | // var rs = new stream.Readable({read () {}}) 270 | // var ws = new stream.Writable({write () {}}) 271 | var rs = through() 272 | var ws = through() 273 | var end = duplexify(ws, rs) 274 | 275 | var transRec = through() 276 | var transSend = through() 277 | var trans = duplexify(transRec, transSend) 278 | 279 | // var end = duplexify() 280 | // end.setReadable(rs) 281 | // end.setWritable(ws) 282 | 283 | // var end = through() 284 | end.on('data', (d) => console.log('end data', d.toString())) 285 | // end.on('end', () => 'end closed.') 286 | end.write('hello from end') 287 | // end.write('foo') 288 | // pump(ds, transport, end) 289 | // pump(ds, transport1, end, transport2, ds) 290 | // pump(ws, transport1, ds, transport1, rs) 291 | 292 | pump(ws, trans) 293 | pump(trans, rs) 294 | pump(trans, ds) 295 | pump(ds, trans) 296 | 297 | setInterval(() => {}, 1000) 298 | }) 299 | 300 | tape('duplex stream', function (t) { 301 | // t.plan(4) 302 | var api = (name) => ({ 303 | echo: (prefix, cb) => { 304 | var reader = [1, 0] 305 | var s = new stream.Duplex({ 306 | read () { 307 | if (reader.length) { 308 | this.push(prefix + reader.pop()) 309 | } else { 310 | this.push(null) 311 | } 312 | }, 313 | write (chunk, enc, next) { 314 | t.equal(chunk.toString(), prefix + ' there', 'write on ' + name + ' is correct') 315 | next() 316 | } 317 | }) 318 | console.log(name + ' ds created') 319 | s.on('finish', () => { 320 | maybeDone(s, name + ' write finished') 321 | }) 322 | cb(null, s) 323 | } 324 | }) 325 | 326 | var client = rpc(api('client'), {name: 'client', log: false}) 327 | var server = rpc(api('server'), {name: 'server', log: false}) 328 | 329 | pump(client, server, client) 330 | 331 | client.on('remote', (api) => { 332 | api.echo('hi', (err, ds) => { 333 | t.equal(err, null, 'client init no err') 334 | ds.write('hi there') 335 | ds.end() 336 | var i = 0 337 | ds.on('data', (data) => { 338 | t.equal(data.toString(), 'hi' + i, 'client read correct') 339 | i++ 340 | }) 341 | ds.on('end', () => maybeDone(ds, 'client read finished')) 342 | }) 343 | }) 344 | server.on('remote', (api) => { 345 | api.echo('hello', (err, ds) => { 346 | t.equal(err, null, 'no err') 347 | ds.write('hello there') 348 | ds.end() 349 | var i = 0 350 | ds.on('data', (data) => { 351 | t.equal(data.toString(), 'hello' + i, 'server read correct') 352 | i++ 353 | }) 354 | ds.on('end', () => maybeDone(ds, 'server read finished')) 355 | }) 356 | }) 357 | 358 | var doneStreams = [] 359 | function maybeDone (s, log) { 360 | doneStreams.push(s) 361 | if (doneStreams.length === 4) { 362 | t.end() 363 | } 364 | } 365 | }) 366 | 367 | tape('nested callbacks', function (t) { 368 | var api = { 369 | process: function (method, prefix, onFoo, onBar) { 370 | function toUpper (str, cb) { 371 | cb(null, prefix + str.toUpperCase()) 372 | } 373 | if (method === 'foo') onFoo(toUpper) 374 | if (method === 'bar') onBar(toUpper) 375 | } 376 | } 377 | 378 | var server = rpc(api, {name: 'server', log: true}) 379 | var client = rpc(null, {name: 'client', log: true}) 380 | pump(server, client, server) 381 | 382 | client.on('remote', (api) => { 383 | api.process('foo', 'test', onFoo) 384 | api.process('bar', 'ba', onFoo, onBar) 385 | function onFoo (remoteUpper) { 386 | remoteUpper('yeah', (err, res) => { 387 | t.error(err) 388 | t.equal(res, 'testYEAH') 389 | maybeEnd() 390 | }) 391 | } 392 | function onBar (remoteUpper) { 393 | remoteUpper('boo', (err, res) => { 394 | t.error(err) 395 | t.equal(res, 'baBOO') 396 | maybeEnd() 397 | }) 398 | } 399 | }) 400 | var done = 0 401 | function maybeEnd () { 402 | done++ 403 | if (done === 2) t.end() 404 | } 405 | }) 406 | 407 | tape('promises', function (t) { 408 | var api = { 409 | promtest: function (str) { 410 | return new Promise((resolve, reject) => { 411 | if (str) resolve(str.toUpperCase()) 412 | else reject(new Error('no foo')) 413 | }) 414 | }, 415 | promtestAsync: async function (str) { 416 | if (str) return str.toUpperCase() 417 | else throw new Error('no foo') 418 | } 419 | } 420 | 421 | var server = rpc(api, {promise: true, log: true}) 422 | var client = rpc(null, {promise: true, log: true}) 423 | 424 | pump(server, client, server) 425 | 426 | client.on('remote', async (api) => { 427 | var promise = api.promtest('foo') 428 | promise.then((data) => { 429 | t.equal(data, 'FOO', 'foo correct') 430 | }) 431 | 432 | var foo = await api.promtest('yeah') 433 | t.equal(foo, 'YEAH', 'Yeah correct') 434 | 435 | var values = ['test', null] 436 | values.map(async (val) => { 437 | try { 438 | var bar = await api.promtestAsync(val) 439 | t.equal(bar, 'TEST', 'async await works') 440 | } catch (err) { 441 | t.equal(err.message, 'Error: no foo', 'rejection works') 442 | t.end() 443 | } 444 | }) 445 | }) 446 | }) 447 | 448 | tape('nested object apis', function (t) { 449 | t.plan(4) 450 | var api = { 451 | foo: { 452 | bar: (str, cb) => cb(null, str.toUpperCase()) 453 | }, 454 | nested: { 455 | in: { 456 | two: (a, cb) => cb(null, a * 2) 457 | }, 458 | three: (a, cb) => cb(null, a * 3) 459 | } 460 | } 461 | 462 | var server = rpc(api, opts('s')) 463 | var client = rpc(null, opts('c')) 464 | pump(server, client, server) 465 | 466 | client.on('remote', (api) => { 467 | api.foo.bar('test', (err, str) => { 468 | t.error(err) 469 | t.equal(str, 'TEST') 470 | }) 471 | api.nested.in.two(4, (_, val) => t.equal(val, 8)) 472 | api.nested.three(4, (_, val) => t.equal(val, 12)) 473 | }) 474 | }) 475 | 476 | tape('buffer encoding/decoding', function (t) { 477 | var api = { 478 | getBuf: (cb) => cb(Buffer.from('foo', 'utf8')) 479 | } 480 | var server = rpc(api, opts('s')) 481 | var client = rpc(null, opts('c')) 482 | pump(server, client, server) 483 | client.on('remote', (api) => { 484 | api.getBuf((buf) => { 485 | t.equal(Buffer.isBuffer(buf), true) 486 | t.equal(buf.toString('utf-8'), 'foo') 487 | t.end() 488 | }) 489 | }) 490 | }) 491 | 492 | // var ApiObj = api.ApiObj('foo') 493 | // ApiObj.toUpper('bar', (err, str) { 494 | // t.equal(err, null) 495 | // t.equal(str, 'fooBAR') 496 | // }) 497 | // }) 498 | 499 | // var proxy = require('./proxy') 500 | // tape('object proxy', function (t) { 501 | // var ApiObj = function (prefix) { 502 | // if (!(this instanceof ApiObj)) return new ApiObj() 503 | // this.prefix = prefix 504 | // } 505 | // ApiObj.prototype.toUpper = function (str, cb) { 506 | // cb(null, this.prefix + str.toUpperCase()) 507 | // } 508 | 509 | // var api = { 510 | // ApiObj: proxy(ApiObj) 511 | // } 512 | 513 | // var server = rpc(api) 514 | // var client = rpc(null) 515 | // pump(server, client, server) 516 | 517 | // client.on('remote', (api) => { 518 | // var ApiObj = api.ApiObj('foo') 519 | // ApiObj.toUpper('bar', (err, str) { 520 | // t.equal(err, null) 521 | // t.equal(str, 'fooBAR') 522 | // }) 523 | // }) 524 | 525 | // }) 526 | 527 | // tape.only('split binary stream', function (t) { 528 | // var api = { 529 | // upper: (str, cb) => cb(null, str.toUpperCase()) 530 | // } 531 | 532 | // var server = rpc(api, {name: 'server', log: true}) 533 | // var client = rpc(null, {name: 'client', log: true}) 534 | // var transport1 = through(split) 535 | // var transport2 = through(split) 536 | 537 | // var last = null 538 | // function split (chunk, encoding, next) { 539 | // if (last) { 540 | // this.push(Buffer.concat([last, chunk])) 541 | // next() 542 | // } else { 543 | // last = chunk 544 | // next() 545 | // } 546 | // } 547 | 548 | // pump(server, transport1, client) 549 | // pump(client, transport2, server) 550 | 551 | // client.on('remote', (remote) => { 552 | // console.log('got remote', remote) 553 | // remote.upper('foo', (err, res) => { 554 | // t.equal(err, null) 555 | // t.equal(res, 'FOO') 556 | // t.end() 557 | // }) 558 | // }) 559 | // }) 560 | -------------------------------------------------------------------------------- /test/object.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape') 2 | var rpc = require('..') 3 | var rpcify = require('../rpcify') 4 | var pump = require('pump') 5 | 6 | function MyClass (key) { 7 | if (!(this instanceof MyClass)) return new MyClass(key) 8 | 9 | this.key = key 10 | this.init = true 11 | this.prefix = '' 12 | } 13 | 14 | MyClass.prototype.setPrefix = function (prefix) { 15 | this.prefix = prefix 16 | } 17 | 18 | MyClass.prototype.getUpper = function (suffix, cb) { 19 | var str = this.prefix + this.key + suffix 20 | cb(str.toUpperCase()) 21 | } 22 | 23 | MyClass.prototype._private = function () {} 24 | 25 | tape('rpcify objects', function (t) { 26 | t.plan(2) 27 | var api = { 28 | upper: (str, cb) => cb(null, str.toUpperCase()), 29 | myclass: rpcify(MyClass) 30 | } 31 | 32 | var server = rpc(api, {name: 'server', debug: true}) 33 | var client = rpc(null, {name: 'client', debug: true}) 34 | 35 | pump(server, client, server) 36 | 37 | client.on('remote', (remote) => { 38 | var myobj1 = remote.myclass('moon') 39 | var myobj2 = remote.myclass('this') 40 | myobj1.setPrefix('hello ') 41 | 42 | myobj1.getUpper(' bye', (str) => t.equal(str, 'HELLO MOON BYE', 'first instance getUpper works!')) 43 | myobj2.getUpper(' works', (str) => t.equal(str, 'THIS WORKS', 'second instance getUpper works!')) 44 | }) 45 | }) 46 | 47 | tape('skip private', function (t) { 48 | t.plan(1) 49 | var api = { 50 | upper: (str, cb) => cb(null, str.toUpperCase()), 51 | myclass: rpcify(MyClass) 52 | } 53 | 54 | var server = rpc(api) 55 | var client = rpc(null) 56 | 57 | pump(server, client, server) 58 | 59 | client.on('remote', (remote) => { 60 | var myobj1 = remote.myclass('moon') 61 | t.equal(myobj1._private, undefined, 'private method skipped') 62 | }) 63 | }) 64 | 65 | tape('include private', function (t) { 66 | t.plan(1) 67 | var api = { 68 | upper: (str, cb) => cb(null, str.toUpperCase()), 69 | myclass: rpcify(MyClass, {skipPrivate: false}) 70 | } 71 | 72 | var server = rpc(api) 73 | var client = rpc(null) 74 | 75 | pump(server, client, server) 76 | 77 | client.on('remote', (remote) => { 78 | var myobj1 = remote.myclass('moon') 79 | t.equal(typeof myobj1._private, 'function', 'private method not skipped') 80 | }) 81 | }) 82 | 83 | tape('limit api', function (t) { 84 | var api = { 85 | upper: (str, cb) => cb(null, str.toUpperCase()), 86 | myclass: rpcify(MyClass, {include: ['getUpper']}), 87 | myclass2: rpcify(MyClass, {exclude: ['getUpper']}) 88 | } 89 | 90 | var server = rpc(api) 91 | var client = rpc(null) 92 | 93 | pump(server, client, server) 94 | 95 | client.on('remote', (remote) => { 96 | var myobj1 = remote.myclass('moon') 97 | t.equal(typeof myobj1.getUpper, 'function', 'method included') 98 | t.equal(typeof myobj1.setPrefix, 'undefined', 'method skipped') 99 | var myobj2 = remote.myclass2('moon') 100 | t.equal(typeof myobj2.getUpper, 'undefined', 'method included') 101 | t.equal(typeof myobj2.setPrefix, 'function', 'method skipped') 102 | t.end() 103 | }) 104 | }) 105 | 106 | tape('check access', function (t) { 107 | var api = { 108 | upper: (str, cb) => cb(null, str.toUpperCase()), 109 | myclass: rpcify(MyClass, { access: access }) 110 | } 111 | 112 | function access (obj, method, args) { 113 | if (method === 'setPrefix' && args[0] === 'forbidden') return false 114 | else return true 115 | } 116 | 117 | var server = rpc(api) 118 | var client = rpc(null) 119 | 120 | pump(server, client, server) 121 | 122 | client.on('remote', (remote) => { 123 | var myobj1 = remote.myclass('key') 124 | myobj1.setPrefix('foo') 125 | myobj1.getUpper('x', (str) => t.equal(str, 'FOOKEYX', 'normal prefix works')) 126 | myobj1.setPrefix('forbidden') 127 | myobj1.getUpper('x', (str) => t.equal(str, 'FOOKEYX', 'check worked')) 128 | myobj1.setPrefix('bar') 129 | myobj1.getUpper('x', (str) => t.equal(str, 'BARKEYX', 'normal prefix works again')) 130 | t.end() 131 | }) 132 | }) 133 | 134 | tape('return rpcified objects', function (t) { 135 | // server 136 | var obj = { 137 | first: MyClass('hello'), 138 | second: MyClass('world') 139 | } 140 | var api = { 141 | getObj: (id, cb) => { 142 | cb(null, rpcify(obj[id])) 143 | } 144 | } 145 | var server = rpc(api, {debug: true}) 146 | var client = rpc(null, {debug: true}) 147 | pump(server, client, server) 148 | client.on('remote', (remote) => { 149 | remote.getObj('first', (err, obj) => { 150 | t.error(err) 151 | obj.getUpper('!', (str) => { 152 | t.equal(str, 'HELLO!', 'string matches') 153 | t.end() 154 | }) 155 | }) 156 | }) 157 | }) 158 | --------------------------------------------------------------------------------