├── .gitignore ├── .npmignore ├── melf.png ├── test ├── run.sh ├── server.js ├── alice.js └── bob.js ├── package.json ├── LICENSE ├── lib ├── distributor.js └── main.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules/ 3 | /test/ 4 | /melf.png -------------------------------------------------------------------------------- /melf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freynder/melf/master/melf.png -------------------------------------------------------------------------------- /test/run.sh: -------------------------------------------------------------------------------- 1 | rm /tmp/melf-test.sock 2 | node server.js /tmp/melf-test.sock & 3 | pid=$! 4 | sleep 1 5 | node alice.js /tmp/melf-test.sock & 6 | node bob.js /tmp/melf-test.sock 7 | wait $pid -------------------------------------------------------------------------------- /test/server.js: -------------------------------------------------------------------------------- 1 | const Net = require("net"); 2 | const Distributor = require("../lib/distributor.js"); 3 | const distributor = Distributor((origin, recipient, message) => { 4 | console.log(origin+" >> "+recipient+": "+message); 5 | }); 6 | const server = Net.createServer(); 7 | server.on("connection", distributor.ConnectionListener()); 8 | server.listen(process.argv[process.argv.length-1]); 9 | setTimeout(() => { server.close() }, 2000); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "melf", 3 | "description": "(A)Synchronous remote procedure calls", 4 | "version": "2.2.14", 5 | "author": { 6 | "name": "Laurent Christophe", 7 | "email": "lachrist@vub.ac.be" 8 | }, 9 | "repository": "lachrist/melf", 10 | "homepage": "http://github.com/lachrist/melf", 11 | "license": "MIT", 12 | "dependencies": { 13 | "antena": "4.0.10" 14 | }, 15 | "main": "lib/main.js", 16 | "keywords": [ 17 | "JavaScript", 18 | "Remote Procedure Call", 19 | "Processes", 20 | "Communication", 21 | "Synchronous", 22 | "Asynchronous" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /test/alice.js: -------------------------------------------------------------------------------- 1 | const Melf = require("../lib/main.js"); 2 | Melf(process.argv[process.argv.length-1], "alice", (error, melf) => { 3 | if (error) 4 | throw error; 5 | melf.catch((error) => { throw error }); 6 | melf.rprocedures.greeting = (origin, data, callback) => { 7 | melf.rpcall(origin, "echo", "Hello "+origin+", you said: "+JSON.stringify(data), callback); 8 | }; 9 | melf.rprocedures.error = (origin, data, callback) => { 10 | callback(new Error("Sorry, "+origin+" there is an error...")); 11 | }; 12 | melf.rprocedures.terminate = (origin, data, callback) => { 13 | melf.onterminate = () => { callback(null, null) }; 14 | melf.terminate(); 15 | }; 16 | }); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Laurent Christophe 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 | -------------------------------------------------------------------------------- /test/bob.js: -------------------------------------------------------------------------------- 1 | const Melf = require("../lib/main.js"); 2 | Melf(process.argv[process.argv.length-1], "bob", (error, melf) => { 3 | // RProcedures can be executed in the middle of a synchronous rcall! 4 | melf.rprocedures.echo = (origin, data, callback) => { 5 | console.log("echoing to "+origin); 6 | callback(null, data); 7 | }; 8 | melf.catch((error) => { throw error }); 9 | const test = (recipient, rpname, data, callback) => { 10 | console.log("BEGIN "+recipient+" "+rpname); 11 | // Synchronous remote procedure call // 12 | try { 13 | console.log(rpname+"-sync-data: "+melf.rpcall(recipient, rpname, data)); 14 | } catch (error) { 15 | console.log(rpname+"-sync-error", error.stack); 16 | } 17 | // Asynchornous remote procedure call // 18 | melf.rpcall(recipient, rpname, data, (error, data) => { 19 | if (error) { 20 | console.log(rpname+"-async-error", error.stack); 21 | } else { 22 | console.log(rpname+"-async-data: "+data); 23 | } 24 | console.log("END "+recipient+" "+rpname); 25 | callback(); 26 | }); 27 | }; 28 | test("alice", "greeting", "fablabla?", () => { 29 | test("alice", "error", null, () => { 30 | test("alice", "greetying", null, () => { 31 | melf.rpcall("alice", "terminate", null); 32 | melf.terminate(); 33 | }); 34 | }); 35 | }); 36 | }); -------------------------------------------------------------------------------- /lib/distributor.js: -------------------------------------------------------------------------------- 1 | 2 | const AntenaReceptor = require("antena/lib/receptor"); 3 | 4 | const default_logger = (origin, recipient, meteor) => { 5 | console.log(origin+" >> "+recipient+": "+meteor); 6 | }; 7 | 8 | module.exports = (logger) => { 9 | const receptor = AntenaReceptor(); 10 | const distributor = { 11 | _receptor: receptor, 12 | _pendings: {__proto__:null}, 13 | _callbacks: {__proto__:null}, 14 | _logger: typeof logger === "function" ? logger : (logger && default_logger), 15 | ConnectionListener, 16 | RequestMiddleware, 17 | UpgradeMiddleware 18 | }; 19 | receptor.onpost = onpost; 20 | receptor.onpull = onpull; 21 | receptor._melf = distributor; 22 | return distributor; 23 | }; 24 | 25 | function ConnectionListener () { 26 | return this._receptor.ConnectionListener(); 27 | } 28 | 29 | function RequestMiddleware (splitter) { 30 | return this._receptor.RequestMiddleware(splitter); 31 | } 32 | 33 | function UpgradeMiddleware (splitter) { 34 | return this._receptor.UpgradeMiddleware(splitter); 35 | } 36 | 37 | function onpost (origin, message) { 38 | const recipient = message.substring(0, message.indexOf("/")); 39 | const meteor = message.substring(recipient.length + 1); 40 | if (this._melf._logger) 41 | this._melf._logger(origin, recipient, meteor); 42 | this.push(recipient, meteor); 43 | if (this._melf._callbacks[recipient]) { 44 | this._melf._callbacks[recipient](meteor); 45 | this._melf._callbacks[recipient] = null; 46 | } else { 47 | if (!(recipient in this._melf._pendings)) 48 | this._melf._pendings[recipient] = []; 49 | this._melf._pendings[recipient].push(meteor); 50 | } 51 | } 52 | 53 | function onpull (session, message, callback) { 54 | if (message) 55 | this.onpost(session, message); 56 | if (this._melf._pendings[session] && this._melf._pendings[session].length) { 57 | callback(this._melf._pendings[session].join("\n")); 58 | this._melf._pendings[session].length = 0; 59 | } else { 60 | this._melf._callbacks[session] = callback; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Melf melf-logo 2 | 3 | [npm module](https://www.npmjs.com/package/melf) to perform (a)synchronous remote procedure calls for JavaScript processes. 4 | Synchronous remote procedure calls can be interleaved (only) by locally declared remote procedures without deadlocks. 5 | 6 | ## Client API 7 | 8 | ### `require("melf")(address, alias, callback)` 9 | 10 | * `address :: string | object | melf.Receptor` 11 | * `alias :: string` 12 | * `callback(error, melf)` 13 | * `error :: Error | null` 14 | * `melf :: melf.Melf` 15 | 16 | ### `output = melf.rpcall(recipient, rpname, input)` 17 | 18 | * `recipient :: string` 19 | * `rpname :: string` 20 | * `input :: JSON` 21 | * `output :: JSON` 22 | 23 | ### `melf.rpcall(recipient, rpname, input, (error, output) => {...})` 24 | 25 | * `recipient :: string` 26 | * `rpname :: string` 27 | * `input :: JSON` 28 | * `error :: Error | null` 29 | * `output :: JSON` 30 | 31 | ### `melf.rprocedures[rpname] = (origin, input, callback) => {...}` 32 | 33 | * `rpname :: string` 34 | * `origin :: string` 35 | * `input :: JSON` 36 | * `callback(error, output)` 37 | * `error :: Error | null` 38 | * `output :: JSON` 39 | 40 | ### `melf.destroy()` 41 | 42 | ### `melf.terminate()` 43 | 44 | ### `melf.onterminate = () => { ... }` 45 | 46 | ## Server API 47 | 48 | ### `distributor = require("melf/distributor")(logger)` 49 | 50 | * `logger(origin, recipient, meteor) | undefined | boolean` 51 | * `origin :: string` 52 | * `recipient :: string` 53 | * `meteor :: string` 54 | * `distributor :: melf.Distributor` 55 | 56 | ### `listener = orchestrator.ConnectionListener()` 57 | 58 | * `listener(net.Socket)` 59 | 60 | ### `middleware = distributor.RequestMiddleware(splitter)` 61 | 62 | * `splitter :: string | undefined` 63 | * `handled = middleware(request, response, next)` 64 | * `request :: http.Request` 65 | * `response :: http.Response` 66 | * `next()` 67 | * `handled :: boolean` 68 | 69 | ### `middleware = distributor.UpgradeMiddleware(splitter)` 70 | 71 | * `splitter :: string | undefined` 72 | * `handled = middleware(request, socket, head, next)` 73 | * `request :: http.Request` 74 | * `socket :: net.Socket` 75 | * `head :: Buffer` 76 | * `next()` 77 | * `handled :: boolean` 78 | -------------------------------------------------------------------------------- /lib/main.js: -------------------------------------------------------------------------------- 1 | 2 | // alias#token%rpname/body 3 | // &token/body 4 | // |token/body 5 | 6 | const AntenaEmitter = require("antena/lib/emitter"); 7 | 8 | const max = parseInt("zzzzzz", 36); 9 | 10 | const rpcallhelper = (melf, recipient, rpname, data, callback) => { 11 | melf._counter++; 12 | if (melf._counter === max) 13 | melf._counter = 1; 14 | const token = melf._counter.toString(36); 15 | melf._callbacks[token] = callback; 16 | return recipient+"/"+melf.alias+"#"+token+"%"+rpname+"/"+JSON.stringify(data); 17 | }; 18 | 19 | const localize = (alias) => (line) => line.startsWith(" at ") ? 20 | " " + alias + " " + line.substring(4) : 21 | line; 22 | 23 | const makeonmeteor = (melf) => (meteor) => { 24 | const head = meteor.substring(0, meteor.indexOf("/")); 25 | if (!melf._done.delete(head)) { 26 | melf._done.add(head); 27 | const body = JSON.parse(meteor.substring(head.length+1)); 28 | if (head[0] === "&" || head[0] === "|") { 29 | const token = head.substring(1); 30 | const callback = melf._callbacks[token]; 31 | delete melf._callbacks[token]; 32 | if (head[0] === "&") { 33 | callback(null, body); 34 | } else { 35 | error = new Error(body[1]); 36 | error.name = body[0]; 37 | error.stack = body[0]+": "+body[1]+"\n"+body[2].join("\n"); 38 | callback(error); 39 | } 40 | } else { 41 | const alias = head.substring(0, head.indexOf("#")); 42 | const token = head.substring(alias.length+1, head.indexOf("%")); 43 | const pname = head.substring(alias.length+1+token.length+1); 44 | const callback = (error, result) => { 45 | if (error) { 46 | const lines = error.stack.substring(error.name.length + 2 + error.message.length + 1).split("\n"); 47 | melf._emitter.post(alias+"/|"+token+"/"+JSON.stringify([error.name, error.message, lines.map(localize(melf.alias))])); 48 | } else { 49 | melf._emitter.post(alias+"/&"+token+"/"+JSON.stringify(result)); 50 | } 51 | }; 52 | if (pname in melf.rprocedures) { 53 | melf.rprocedures[pname](alias, body, callback); 54 | } else { 55 | callback(new Error("Procedure not found: "+pname)); 56 | } 57 | } 58 | } 59 | }; 60 | 61 | function rpcall (recipient, rpname, data, callback) { 62 | if (callback) 63 | return this._emitter.post(rpcallhelper(this, recipient, rpname, data, callback)); 64 | let pending = true; 65 | let result = null; 66 | this._emitter.pull(rpcallhelper(this, recipient, rpname, data, (error, data) => { 67 | if (error) { 68 | error.stack = ( 69 | error.name + ": " + 70 | error.message + "\n" + 71 | error.stack.substring(error.name.length+2+error.message.length+1) + "\n" + 72 | (new Error("foo")).stack.substring("Error: foo\n".length).split("\n").map(localize(this.alias)).join("\n")); 73 | throw error; 74 | } 75 | pending = false; 76 | result = data; 77 | })).split("\n").forEach(this._onmeteor); 78 | while (pending) 79 | this._emitter.pull("").split("\n").forEach(this._onmeteor); 80 | return result; 81 | } 82 | 83 | function terminate () { 84 | return this._emitter.terminate(); 85 | } 86 | 87 | function destroy () { 88 | return this._emitter.destroy(); 89 | } 90 | 91 | function onterminate () { 92 | this._melf.onterminate(); 93 | }; 94 | 95 | const noop = () => {}; 96 | 97 | module.exports = (address, alias, callback) => { 98 | if (address && typeof address === "object" && "_receptor" in address) 99 | address = address._receptor; 100 | AntenaEmitter(address, alias, (error, emitter) => { 101 | if (error) 102 | return callback(error); 103 | const melf = new Promise((resolve, reject) => { emitter.then(resolve, reject) }); 104 | const onmeteor = makeonmeteor(melf); 105 | melf._emitter = emitter; 106 | melf._callbacks = {__proto__:null}; 107 | melf._counter = 0; 108 | melf._done = new Set(); 109 | melf._onmeteor = onmeteor; 110 | melf.rprocedures = {__proto__:null}; 111 | melf.rpcall = rpcall; 112 | melf.alias = alias; 113 | melf.destroy = destroy; 114 | melf.terminate = terminate; 115 | melf.onterminate = noop; 116 | emitter._melf = melf; 117 | emitter.onpush = onmeteor; 118 | emitter.onterminate = onterminate; 119 | callback(null, melf); 120 | }); 121 | }; 122 | --------------------------------------------------------------------------------