├── .eslintrc.json ├── .gitignore ├── .npmignore ├── .npmrc ├── .travis.yml ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.MD ├── build.js ├── index.js ├── index_webpack.js ├── jsconfig.json ├── package.json ├── src ├── BidirectionalElectronIPC.js ├── BidirectionalWebRTCRouter.js ├── BidirectionalWebsocketRouter.js ├── BidirectionalWorkerRouter.js ├── BidirectionalWorkerThreadRouter.js ├── Client.js ├── ClientPluginBase.js ├── EndpointBase.js ├── Exception.js ├── IncomingRequest.js ├── NodeClusterBase │ ├── MasterClient.js │ ├── MasterEndpoint.js │ ├── README.MD │ ├── WorkerClient.js │ ├── WorkerEndpoint.js │ └── index.js ├── NodeMultiCoreCPUBase │ ├── MasterClient.js │ ├── MasterEndpoint.js │ ├── README.MD │ ├── WorkerClient.js │ ├── WorkerEndpoint.js │ └── index.js ├── NodeWorkerThreadsBase │ ├── MasterClient.js │ ├── MasterEndpoint.js │ ├── README.MD │ ├── WorkerClient.js │ ├── WorkerEndpoint.js │ └── index.js ├── OutgoingRequest.js ├── Plugins │ ├── Client │ │ ├── Cache.js │ │ ├── DebugLogger.js │ │ ├── ElectronIPCTransport.js │ │ ├── PrettyBrowserConsoleErrors.js │ │ ├── ProcessStdIOTransport.js │ │ ├── SignatureAdd.js │ │ ├── WebRTCTransport.js │ │ ├── WebSocketTransport.js │ │ ├── WorkerThreadTransport.js │ │ ├── WorkerTransport.js │ │ └── index.js │ └── Server │ │ ├── AuthenticationSkip.js │ │ ├── AuthorizeAll.js │ │ ├── DebugLogger.js │ │ ├── PerformanceCounters.js │ │ ├── URLPublic.js │ │ └── index.js ├── RouterBase.js ├── Server.js ├── ServerPluginBase.js ├── Utils.js └── WebSocketAdapters │ ├── WebSocketWrapperBase.js │ └── uws │ └── WebSocketWrapper.js ├── test.js ├── tests ├── Browser │ ├── TestEndpoint.js │ ├── Worker.js │ ├── index.html │ └── main_server.js ├── BrowserWebRTC │ ├── TestEndpoint.js │ ├── WebRTC.html │ └── main_server.js ├── Tests │ ├── AllTests.js │ ├── Plugins │ │ ├── Client │ │ │ ├── DebugMarker.js │ │ │ └── InvalidRequestJSON.js │ │ └── Server │ │ │ ├── DebugMarker.js │ │ │ ├── InvalidResponseJSON.js │ │ │ └── WebSocketAuthorize.js │ ├── TestClient.js │ └── TestEndpoint.js ├── benchmark.js ├── benchmark_endless_new_websockets.js ├── bug_uws_close_await_hang.js ├── bug_uws_send_large_string_connection_closed.js ├── main.js ├── main_NodeClusterBase.js └── main_NodeWorkerThreadsBase.js └── webpack.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 7, 4 | "sourceType": "module", 5 | "ecmaFeatures": { 6 | "jsx": true 7 | } 8 | }, 9 | 10 | "plugins": [ 11 | "jsdoc" 12 | ], 13 | 14 | "ecmaFeatures": { 15 | "modules": true, 16 | "experimentalObjectRestSpread": true 17 | }, 18 | 19 | "parser": "babel-eslint", 20 | 21 | "env": { 22 | "browser": false, 23 | "es6": true, 24 | "node": true, 25 | "mocha": true 26 | }, 27 | 28 | "globals": { 29 | "document": false, 30 | "navigator": false, 31 | "window": false 32 | }, 33 | 34 | "rules": { 35 | "jsdoc/check-param-names": 2, 36 | "jsdoc/check-tag-names": 2, 37 | "jsdoc/check-types": 2, 38 | "jsdoc/newline-after-description": 2, 39 | "jsdoc/require-description-complete-sentence": 0, 40 | "jsdoc/require-hyphen-before-param-description": 0, 41 | "jsdoc/require-param": 2, 42 | "jsdoc/require-param-description": 0, 43 | "jsdoc/require-param-type": 2, 44 | "jsdoc/require-returns-description": 0, 45 | "jsdoc/require-returns-type": 2, 46 | 47 | "accessor-pairs": 2, 48 | "arrow-spacing": [2, { "before": true, "after": true }], 49 | "block-spacing": [2, "always"], 50 | "brace-style": [1, "allman", { "allowSingleLine": false }], 51 | "comma-dangle": [2, "never"], 52 | "comma-spacing": [2, { "before": false, "after": true }], 53 | "comma-style": [2, "last"], 54 | "constructor-super": 2, 55 | "curly": [2, "multi-line"], 56 | "dot-location": [2, "property"], 57 | "eol-last": 2, 58 | "eqeqeq": [2, "allow-null"], 59 | "generator-star-spacing": [2, { "before": true, "after": true }], 60 | "handle-callback-err": [2, "^(err|error)$" ], 61 | "indent": ["error", "tab", { "SwitchCase": 1 }], 62 | "key-spacing": [2, { "beforeColon": false, "afterColon": true }], 63 | "keyword-spacing": [0, { "before": true, "after": true }], 64 | "new-cap": [2, { "newIsCap": true, "capIsNew": false }], 65 | "new-parens": 2, 66 | "no-array-constructor": 2, 67 | "no-caller": 2, 68 | "no-class-assign": 2, 69 | "no-cond-assign": 2, 70 | "no-const-assign": 2, 71 | "no-control-regex": 2, 72 | "no-debugger": 2, 73 | "no-delete-var": 2, 74 | "no-dupe-args": 2, 75 | "no-dupe-class-members": 2, 76 | "no-dupe-keys": 2, 77 | "no-duplicate-case": 2, 78 | "no-empty-character-class": 2, 79 | "no-eval": 2, 80 | "no-ex-assign": 2, 81 | "no-extend-native": 2, 82 | "no-extra-bind": 0, 83 | "no-extra-boolean-cast": 0, 84 | "no-extra-parens": [2, "functions"], 85 | "no-fallthrough": 2, 86 | "no-floating-decimal": 2, 87 | "no-func-assign": 2, 88 | "no-implied-eval": 2, 89 | "no-inner-declarations": [2, "functions"], 90 | "no-invalid-regexp": 2, 91 | "no-irregular-whitespace": 2, 92 | "no-iterator": 2, 93 | "no-label-var": 2, 94 | "no-labels": 0, 95 | "no-lone-blocks": 2, 96 | "no-mixed-spaces-and-tabs": 2, 97 | "no-multi-spaces": 2, 98 | "no-multi-str": 2, 99 | "no-multiple-empty-lines": [2, { "max": 2 }], 100 | "no-native-reassign": 0, 101 | "no-negated-in-lhs": 2, 102 | "no-new": 2, 103 | "no-new-func": 2, 104 | "no-new-object": 2, 105 | "no-new-require": 2, 106 | "no-new-wrappers": 2, 107 | "no-obj-calls": 2, 108 | "no-octal": 2, 109 | "no-octal-escape": 2, 110 | "no-proto": 0, 111 | "no-redeclare": 2, 112 | "no-regex-spaces": 2, 113 | "no-return-assign": 2, 114 | "no-self-compare": 2, 115 | "no-sequences": 2, 116 | "no-shadow-restricted-names": 2, 117 | "no-spaced-func": 2, 118 | "no-sparse-arrays": 2, 119 | "no-this-before-super": 2, 120 | "no-throw-literal": 2, 121 | "no-trailing-spaces": 0, 122 | "no-undef": 2, 123 | "no-undef-init": 2, 124 | "no-unexpected-multiline": 2, 125 | "no-unneeded-ternary": [2, { "defaultAssignment": false }], 126 | "no-unreachable": 2, 127 | "no-unused-vars": [2, { "vars": "all", "args": "none" }], 128 | "no-useless-call": 0, 129 | "no-with": 2, 130 | "one-var": [0, { "initialized": "never" }], 131 | "operator-linebreak": [0, "after", { "overrides": { "?": "before", ":": "before" } }], 132 | "padded-blocks": [0, "never"], 133 | "quotes": [2, "double", {"avoidEscape": true, "allowTemplateLiterals": true}], 134 | "radix": 0, 135 | "semi": [2, "always"], 136 | "semi-spacing": [0, { "before": false, "after": true }], 137 | "space-before-blocks": [2, "always"], 138 | "space-before-function-paren": [2, "never"], 139 | "space-in-parens": [2, "never"], 140 | "space-infix-ops": 2, 141 | "space-unary-ops": [2, { "words": true, "nonwords": false }], 142 | "spaced-comment": [0, "always", { "markers": ["global", "globals", "eslint", "eslint-disable", "*package", "!", ","] }], 143 | "use-isnan": 2, 144 | "valid-typeof": 2, 145 | "wrap-iife": [2, "any"], 146 | "yoda": [2, "never"] 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | .idea 3 | *.log 4 | builds/* 5 | stats.json 6 | stats.html 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | .idea 3 | *.log 4 | tests/* 5 | .vscode/* 6 | npm.bat 7 | stats.json 8 | stats.html 9 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | dist: trusty 3 | sudo: false 4 | node_js: 5 | - "10.12.0" 6 | 7 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Test", 11 | "program": "${workspaceRoot}\\tests\\main.js", 12 | //"args": ["--cli-run"], 13 | //"runtimeArgs": ["--harmony_async_await"], 14 | "cwd": "${workspaceRoot}" 15 | }, 16 | { 17 | "type": "node", 18 | "request": "attach", 19 | "name": "Attach to Process", 20 | "port": 5858 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | // Configure glob patterns for excluding files and folders. 4 | "files.exclude": { 5 | "**/.git": true, 6 | "**/.svn": true, 7 | "**/.hg": true, 8 | "**/CVS": true, 9 | "**/.DS_Store": true, 10 | "*.min.js": true, 11 | "*.js.map": true, 12 | "builds/**": true 13 | }, 14 | "window.title": "JSONRPC" 15 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Bigstep Cloud SRL 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. -------------------------------------------------------------------------------- /build.js: -------------------------------------------------------------------------------- 1 | const ChildProcess = require("child_process"); 2 | const fs = require("fs"); 3 | const os = require("os"); 4 | const path = require("path"); 5 | 6 | 7 | // Avoiding "DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code." 8 | // by actually doing the respective exit with non-zero code. 9 | // This allows the watcher to restart this process. 10 | process.on( 11 | "unhandledRejection", 12 | async(reason, promise) => 13 | { 14 | console.log("Unhandled Rejection at: Promise", promise, "reason", reason); 15 | process.exit(1); 16 | } 17 | ); 18 | 19 | 20 | async function spawnPassthru(strExecutablePath, arrParams = []) 21 | { 22 | const childProcess = ChildProcess.spawn(strExecutablePath, arrParams, {stdio: "inherit"}); 23 | //childProcess.stdout.pipe(process.stdout); 24 | //childProcess.stderr.pipe(process.stderr); 25 | return new Promise(async(fnResolve, fnReject) => { 26 | childProcess.on("error", fnReject); 27 | childProcess.on("exit", (nCode) => { 28 | if(nCode === 0) 29 | { 30 | fnResolve(); 31 | } 32 | else 33 | { 34 | fnReject(new Error(`Exec process exited with error code ${nCode}`)); 35 | } 36 | }); 37 | }); 38 | } 39 | 40 | 41 | (async() => { 42 | const objPackageJSON = JSON.parse(fs.readFileSync("package.json")); 43 | 44 | const arrVersionParts = objPackageJSON.version.split("."); 45 | arrVersionParts[arrVersionParts.length - 1] = parseInt(arrVersionParts[arrVersionParts.length - 1], 10); 46 | arrVersionParts[arrVersionParts.length - 1]++; 47 | objPackageJSON.version = arrVersionParts.join("."); 48 | fs.writeFileSync("package.json", JSON.stringify(objPackageJSON, undefined, " ")); 49 | 50 | 51 | console.log("Building."); 52 | await spawnPassthru(path.resolve("./node_modules/.bin/webpack" + (os.platform() === "win32" ? ".cmd" : "")), [/*"--display-modules"*/]); 53 | //process.chdir(__dirname); 54 | 55 | console.log("Done."); 56 | })(); 57 | 58 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Do not use const here, webpack/babel issues. 2 | var objExports = { 3 | Client: require("./src/Client"), 4 | ClientPluginBase: require("./src/ClientPluginBase"), 5 | 6 | Server: require("./src/Server"), 7 | ServerPluginBase: require("./src/ServerPluginBase"), 8 | 9 | EndpointBase: require("./src/EndpointBase"), 10 | 11 | BidirectionalWorkerThreadRouter: require("./src/BidirectionalWorkerThreadRouter"), 12 | BidirectionalWebsocketRouter: require("./src/BidirectionalWebsocketRouter"), 13 | BidirectionalWorkerRouter: require("./src/BidirectionalWorkerRouter"), 14 | BidirectionalWebRTCRouter: require("./src/BidirectionalWebRTCRouter"), 15 | BidirectionalElectronIPC: require("./src/BidirectionalElectronIPC"), 16 | RouterBase: require("./src/RouterBase"), 17 | 18 | Exception: require("./src/Exception"), 19 | 20 | Utils: require("./src/Utils"), 21 | 22 | Plugins: { 23 | Client: require("./src/Plugins/Client"), 24 | Server: require("./src/Plugins/Server") 25 | }, 26 | 27 | WebSocketAdapters: { 28 | WebSocketWrapperBase: require("./src/WebSocketAdapters/WebSocketWrapperBase"), 29 | uws: { 30 | WebSocketWrapper: require("./src/WebSocketAdapters/uws/WebSocketWrapper") 31 | } 32 | }, 33 | 34 | NodeClusterBase: require("./src/NodeClusterBase"), 35 | NodeWorkerThreadsBase: require("./src/NodeWorkerThreadsBase") 36 | }; 37 | 38 | 39 | if(process && parseInt(process.version.replace("v", "").split(".", 2)[0]) >= 10) 40 | { 41 | objExports.BidirectionalWorkerThreadRouter = require("./src/BidirectionalWorkerThreadRouter"); 42 | } 43 | 44 | module.exports = objExports; 45 | -------------------------------------------------------------------------------- /index_webpack.js: -------------------------------------------------------------------------------- 1 | // Do not use const here, webpack/babel issues. 2 | var objExports = { 3 | Client: require("./src/Client"), 4 | ClientPluginBase: require("./src/ClientPluginBase"), 5 | 6 | Server: require("./src/Server"), 7 | ServerPluginBase: require("./src/ServerPluginBase"), 8 | 9 | EndpointBase: require("./src/EndpointBase"), 10 | 11 | BidirectionalWebsocketRouter: require("./src/BidirectionalWebsocketRouter"), 12 | BidirectionalWorkerRouter: require("./src/BidirectionalWorkerRouter"), 13 | BidirectionalWebRTCRouter: require("./src/BidirectionalWebRTCRouter"), 14 | BidirectionalElectronIPC: require("./src/BidirectionalElectronIPC"), 15 | RouterBase: require("./src/RouterBase"), 16 | 17 | Exception: require("./src/Exception"), 18 | 19 | Utils: require("./src/Utils"), 20 | 21 | Plugins: { 22 | Client: require("./src/Plugins/Client"), 23 | Server: require("./src/Plugins/Server") 24 | }, 25 | 26 | WebSocketAdapters: { 27 | WebSocketWrapperBase: require("./src/WebSocketAdapters/WebSocketWrapperBase"), 28 | uws: { 29 | WebSocketWrapper: require("./src/WebSocketAdapters/uws/WebSocketWrapper") 30 | } 31 | } 32 | }; 33 | 34 | 35 | module.exports = {JSONRPC: objExports}; 36 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5" 4 | }, 5 | "files": [ 6 | "src/" 7 | ] 8 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsonrpc-bidirectional", 3 | "description": "Bidirectional JSONRPC over web sockets or HTTP with extensive plugin support.", 4 | "version": "10.0.11", 5 | "scripts": { 6 | "build": "node --experimental-worker build.js", 7 | "prepublish": "node --experimental-worker build.js && node --expose-gc --max-old-space-size=1024 --experimental-worker tests/main.js", 8 | "test": "node test.js", 9 | "test_lib": "node --expose-gc --max-old-space-size=1024 --experimental-worker tests/main.js", 10 | "test_rtc": "node --expose-gc --max-old-space-size=1024 --experimental-worker tests/BrowserWebRTC/main_server.js", 11 | "test_cluster": "node --expose-gc --max-old-space-size=1024 --experimental-worker tests/main_NodeClusterBase.js", 12 | "test_worker_threads": "node --expose-gc --max-old-space-size=1024 --experimental-worker tests/main_NodeWorkerThreadsBase.js", 13 | "benchmark": "node --expose-gc --max-old-space-size=1024 --experimental-worker tests/benchmark.js", 14 | "benchmark_endless_new_websockets": "node --expose-gc --max-old-space-size=1024 --experimental-worker tests/benchmark_endless_new_websockets.js", 15 | "lint": "eslint src tests --quiet" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/bigstepinc/jsonrpc-bidirectional.git" 20 | }, 21 | "homepage": "https://github.com/bigstepinc/jsonrpc-bidirectional", 22 | "author": "Ionut Stan ", 23 | "license": "MIT", 24 | "contributors": [ 25 | { 26 | "name": "Ionut Stan", 27 | "email": "Ionut.Stan@bigstep.com" 28 | }, 29 | { 30 | "name": "Ionut Stan", 31 | "email": "oxygenus@gmail.com" 32 | } 33 | ], 34 | "preferGlobal": false, 35 | "engines": { 36 | "node": ">=10.15.3" 37 | }, 38 | "browser": { 39 | "child_process": false 40 | }, 41 | "dependencies": { 42 | "extendable-error-class": "^0.1.1", 43 | "fs-extra": "^7.0.0", 44 | "node-fetch": "^2.2.0", 45 | "sleep-promise": "^2.0.0" 46 | }, 47 | "optionalDependencies": { 48 | "babel-polyfill": "^6.23.0", 49 | "babel-runtime": "^6.23.0", 50 | "es6-promise": "^4.1.0", 51 | "jssha": "^2.2.0", 52 | "node-forge": "^0.7.1", 53 | "typescript-parser": "^2.6.1", 54 | "whatwg-fetch": "^2.0.3", 55 | "ws": "^5.1.1" 56 | }, 57 | "devDependencies": { 58 | "@types/node": "^7.0.52", 59 | "babel-core": "^6.24.1", 60 | "babel-eslint": "^7.2.2", 61 | "babel-loader": "^6.4.1", 62 | "babel-minify-webpack-plugin": "^0.3.1", 63 | "babel-plugin-async-to-promises": "^1.0.5", 64 | "babel-plugin-remove-comments": "^2.0.0", 65 | "babel-preset-es2015": "^6.24.1", 66 | "babel-preset-stage-3": "^6.24.1", 67 | "chalk": "^2.4.1", 68 | "electron": "^1.7.9", 69 | "eslint": "^6.7.2", 70 | "eslint-plugin-jsdoc": "^18.4.3", 71 | "phantom": "^6.3.0", 72 | "recursive-keys": "^0.9.0", 73 | "uglify-js": "^2.8.22", 74 | "uws": "^0.14.5", 75 | "webpack": "^2.7.0", 76 | "webpack-bundle-analyzer": "^3.6.0" 77 | }, 78 | "files": [ 79 | "builds/browser/es5/jsonrpc.min.js", 80 | "builds/browser/es5/jsonrpc.min.js.map", 81 | "builds/browser/es7/jsonrpc.min.js", 82 | "builds/browser/es7/jsonrpc.min.js.map", 83 | "LICENSE", 84 | "src/*", 85 | "index.js", 86 | "README.MD", 87 | "node_modules/babel-polyfill/dist/polyfill.min.js", 88 | "node_modules/whatwg-fetch/fetch.js", 89 | "node_modules/regenerator-runtime/runtime.js", 90 | "node_modules/es6-promise/dist/es6-promise.auto.min.js", 91 | "node_modules/es6-promise/dist/es6-promise.auto.min.js.map" 92 | ] 93 | } -------------------------------------------------------------------------------- /src/BidirectionalWebRTCRouter.js: -------------------------------------------------------------------------------- 1 | const JSONRPC = { 2 | Exception: require("./Exception"), 3 | Server: require("./Server"), 4 | IncomingRequest: require("./IncomingRequest"), 5 | EndpointBase: require("./EndpointBase"), 6 | RouterBase: require("./RouterBase"), 7 | Plugins: { 8 | Client: require("./Plugins/Client") 9 | }, 10 | Utils: require("./Utils") 11 | }; 12 | 13 | /** 14 | * @event madeReverseCallsClient 15 | * The "madeReverseCallsClient" event offers automatically instantiated API clients (API clients are instantiated for each connection, lazily). 16 | */ 17 | module.exports = 18 | class BidirectionalWebRTCRouter extends JSONRPC.RouterBase 19 | { 20 | /** 21 | * This function must be synchronous, otherwise it will allow of race conditions where critical plugins (if any) haven't been initialized yet. 22 | * 23 | * Returns the connection ID. 24 | * 25 | * RTCDataChannel instances which will emit an error or close event will get automatically removed. 26 | * 27 | * Already closed RTCDataChannel instances are ignored by this function. 28 | * 29 | * @param {RTCDataChannel} dataChannel 30 | * 31 | * @returns {number} 32 | */ 33 | addRTCDataChannelSync(dataChannel) 34 | { 35 | if(dataChannel.readyState === "closed") 36 | { 37 | // "closing" should be followed by a closed event. 38 | // "open" is desired. 39 | // "connecting" should emit an error event if it will not become open. 40 | // @TODO: test cases for the above, somehow. 41 | 42 | // "closed" would not recover and should never be added, because it would not get cleaned up. 43 | console.log("[" + process.pid + "] addRTCDataChannelSync ignoring closed dataChannel."); 44 | 45 | return; 46 | } 47 | 48 | const nConnectionID = ++this._nConnectionIDCounter; 49 | 50 | const strEndpointPath = JSONRPC.EndpointBase.normalizePath(dataChannel.label); 51 | 52 | const objSession = { 53 | dataChannel: dataChannel, 54 | nConnectionID: nConnectionID, 55 | clientReverseCalls: null, 56 | clientWebRTCTransportPlugin: null, 57 | strEndpointPath: strEndpointPath 58 | }; 59 | 60 | this._objSessions[nConnectionID] = objSession; 61 | 62 | const fnOnMessage = async(messageEvent) => { 63 | await this._routeMessage(messageEvent.data, objSession); 64 | }; 65 | const fnOnError = (error) => { 66 | console.error(error); 67 | 68 | this.onConnectionEnded(nConnectionID); 69 | 70 | if(dataChannel.readyState === "open") 71 | { 72 | dataChannel.close(); 73 | } 74 | }; 75 | const fnOnClose = (closeEvent) => { 76 | this.onConnectionEnded(nConnectionID); 77 | 78 | dataChannel.removeEventListener("message", fnOnMessage); 79 | dataChannel.removeEventListener("close", fnOnClose); 80 | dataChannel.removeEventListener("error", fnOnError); 81 | }; 82 | 83 | dataChannel.addEventListener("message", fnOnMessage); 84 | dataChannel.addEventListener("close", fnOnClose); 85 | dataChannel.addEventListener("error", fnOnError); 86 | 87 | return nConnectionID; 88 | } 89 | 90 | 91 | /** 92 | * Overridable to allow configuring the client further. 93 | * 94 | * @param {Class} ClientClass 95 | * @param {object} objSession 96 | * 97 | * @returns {JSONRPC.Client} 98 | */ 99 | _makeReverseCallsClient(ClientClass, objSession) 100 | { 101 | const clientReverseCalls = new ClientClass(objSession.strEndpointPath); 102 | 103 | objSession.clientWebRTCTransportPlugin = new JSONRPC.Plugins.Client.WebRTCTransport(objSession.dataChannel, /*bBidirectionalWebRTCMode*/ true); 104 | clientReverseCalls.addPlugin(objSession.clientWebRTCTransportPlugin); 105 | 106 | this.emit("madeReverseCallsClient", clientReverseCalls); 107 | 108 | return clientReverseCalls; 109 | } 110 | 111 | 112 | /** 113 | * Routes RTCDataChannel messages to either the client or the server WenRTC plugin. 114 | * 115 | * @param {string} strMessage 116 | * @param {object} objSession 117 | */ 118 | async _routeMessage(strMessage, objSession) 119 | { 120 | const dataChannel = objSession.dataChannel; 121 | const nConnectionID = objSession.nConnectionID; 122 | 123 | if(!strMessage.trim().length) 124 | { 125 | console.log("[" + process.pid + "] WebRTCBidirectionalRouter: Received empty message. Ignoring."); 126 | return; 127 | } 128 | 129 | let objMessage; 130 | 131 | try 132 | { 133 | objMessage = JSONRPC.Utils.jsonDecodeSafe(strMessage); 134 | } 135 | catch(error) 136 | { 137 | console.error(error); 138 | console.error("Unable to parse JSON. RAW remote message: " + strMessage); 139 | 140 | if( 141 | this._jsonrpcServer 142 | && this._objSessions.hasOwnProperty(nConnectionID) 143 | && this._objSessions[nConnectionID].clientWebRTCTransportPlugin === null 144 | && dataChannel.readyState === "open" 145 | ) 146 | { 147 | dataChannel.send(JSON.stringify({ 148 | id: null, 149 | jsonrpc: "2.0", 150 | error: { 151 | message: "Invalid JSON: " + JSON.stringify(strMessage) + ".", 152 | code: JSONRPC.Exception.PARSE_ERROR 153 | } 154 | }, undefined, "\t")); 155 | } 156 | 157 | console.log("[" + process.pid + "] Unclean state. Unable to match RTCDataChannel message to an existing Promise or qualify it as a request or response."); 158 | if(dataChannel.readyState === "open") 159 | { 160 | dataChannel.close(); 161 | } 162 | 163 | return; 164 | } 165 | 166 | let bNotification = !objMessage.hasOwnProperty("id"); 167 | 168 | try 169 | { 170 | if(objMessage.hasOwnProperty("method")) 171 | { 172 | if(!this._jsonrpcServer) 173 | { 174 | if(!bNotification && dataChannel.readyState === "open") 175 | { 176 | dataChannel.send(JSON.stringify({ 177 | id: null, 178 | jsonrpc: "2.0", 179 | error: { 180 | message: "JSONRPC.Server not initialized on this RTCDataChannel. Raw request: " + strMessage + ".", 181 | code: JSONRPC.Exception.PARSE_ERROR 182 | } 183 | }, undefined, "\t")); 184 | } 185 | 186 | throw new Error("JSONRPC.Server not initialized on this RTCDataChannel."); 187 | } 188 | 189 | 190 | const incomingRequest = new JSONRPC.IncomingRequest(); 191 | 192 | incomingRequest.connectionID = nConnectionID; 193 | incomingRequest.router = this; 194 | 195 | 196 | try 197 | { 198 | const strEndpointPath = this._objSessions[nConnectionID].strEndpointPath; 199 | 200 | if(!this._jsonrpcServer.endpoints.hasOwnProperty(strEndpointPath)) 201 | { 202 | throw new JSONRPC.Exception("Unknown JSONRPC endpoint " + strEndpointPath + ".", JSONRPC.Exception.METHOD_NOT_FOUND); 203 | } 204 | 205 | incomingRequest.endpoint = this._jsonrpcServer.endpoints[strEndpointPath]; 206 | 207 | incomingRequest.requestBody = strMessage; 208 | incomingRequest.requestObject = objMessage; 209 | } 210 | catch(error) 211 | { 212 | incomingRequest.callResult = error; 213 | } 214 | 215 | 216 | await this._jsonrpcServer.processRequest(incomingRequest); 217 | 218 | if(dataChannel.readyState !== "open") 219 | { 220 | console.error("dataChannel.readyState: " + JSON.stringify(dataChannel.readyState) + ". Request was " + strMessage + ". Attempted responding with " + JSON.stringify(incomingRequest.callResultToBeSerialized, undefined, "\t") + "."); 221 | } 222 | 223 | if(!bNotification) 224 | { 225 | dataChannel.send(incomingRequest.callResultSerialized); 226 | } 227 | } 228 | else if(objMessage.hasOwnProperty("result") || objMessage.hasOwnProperty("error")) 229 | { 230 | if( 231 | this._objSessions.hasOwnProperty(nConnectionID) 232 | && this._objSessions[nConnectionID].clientWebRTCTransportPlugin === null 233 | ) 234 | { 235 | if(!this._jsonrpcServer) 236 | { 237 | if(!bNotification && dataChannel.readyState === "open") 238 | { 239 | dataChannel.send(JSON.stringify({ 240 | id: null, 241 | jsonrpc: "2.0", 242 | error: { 243 | message: "JSONRPC.Client not initialized on this RTCConnection. Raw message: " + strMessage + ".", 244 | code: JSONRPC.Exception.PARSE_ERROR 245 | } 246 | }, undefined, "\t")); 247 | } 248 | } 249 | 250 | if(dataChannel.readyState === "open") 251 | { 252 | dataChannel.close(); 253 | } 254 | 255 | throw new Error("How can the client be not initialized, and yet getting responses from phantom requests?"); 256 | } 257 | 258 | if(this._objSessions.hasOwnProperty(nConnectionID)) 259 | { 260 | await this._objSessions[nConnectionID].clientWebRTCTransportPlugin.processResponse(strMessage, objMessage); 261 | } 262 | else 263 | { 264 | console.error("Connection ID " + nConnectionID + " is closed and session is missing. Ignoring response: " + strMessage); 265 | } 266 | } 267 | else 268 | { 269 | // Malformed message, will attempt to send a response. 270 | bNotification = false; 271 | 272 | throw new Error("Unable to qualify the message as a JSONRPC request or response."); 273 | } 274 | } 275 | catch(error) 276 | { 277 | console.error(error); 278 | console.error("Uncaught error. RAW remote message: " + strMessage); 279 | 280 | if( 281 | this._jsonrpcServer 282 | && this._objSessions.hasOwnProperty(nConnectionID) 283 | && this._objSessions[nConnectionID].clientWebRTCTransportPlugin === null 284 | ) 285 | { 286 | if(!bNotification && dataChannel.readyState === "open") 287 | { 288 | dataChannel.send(JSON.stringify({ 289 | id: null, 290 | jsonrpc: "2.0", 291 | error: { 292 | message: "Internal error: " + error.message + ".", 293 | code: JSONRPC.Exception.INTERNAL_ERROR 294 | } 295 | }, undefined, "\t")); 296 | } 297 | } 298 | 299 | if(dataChannel.readyState === "open") 300 | { 301 | console.log("[" + process.pid + "] Unclean state. Closing data channel."); 302 | dataChannel.close(); 303 | } 304 | 305 | return; 306 | } 307 | } 308 | }; 309 | -------------------------------------------------------------------------------- /src/ClientPluginBase.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require("events"); 2 | 3 | 4 | /** 5 | * JSONRPC.Client plugins need to extend this class. 6 | */ 7 | class ClientPluginBase extends EventEmitter 8 | { 9 | /** 10 | * This can be used to have Client.rpc() wait for a plugin's various initialization or re-initialization, or trigger lazy initialization or re-initialization. 11 | * 12 | * For example, Client.rpc() could wait for the authorization of a WebSocket to finish, 13 | * or the reinitialization of a WebSocket (new WebSocket and problably login API calls before the transport can be used for API calls). 14 | * 15 | * This function is allowed to throw as if .rpc() threw an error. 16 | * 17 | * Wether a timeout is implemented to eventually throw and not block .rpc() forever is left to be decided on a per plugin basis. 18 | */ 19 | async waitReady() 20 | { 21 | } 22 | 23 | 24 | /** 25 | * Gives a chance to modify the client request object before sending it out. 26 | * 27 | * Normally, this allows extending the protocol. 28 | * 29 | * @param {JSONRPC.OutgoingRequest} outgoingRequest 30 | */ 31 | async beforeJSONEncode(outgoingRequest) 32 | { 33 | // outgoingRequest.requestObject is available here. 34 | 35 | // outgoingRequest.headers and outgoingRequest.enpointURL may be modified here. 36 | // outgoingRequest.requestBody may be set to a non-NULL value to replace JSON.stringy() (and thus replace serialization). 37 | } 38 | 39 | 40 | /** 41 | * Gives a chance to encrypt, sign or log RAW outgoing requests. 42 | * 43 | * @param {JSONRPC.OutgoingRequest} outgoingRequest 44 | */ 45 | async afterJSONEncode(outgoingRequest) 46 | { 47 | // outgoingRequest.requestBody is available here. 48 | 49 | // outgoingRequest.headers and outgoingRequest.enpointURL may be modified here. 50 | } 51 | 52 | 53 | /** 54 | * If a plugin chooses to actually make the call here, 55 | * it must set the result in the outgoingRequest.callResult property. 56 | * 57 | * @param {JSONRPC.OutgoingRequest} outgoingRequest 58 | * 59 | * @returns {Promise.} 60 | */ 61 | async makeRequest(outgoingRequest) 62 | { 63 | // outgoingRequest.callResult may be written here. 64 | } 65 | 66 | 67 | /** 68 | * @param {JSONRPC.OutgoingRequest} outgoingRequest 69 | */ 70 | async beforeJSONDecode(outgoingRequest) 71 | { 72 | // outgoingRequest.responseBody is available here. 73 | // outgoingRequest.responseObject may be set to a non-NULL value to replace JSON.parse() (and thus replace serialization). 74 | } 75 | 76 | 77 | /** 78 | * @param {JSONRPC.OutgoingRequest} outgoingRequest 79 | */ 80 | async afterJSONDecode(outgoingRequest) 81 | { 82 | // outgoingRequest.responseObject is available here. 83 | } 84 | 85 | 86 | /** 87 | * Should be used to log exceptions or replace exceptions with other exceptions. 88 | * 89 | * This is only called if outgoingRequest.callResult is a subclass of Error or an instance of Error. 90 | * 91 | * @param {JSONRPC.OutgoingRequest} outgoingRequest 92 | */ 93 | async exceptionCatch(outgoingRequest) 94 | { 95 | // outgoingRequest.callResult is available here, and it is a subclass of Error. 96 | } 97 | 98 | 99 | /** 100 | * @returns {null} 101 | */ 102 | dispose() 103 | { 104 | this.emit("disposed"); 105 | } 106 | }; 107 | 108 | 109 | module.exports = ClientPluginBase; 110 | -------------------------------------------------------------------------------- /src/EndpointBase.js: -------------------------------------------------------------------------------- 1 | const url = require("url"); 2 | 3 | const assert = require("assert"); 4 | 5 | const EventEmitter = require("events"); 6 | 7 | let TypescriptParserNamespace = null; 8 | try 9 | { 10 | TypescriptParserNamespace = require("typescript-parser"); 11 | } 12 | catch(error) 13 | { 14 | // Ignored optional dependency. 15 | } 16 | 17 | 18 | /** 19 | * This class is suposed to be extended by JSONRPC endpoints. 20 | * Endpoints hold exported RPC functions. 21 | * 22 | * All exported functions must accept a JSONRPC.IncomingRequest class instance as first param. 23 | * 24 | * Methods defined by subclasses, which are to be exported through RPC, 25 | * must each return a single Promise object or simply decorated with async so they are awaitable. 26 | * 27 | * @event disposed 28 | */ 29 | class EndpointBase extends EventEmitter 30 | { 31 | /** 32 | * @param {string} strName 33 | * @param {string} strPath 34 | * @param {object} objReflection 35 | * @param {Class|null} classReverseCallsClient 36 | */ 37 | constructor(strName, strPath, objReflection, classReverseCallsClient) 38 | { 39 | super(); 40 | 41 | assert.strictEqual(typeof strName, "string"); 42 | assert.strictEqual(typeof strPath, "string"); 43 | assert.strictEqual(typeof objReflection, "object"); 44 | 45 | this._strName = strName; 46 | this._strPath = EndpointBase.normalizePath(strPath); 47 | this._objReflection = objReflection; 48 | this._classReverseCallsClient = classReverseCallsClient; 49 | } 50 | 51 | 52 | /** 53 | * @returns {null} 54 | */ 55 | dispose() 56 | { 57 | this.emit("disposed"); 58 | } 59 | 60 | 61 | /** 62 | * Brings methods of some class instance into this class (mixins, traits). 63 | * 64 | * The mixin functions will be executed in the context of this class instance, 65 | * so the source class shouldn't have any member properties, or the member properties' code would need to be duplicated in this class. 66 | * 67 | * The source class instance is considered a trait. 68 | * 69 | * @param {*} classInstance 70 | */ 71 | _mixTraitIntoThis(classInstance) 72 | { 73 | for(const strFunctionName of Object.getOwnPropertyNames(classInstance.constructor.prototype)) 74 | { 75 | if(typeof classInstance.constructor.prototype[strFunctionName] === "function" && strFunctionName !== "constructor") 76 | { 77 | this.constructor.prototype[strFunctionName] = classInstance[strFunctionName]; 78 | } 79 | } 80 | }; 81 | 82 | 83 | /** 84 | * Utility function to be used in a build process. 85 | * 86 | * Example usage: await this._buildAPIClientSourceCode([this]); 87 | * 88 | * @param {Array<*>} arrAPITraits 89 | * @param {string} strClassName 90 | * @param {string|null} strTemplate = null 91 | */ 92 | static async _buildAPIClientSourceCode(arrAPITraits, strClassName, strTemplate = null) 93 | { 94 | assert(Array.isArray(arrAPITraits), "arrAPITraits needs to be an Array"); 95 | assert(typeof strClassName === "string", "strClassName was suposed to be of type string."); 96 | assert(typeof strTemplate === "string" || strTemplate === null, "strTemplate was suposed to be of type string or null."); 97 | 98 | let strServerAPIClientMethods = ""; 99 | for(const classInstance of arrAPITraits) 100 | { 101 | const objParsedJavaScript = await (new TypescriptParserNamespace.TypescriptParser()).parseSource(classInstance.constructor.toString()); 102 | 103 | for(const objMethod of objParsedJavaScript.declarations[0].methods) 104 | { 105 | if(!objMethod.name.startsWith("_") && objMethod !== "constructor") 106 | { 107 | const arrParameterNames = []; 108 | 109 | if(!objMethod.parameters.length || objMethod.parameters[0].name !== "incomingRequest") 110 | { 111 | console.error(`Warning. First parameter of ${classInstance.constructor.name}.${objMethod.name}() is not incomingRequest. That param is mandatory for API exported functions.`); 112 | // process.exit(1); 113 | continue; 114 | } 115 | 116 | objMethod.parameters.splice(0, 1); 117 | 118 | for(const objParameter of objMethod.parameters) 119 | { 120 | if(objParameter.startCharacter === "{") 121 | { 122 | arrParameterNames.push("objDestructuringParam_" + objParameter.name.replace(/[^A-Za-z0-9_]+/g, "__") + "={}"); 123 | } 124 | else 125 | { 126 | arrParameterNames.push(objParameter.name); 127 | } 128 | } 129 | strServerAPIClientMethods += ` 130 | async ${objMethod.name}(${arrParameterNames.join(", ")}) 131 | { 132 | return this.rpc("${objMethod.name}", [...arguments]); 133 | } 134 | `.replace(/^\t{5}/gm, ""); 135 | } 136 | } 137 | 138 | /*for(const strFunctionName of Object.getOwnPropertyNames(classInstance.constructor.prototype)) 139 | { 140 | if(typeof classInstance.constructor.prototype[strFunctionName] === "function" && !strFunctionName.startsWith("_") && strFunctionName !== "constructor") 141 | { 142 | strServerAPIClientMethods += ` 143 | async ${strFunctionName}() 144 | { 145 | return this.rpc("${strFunctionName}", [...arguments]); 146 | } 147 | `; 148 | } 149 | }*/ 150 | } 151 | 152 | let strAPIClient = (strTemplate || ` 153 | const JSONRPC = require("jsonrpc-bidirectional"); 154 | class ${strClassName} extends JSONRPC.Client 155 | { 156 | _INSERT_METHODS_HERE_ 157 | }; 158 | module.exports = ${strClassName}; 159 | `).replace(/^\t{3}/gm, "").replace("_INSERT_METHODS_HERE_", strServerAPIClientMethods); 160 | 161 | return strAPIClient; 162 | } 163 | 164 | 165 | /** 166 | * @returns {string} 167 | */ 168 | get path() 169 | { 170 | return this._strPath; 171 | } 172 | 173 | 174 | /** 175 | * @returns {string} 176 | */ 177 | get name() 178 | { 179 | return this._strName; 180 | } 181 | 182 | 183 | /** 184 | * @returns {object} 185 | */ 186 | get reflection() 187 | { 188 | return this._objReflection; 189 | } 190 | 191 | 192 | /** 193 | * @returns {Class|null} 194 | */ 195 | get ReverseCallsClientClass() 196 | { 197 | return this._classReverseCallsClient; 198 | } 199 | 200 | 201 | /** 202 | * @param {string} strURL 203 | * 204 | * @returns {string} 205 | */ 206 | static normalizePath(strURL) 207 | { 208 | const objURLParsed = url.parse(strURL); 209 | let strPath = objURLParsed.pathname ? objURLParsed.pathname.trim() : "/"; 210 | if(!strPath.length || strPath.substr(-1) !== "/") 211 | { 212 | strPath += "/"; 213 | } 214 | 215 | if(strPath.substr(0, 1) !== "/") 216 | { 217 | strPath = "/" + strPath; 218 | } 219 | 220 | return strPath; 221 | } 222 | }; 223 | 224 | module.exports = EndpointBase; 225 | -------------------------------------------------------------------------------- /src/Exception.js: -------------------------------------------------------------------------------- 1 | const assert = require("assert"); 2 | 3 | const ExtendableError = require("extendable-error-class"); 4 | 5 | 6 | module.exports = 7 | class Exception extends ExtendableError 8 | { 9 | /** 10 | * @param {string} strMessage 11 | * @param {number} nCode 12 | * @param {object} objData 13 | */ 14 | constructor(strMessage, nCode = 0, objData = {}) 15 | { 16 | super(strMessage); 17 | 18 | this.strMessage = strMessage; 19 | this.code = (nCode === undefined || nCode === null) ? 0 : nCode; 20 | 21 | // Do not use the setter as it only allows an Object (validates), 22 | // while the JSONRPC 2.0 specification only requires a "structured" data type. 23 | this.objData = objData; 24 | } 25 | 26 | 27 | /** 28 | * @returns {number} 29 | */ 30 | get code() 31 | { 32 | return this.nCode; 33 | } 34 | 35 | 36 | /** 37 | * @param {number} nCode 38 | */ 39 | set code(nCode) 40 | { 41 | assert(typeof nCode === "number" || String(parseInt(nCode)) === nCode, "The JSONRPC.Exception error code must be of type number."); 42 | this.nCode = parseInt(nCode); 43 | } 44 | 45 | 46 | /** 47 | * @returns {object} 48 | */ 49 | get data() 50 | { 51 | return this.objData; 52 | } 53 | 54 | 55 | /** 56 | * @param {object} objData 57 | */ 58 | set data(objData) 59 | { 60 | assert(typeof objData === "object" && objData !== null, "The JSONRPC.Exception data property must be an Object."); 61 | this.objData = objData; 62 | } 63 | 64 | 65 | /** 66 | * Bad credentials (user, password, signing hash, account does not exist, etc.). 67 | * Not part of JSON-RPC 2.0 spec. 68 | * 69 | * @returns {number} 70 | */ 71 | static get NOT_AUTHENTICATED() 72 | { 73 | return -1; 74 | } 75 | 76 | /** 77 | * The authenticated user is not authorized to make any or some requests. 78 | * Not part of JSON-RPC 2.0 spec. 79 | * 80 | * @returns {number} 81 | */ 82 | static get NOT_AUTHORIZED() 83 | { 84 | return -2; 85 | } 86 | 87 | /** 88 | * The request has expired. The requester must create or obtain a new request. 89 | * Not part of JSON-RPC 2.0 spec. 90 | * 91 | * @returns {number} 92 | */ 93 | static get REQUEST_EXPIRED() 94 | { 95 | return -3; 96 | } 97 | 98 | /** 99 | * Did not receive a proper response from the server. 100 | * On HTTP, a HTTP response code was not received. 101 | * Not part of JSON-RPC 2.0 spec. 102 | * 103 | * @returns {number} 104 | */ 105 | static get NETWORK_ERROR() 106 | { 107 | return -4; 108 | } 109 | 110 | /** 111 | * Parse error. 112 | * Invalid JSON was received by the server. 113 | * An error occurred on the server while parsing the JSON text. 114 | * 115 | * @returns {number} 116 | */ 117 | static get PARSE_ERROR() 118 | { 119 | return -32700; 120 | } 121 | 122 | /** 123 | * Invalid Request. 124 | * The JSON sent is not a valid Request object. 125 | * 126 | * @returns {number} 127 | */ 128 | static get INVALID_REQUEST() 129 | { 130 | return -32600; 131 | } 132 | 133 | /** 134 | * Method not found. 135 | * The method does not exist / is not available. 136 | * 137 | * @returns {number} 138 | */ 139 | static get METHOD_NOT_FOUND() 140 | { 141 | return -32601; 142 | } 143 | 144 | /** 145 | * Invalid params. 146 | * Invalid method parameter(s). 147 | * 148 | * @returns {number} 149 | */ 150 | static get INVALID_PARAMS() 151 | { 152 | return -32602; 153 | } 154 | 155 | /** 156 | * Internal error. 157 | * Internal JSON-RPC error. 158 | * 159 | * @returns {number} 160 | */ 161 | static get INTERNAL_ERROR() 162 | { 163 | return -32603; 164 | } 165 | 166 | // -32000 to -32099 Server error. Reserved for implementation-defined server-errors. 167 | }; 168 | -------------------------------------------------------------------------------- /src/NodeClusterBase/MasterClient.js: -------------------------------------------------------------------------------- 1 | const cluster = require("cluster"); 2 | 3 | const NodeMultiCoreCPUBase = require("../NodeMultiCoreCPUBase"); 4 | 5 | /** 6 | * Extend this class to link to extra master RPC APIs on workers. 7 | */ 8 | class MasterClient extends NodeMultiCoreCPUBase.MasterClient 9 | { 10 | async getPersistentIDForWorkerID(nWorkerIDRequester = null) 11 | { 12 | if(nWorkerIDRequester === null && !cluster.isMaster) 13 | { 14 | nWorkerIDRequester = cluster.worker.id; 15 | } 16 | 17 | return this.rpc("getPersistentIDForWorkerID", [nWorkerIDRequester]); 18 | } 19 | }; 20 | 21 | module.exports = MasterClient; 22 | -------------------------------------------------------------------------------- /src/NodeClusterBase/MasterEndpoint.js: -------------------------------------------------------------------------------- 1 | const cluster = require("cluster"); 2 | const os = require("os"); 3 | const assert = require("assert"); 4 | 5 | const sleep = require("sleep-promise"); 6 | 7 | const NodeMultiCoreCPUBase = require("../NodeMultiCoreCPUBase"); 8 | 9 | const JSONRPC = { 10 | BidirectionalWorkerRouter: require("../BidirectionalWorkerRouter") 11 | }; 12 | 13 | /** 14 | * Extend this class to export extra master RPC APIs. 15 | * 16 | * Counter-intuitively, this endpoint instantiates its own JSONRPC.Server and JSONRPC.BidirectionalWorkerRouter, 17 | * inside .start(). 18 | * 19 | * The "workerReady" event is issued when a new worker is ready to receive RPC calls. The event is called with the JSORNPC client as first param. 20 | * 21 | * @event workerReady 22 | */ 23 | class MasterEndpoint extends NodeMultiCoreCPUBase.MasterEndpoint 24 | { 25 | constructor(classReverseCallsClient = null) 26 | { 27 | if(classReverseCallsClient === null) 28 | { 29 | classReverseCallsClient = require("./WorkerClient"); 30 | } 31 | 32 | console.log(`Fired up cluster ${cluster.isWorker ? "worker" : "master"} with PID ${process.pid}`); 33 | 34 | if(!cluster.isMaster) 35 | { 36 | throw new Error("MasterEndpoint can only be instantiated in the master process."); 37 | } 38 | 39 | super(classReverseCallsClient); 40 | } 41 | 42 | 43 | async _configureBeforeStart() 44 | { 45 | cluster.on( 46 | "fork", 47 | async(worker) => { 48 | try 49 | { 50 | let nPersistentWorkerID; 51 | for(let nPersistentWorkerIDIterator in this.objPersistentWorkerIDToWorkerID) 52 | { 53 | if(this.objPersistentWorkerIDToWorkerID[nPersistentWorkerIDIterator] === worker.id) 54 | { 55 | nPersistentWorkerID = Number(nPersistentWorkerIDIterator); 56 | break; 57 | } 58 | } 59 | 60 | assert(nPersistentWorkerID !== undefined, `Something went wrong, as worker with PID ${worker.id} wasn't assigned a persistentID before fork.`); 61 | 62 | this.objWorkerIDToState[worker.id] = { 63 | client: null, 64 | ready: false, 65 | exited: false, 66 | persistentID: nPersistentWorkerID 67 | }; 68 | 69 | console.log("Adding worker ID " + worker.id + " and persistent ID " + nPersistentWorkerID + " to BidirectionalWorkerRouter."); 70 | const nConnectionID = await this._bidirectionalWorkerRouter.addWorker(worker, /*strEndpointPath*/ this.path, 120 * 1000 /*Readiness timeout in milliseconds*/); 71 | 72 | this.objWorkerIDToState[worker.id].client = this._bidirectionalWorkerRouter.connectionIDToSingletonClient(nConnectionID, this.ReverseCallsClientClass); 73 | 74 | this.emit("workerReady", this.objWorkerIDToState[worker.id].client); 75 | } 76 | catch(error) 77 | { 78 | console.error(error); 79 | console.error("Cluster master process, on fork event handler unexpected error. Don't know how to handle."); 80 | process.exit(1); 81 | } 82 | } 83 | ); 84 | 85 | cluster.on( 86 | "exit", 87 | async(worker, nExitCode, nKillSignal) => { 88 | try 89 | { 90 | let nPersistentWorkerID; 91 | if(this.objWorkerIDToState[worker.id] !== undefined) 92 | { 93 | this.objWorkerIDToState[worker.id].exited = true; 94 | nPersistentWorkerID = this.objWorkerIDToState[worker.id].persistentID; 95 | } 96 | 97 | console.log(`Worker with PID ${worker.process.pid} and persistentId ${nPersistentWorkerID} died. Exit code: ${nExitCode}. Signal: ${nKillSignal}.`); 98 | 99 | this.arrFailureTimestamps.push(new Date().getTime()); 100 | this.arrFailureTimestamps = this.arrFailureTimestamps.filter((nMillisecondsUnixTimeOfFailure) => { 101 | return nMillisecondsUnixTimeOfFailure >= new Date().getTime() - (60 * 2 * 1000); 102 | }); 103 | 104 | const nMaxFailuresPerMaxWorkers = 20 /*times*/; 105 | 106 | if(process.uptime() < 20) 107 | { 108 | this.arrFailureTimestamps.splice(0); 109 | } 110 | 111 | if(this.arrFailureTimestamps.length / Math.max(this.maxWorkersCount, 1) > nMaxFailuresPerMaxWorkers) 112 | { 113 | console.error(`[Master] *Not* adding a worker because another worker has died. Doing a .gracefulExit() instead because the number of worker failures divided by .maxWorkersCount is greater than ${nMaxFailuresPerMaxWorkers} over the last 2 minutes. ${this.arrFailureTimestamps.length / Math.max(this.maxWorkersCount, 1)} > ${nMaxFailuresPerMaxWorkers}. Process uptime is ${process.uptime()} seconds.`); 114 | await this.gracefulExit(null); 115 | } 116 | else 117 | { 118 | if(!this.bShuttingDown) 119 | { 120 | const nSleepMilliSeconds = Math.max(800 + 1000 * this.readyWorkersCount, 3000); 121 | await sleep(`Sleeping ${nSleepMilliSeconds} milliseconds before replacing exited worker.`); 122 | // cluster.fork(); 123 | 124 | console.error("[Master] Adding a worker because another worker has exited."); 125 | this._addWorker(nPersistentWorkerID); 126 | } 127 | } 128 | } 129 | catch(error) 130 | { 131 | console.error(error); 132 | console.error("Cluster master process, on worker exit event handler unexpected error. Don't know how to handle. Exiting..."); 133 | process.exit(1); 134 | } 135 | } 136 | ); 137 | } 138 | 139 | 140 | async _addWorker(nPersistentWorkerID = null) 141 | { 142 | // First assign persistentId using objPersistentWorkerIDToWorkerID, which will be added to 143 | // objWorkerIDToState in 'fork' event handler defined in _configureBeforeStart. 144 | assert(nPersistentWorkerID === null || typeof nPersistentWorkerID === "number", `Invalid property type for nPersistentWorkerID in MasterEndpoint. Expected "number", but got ${typeof nPersistentWorkerID}.`); 145 | 146 | if(nPersistentWorkerID !== null) 147 | { 148 | const nExistingWorkerID = this.objPersistentWorkerIDToWorkerID[nPersistentWorkerID]; 149 | if(nExistingWorkerID !== undefined) 150 | { 151 | if(this.objWorkerIDToState[nExistingWorkerID].exited !== true) 152 | { 153 | console.log(`Worker with PID ${nExistingWorkerID} that hasn't exited yet already has persistentId ${nPersistentWorkerID}.`); 154 | return; 155 | } 156 | } 157 | } 158 | else 159 | { 160 | nPersistentWorkerID = this._nNextAvailablePersistentWorkerID++; 161 | } 162 | 163 | const workerProcess = cluster.fork(); 164 | 165 | this.objPersistentWorkerIDToWorkerID[nPersistentWorkerID] = workerProcess.id; 166 | } 167 | 168 | 169 | /** 170 | * @param {JSONRPC.Client} reverseCallsClient 171 | * 172 | * @returns {{plugin:JSONRPC.ClientPluginBase, workerID:number, worker:cluster.Worker}} 173 | */ 174 | async _transportPluginFromReverseClient(reverseCallsClient) 175 | { 176 | const workerTransportPlugin = reverseCallsClient.plugins.filter(plugin => plugin.worker && plugin.worker.id && plugin.worker.on)[0]; 177 | 178 | if(!workerTransportPlugin) 179 | { 180 | throw new Error("bFreshlyCachedWorkerProxyMode needs to know the worker ID of the calling worker from incomingRequest.reverseCallsClient.plugins[?].worker.id (and it must be of type number."); 181 | } 182 | 183 | return { 184 | plugin: workerTransportPlugin, 185 | 186 | // Must uniquely identify a worker. 187 | workerID: workerTransportPlugin.worker.id, 188 | 189 | // Has to emit an "exit" event. 190 | worker: workerTransportPlugin.worker 191 | }; 192 | } 193 | 194 | 195 | /** 196 | * @param {JSONRPC.Server} jsonrpcServer 197 | * 198 | * @returns {JSONRPC.RouterBase} 199 | */ 200 | async _makeBidirectionalRouter(jsonrpcServer) 201 | { 202 | return new JSONRPC.BidirectionalWorkerRouter(this._jsonrpcServer); 203 | } 204 | 205 | async getPersistentIDForWorkerID(incomingRequest, nWorkerIDRequester = null) 206 | { 207 | const objWorkerState = this.objWorkerIDToState[nWorkerIDRequester]; 208 | 209 | if(objWorkerState !== undefined) 210 | { 211 | return objWorkerState.persistentID; 212 | } 213 | else 214 | { 215 | return undefined; 216 | } 217 | } 218 | }; 219 | 220 | module.exports = MasterEndpoint; 221 | -------------------------------------------------------------------------------- /src/NodeClusterBase/README.MD: -------------------------------------------------------------------------------- 1 | # Node cluster RPC base 2 | 3 | Useful extendable endpoint classes for easily scaling to all CPUs using node workers. 4 | 5 | Unlike the JSONRPC library, these classes act like an application base (framework), to automatically manage the workers' lifecycle and communication with them. 6 | 7 | The endpoints come with self-hosted JSONRPC.Server configured with `../Plugins/Client/WorkerTransport.js` and `../BidirectionalWorkerRouter.js`. 8 | 9 | It is a fast starting point when clustering, as it also manages the worker lifecycle. However, it might not be very flexible. 10 | 11 | The master process is also a master RPC service. All RPCs should go to or through it (for example, a worker's API call to another worker would have to be API-proxied through the master). 12 | 13 | Potentially (but not limited to) the clients could be used to send API calls to all workers, or specific workers, load balance workload to workers or share the results of an expensive operation with all workers while only running it once. 14 | 15 | To use, simply instantiate a `MasterEndpoint` subclass in the master process and a `WorkerEndpoint` subclass in the worker process, and call their respective `.start()` methods. 16 | 17 | The cluster module uses separate forked child processes, which when crash have no side effects on the master process and release all their allocated memory. This is the best and safest method for scaling to all CPUs, unless SharedArrayBuffer support is needed which is not supported when multiple forked child processes are involved. See NodeWorkerThreadsBase for using the Worker Threads NodeJS module instead, which supports SharedArrayBuffer. 18 | 19 | Usage: 20 | ```Javascript 21 | const WorkersRPC = require("./YourWorkersRPCSubclasses"); 22 | const cluster = require("cluster"); 23 | 24 | (async() => { 25 | if(cluster.isMaster) 26 | { 27 | const masterEndpoint = new WorkersRPC.MasterEndpoint(/*your custom params*/); 28 | await masterEndpoint.start(); 29 | 30 | // Optional: 31 | await masterEndpoint.watchForUpgrade("/path/to/package.json"); 32 | } 33 | else 34 | { 35 | const workerEndpoint = new WorkersRPC.WorkerEndpoint(/*your custom params*/); 36 | await workerEndpoint.start(); 37 | } 38 | })(); 39 | 40 | -------------------------------------------------------------------------------- /src/NodeClusterBase/WorkerClient.js: -------------------------------------------------------------------------------- 1 | const NodeMultiCoreCPUBase = require("../NodeMultiCoreCPUBase"); 2 | 3 | /** 4 | * Extend this class to link to extra worker RPC APIs on the master. 5 | */ 6 | class WorkerClient extends NodeMultiCoreCPUBase.WorkerClient 7 | { 8 | }; 9 | 10 | module.exports = WorkerClient; 11 | -------------------------------------------------------------------------------- /src/NodeClusterBase/WorkerEndpoint.js: -------------------------------------------------------------------------------- 1 | const cluster = require("cluster"); 2 | const NodeMultiCoreCPUBase = require("../NodeMultiCoreCPUBase"); 3 | const sleep = require("sleep-promise"); 4 | 5 | const JSONRPC = { 6 | BidirectionalWorkerRouter: require("../BidirectionalWorkerRouter") 7 | }; 8 | 9 | /** 10 | * Extend this class to export extra worker RPC APIs. 11 | * 12 | * Counter-intuitively, this endpoint instantiates its own JSONRPC.Server and JSONRPC.BidirectionalWorkerRouter, 13 | * inside .start(). 14 | */ 15 | class WorkerEndpoint extends NodeMultiCoreCPUBase.WorkerEndpoint 16 | { 17 | constructor(classReverseCallsClient = null) 18 | { 19 | if(classReverseCallsClient === null) 20 | { 21 | classReverseCallsClient = require("./MasterClient"); 22 | } 23 | 24 | console.log(`Fired up cluster ${cluster.isWorker ? "worker" : "master"} with PID ${process.pid}`); 25 | 26 | if(cluster.isMaster) 27 | { 28 | throw new Error("WorkerEndpoint can only be instantiated in a worker process."); 29 | } 30 | 31 | super(classReverseCallsClient); 32 | } 33 | 34 | 35 | /** 36 | * @returns {number} 37 | */ 38 | async _currentWorkerID() 39 | { 40 | // https://github.com/nodejs/node/issues/1269 41 | if( 42 | !this._bAlreadyDelayedReadingWorkerID 43 | && ( 44 | !cluster.worker 45 | || cluster.worker.id === null 46 | || cluster.worker.id === undefined 47 | ) 48 | ) 49 | { 50 | await sleep(2000); 51 | this._bAlreadyDelayedReadingWorkerID = true; 52 | } 53 | 54 | 55 | if( 56 | !cluster.worker 57 | || cluster.worker.id === null 58 | || cluster.worker.id === undefined 59 | ) 60 | { 61 | console.error("cluster.worker: ", cluster.worker); 62 | console.error("cluster.worker.id: ", cluster.worker ? cluster.worker.id : ""); 63 | console.error(`Returning 0 as cluster.worker.id.`); 64 | return 0; 65 | } 66 | 67 | return cluster.worker.id; 68 | } 69 | 70 | 71 | /** 72 | * @returns {process} 73 | */ 74 | async _currentWorker() 75 | { 76 | return process; 77 | } 78 | 79 | 80 | /** 81 | * @param {JSONRPC.Server} jsonrpcServer 82 | * 83 | * @returns {JSONRPC.RouterBase} 84 | */ 85 | async _makeBidirectionalRouter(jsonrpcServer) 86 | { 87 | return new JSONRPC.BidirectionalWorkerRouter(this._jsonrpcServer); 88 | } 89 | }; 90 | 91 | module.exports = WorkerEndpoint; 92 | -------------------------------------------------------------------------------- /src/NodeClusterBase/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | MasterClient: require("./MasterClient"), 3 | MasterEndpoint: require("./MasterEndpoint"), 4 | WorkerClient: require("./WorkerClient"), 5 | WorkerEndpoint: require("./WorkerEndpoint") 6 | }; 7 | -------------------------------------------------------------------------------- /src/NodeMultiCoreCPUBase/MasterClient.js: -------------------------------------------------------------------------------- 1 | const JSONRPC = { 2 | Client: require("../Client") 3 | }; 4 | 5 | 6 | /** 7 | * Extend this class to link to extra master RPC APIs on workers. 8 | */ 9 | class MasterClient extends JSONRPC.Client 10 | { 11 | /** 12 | * Signals to the master, that this worker's JSONRPC endpoint is ready to receive calls. 13 | * 14 | * @param {number} nWorkerID 15 | */ 16 | async workerServicesReady(nWorkerID) 17 | { 18 | return this.rpc("workerServicesReady", [nWorkerID]); 19 | } 20 | 21 | 22 | /** 23 | * @returns {never} 24 | */ 25 | async gracefulExit() 26 | { 27 | return this.rpc("gracefulExit", []); 28 | } 29 | 30 | 31 | /** 32 | * @param {string} strReturn 33 | * 34 | * @returns {string} 35 | */ 36 | async ping(strReturn) 37 | { 38 | return this.rpc("ping", [strReturn]); 39 | } 40 | 41 | async sendTransferListTest(arrayBufferForTest) 42 | { 43 | return this.rpc("sendTransferListTest", [...arguments], /*bNotification*/ true, [arrayBufferForTest]); 44 | } 45 | 46 | /** 47 | * @param {number} nWorkerID 48 | * @param {string} strFunctionName 49 | * @param {Array} arrParams 50 | * @param {boolean} bNotification = false 51 | * 52 | * @returns {*} 53 | */ 54 | async rpcWorker(nWorkerID, strFunctionName, arrParams, bNotification = false) 55 | { 56 | return this.rpc("rpcWorker", [...arguments]); 57 | } 58 | 59 | async getPersistentIDForWorkerID(nWorkerIDRequester = null) 60 | { 61 | throw new Error("Subclass must implement getPersistentIDForWorkerID()"); 62 | } 63 | }; 64 | 65 | module.exports = MasterClient; 66 | -------------------------------------------------------------------------------- /src/NodeMultiCoreCPUBase/README.MD: -------------------------------------------------------------------------------- 1 | Reusable base for NodeClusterBase and NodeWorkerThreadsBase. See their respective documentation. -------------------------------------------------------------------------------- /src/NodeMultiCoreCPUBase/WorkerClient.js: -------------------------------------------------------------------------------- 1 | const JSONRPC = { 2 | Client: require("../Client") 3 | }; 4 | 5 | 6 | /** 7 | * Extend this class to link to extra worker RPC APIs on the master. 8 | */ 9 | class WorkerClient extends JSONRPC.Client 10 | { 11 | /** 12 | * @returns {never} 13 | */ 14 | async gracefulExit() 15 | { 16 | return this.rpc("gracefulExit", [], /*bNotification*/ true); 17 | } 18 | 19 | 20 | /** 21 | * This works as an internal router to a JSONRPC.Server's endpoints, used as libraries. 22 | * 23 | * Proxies RPC requests directly into potentially an internet facing JSONRPC.Server's registered endpoints. 24 | * 25 | * strEndpointPath is an endpoint path such as "/api-ws/ipc/bsi". 26 | * 27 | * **************** SKIPS ANY AUTHENTICATION OR AUTHORIZATION LAYERS*********************** 28 | * **************** as well as any other JSONRPC plugins *************************** 29 | * 30 | * @param {string} strEndpointPath 31 | * @param {string} strFunctionName 32 | * @param {Array} arrParams 33 | * @param {boolean} bNotification = false 34 | */ 35 | async rpcToInternalEndpointAsLibrary(strEndpointPath, strFunctionName, arrParams, bNotification = false) 36 | { 37 | return this.rpc("rpcToInternalEndpointAsLibrary", [...arguments]); 38 | } 39 | }; 40 | 41 | module.exports = WorkerClient; 42 | -------------------------------------------------------------------------------- /src/NodeMultiCoreCPUBase/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | MasterClient: require("./MasterClient"), 3 | MasterEndpoint: require("./MasterEndpoint"), 4 | WorkerClient: require("./WorkerClient"), 5 | WorkerEndpoint: require("./WorkerEndpoint") 6 | }; 7 | -------------------------------------------------------------------------------- /src/NodeWorkerThreadsBase/MasterClient.js: -------------------------------------------------------------------------------- 1 | const NodeMultiCoreCPUBase = require("../NodeMultiCoreCPUBase"); 2 | 3 | let Threads; 4 | try 5 | { 6 | Threads = require("worker_threads"); 7 | } 8 | catch(error) 9 | { 10 | // console.error(error); 11 | } 12 | 13 | /** 14 | * Extend this class to link to extra master RPC APIs on workers. 15 | */ 16 | class MasterClient extends NodeMultiCoreCPUBase.MasterClient 17 | { 18 | async getPersistentIDForWorkerID(nWorkerIDRequester = null) 19 | { 20 | if(nWorkerIDRequester === null && !Threads.isMainThread) 21 | { 22 | nWorkerIDRequester = Threads.threadId; 23 | } 24 | 25 | return this.rpc("getPersistentIDForWorkerID", [nWorkerIDRequester]); 26 | } 27 | }; 28 | 29 | module.exports = MasterClient; 30 | -------------------------------------------------------------------------------- /src/NodeWorkerThreadsBase/MasterEndpoint.js: -------------------------------------------------------------------------------- 1 | const assert = require("assert") 2 | 3 | const NodeMultiCoreCPUBase = require("../NodeMultiCoreCPUBase"); 4 | 5 | const JSONRPC = { 6 | BidirectionalWorkerThreadRouter: require("../BidirectionalWorkerThreadRouter") 7 | }; 8 | 9 | const sleep = require("sleep-promise"); 10 | 11 | const os = require("os"); 12 | 13 | let Threads; 14 | try 15 | { 16 | Threads = require("worker_threads"); 17 | } 18 | catch(error) 19 | { 20 | // console.error(error); 21 | } 22 | 23 | 24 | /** 25 | * Extend this class to export extra master RPC APIs. 26 | * 27 | * Counter-intuitively, this endpoint instantiates its own JSONRPC.Server and JSONRPC.BidirectionalWorkerRouter, 28 | * inside .start(). 29 | * 30 | * The "workerReady" event is issued when a new worker is ready to receive RPC calls. The event is called with the JSORNPC client as first param. 31 | * 32 | * @event workerReady 33 | */ 34 | class MasterEndpoint extends NodeMultiCoreCPUBase.MasterEndpoint 35 | { 36 | constructor(classReverseCallsClient) 37 | { 38 | console.log(`Fired up ${Threads.isMainThread ? "main" : "worker"} thread with threadId ${Threads.threadId}`); 39 | 40 | if(!Threads.isMainThread) 41 | { 42 | throw new Error("MasterEndpoint can only be instantiated in the main thread."); 43 | } 44 | 45 | super(classReverseCallsClient); 46 | } 47 | 48 | 49 | async _configureBeforeStart() 50 | { 51 | } 52 | 53 | 54 | async _addWorker(nPersistentWorkerID = null) 55 | { 56 | assert(nPersistentWorkerID === null || typeof nPersistentWorkerID === "number", `Invalid property type for nPersistentWorkerID in MasterEndpoint. Expected "number", but got ${typeof nPersistentWorkerId}.`); 57 | 58 | if(nPersistentWorkerID !== null) 59 | { 60 | const nExistingThreadID = this.objPersistentWorkerIDToWorkerID[nPersistentWorkerID]; 61 | if(nExistingThreadID !== undefined) 62 | { 63 | if(this.objWorkerIDToState[nExistingThreadID].exited !== true) 64 | { 65 | console.log(`Worker with threadId ${nExistingThreadID} that hasn't exited yet already has persistentId ${nPersistentWorkerID}.`); 66 | return; 67 | } 68 | } 69 | } 70 | else 71 | { 72 | nPersistentWorkerID = this._nNextAvailablePersistentWorkerID++; 73 | } 74 | 75 | const workerThread = new Threads.Worker(process.mainModule.filename); 76 | await new Promise((fnResolve, fnReject) => { 77 | workerThread.on("online", fnResolve); 78 | setTimeout(fnReject, 10000); 79 | }); 80 | 81 | const nThreadID = workerThread.threadId; 82 | 83 | this.objWorkerIDToState[nThreadID] = { 84 | client: null, 85 | ready: false, 86 | exited: false, 87 | persistentID: nPersistentWorkerID 88 | }; 89 | 90 | this.objPersistentWorkerIDToWorkerID[nPersistentWorkerID] = nThreadID; 91 | 92 | console.log("Adding worker thread ID " + nThreadID + " to BidirectionalWorkerThreadRouter."); 93 | 94 | workerThread.on( 95 | "exit", 96 | async(nExitCode) => { 97 | try 98 | { 99 | if(this.objWorkerIDToState[nThreadID] !== undefined) 100 | { 101 | this.objWorkerIDToState[nThreadID].exited = true; 102 | } 103 | console.log(`Worker thread with threadId ${nThreadID} and persistentId ${nPersistentWorkerID} died. Exit code: ${nExitCode}.`); 104 | 105 | this.arrFailureTimestamps.push(new Date().getTime()); 106 | this.arrFailureTimestamps = this.arrFailureTimestamps.filter((nMillisecondsUnixTime) => { 107 | return nMillisecondsUnixTime >= new Date().getTime() - (60 * 2 * 1000); 108 | }); 109 | 110 | if(this.arrFailureTimestamps.length / Math.max(os.cpus().length, 1) > 4) 111 | { 112 | await this.gracefulExit(null); 113 | } 114 | else 115 | { 116 | if(!this.bShuttingDown) 117 | { 118 | await sleep(500); 119 | this._addWorker(nPersistentWorkerID); 120 | } 121 | } 122 | } 123 | catch(error) 124 | { 125 | console.error("Main worker thread, when a child worker thread exited: unexpected error when handling the exit. Don't know how to handle. Exiting...", error); 126 | process.exit(1); 127 | } 128 | } 129 | ); 130 | 131 | try 132 | { 133 | const nConnectionID = await this._bidirectionalWorkerRouter.addWorker(workerThread, /*strEndpointPath*/ this.path, 120 * 1000 /*Readiness timeout in milliseconds*/); 134 | 135 | this.objWorkerIDToState[nThreadID].client = this._bidirectionalWorkerRouter.connectionIDToSingletonClient(nConnectionID, this.ReverseCallsClientClass); 136 | 137 | this.emit("workerReady", this.objWorkerIDToState[nThreadID].client); 138 | } 139 | catch(error) 140 | { 141 | console.error("Main worker thread, when adding new worker thread: unexpected error. Don't know how to handle. Exiting", error); 142 | process.exit(1); 143 | } 144 | } 145 | 146 | 147 | /** 148 | * @param {JSONRPC.Client} reverseCallsClient 149 | * 150 | * @returns {{plugin:JSONRPC.ClientPluginBase, workerID:number, worker:cluster.Worker}} 151 | */ 152 | async _transportPluginFromReverseClient(reverseCallsClient) 153 | { 154 | const workerThreadTransportPlugin = reverseCallsClient.plugins.filter(plugin => plugin.threadWorker && plugin.threadWorker.threadId && plugin.threadWorker.on)[0]; 155 | 156 | if(!workerThreadTransportPlugin) 157 | { 158 | throw new Error("bFreshlyCachedWorkerProxyMode needs to know the worker ID of the calling worker thread from incomingRequest.reverseCallsClient.plugins[?].threadWorker.threadId (and it must be of type number."); 159 | } 160 | 161 | return { 162 | plugin: workerThreadTransportPlugin, 163 | 164 | // Must uniquely identify a worker thread. 165 | workerID: workerThreadTransportPlugin.threadWorker.threadId, 166 | 167 | // Has to emit an "exit" event. 168 | worker: workerThreadTransportPlugin.threadWorker 169 | }; 170 | } 171 | 172 | 173 | /** 174 | * @returns {JSONRPC.RouterBase} 175 | */ 176 | async _makeBidirectionalRouter() 177 | { 178 | return new JSONRPC.BidirectionalWorkerThreadRouter(this._jsonrpcServer); 179 | } 180 | 181 | async getPersistentIDForWorkerID(incomingRequest, nWorkerIDRequester = null) 182 | { 183 | const objWorkerState = this.objWorkerIDToState[nWorkerIDRequester]; 184 | 185 | if(objWorkerState !== undefined) 186 | { 187 | return objWorkerState.persistentID; 188 | } 189 | else 190 | { 191 | return undefined; 192 | } 193 | } 194 | }; 195 | 196 | module.exports = MasterEndpoint; 197 | -------------------------------------------------------------------------------- /src/NodeWorkerThreadsBase/README.MD: -------------------------------------------------------------------------------- 1 | # Node worker threads RPC base 2 | 3 | Useful extendable endpoint classes for easily scaling to all CPUs using node worker threads. 4 | 5 | Unlike the JSONRPC library, these classes act like an application base (framework), to automatically manage the worker threads' lifecycle and communication with them. 6 | 7 | The endpoints come with self-hosted JSONRPC.Server configured with `../Plugins/Client/WorkerThreadTransport.js` and `../BidirectionalWorkerThreadRouter.js`. 8 | 9 | It is a fast starting point when scaling to all CPUs using worker threads, as it also manages the worker thread lifecycle. However, it might not be very flexible. 10 | 11 | The master process is also a master RPC service. All RPCs should go to or through it (for example, a worker thread's API call to another worker thread would have to be API-proxied through the master). 12 | 13 | Potentially (but not limited to) the clients could be used to send API calls to all worker threads, or specific worker threads, load balance workload to worker threads or share the results of an expensive operation with all worker threads while only running it once. 14 | 15 | To use, simply instantiate a `MasterEndpoint` subclass in the master process and a `WorkerEndpoint` subclass in the worker process, and call their respective `.start()` methods. 16 | 17 | The main advantage of using Worker Threads and not the Cluster module (basically forked child processes) is shared memory between threads. For example, the master process could be used to keep a collection references to SharedArrayBuffer instances and then worker threads could ask (using RPC APIs naturally) for a specific SharedArrayBuffer reference when first needed. 18 | 19 | The greatest disadvantage of using Worker Threads is instability. The cluster module uses separate processes, which when crash have no side effects on the master process and release all their allocated memory. Worker Threads on the other hand have several issues: 20 | * Before crashing with uncaught errors or not handled rejected promises a sleep of a few seconds is required to wait for stdout and stderr to finish flushing into the main thread's stdout and stderr, or else they are lost forever, losing important debugging information. 21 | * The NodeJS documentation states that [terminating worker threads at the wrong time](https://nodejs.org/api/worker_threads.html#worker_threads_worker_terminate_callback) could crash the whole process (which also means the main thread and all other threads): _Warning: Currently, not all code in the internals of Node.js is prepared to expect termination at arbitrary points in time and may crash if it encounters that condition. Consequently, you should currently only call .terminate() if it is known that the Worker thread is not accessing Node.js core modules other than what is exposed in the worker module._ __When using this "framework" (JSONRPC.NodeWorkerThreadsBase) this problem is alleviated__ because the master endpoint never tries to terminate worker threads (worker threads are alive forever and do whatever work multiple RPC API calls ask for). Worker threads may terminate on their own because of an unrecoverable error though (bad error handling: uncaught error or not handled promise rejection). 22 | 23 | From the NodeJS documentation on Worker Threads: 24 | 25 | _Workers are useful for performing CPU-intensive JavaScript operations; do not use them for I/O, since Node.js’s built-in mechanisms for performing operations asynchronously already treat it more efficiently than Worker threads can._ 26 | 27 | _Workers, unlike child processes or when using the cluster module, can also share memory efficiently by transferring ArrayBuffer instances or sharing SharedArrayBuffer instances between them._ 28 | 29 | 30 | Usage: 31 | ```Javascript 32 | const WorkerThreadsRPC = require("./YourWorkersRPCSubclasses"); 33 | const Threads = require("worker_threads"); 34 | 35 | (async() => { 36 | if(Threads.isMainThread) 37 | { 38 | const masterEndpoint = new WorkerThreadsRPC.MasterEndpoint(/*your custom params*/); 39 | await masterEndpoint.start(); 40 | 41 | // Optional: 42 | await masterEndpoint.watchForUpgrade("/path/to/package.json"); 43 | } 44 | else 45 | { 46 | const workerEndpoint = new WorkerThreadsRPC.WorkerEndpoint(/*your custom params*/); 47 | await workerEndpoint.start(); 48 | } 49 | })(); 50 | 51 | -------------------------------------------------------------------------------- /src/NodeWorkerThreadsBase/WorkerClient.js: -------------------------------------------------------------------------------- 1 | const NodeMultiCoreCPUBase = require("../NodeMultiCoreCPUBase"); 2 | 3 | /** 4 | * Extend this class to link to extra worker RPC APIs on the master. 5 | */ 6 | class WorkerClient extends NodeMultiCoreCPUBase.WorkerClient 7 | { 8 | }; 9 | 10 | module.exports = WorkerClient; 11 | -------------------------------------------------------------------------------- /src/NodeWorkerThreadsBase/WorkerEndpoint.js: -------------------------------------------------------------------------------- 1 | const NodeMultiCoreCPUBase = require("../NodeMultiCoreCPUBase"); 2 | 3 | const JSONRPC = { 4 | BidirectionalWorkerThreadRouter: require("../BidirectionalWorkerThreadRouter") 5 | }; 6 | 7 | const sleep = require("sleep-promise"); 8 | 9 | let Threads; 10 | try 11 | { 12 | Threads = require("worker_threads"); 13 | } 14 | catch(error) 15 | { 16 | // console.error(error); 17 | } 18 | 19 | 20 | /** 21 | * Extend this class to export extra worker RPC APIs. 22 | * 23 | * Counter-intuitively, this endpoint instantiates its own JSONRPC.Server and JSONRPC.BidirectionalWorkerRouter, 24 | * inside .start(). 25 | */ 26 | class WorkerEndpoint extends NodeMultiCoreCPUBase.WorkerEndpoint 27 | { 28 | constructor(classReverseCallsClient) 29 | { 30 | console.log(`Fired up ${Threads.isMainThread ? "main" : "worker"} thread with threadId ${Threads.threadId}`); 31 | 32 | if(Threads.isMainThread) 33 | { 34 | throw new Error("WorkerEndpoint can only be instantiated in a worker thread."); 35 | } 36 | 37 | super(classReverseCallsClient); 38 | } 39 | 40 | 41 | /** 42 | * @returns {number} 43 | */ 44 | async _currentWorkerID() 45 | { 46 | // https://github.com/nodejs/node/issues/1269 47 | if( 48 | !this._bAlreadyDelayedReadingWorkerID 49 | && ( 50 | !Threads 51 | || Threads.threadId === null 52 | || Threads.threadId === undefined 53 | ) 54 | ) 55 | { 56 | await sleep(2000); 57 | this._bAlreadyDelayedReadingWorkerID = true; 58 | } 59 | 60 | 61 | if( 62 | !Threads 63 | || Threads.threadId === null 64 | || Threads.threadId === undefined 65 | ) 66 | { 67 | console.error("Threads: ", Threads); 68 | console.error("Threads.threadId: ", Threads ? Threads.threadId : ""); 69 | console.error(`Returning 0 as Threads.threadId.`); 70 | return 0; 71 | } 72 | 73 | return Threads.threadId; 74 | } 75 | 76 | 77 | /** 78 | * @returns {worker_threads} 79 | */ 80 | async _currentWorker() 81 | { 82 | return Threads; 83 | } 84 | 85 | 86 | /** 87 | * @param {JSONRPC.Server} jsonrpcServer 88 | * 89 | * @returns {JSONRPC.RouterBase} 90 | */ 91 | async _makeBidirectionalRouter(jsonrpcServer) 92 | { 93 | return new JSONRPC.BidirectionalWorkerThreadRouter(this._jsonrpcServer); 94 | } 95 | }; 96 | 97 | module.exports = WorkerEndpoint; 98 | -------------------------------------------------------------------------------- /src/NodeWorkerThreadsBase/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | MasterClient: require("./MasterClient"), 3 | MasterEndpoint: require("./MasterEndpoint"), 4 | WorkerClient: require("./WorkerClient"), 5 | WorkerEndpoint: require("./WorkerEndpoint") 6 | }; 7 | -------------------------------------------------------------------------------- /src/OutgoingRequest.js: -------------------------------------------------------------------------------- 1 | const assert = require("assert"); 2 | 3 | const JSONRPC = {}; 4 | JSONRPC.Client = require("./Client"); 5 | JSONRPC.Exception = require("./Exception"); 6 | 7 | module.exports = 8 | class OutgoingRequest 9 | { 10 | /** 11 | * An undefined mxCallID value represents a JSONRPC 2.0 notification request which results in omitting the "id" property in the JSONRPC 2.0 request. 12 | * 13 | * A mxCallID null is not allowed for this JSONRPC 2.0 client library as it cannot be used to match asynchronous requests to out of order responses. 14 | * The spec also recommends in avoiding null when composing requests. 15 | * 16 | * arrTransferList is passed as the second param of postMessage further down the road: 17 | * https://nodejs.org/dist/latest-v10.x/docs/api/worker_threads.html#worker_threads_port_postmessage_value_transferlist 18 | * https://nodejs.org/dist/latest-v10.x/docs/api/worker_threads.html#worker_threads_worker_postmessage_value_transferlist 19 | * https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage 20 | * 21 | * @param {string} strMethod 22 | * @param {Array} arrParams 23 | * @param {number|string|undefined} mxCallID 24 | * @param {ArrayBuffer[]|Transferable[]} arrTransferList = [] 25 | * @param {{bSkipWaitReadyOnConnect: boolean}} objDestructuringParam 26 | */ 27 | constructor(strMethod, arrParams, mxCallID, arrTransferList = [], {bSkipWaitReadyOnConnect = false, nTimeoutSeconds = null} = {}) 28 | { 29 | this._strMethod = strMethod; 30 | this._arrParams = arrParams; 31 | 32 | this._bSkipWaitReadyOnConnect = bSkipWaitReadyOnConnect; 33 | this._nTimeoutSeconds = nTimeoutSeconds; 34 | 35 | this._requestObject = null; 36 | this._mxRequestBody = null; 37 | 38 | this._mxResponseBody = null; 39 | this._responseObject = null; 40 | 41 | this._mxResult = null; 42 | 43 | this._strEndpointURL = null; 44 | 45 | this._objHeaders = {}; 46 | 47 | this._bCalled = false; 48 | 49 | //this._webSocket 50 | //this._httpRequest 51 | 52 | this._mxCallID = mxCallID; 53 | 54 | this._arrTransferList = arrTransferList; 55 | 56 | Object.seal(this); 57 | } 58 | 59 | 60 | /** 61 | * An undefined value represents a JSONRPC 2.0 notification request which results in omitting the "id" property in the JSONRPC 2.0 request. 62 | * 63 | * null is not allowed for this JSONRPC 2.0 client library as it cannot be used to match asynchronous requests to out of order responses. 64 | * The spec also recommends in avoiding null when composing requests. 65 | * 66 | * @returns {number|string|undefined} 67 | */ 68 | get callID() 69 | { 70 | assert( 71 | typeof this._mxCallID === "number" || typeof this._mxCallID === "string" || typeof this._mxCallID === "undefined", 72 | "this._mxCallID must be of type number." 73 | ); 74 | return this._mxCallID; 75 | } 76 | 77 | 78 | /** 79 | * JSON-RPC 2.0 specification: 80 | * An identifier established by the Client that MUST contain a String, Number, or NULL value if included. 81 | * If it is not included it is assumed to be a notification. 82 | * The value SHOULD normally not be Null and Numbers SHOULD NOT contain fractional parts. 83 | * 84 | * @returns {boolean} 85 | */ 86 | get isNotification() 87 | { 88 | return typeof this._mxCallID === "undefined"; 89 | } 90 | 91 | 92 | /** 93 | * @returns {Array|null} 94 | */ 95 | get params() 96 | { 97 | return this._arrParams; 98 | } 99 | 100 | 101 | /** 102 | * @param {Array} arrParams 103 | */ 104 | set params(arrParams) 105 | { 106 | assert(Array.isArray(arrParams), "arrParams must be of type Array."); 107 | this._arrParams = arrParams; 108 | } 109 | 110 | 111 | /** 112 | * @returns {string|null} 113 | */ 114 | get methodName() 115 | { 116 | return this._strMethod; 117 | } 118 | 119 | 120 | /** 121 | * @param {string} strMethod 122 | */ 123 | set methodName(strMethod) 124 | { 125 | assert(typeof strMethod === "string", "strMethod must be of type string."); 126 | 127 | this._strMethod = strMethod; 128 | } 129 | 130 | 131 | /** 132 | * @returns {boolean} 133 | */ 134 | get skipWaitReadyOnConnect() 135 | { 136 | return this._bSkipWaitReadyOnConnect; 137 | } 138 | 139 | 140 | /** 141 | * @returns {number|null} 142 | */ 143 | get timeoutSeconds() 144 | { 145 | return this._nTimeoutSeconds; 146 | } 147 | 148 | 149 | /** 150 | * @returns {object} 151 | */ 152 | get headers() 153 | { 154 | return this._objHeaders; 155 | } 156 | 157 | 158 | /** 159 | * @returns {string|null} 160 | */ 161 | get endpointURL() 162 | { 163 | return this._strEndpointURL; 164 | } 165 | 166 | 167 | /** 168 | * @param {string} strEndpointURL 169 | */ 170 | set endpointURL(strEndpointURL) 171 | { 172 | assert(typeof strEndpointURL === "string", "strEndpointURL must be of type string."); 173 | 174 | this._strEndpointURL = strEndpointURL; 175 | } 176 | 177 | 178 | /** 179 | * @returns {object|Array|null} 180 | */ 181 | get requestObject() 182 | { 183 | return this._requestObject; 184 | } 185 | 186 | 187 | /** 188 | * @param {object|Array} objRequest 189 | */ 190 | set requestObject(objRequest) 191 | { 192 | assert(typeof objRequest === "object" || Array.isArray(objRequest), "objRequest must be of type Object or Array."); 193 | assert(objRequest.hasOwnProperty("method") || objRequest.hasOwnProperty("params"), JSON.stringify(objRequest), "objRequest must have either a method or params property."); 194 | 195 | this._requestObject = objRequest; 196 | } 197 | 198 | 199 | /** 200 | * @returns {string|null} 201 | */ 202 | get requestBody() 203 | { 204 | return this._mxRequestBody; 205 | } 206 | 207 | 208 | /** 209 | * @param {string|object} mxRequestBody 210 | */ 211 | set requestBody(mxRequestBody) 212 | { 213 | this._mxRequestBody = mxRequestBody; 214 | } 215 | 216 | 217 | /** 218 | * @returns {boolean} 219 | */ 220 | get isMethodCalled() 221 | { 222 | return this._bCalled; 223 | } 224 | 225 | 226 | /** 227 | * @param {boolean} bCalled 228 | */ 229 | set isMethodCalled(bCalled) 230 | { 231 | //assert(bCalled); 232 | this._bCalled = bCalled; 233 | } 234 | 235 | 236 | /** 237 | * @returns {string|object|null} 238 | */ 239 | get responseBody() 240 | { 241 | return this._mxResponseBody; 242 | } 243 | 244 | 245 | /** 246 | * @param {string|object} mxResponseBody 247 | */ 248 | set responseBody(mxResponseBody) 249 | { 250 | assert(typeof mxResponseBody === "string" || typeof mxResponseBody === "object", "mxResponseBody must be of type string or Object."); 251 | 252 | this._mxResponseBody = mxResponseBody; 253 | } 254 | 255 | 256 | /** 257 | * @returns {object|Array|null} 258 | */ 259 | get responseObject() 260 | { 261 | return this._responseObject; 262 | } 263 | 264 | 265 | /** 266 | * @param {object|Array} objResponse 267 | */ 268 | set responseObject(objResponse) 269 | { 270 | if( 271 | typeof objResponse !== "object" 272 | && !objResponse.hasOwnProperty("result") 273 | && !objResponse.hasOwnProperty("error") 274 | ) 275 | { 276 | throw new JSONRPC.Exception("Invalid response structure. RAW response: " + JSON.stringify(this._mxResponseBody, undefined, "\t"), JSONRPC.Exception.PARSE_ERROR); 277 | } 278 | 279 | this._responseObject = objResponse; 280 | } 281 | 282 | 283 | /** 284 | * @returns {number|string|null|object|Array|Error} 285 | */ 286 | get callResult() 287 | { 288 | this.isMethodCalled = true; 289 | 290 | return this._mxResult; 291 | } 292 | 293 | 294 | /** 295 | * @param {number|string|null|object|Array|Error} mxResult 296 | */ 297 | set callResult(mxResult) 298 | { 299 | //assert(!this.isMethodCalled, "JSONRPC.OutgoingRequest.isMethodCalled is already true, set by another plugin maybe?"); 300 | 301 | this.isMethodCalled = true; 302 | this._mxResult = mxResult; 303 | } 304 | 305 | /** 306 | * @returns {ArrayBuffer[]|Transferable[]} 307 | */ 308 | get transferList() 309 | { 310 | return this._arrTransferList; 311 | } 312 | 313 | /** 314 | * @returns {object} 315 | */ 316 | toRequestObject() 317 | { 318 | assert(this.methodName !== null, "this.methodName cannot be null."); 319 | assert(Array.isArray(this.params), "this.params must be an Array."); 320 | 321 | if(typeof this.callID !== "undefined") 322 | { 323 | return { 324 | "method": this.methodName, 325 | "params": this.params, 326 | 327 | // The "id" property can never be null in an asynchronous JSONRPC 2.0 client, because out of order responses must be matched to asynchronous requests. 328 | // The spec recommends against null values in general anyway. 329 | 330 | "id": this.callID, 331 | "jsonrpc": "2.0" 332 | }; 333 | } 334 | else 335 | { 336 | // JSONRPC 2.0 notification request, which does not expect an answer at all from the server. 337 | 338 | return { 339 | "method": this.methodName, 340 | "params": this.params, 341 | 342 | // The ID property must be omitted entirely for JSONRPC 2.0 notification requests. 343 | // A setting of undefined will ignore it when serializing to JSON, 344 | // however it is safer for custom non-JSON serializations to omit it explicitly here. 345 | 346 | "jsonrpc": "2.0" 347 | }; 348 | } 349 | } 350 | }; 351 | -------------------------------------------------------------------------------- /src/Plugins/Client/Cache.js: -------------------------------------------------------------------------------- 1 | const JSONRPC = {}; 2 | JSONRPC.ClientPluginBase = require("../../ClientPluginBase"); 3 | 4 | module.exports = 5 | class Cache extends JSONRPC.ClientPluginBase 6 | { 7 | /** 8 | * @param {object} objFunctionNameToCacheSeconds . The keys are function names and the values are the number of seconds until the cached result expires. The values must be numbers greater than 0. 9 | * @param {boolean} bDeepFreeze . If true, deep freeze the returned value using recursive Object.freeze. 10 | * @param {boolean} bReturnDeepCopy . If true, return a deep copy of the cached value. 11 | * @param {number} nMaxEntries . The maximum number of entries in the cache. When this limit is reached, clear the cache. 12 | */ 13 | constructor(objFunctionNameToCacheSeconds, bDeepFreeze = false, bReturnDeepCopy = false, nMaxEntries = 5000) 14 | { 15 | super(); 16 | 17 | if(typeof objFunctionNameToCacheSeconds !== "object" || Array.isArray(objFunctionNameToCacheSeconds)) 18 | { 19 | throw new Error("Invalid objFunctionNameToCacheSeconds parameter given."); 20 | } 21 | 22 | Object.entries(objFunctionNameToCacheSeconds).forEach(([strFunctionName, nCacheDurationSeconds]) => { 23 | if(nCacheDurationSeconds <= 0) 24 | { 25 | throw new Error(`Invalid cache duration ${nCacheDurationSeconds} given for function ${strFunctionName}. It must be a number of seconds greater than 0.`); 26 | } 27 | }); 28 | 29 | this._mapFunctionNameToCacheSeconds = new Map(Object.entries(objFunctionNameToCacheSeconds)); 30 | this._bDeepFreeze = bDeepFreeze; 31 | this._bReturnDeepCopy = bReturnDeepCopy; 32 | this._nMaxEntries = nMaxEntries; 33 | 34 | this.mapCache = new Map(); 35 | } 36 | 37 | 38 | dispose() 39 | { 40 | this.clear(); 41 | } 42 | 43 | 44 | clear() 45 | { 46 | this.mapCache.clear(); 47 | } 48 | 49 | 50 | deleteKeys(arrKeys = []) 51 | { 52 | arrKeys.forEach(strCacheKey => this.mapCache.delete(strCacheKey)); 53 | } 54 | 55 | 56 | /** 57 | * If the function result is already cached and the cache hasn't yet expired, skip the HTTP request and use a JSONRPC response object with null result. 58 | * It will be populated from the cache during the afterJSONDecode step. 59 | * 60 | * @param {JSONRPC.OutgoingRequest} outgoingRequest 61 | */ 62 | async afterJSONEncode(outgoingRequest) 63 | { 64 | if(outgoingRequest.isNotification) 65 | { 66 | return; 67 | } 68 | 69 | if(this._mapFunctionNameToCacheSeconds.has(outgoingRequest.methodName)) 70 | { 71 | const strKey = this.constructor.getCacheKey(outgoingRequest.methodName, outgoingRequest.params); 72 | 73 | if( 74 | (this.mapCache.has(strKey) && this.mapCache.size > this._nMaxEntries) 75 | || (!this.mapCache.has(strKey) && this.mapCache.size >= this._nMaxEntries) 76 | ) 77 | { 78 | this.clear(); 79 | } 80 | 81 | if(this.mapCache.has(strKey)) 82 | { 83 | if(this._isCacheEntryExpired(strKey)) 84 | { 85 | this.mapCache.delete(strKey); 86 | } 87 | else 88 | { 89 | outgoingRequest.responseObject = { 90 | "jsonrpc": this.constructor.DEFAULT_JSONRPC_VERSION, 91 | "result": null, 92 | "id": outgoingRequest.callID 93 | }; 94 | 95 | outgoingRequest.responseBody = JSON.stringify(outgoingRequest.responseObject, undefined, "\t"); 96 | outgoingRequest.isMethodCalled = true; 97 | } 98 | } 99 | } 100 | } 101 | 102 | 103 | /** 104 | * Store the responseObject in the cache if the coresponding entry is unset, or return it from the cache otherwise. 105 | * 106 | * @param {JSONRPC.OutgoingRequest} outgoingRequest 107 | */ 108 | async afterJSONDecode(outgoingRequest) 109 | { 110 | if(outgoingRequest.isNotification) 111 | { 112 | return; 113 | } 114 | 115 | if(outgoingRequest.methodName && this._mapFunctionNameToCacheSeconds.has(outgoingRequest.methodName)) 116 | { 117 | const strKey = this.constructor.getCacheKey(outgoingRequest.methodName, outgoingRequest.params); 118 | 119 | if(this.mapCache.has(strKey)) 120 | { 121 | const cachedValue = this.mapCache.get(strKey).value; 122 | 123 | if(this._bReturnDeepCopy) 124 | { 125 | outgoingRequest.responseObject.result = this.constructor._deepCopy(cachedValue); 126 | 127 | if(this._bDeepFreeze) 128 | { 129 | this.constructor._deepFreeze(outgoingRequest.responseObject.result); 130 | } 131 | } 132 | else 133 | { 134 | outgoingRequest.responseObject.result = cachedValue; 135 | } 136 | } 137 | else 138 | { 139 | const nFunctionCacheDurationSeconds = this._mapFunctionNameToCacheSeconds.get(outgoingRequest.methodName); 140 | const nExpiresAtUnixTimestampMilliseconds = nFunctionCacheDurationSeconds * 1000 + Date.now(); 141 | 142 | if(this._bDeepFreeze && !this._bReturnDeepCopy) 143 | { 144 | this.constructor._deepFreeze(outgoingRequest.responseObject.result); 145 | this.mapCache.set(strKey, { 146 | expiresAt: nExpiresAtUnixTimestampMilliseconds, 147 | value: outgoingRequest.responseObject.result 148 | }); 149 | } 150 | else 151 | { 152 | const objectForCache = this.constructor._deepCopy(outgoingRequest.responseObject.result); 153 | this.mapCache.set(strKey, { 154 | expiresAt: nExpiresAtUnixTimestampMilliseconds, 155 | value: objectForCache 156 | }); 157 | 158 | if(this.bDeepFreeze) 159 | { 160 | this.constructor._deepFreeze(objectForCache); 161 | this.constructor._deepFreeze(outgoingRequest.responseObject.result); 162 | } 163 | } 164 | } 165 | } 166 | } 167 | 168 | /** 169 | * Checks if the cache entry has expired. It doesn't unset the entry if it did though (this is done outside of this function). 170 | * 171 | * @param {string} strKey 172 | * @returns {boolean} 173 | */ 174 | _isCacheEntryExpired(strKey) 175 | { 176 | if(!this.mapCache.has(strKey)) 177 | { 178 | return true; 179 | } 180 | 181 | return Date.now() > this.mapCache.get(strKey).expiresAt; 182 | } 183 | 184 | 185 | /** 186 | * @param {string} strMethodName 187 | * @param {object} objParams 188 | * 189 | * @returns {string} 190 | */ 191 | static getCacheKey(strMethodName, objParams) 192 | { 193 | return `${strMethodName}__${JSON.stringify(objParams)}`; 194 | } 195 | 196 | 197 | static _deepFreeze(object) 198 | { 199 | if(typeof object !== "object") 200 | { 201 | return; 202 | } 203 | 204 | Object.getOwnPropertyNames(object).forEach((propName) => 205 | { 206 | const propValue = object[propName]; 207 | 208 | if(typeof propValue === "object" && propValue != null) 209 | { 210 | Cache._deepFreeze(propValue); 211 | } 212 | }); 213 | 214 | return Object.freeze(object); 215 | } 216 | 217 | 218 | /** 219 | * @param {object} object 220 | * @returns {object} 221 | */ 222 | static _deepCopy(object) 223 | { 224 | return JSON.parse(JSON.stringify(object)); 225 | } 226 | 227 | 228 | /** 229 | * @returns {string} 230 | */ 231 | static get DEFAULT_JSONRPC_VERSION() 232 | { 233 | return "2.0"; 234 | } 235 | }; 236 | -------------------------------------------------------------------------------- /src/Plugins/Client/DebugLogger.js: -------------------------------------------------------------------------------- 1 | const JSONRPC = {}; 2 | JSONRPC.ClientPluginBase = require("../../ClientPluginBase"); 3 | 4 | module.exports = 5 | class DebugLogger extends JSONRPC.ClientPluginBase 6 | { 7 | /** 8 | * @param {number|null} nMaxMessagesCount = null 9 | */ 10 | constructor(nMaxMessagesCount = null) 11 | { 12 | super(); 13 | 14 | this.nMaxMessagesCount = nMaxMessagesCount; 15 | this.nMessagesCount = 0; 16 | } 17 | 18 | 19 | /** 20 | * @param {JSONRPC.OutgoingRequest} outgoingRequest 21 | */ 22 | async afterJSONEncode(outgoingRequest) 23 | { 24 | const strBody = typeof outgoingRequest.requestBody === "string" ? outgoingRequest.requestBody : JSON.stringify(outgoingRequest.requestBody); 25 | 26 | if(++this.nMessagesCount > this.nMaxMessagesCount) 27 | { 28 | return; 29 | } 30 | 31 | if(strBody.length > 1024 * 1024) 32 | { 33 | console.log("[" + process.pid + "] [" + (new Date()).toISOString() + "] Sent JSONRPC request, " + outgoingRequest.requestObject.method + "(). Larger than 1 MB, not logging. \n"); 34 | } 35 | else 36 | { 37 | console.log("[" + process.pid + "] [" + (new Date()).toISOString() + "] Sent JSONRPC request: " + strBody + "\n"); 38 | } 39 | } 40 | 41 | /** 42 | * @param {JSONRPC.OutgoingRequest} outgoingRequest 43 | */ 44 | async beforeJSONDecode(outgoingRequest) 45 | { 46 | const strBody = typeof outgoingRequest.responseBody === "string" ? outgoingRequest.responseBody : JSON.stringify(outgoingRequest.responseBody); 47 | 48 | if(++this.nMessagesCount > this.nMaxMessagesCount) 49 | { 50 | return; 51 | } 52 | 53 | if(strBody.length > 1024 * 1024) 54 | { 55 | console.log("[" + process.pid + "] [" + (new Date()).toISOString() + "] Received JSONRPC response, " + outgoingRequest.requestObject.method + "(). Larger than 1 MB, not logging. \n"); 56 | } 57 | else 58 | { 59 | console.log("[" + process.pid + "] [" + (new Date()).toISOString() + "] Received JSONRPC response: " + strBody + "\n"); 60 | } 61 | } 62 | }; 63 | -------------------------------------------------------------------------------- /src/Plugins/Client/ElectronIPCTransport.js: -------------------------------------------------------------------------------- 1 | let electron = null; 2 | 3 | if(process && process.versions["electron"]) 4 | { 5 | /* eslint-disable*/ 6 | electron = require("electron"); 7 | 8 | // Browser environment 9 | if(!electron && (window || self) && typeof (window || self).require === "function") 10 | { 11 | electron = (window || self).require("electron"); 12 | } 13 | /* eslint-enable*/ 14 | } 15 | 16 | const JSONRPC = {}; 17 | JSONRPC.ClientPluginBase = require("../../ClientPluginBase"); 18 | JSONRPC.Utils = require("../../Utils"); 19 | 20 | const assert = require("assert"); 21 | 22 | module.exports = 23 | /** 24 | * this.rejectAllPromises(error) has to be called manually in a browser environment when a BrowserWindow is terminated or has finished working. 25 | */ 26 | class ElectronIPCTransport extends JSONRPC.ClientPluginBase 27 | { 28 | /** 29 | * browserWindow is ignored inside a BrowserWindow instance. Should only be set inside the master process. 30 | * 31 | * @param {boolean|undefined} bBidirectionalMode 32 | * @param {BrowserWindow|null} browserWindow = null 33 | */ 34 | constructor(bBidirectionalMode, browserWindow) 35 | { 36 | super(); 37 | 38 | 39 | this._arrDisposeCalls = []; 40 | 41 | 42 | // JSONRPC call ID as key, {promise: {Promise}, fnResolve: {Function}, fnReject: {Function}, outgoingRequest: {OutgoingRequest}} as values. 43 | this._objBrowserWindowRequestsPromises = {}; 44 | 45 | 46 | this._bBidirectionalMode = !!bBidirectionalMode; 47 | this._browserWindow = browserWindow; 48 | 49 | // eslint-disable-next-line no-undef 50 | this._strChannel = "jsonrpc_winid_" + (browserWindow ? browserWindow.id : (window || self).require("electron").remote.getCurrentWindow().id); 51 | 52 | 53 | this._setupIPCTransport(); 54 | } 55 | 56 | 57 | /** 58 | * @returns {null} 59 | */ 60 | dispose() 61 | { 62 | for(const fnDispose of this._arrDisposeCalls) 63 | { 64 | fnDispose(); 65 | } 66 | this._arrDisposeCalls.slice(0); 67 | 68 | this.rejectAllPromises(); 69 | 70 | super.dispose(); 71 | } 72 | 73 | 74 | /** 75 | * @returns {BrowserWindow|null} 76 | */ 77 | get browserWindow() 78 | { 79 | return this._browserWindow; 80 | } 81 | 82 | 83 | get channel() 84 | { 85 | return this._strChannel; 86 | } 87 | 88 | 89 | /** 90 | * @param {JSONRPC.OutgoingRequest} outgoingRequest 91 | */ 92 | async beforeJSONEncode(outgoingRequest) 93 | { 94 | // No serialization. 95 | outgoingRequest.requestBody = outgoingRequest.requestObject; 96 | } 97 | 98 | 99 | /** 100 | * objResponse is the object obtained after JSON parsing for strResponse. 101 | * 102 | * @param {object|undefined} objResponse 103 | */ 104 | async processResponse(objResponse) 105 | { 106 | if( 107 | ( 108 | typeof objResponse.id !== "number" 109 | && typeof objResponse.id !== "string" 110 | ) 111 | || !this._objBrowserWindowRequestsPromises[objResponse.id] 112 | ) 113 | { 114 | console.error(new Error("Couldn't find JSONRPC response call ID in this._objWorkerRequestsPromises. RAW response: " + JSON.stringify(objResponse))); 115 | console.error(new Error("RAW remote message: " + JSON.stringify(objResponse))); 116 | console.error("[" + process.pid + "] Unclean state. Unable to match message to an existing Promise or qualify it as a request."); 117 | 118 | return; 119 | } 120 | 121 | this._objBrowserWindowRequestsPromises[objResponse.id].outgoingRequest.responseBody = objResponse; 122 | this._objBrowserWindowRequestsPromises[objResponse.id].outgoingRequest.responseObject = objResponse; 123 | 124 | this._objBrowserWindowRequestsPromises[objResponse.id].fnResolve(null); 125 | // Sorrounding code will parse the result and throw if necessary. fnReject is not going to be used in this function. 126 | 127 | delete this._objBrowserWindowRequestsPromises[objResponse.id]; 128 | } 129 | 130 | 131 | /** 132 | * Populates the the OutgoingRequest class instance (outgoingRequest) with the RAW JSON response and the JSON parsed response object. 133 | * 134 | * @param {JSONRPC.OutgoingRequest} outgoingRequest 135 | * 136 | * @returns {Promise.} 137 | */ 138 | async makeRequest(outgoingRequest) 139 | { 140 | if(outgoingRequest.isMethodCalled) 141 | { 142 | return; 143 | } 144 | 145 | outgoingRequest.isMethodCalled = true; 146 | 147 | if(outgoingRequest.isNotification) 148 | { 149 | // JSONRPC 2.0 notification requests don't have the id property at all, not even null. JSONRPC 2.0 servers do not send a response at all for these types of requests. 150 | } 151 | else 152 | { 153 | /** 154 | * http://www.jsonrpc.org/specification#notification 155 | * 156 | * id 157 | * An identifier established by the Client that MUST contain a String, Number, or NULL value if included. If it is not included it is assumed to be a notification. The value SHOULD normally not be Null [1] and Numbers SHOULD NOT contain fractional parts [2] 158 | * The Server MUST reply with the same value in the Response object if included. This member is used to correlate the context between the two objects. 159 | * 160 | * [1] The use of Null as a value for the id member in a Request object is discouraged, because this specification uses a value of Null for Responses with an unknown id. Also, because JSON-RPC 1.0 uses an id value of Null for Notifications this could cause confusion in handling. 161 | * 162 | * [2] Fractional parts may be problematic, since many decimal fractions cannot be represented exactly as binary fractions. 163 | * 164 | * ===================================== 165 | * 166 | * Asynchronous JSONRPC 2.0 clients must set the "id" property to be able to match responses to requests, as they arrive out of order. 167 | * The "id" property cannot be null, but it can be omitted in the case of notification requests, which expect no response at all from the server. 168 | */ 169 | assert( 170 | typeof outgoingRequest.requestObject.id === "number" || typeof outgoingRequest.requestObject.id === "string", 171 | "outgoingRequest.requestObject.id must be of type number or string." 172 | ); 173 | 174 | this._objBrowserWindowRequestsPromises[outgoingRequest.requestObject.id] = { 175 | // unixtimeMilliseconds: (new Date()).getTime(), 176 | outgoingRequest: outgoingRequest, 177 | promise: null 178 | }; 179 | 180 | this._objBrowserWindowRequestsPromises[outgoingRequest.requestObject.id].promise = new Promise((fnResolve, fnReject) => { 181 | this._objBrowserWindowRequestsPromises[outgoingRequest.requestObject.id].fnResolve = fnResolve; 182 | this._objBrowserWindowRequestsPromises[outgoingRequest.requestObject.id].fnReject = fnReject; 183 | }); 184 | } 185 | 186 | 187 | if(this.browserWindow) 188 | { 189 | this.browserWindow.webContents.send(this.channel, outgoingRequest.requestObject); 190 | } 191 | else 192 | { 193 | // eslint-disable-next-line no-undef 194 | (window || self).require("electron").ipcRenderer.send(this.channel, outgoingRequest.requestObject); 195 | } 196 | 197 | 198 | if(outgoingRequest.isNotification) 199 | { 200 | // JSONRPC 2.0 notification requests don't have the id property at all, not even null. JSONRPC 2.0 servers do not send a response at all for these types of requests. 201 | } 202 | else 203 | { 204 | return this._objBrowserWindowRequestsPromises[outgoingRequest.requestObject.id].promise; 205 | } 206 | } 207 | 208 | 209 | /** 210 | * @param {JSONRPC.OutgoingRequest} outgoingRequest 211 | */ 212 | async beforeJSONDecode(outgoingRequest) 213 | { 214 | } 215 | 216 | 217 | /** 218 | * @param {Error} error 219 | */ 220 | rejectAllPromises(error) 221 | { 222 | //console.error(error); 223 | console.log("[" + process.pid + "] Rejecting all Promise instances in ElectronIPCTransport."); 224 | 225 | let nCount = 0; 226 | 227 | for(let nCallID in this._objBrowserWindowRequestsPromises) 228 | { 229 | this._objBrowserWindowRequestsPromises[nCallID].fnReject(error); 230 | delete this._objBrowserWindowRequestsPromises[nCallID]; 231 | 232 | nCount++; 233 | } 234 | 235 | if(nCount) 236 | { 237 | console.error("[" + process.pid + "] Rejected " + nCount + " Promise instances in ElectronIPCTransport."); 238 | } 239 | } 240 | 241 | 242 | /** 243 | * @protected 244 | */ 245 | _setupIPCTransport() 246 | { 247 | if(!this.browserWindow) 248 | { 249 | if(!this._bBidirectionalMode) 250 | { 251 | const fnOnChannel = async(event, objJSONRPCRequest) => { 252 | await this.processResponse(objJSONRPCRequest); 253 | }; 254 | 255 | // eslint-disable-next-line no-undef 256 | (window || self).require("electron").ipcRenderer.on(this._strChannel, fnOnChannel); 257 | 258 | // eslint-disable-next-line no-undef 259 | this._arrDisposeCalls.push(() => { (window || self).require("electron").ipcRenderer.removeListener(this._strChannel, fnOnChannel); }); 260 | } 261 | } 262 | else 263 | { 264 | const fnOnClosed = () => { 265 | this.rejectAllPromises(new Error(`BrowserWindow ${this.browserWindow.id} closed`)); 266 | }; 267 | this._browserWindow.on("closed", fnOnClosed); 268 | this._arrDisposeCalls.push(() => { this._browserWindow.removeListener("closed", fnOnClosed); }); 269 | 270 | if(!this._bBidirectionalMode) 271 | { 272 | const fnOnChannel = async(event, objJSONRPCRequest) => { 273 | await this.processResponse(objJSONRPCRequest); 274 | }; 275 | 276 | electron.ipcMain.on(this.channel, fnOnChannel); 277 | this._arrDisposeCalls.push(() => { electron.ipcMain.removeListener(this.channel, fnOnChannel); }); 278 | } 279 | } 280 | } 281 | }; 282 | -------------------------------------------------------------------------------- /src/Plugins/Client/PrettyBrowserConsoleErrors.js: -------------------------------------------------------------------------------- 1 | const JSONRPC = {}; 2 | JSONRPC.ClientPluginBase = require("../../ClientPluginBase"); 3 | JSONRPC.Exception = require("../../Exception"); 4 | 5 | module.exports = 6 | class PrettyBrowserConsoleErrors extends JSONRPC.ClientPluginBase 7 | { 8 | /** 9 | * @param {JSONRPC.OutgoingRequest} outgoingRequest 10 | */ 11 | async exceptionCatch(outgoingRequest) 12 | { 13 | console.error(outgoingRequest.callResult); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/Plugins/Client/ProcessStdIOTransport.js: -------------------------------------------------------------------------------- 1 | const JSONRPC = {}; 2 | JSONRPC.ClientPluginBase = require("../../ClientPluginBase"); 3 | JSONRPC.Utils = require("../../Utils"); 4 | 5 | const ChildProcess = require("child_process"); 6 | 7 | module.exports = 8 | class ProcessStdIOTransport extends JSONRPC.ClientPluginBase 9 | { 10 | /** 11 | * @param {string} strExePath 12 | * @param {string} strWorkingDirectoryPath 13 | * @param {string[]} arrArguments 14 | */ 15 | constructor(strExePath, strWorkingDirectoryPath, arrArguments = []) 16 | { 17 | super(); 18 | 19 | this._strExePath = strExePath; 20 | this._strWorkingDirectoryPath = strWorkingDirectoryPath; 21 | this._arrArguments = arrArguments; 22 | } 23 | 24 | 25 | /** 26 | * Populates the OutgoingRequest class instance (outgoingRequest) with the RAW JSON response and the JSON parsed response object. 27 | * 28 | * @param {JSONRPC.OutgoingRequest} outgoingRequest 29 | * 30 | * @returns {Promise.} 31 | */ 32 | async makeRequest(outgoingRequest) 33 | { 34 | if(outgoingRequest.isMethodCalled) 35 | { 36 | return; 37 | } 38 | 39 | outgoingRequest.isMethodCalled = true; 40 | 41 | 42 | const objExecOptions = { 43 | cwd: this._strWorkingDirectoryPath, 44 | maxBuffer: 10 * 1024 * 1024 45 | }; 46 | 47 | const child = ChildProcess.spawn(this._strExePath, this._arrArguments, objExecOptions); 48 | 49 | return new Promise((fnResolve, fnReject) => { 50 | child.on( 51 | "close", 52 | (code) => { 53 | child.stdin.end(); 54 | 55 | fnResolve(null); 56 | } 57 | ); 58 | 59 | outgoingRequest.responseBody = ""; 60 | child.stdout.on( 61 | "data", 62 | (data) => { 63 | outgoingRequest.responseBody += data; 64 | } 65 | ); 66 | 67 | child.on( 68 | "error", 69 | (error) => { 70 | fnReject(error); 71 | } 72 | ); 73 | 74 | child.stdin.setEncoding("utf-8"); 75 | child.stdin.write(outgoingRequest.requestBody); 76 | }); 77 | } 78 | }; 79 | 80 | -------------------------------------------------------------------------------- /src/Plugins/Client/SignatureAdd.js: -------------------------------------------------------------------------------- 1 | const JSONRPC = {}; 2 | JSONRPC.ClientPluginBase = require("../../ClientPluginBase"); 3 | 4 | const JSSHA = require("jssha"); 5 | //const HMAC_SHA256 = require("crypto-js/hmac-sha256"); 6 | 7 | /** 8 | * This has purpose at Bigstep (the company which originally created this project). 9 | * It is intended to be used only together with Bigstep extending API clients. 10 | * Please ignore otherwise. 11 | */ 12 | module.exports = 13 | class SignatureAdd extends JSONRPC.ClientPluginBase 14 | { 15 | /** 16 | * @param {string} strAPIKey 17 | * @param {Array} arrExtraURLVariables 18 | */ 19 | constructor(strAPIKey, arrExtraURLVariables) 20 | { 21 | super(); 22 | 23 | this.strAPIKey = strAPIKey; 24 | this._arrExtraURLVariables = arrExtraURLVariables; 25 | this.strKeyMetaData = SignatureAdd.getKeyMetaData(strAPIKey); 26 | } 27 | 28 | 29 | /** 30 | * @param {string} strKey 31 | * @returns {string} 32 | */ 33 | static getKeyMetaData(strKey) 34 | { 35 | let strMeta = null; 36 | const arrAPIKey = strKey.split(":", 2); 37 | 38 | if(arrAPIKey.length !== 1) 39 | { 40 | strMeta = arrAPIKey[0]; 41 | } 42 | 43 | return strMeta; 44 | } 45 | 46 | 47 | /** 48 | * @param {JSONRPC.OutgoingRequest} outgoingRequest 49 | */ 50 | async beforeJSONEncode(outgoingRequest) 51 | { 52 | outgoingRequest.requestObject["expires"] = parseInt((new Date().getTime()) / 1000 + 86400, 10); 53 | } 54 | 55 | 56 | /** 57 | * @param {JSONRPC.OutgoingRequest} outgoingRequest 58 | */ 59 | async afterJSONEncode(outgoingRequest) 60 | { 61 | const sha = new JSSHA("SHA-256", "TEXT"); 62 | sha.setHMACKey(this.strAPIKey, "TEXT"); 63 | sha.update(outgoingRequest.requestBody); 64 | let strVerifyHash = sha.getHMAC("HEX"); 65 | //let strVerifyHash = HMAC_SHA256(outgoingRequest.requestBody, this.strAPIKey); 66 | 67 | if(this.strKeyMetaData !== null) 68 | { 69 | strVerifyHash = this.strKeyMetaData + ":" + strVerifyHash; 70 | } 71 | 72 | if(outgoingRequest.endpointURL.indexOf("?") > -1) 73 | { 74 | outgoingRequest.endpointURL += "&"; 75 | } 76 | else 77 | { 78 | outgoingRequest.endpointURL += "?"; 79 | } 80 | 81 | if(outgoingRequest.endpointURL.indexOf("verify") === -1) 82 | { 83 | outgoingRequest.endpointURL += "verify=" + (strVerifyHash); 84 | } 85 | 86 | if(outgoingRequest.endpointURL.charAt(outgoingRequest.endpointURL.length - 1) === "&") 87 | { 88 | outgoingRequest.endpointURL = outgoingRequest.endpointURL.slice(0, -1); 89 | } 90 | 91 | for(let strName in this._arrExtraURLVariables) 92 | { 93 | outgoingRequest.endpointURL += "&" + strName + "=" + this._arrExtraURLVariables[strName]; 94 | } 95 | } 96 | }; 97 | -------------------------------------------------------------------------------- /src/Plugins/Client/WebRTCTransport.js: -------------------------------------------------------------------------------- 1 | const JSONRPC = {}; 2 | JSONRPC.ClientPluginBase = require("../../ClientPluginBase"); 3 | JSONRPC.Utils = require("../../Utils"); 4 | 5 | const assert = require("assert"); 6 | 7 | 8 | module.exports = 9 | class WebRTCTransport extends JSONRPC.ClientPluginBase 10 | { 11 | /** 12 | * @param {RTCDataChannel} dataChannel 13 | * @param {boolean|undefined} bBidirectionalWebRTCMode 14 | */ 15 | constructor(dataChannel, bBidirectionalWebRTCMode) 16 | { 17 | super(); 18 | 19 | 20 | // JSONRPC call ID as key, {promise: {Promise}, fnResolve: {Function}, fnReject: {Function}, outgoingRequest: {OutgoingRequest}} as values. 21 | this._objRTCDataChannelRequestsPromises = {}; 22 | 23 | 24 | this._bBidirectionalWebRTCMode = !!bBidirectionalWebRTCMode; 25 | this._dataChannel = dataChannel; 26 | 27 | 28 | this._setupRTCDataChannel(); 29 | } 30 | 31 | 32 | /** 33 | * @returns {null} 34 | */ 35 | dispose() 36 | { 37 | try 38 | { 39 | this._dataChannel.close(); 40 | } 41 | catch(error) 42 | { 43 | console.error(error); 44 | } 45 | 46 | super.dispose(); 47 | } 48 | 49 | 50 | /** 51 | * @returns {RTCDataChannel} 52 | */ 53 | get dataChannel() 54 | { 55 | return this._dataChannel; 56 | } 57 | 58 | 59 | /** 60 | * strResponse is a string with the response JSON. 61 | * objResponse is the object obtained after JSON parsing for strResponse. 62 | * 63 | * @param {string} strResponse 64 | * @param {object|undefined} objResponse 65 | */ 66 | async processResponse(strResponse, objResponse) 67 | { 68 | if(!objResponse) 69 | { 70 | try 71 | { 72 | objResponse = JSONRPC.Utils.jsonDecodeSafe(strResponse); 73 | } 74 | catch(error) 75 | { 76 | console.error(error); 77 | console.error("Unable to parse JSON. RAW remote response: " + strResponse); 78 | 79 | if(this._dataChannel.readyState === "open") 80 | { 81 | this._dataChannel.close(); 82 | } 83 | 84 | return; 85 | } 86 | } 87 | 88 | if( 89 | ( 90 | typeof objResponse.id !== "number" 91 | && typeof objResponse.id !== "string" 92 | ) 93 | || !this._objRTCDataChannelRequestsPromises[objResponse.id] 94 | ) 95 | { 96 | console.error(new Error("Couldn't find JSONRPC response call ID in this._objRTCDataChannelRequestsPromises. RAW response: " + strResponse)); 97 | console.error(new Error("RAW remote message: " + strResponse)); 98 | console.log("Unclean state. Unable to match WebRTC message to an existing Promise or qualify it as a request."); 99 | 100 | if(this._dataChannel.readyState === "open") 101 | { 102 | this.dataChannel.close( 103 | /*CLOSE_NORMAL*/ 1000, // Chrome only supports 1000 or the 3000-3999 range ///* CloseEvent.Internal Error */ 1011, 104 | "Unclean state. Unable to match WebRTC message to an existing Promise or qualify it as a request." 105 | ); 106 | } 107 | 108 | return; 109 | } 110 | 111 | this._objRTCDataChannelRequestsPromises[objResponse.id].outgoingRequest.responseBody = strResponse; 112 | this._objRTCDataChannelRequestsPromises[objResponse.id].outgoingRequest.responseObject = objResponse; 113 | 114 | this._objRTCDataChannelRequestsPromises[objResponse.id].fnResolve(null); 115 | // Sorrounding code will parse the result and throw if necessary. fnReject is not going to be used in this function. 116 | 117 | delete this._objRTCDataChannelRequestsPromises[objResponse.id]; 118 | } 119 | 120 | 121 | /** 122 | * Populates the the OutgoingRequest class instance (outgoingRequest) with the RAW JSON response and the JSON parsed response object. 123 | * 124 | * @param {JSONRPC.OutgoingRequest} outgoingRequest 125 | * 126 | * @returns {Promise.} 127 | */ 128 | async makeRequest(outgoingRequest) 129 | { 130 | if(outgoingRequest.isMethodCalled) 131 | { 132 | return; 133 | } 134 | 135 | if(this.dataChannel.readyState !== "open") 136 | { 137 | throw new Error("RTCDataChannel not connected."); 138 | } 139 | 140 | outgoingRequest.isMethodCalled = true; 141 | 142 | 143 | if(outgoingRequest.isNotification) 144 | { 145 | // JSONRPC 2.0 notification requests don't have the id property at all, not even null. JSONRPC 2.0 servers do not send a response at all for these types of requests. 146 | } 147 | else 148 | { 149 | /** 150 | * http://www.jsonrpc.org/specification#notification 151 | * 152 | * id 153 | * An identifier established by the Client that MUST contain a String, Number, or NULL value if included. If it is not included it is assumed to be a notification. The value SHOULD normally not be Null [1] and Numbers SHOULD NOT contain fractional parts [2] 154 | * The Server MUST reply with the same value in the Response object if included. This member is used to correlate the context between the two objects. 155 | * 156 | * [1] The use of Null as a value for the id member in a Request object is discouraged, because this specification uses a value of Null for Responses with an unknown id. Also, because JSON-RPC 1.0 uses an id value of Null for Notifications this could cause confusion in handling. 157 | * 158 | * [2] Fractional parts may be problematic, since many decimal fractions cannot be represented exactly as binary fractions. 159 | * 160 | * ===================================== 161 | * 162 | * Asynchronous JSONRPC 2.0 clients must set the "id" property to be able to match responses to requests, as they arrive out of order. 163 | * The "id" property cannot be null, but it can be omitted in the case of notification requests, which expect no response at all from the server. 164 | */ 165 | assert( 166 | typeof outgoingRequest.requestObject.id === "number" || typeof outgoingRequest.requestObject.id === "string", 167 | "outgoingRequest.requestObject.id must be of type number or string." 168 | ); 169 | 170 | this._objRTCDataChannelRequestsPromises[outgoingRequest.requestObject.id] = { 171 | // unixtimeMilliseconds: (new Date()).getTime(), 172 | outgoingRequest: outgoingRequest, 173 | promise: null 174 | }; 175 | 176 | this._objRTCDataChannelRequestsPromises[outgoingRequest.requestObject.id].promise = new Promise((fnResolve, fnReject) => { 177 | this._objRTCDataChannelRequestsPromises[outgoingRequest.requestObject.id].fnResolve = fnResolve; 178 | this._objRTCDataChannelRequestsPromises[outgoingRequest.requestObject.id].fnReject = fnReject; 179 | }); 180 | } 181 | 182 | 183 | this.dataChannel.send(outgoingRequest.requestBody); 184 | 185 | 186 | if(outgoingRequest.isNotification) 187 | { 188 | // JSONRPC 2.0 notification requests don't have the id property at all, not even null. JSONRPC 2.0 servers do not send a response at all for these types of requests. 189 | } 190 | else 191 | { 192 | return this._objRTCDataChannelRequestsPromises[outgoingRequest.requestObject.id].promise; 193 | } 194 | } 195 | 196 | 197 | /** 198 | * @param {Error} error 199 | */ 200 | rejectAllPromises(error) 201 | { 202 | //console.error(error); 203 | console.log("Rejecting all Promise instances in WebRTCTransport."); 204 | 205 | let nCount = 0; 206 | 207 | for(let nCallID in this._objRTCDataChannelRequestsPromises) 208 | { 209 | this._objRTCDataChannelRequestsPromises[nCallID].fnReject(error); 210 | delete this._objRTCDataChannelRequestsPromises[nCallID]; 211 | 212 | nCount++; 213 | } 214 | 215 | if(nCount) 216 | { 217 | console.error("Rejected " + nCount + " Promise instances in WebRTCTransport."); 218 | } 219 | } 220 | 221 | 222 | /** 223 | * @protected 224 | */ 225 | _setupRTCDataChannel() 226 | { 227 | const fnOnError = (error) => { 228 | this.rejectAllPromises(error); 229 | }; 230 | const fnOnMessage = async(messageEvent) => { 231 | await this.processResponse(messageEvent.data); 232 | }; 233 | const fnOnClose = (closeEvent) => { 234 | this.rejectAllPromises(new Error("RTCDataChannel closed.")); 235 | 236 | this._dataChannel.removeEventListener("close", fnOnClose); 237 | this._dataChannel.removeEventListener("error", fnOnError); 238 | 239 | if(!this._bBidirectionalWebRTCMode) 240 | { 241 | this._dataChannel.removeEventListener("message", fnOnMessage); 242 | } 243 | }; 244 | 245 | 246 | this._dataChannel.addEventListener("close", fnOnClose); 247 | this._dataChannel.addEventListener("error", fnOnError); 248 | 249 | if(!this._bBidirectionalWebRTCMode) 250 | { 251 | this._dataChannel.addEventListener("message", fnOnMessage); 252 | } 253 | } 254 | }; 255 | -------------------------------------------------------------------------------- /src/Plugins/Client/WorkerThreadTransport.js: -------------------------------------------------------------------------------- 1 | const JSONRPC = {}; 2 | JSONRPC.ClientPluginBase = require("../../ClientPluginBase"); 3 | JSONRPC.Utils = require("../../Utils"); 4 | 5 | const assert = require("assert"); 6 | 7 | let Threads; 8 | try 9 | { 10 | Threads = require("worker_threads"); 11 | } 12 | catch(error) 13 | { 14 | // console.error(error); 15 | } 16 | 17 | class WorkerThreadTransport extends JSONRPC.ClientPluginBase 18 | { 19 | /** 20 | * @param {worker_threads.Worker|Threads} threadWorker 21 | * @param {boolean|undefined} bBidirectionalMode 22 | */ 23 | constructor(threadWorker, bBidirectionalMode) 24 | { 25 | super(); 26 | 27 | 28 | this._arrDisposeCalls = []; 29 | 30 | 31 | assert(threadWorker instanceof Threads.Worker || threadWorker === Threads); 32 | 33 | 34 | // JSONRPC call ID as key, {promise: {Promise}, fnResolve: {Function}, fnReject: {Function}, outgoingRequest: {OutgoingRequest}} as values. 35 | this._objWorkerRequestsPromises = {}; 36 | 37 | 38 | this._bBidirectionalMode = !!bBidirectionalMode; 39 | this._threadWorker = threadWorker; 40 | this._threadID = threadWorker.threadId; 41 | 42 | 43 | this._setupThreadWorker(); 44 | } 45 | 46 | 47 | /** 48 | * @returns {null} 49 | */ 50 | dispose() 51 | { 52 | for(const fnDispose of this._arrDisposeCalls) 53 | { 54 | fnDispose(); 55 | } 56 | this._arrDisposeCalls.slice(0); 57 | 58 | this.rejectAllPromises(); 59 | 60 | super.dispose(); 61 | } 62 | 63 | 64 | /** 65 | * @returns {worker_threads.Worker|worker_threads} 66 | */ 67 | get threadWorker() 68 | { 69 | return this._threadWorker; 70 | } 71 | 72 | 73 | /** 74 | * @param {JSONRPC.OutgoingRequest} outgoingRequest 75 | */ 76 | async beforeJSONEncode(outgoingRequest) 77 | { 78 | // No serialization. 79 | outgoingRequest.requestBody = outgoingRequest.requestObject; 80 | } 81 | 82 | 83 | /** 84 | * objResponse is the object obtained after JSON parsing for strResponse. 85 | * 86 | * @param {object|undefined} objResponse 87 | */ 88 | async processResponse(objResponse) 89 | { 90 | if( 91 | ( 92 | typeof objResponse.id !== "number" 93 | && typeof objResponse.id !== "string" 94 | ) 95 | || !this._objWorkerRequestsPromises[objResponse.id] 96 | ) 97 | { 98 | console.error(new Error(`Couldn't find JSONRPC response call ID in this._objWorkerRequestsPromises from thread ID ${this._threadID}. RAW response: ${JSON.stringify(objResponse)}`)); 99 | console.error(new Error(`RAW remote message from thread ID ${this._threadID}: ` + JSON.stringify(objResponse))); 100 | console.log(`Unclean state in WorkerThreadTransport. Unable to match message from thread Worker thread ID ${this._threadID} to an existing Promise or qualify it as a request.`); 101 | 102 | if(Threads.isMainThread) 103 | { 104 | this.threadWorker.terminate(); 105 | } 106 | else 107 | { 108 | process.exit(1); 109 | } 110 | 111 | return; 112 | } 113 | 114 | this._objWorkerRequestsPromises[objResponse.id].outgoingRequest.responseBody = objResponse; 115 | this._objWorkerRequestsPromises[objResponse.id].outgoingRequest.responseObject = objResponse; 116 | 117 | this._objWorkerRequestsPromises[objResponse.id].fnResolve(null); 118 | // Sorrounding code will parse the result and throw if necessary. fnReject is not going to be used in this function. 119 | 120 | delete this._objWorkerRequestsPromises[objResponse.id]; 121 | } 122 | 123 | 124 | /** 125 | * Populates the the OutgoingRequest class instance (outgoingRequest) with the RAW JSON response and the JSON parsed response object. 126 | * 127 | * @param {JSONRPC.OutgoingRequest} outgoingRequest 128 | * 129 | * @returns {Promise.} 130 | */ 131 | async makeRequest(outgoingRequest) 132 | { 133 | if(outgoingRequest.isMethodCalled) 134 | { 135 | return; 136 | } 137 | 138 | outgoingRequest.isMethodCalled = true; 139 | 140 | if(outgoingRequest.isNotification) 141 | { 142 | // JSONRPC 2.0 notification requests don't have the id property at all, not even null. JSONRPC 2.0 servers do not send a response at all for these types of requests. 143 | } 144 | else 145 | { 146 | /** 147 | * http://www.jsonrpc.org/specification#notification 148 | * 149 | * id 150 | * An identifier established by the Client that MUST contain a String, Number, or NULL value if included. If it is not included it is assumed to be a notification. The value SHOULD normally not be Null [1] and Numbers SHOULD NOT contain fractional parts [2] 151 | * The Server MUST reply with the same value in the Response object if included. This member is used to correlate the context between the two objects. 152 | * 153 | * [1] The use of Null as a value for the id member in a Request object is discouraged, because this specification uses a value of Null for Responses with an unknown id. Also, because JSON-RPC 1.0 uses an id value of Null for Notifications this could cause confusion in handling. 154 | * 155 | * [2] Fractional parts may be problematic, since many decimal fractions cannot be represented exactly as binary fractions. 156 | * 157 | * ===================================== 158 | * 159 | * Asynchronous JSONRPC 2.0 clients must set the "id" property to be able to match responses to requests, as they arrive out of order. 160 | * The "id" property cannot be null, but it can be omitted in the case of notification requests, which expect no response at all from the server. 161 | */ 162 | assert( 163 | typeof outgoingRequest.requestObject.id === "number" || typeof outgoingRequest.requestObject.id === "string", 164 | "outgoingRequest.requestObject.id must be of type number or string." 165 | ); 166 | 167 | this._objWorkerRequestsPromises[outgoingRequest.requestObject.id] = { 168 | // unixtimeMilliseconds: (new Date()).getTime(), 169 | outgoingRequest: outgoingRequest, 170 | promise: null 171 | }; 172 | 173 | this._objWorkerRequestsPromises[outgoingRequest.requestObject.id].promise = new Promise((fnResolve, fnReject) => { 174 | this._objWorkerRequestsPromises[outgoingRequest.requestObject.id].fnResolve = fnResolve; 175 | this._objWorkerRequestsPromises[outgoingRequest.requestObject.id].fnReject = fnReject; 176 | }); 177 | } 178 | 179 | if(Threads.isMainThread) 180 | { 181 | this.threadWorker.postMessage(outgoingRequest.requestObject, outgoingRequest.transferList); 182 | } 183 | else 184 | { 185 | Threads.parentPort.postMessage(outgoingRequest.requestObject, outgoingRequest.transferList); 186 | } 187 | 188 | 189 | if(outgoingRequest.isNotification) 190 | { 191 | // JSONRPC 2.0 notification requests don't have the id property at all, not even null. JSONRPC 2.0 servers do not send a response at all for these types of requests. 192 | } 193 | else 194 | { 195 | return this._objWorkerRequestsPromises[outgoingRequest.requestObject.id].promise; 196 | } 197 | } 198 | 199 | 200 | /** 201 | * @param {JSONRPC.OutgoingRequest} outgoingRequest 202 | */ 203 | async beforeJSONDecode(outgoingRequest) 204 | { 205 | } 206 | 207 | 208 | /** 209 | * @param {Error} error 210 | */ 211 | rejectAllPromises(error) 212 | { 213 | //console.error(error); 214 | console.log(`Rejecting all Promise instances for WorkerThreadTransport thread Id ${this._threadID}.`); 215 | 216 | let nCount = 0; 217 | 218 | for(let nCallID in this._objWorkerRequestsPromises) 219 | { 220 | this._objWorkerRequestsPromises[nCallID].fnReject(error); 221 | delete this._objWorkerRequestsPromises[nCallID]; 222 | 223 | nCount++; 224 | } 225 | 226 | if(nCount) 227 | { 228 | console.error(`Rejected ${nCount} Promise instances for WorkerThreadTransport thread Id ${this._threadID}`); 229 | } 230 | } 231 | 232 | 233 | /** 234 | * @protected 235 | */ 236 | _setupThreadWorker() 237 | { 238 | const fnOnError = (error) => { 239 | this.rejectAllPromises(error); 240 | }; 241 | const fnOnClose = () => { 242 | this.rejectAllPromises(new Error("Thread MessagePort closed.")); 243 | }; 244 | const fnOnMessage = async(objMessage) => { 245 | await this.processResponse(objMessage); 246 | }; 247 | const fnOnExit = (nCode) => { 248 | this.rejectAllPromises(new Error(`Thread Worker with thread ID ${this._threadID} closed. Code: ${JSON.stringify(nCode)}`)); 249 | 250 | if(Threads.isMainThread) 251 | { 252 | this._threadWorker.removeListener("exit", fnOnExit); 253 | this._threadWorker.removeListener("error", fnOnError); 254 | 255 | if(!this._bBidirectionalMode) 256 | { 257 | this._threadWorker.removeListener("message", fnOnMessage); 258 | } 259 | } 260 | else 261 | { 262 | Threads.parentPort.removeListener("close", fnOnClose); 263 | 264 | if(!this._bBidirectionalMode) 265 | { 266 | Threads.parentPort.removeListener("message", fnOnMessage); 267 | } 268 | } 269 | }; 270 | 271 | if(Threads.isMainThread) 272 | { 273 | this._threadWorker.on("exit", fnOnExit); 274 | this._arrDisposeCalls.push(() => { this._threadWorker.removeListener("exit", fnOnExit); }); 275 | 276 | this._threadWorker.on("error", fnOnError); 277 | this._arrDisposeCalls.push(() => { this._threadWorker.removeListener("error", fnOnError); }); 278 | 279 | 280 | if(!this._bBidirectionalMode) 281 | { 282 | this._threadWorker.on("message", fnOnMessage); 283 | this._arrDisposeCalls.push(() => { this._threadWorker.removeListener("message", fnOnMessage); }); 284 | } 285 | } 286 | else 287 | { 288 | Threads.parentPort.on("close", fnOnClose); 289 | this._arrDisposeCalls.push(() => { Threads.parentPort.removeListener("close", fnOnClose); }); 290 | 291 | if(!this._bBidirectionalMode) 292 | { 293 | Threads.parentPort.on("message", fnOnMessage); 294 | this._arrDisposeCalls.push(() => { Threads.parentPort.removeListener("message", fnOnMessage); }); 295 | } 296 | } 297 | } 298 | }; 299 | 300 | module.exports = WorkerThreadTransport; 301 | -------------------------------------------------------------------------------- /src/Plugins/Client/WorkerTransport.js: -------------------------------------------------------------------------------- 1 | const JSONRPC = {}; 2 | JSONRPC.ClientPluginBase = require("../../ClientPluginBase"); 3 | JSONRPC.Utils = require("../../Utils"); 4 | 5 | const assert = require("assert"); 6 | 7 | module.exports = 8 | /** 9 | * this.rejectAllPromises(error) has to be called manually in a browser environment when a Worker is terminated or has finished working. 10 | */ 11 | class WorkerTransport extends JSONRPC.ClientPluginBase 12 | { 13 | /** 14 | * @param {Worker} worker 15 | * @param {boolean|undefined} bBidirectionalWorkerMode 16 | */ 17 | constructor(worker, bBidirectionalWorkerMode) 18 | { 19 | super(); 20 | 21 | 22 | this._arrDisposeCalls = []; 23 | 24 | 25 | // JSONRPC call ID as key, {promise: {Promise}, fnResolve: {Function}, fnReject: {Function}, outgoingRequest: {OutgoingRequest}} as values. 26 | this._objWorkerRequestsPromises = {}; 27 | 28 | 29 | this._bBidirectionalWorkerMode = !!bBidirectionalWorkerMode; 30 | this._worker = worker; 31 | 32 | 33 | this._setupWorker(); 34 | } 35 | 36 | 37 | /** 38 | * @returns {null} 39 | */ 40 | dispose() 41 | { 42 | for(const fnDispose of this._arrDisposeCalls) 43 | { 44 | fnDispose(); 45 | } 46 | this._arrDisposeCalls.slice(0); 47 | 48 | this.rejectAllPromises(); 49 | 50 | super.dispose(); 51 | } 52 | 53 | 54 | /** 55 | * @returns {Worker} 56 | */ 57 | get worker() 58 | { 59 | return this._worker; 60 | } 61 | 62 | 63 | /** 64 | * @param {JSONRPC.OutgoingRequest} outgoingRequest 65 | */ 66 | async beforeJSONEncode(outgoingRequest) 67 | { 68 | // No serialization. 69 | outgoingRequest.requestBody = outgoingRequest.requestObject; 70 | } 71 | 72 | 73 | /** 74 | * objResponse is the object obtained after JSON parsing for strResponse. 75 | * 76 | * @param {object|undefined} objResponse 77 | */ 78 | async processResponse(objResponse) 79 | { 80 | if( 81 | ( 82 | typeof objResponse.id !== "number" 83 | && typeof objResponse.id !== "string" 84 | ) 85 | || !this._objWorkerRequestsPromises[objResponse.id] 86 | ) 87 | { 88 | console.error(new Error("Couldn't find JSONRPC response call ID in this._objWorkerRequestsPromises. RAW response: " + JSON.stringify(objResponse))); 89 | console.error(new Error("RAW remote message: " + JSON.stringify(objResponse))); 90 | console.log("Unclean state. Unable to match Worker message to an existing Promise or qualify it as a request."); 91 | 92 | if(this.worker.terminate) 93 | { 94 | this.worker.terminate(); 95 | } 96 | else if(this.worker !== process) 97 | { 98 | this.worker.kill(); 99 | } 100 | 101 | return; 102 | } 103 | 104 | this._objWorkerRequestsPromises[objResponse.id].outgoingRequest.responseBody = objResponse; 105 | this._objWorkerRequestsPromises[objResponse.id].outgoingRequest.responseObject = objResponse; 106 | 107 | this._objWorkerRequestsPromises[objResponse.id].fnResolve(null); 108 | // Sorrounding code will parse the result and throw if necessary. fnReject is not going to be used in this function. 109 | 110 | delete this._objWorkerRequestsPromises[objResponse.id]; 111 | } 112 | 113 | 114 | /** 115 | * Populates the the OutgoingRequest class instance (outgoingRequest) with the RAW JSON response and the JSON parsed response object. 116 | * 117 | * @param {JSONRPC.OutgoingRequest} outgoingRequest 118 | * 119 | * @returns {Promise.} 120 | */ 121 | async makeRequest(outgoingRequest) 122 | { 123 | if(outgoingRequest.isMethodCalled) 124 | { 125 | return; 126 | } 127 | 128 | if(this.worker.isDead && this.worker.isDead()) 129 | { 130 | throw new Error("Worker not connected."); 131 | } 132 | 133 | outgoingRequest.isMethodCalled = true; 134 | 135 | if(outgoingRequest.isNotification) 136 | { 137 | // JSONRPC 2.0 notification requests don't have the id property at all, not even null. JSONRPC 2.0 servers do not send a response at all for these types of requests. 138 | } 139 | else 140 | { 141 | /** 142 | * http://www.jsonrpc.org/specification#notification 143 | * 144 | * id 145 | * An identifier established by the Client that MUST contain a String, Number, or NULL value if included. If it is not included it is assumed to be a notification. The value SHOULD normally not be Null [1] and Numbers SHOULD NOT contain fractional parts [2] 146 | * The Server MUST reply with the same value in the Response object if included. This member is used to correlate the context between the two objects. 147 | * 148 | * [1] The use of Null as a value for the id member in a Request object is discouraged, because this specification uses a value of Null for Responses with an unknown id. Also, because JSON-RPC 1.0 uses an id value of Null for Notifications this could cause confusion in handling. 149 | * 150 | * [2] Fractional parts may be problematic, since many decimal fractions cannot be represented exactly as binary fractions. 151 | * 152 | * ===================================== 153 | * 154 | * Asynchronous JSONRPC 2.0 clients must set the "id" property to be able to match responses to requests, as they arrive out of order. 155 | * The "id" property cannot be null, but it can be omitted in the case of notification requests, which expect no response at all from the server. 156 | */ 157 | assert( 158 | typeof outgoingRequest.requestObject.id === "number" || typeof outgoingRequest.requestObject.id === "string", 159 | "outgoingRequest.requestObject.id must be of type number or string." 160 | ); 161 | 162 | this._objWorkerRequestsPromises[outgoingRequest.requestObject.id] = { 163 | // unixtimeMilliseconds: (new Date()).getTime(), 164 | outgoingRequest: outgoingRequest, 165 | promise: null 166 | }; 167 | 168 | this._objWorkerRequestsPromises[outgoingRequest.requestObject.id].promise = new Promise((fnResolve, fnReject) => { 169 | this._objWorkerRequestsPromises[outgoingRequest.requestObject.id].fnResolve = fnResolve; 170 | this._objWorkerRequestsPromises[outgoingRequest.requestObject.id].fnReject = fnReject; 171 | }); 172 | } 173 | 174 | 175 | if(this.worker.postMessage) 176 | { 177 | // Internet Explorer 10 does not support transferList. Not sending the param at all if empty. 178 | if(outgoingRequest.transferList.length) 179 | { 180 | this.worker.postMessage(outgoingRequest.requestObject, outgoingRequest.transferList); 181 | } 182 | else 183 | { 184 | this.worker.postMessage(outgoingRequest.requestObject); 185 | } 186 | } 187 | else 188 | { 189 | this.worker.send(outgoingRequest.requestObject); 190 | } 191 | 192 | 193 | if(outgoingRequest.isNotification) 194 | { 195 | // JSONRPC 2.0 notification requests don't have the id property at all, not even null. JSONRPC 2.0 servers do not send a response at all for these types of requests. 196 | } 197 | else 198 | { 199 | return this._objWorkerRequestsPromises[outgoingRequest.requestObject.id].promise; 200 | } 201 | } 202 | 203 | 204 | /** 205 | * @param {JSONRPC.OutgoingRequest} outgoingRequest 206 | */ 207 | async beforeJSONDecode(outgoingRequest) 208 | { 209 | } 210 | 211 | 212 | /** 213 | * @param {Error} error 214 | */ 215 | rejectAllPromises(error) 216 | { 217 | //console.error(error); 218 | console.log("Rejecting all Promise instances in WorkerTransport."); 219 | 220 | let nCount = 0; 221 | 222 | for(let nCallID in this._objWorkerRequestsPromises) 223 | { 224 | this._objWorkerRequestsPromises[nCallID].fnReject(error); 225 | delete this._objWorkerRequestsPromises[nCallID]; 226 | 227 | nCount++; 228 | } 229 | 230 | if(nCount) 231 | { 232 | console.error("Rejected " + nCount + " Promise instances in WorkerTransport."); 233 | } 234 | } 235 | 236 | 237 | /** 238 | * @protected 239 | */ 240 | _setupWorker() 241 | { 242 | if(this._worker.addEventListener) 243 | { 244 | // There's no close/exit event in browser environments. 245 | // Call this manually when appropriate: this.rejectAllPromises(new Error("Worker closed")); 246 | 247 | // TODO: create API to be called to remove event listeners. 248 | 249 | const fnOnError = (error) => { 250 | this.rejectAllPromises(error); 251 | }; 252 | const fnOnMessage = async(messageEvent) => { 253 | await this.processResponse(messageEvent.data); 254 | }; 255 | 256 | this._worker.addEventListener("error", fnOnError); 257 | this._arrDisposeCalls.push(() => { this._worker.removeEventListener("error", fnOnError); }); 258 | 259 | if(!this._bBidirectionalWorkerMode) 260 | { 261 | this._worker.addEventListener("message", fnOnMessage); 262 | this._arrDisposeCalls.push(() => { this._worker.removeEventListener("message", fnOnMessage); }); 263 | } 264 | } 265 | else 266 | { 267 | const fnOnError = (error) => { 268 | this.rejectAllPromises(error); 269 | }; 270 | const fnOnMessage = async(objMessage) => { 271 | await this.processResponse(objMessage); 272 | }; 273 | const fnOnExit = (nCode, nSignal) => { 274 | this.rejectAllPromises(new Error("Worker closed. Code: " + JSON.stringify(nCode) + ". Signal: " + JSON.stringify(nSignal))); 275 | 276 | this._worker.removeListener("exit", fnOnExit); 277 | this._worker.removeListener("error", fnOnError); 278 | 279 | if(!this._bBidirectionalWorkerMode) 280 | { 281 | this._worker.removeListener("message", fnOnMessage); 282 | } 283 | }; 284 | 285 | this._worker.on("exit", fnOnExit); 286 | this._arrDisposeCalls.push(() => { this._worker.removeListener("exit", fnOnExit); }); 287 | 288 | this._worker.on("error", fnOnError); 289 | this._arrDisposeCalls.push(() => { this._worker.removeListener("error", fnOnError); }); 290 | 291 | if(!this._bBidirectionalWorkerMode) 292 | { 293 | this._worker.on("message", fnOnMessage); 294 | this._arrDisposeCalls.push(() => { this._worker.removeListener("message", fnOnMessage); }); 295 | } 296 | } 297 | } 298 | }; 299 | -------------------------------------------------------------------------------- /src/Plugins/Client/index.js: -------------------------------------------------------------------------------- 1 | // Do not use const here, webpack/babel issues. 2 | var objExports = {}; 3 | 4 | objExports.Cache = require("./Cache"); 5 | objExports.DebugLogger = require("./DebugLogger"); 6 | objExports.PrettyBrowserConsoleErrors = require("./PrettyBrowserConsoleErrors"); 7 | objExports.SignatureAdd = require("./SignatureAdd"); 8 | objExports.WebSocketTransport = require("./WebSocketTransport"); 9 | objExports.WorkerTransport = require("./WorkerTransport"); 10 | objExports.ProcessStdIOTransport = require("./ProcessStdIOTransport"); 11 | objExports.WebRTCTransport = require("./WebRTCTransport"); 12 | objExports.ElectronIPCTransport = require("./ElectronIPCTransport"); 13 | 14 | if(process && parseInt(process.version.replace("v", "").split(".", 2)[0]) >= 10) 15 | { 16 | objExports.WorkerThreadTransport = require("./WorkerThreadTransport"); 17 | } 18 | 19 | module.exports = objExports; 20 | -------------------------------------------------------------------------------- /src/Plugins/Server/AuthenticationSkip.js: -------------------------------------------------------------------------------- 1 | const JSONRPC = {}; 2 | JSONRPC.ServerPluginBase = require("../../ServerPluginBase"); 3 | 4 | module.exports = 5 | class AuthenticationSkip extends JSONRPC.ServerPluginBase 6 | { 7 | /** 8 | * @param {JSONRPC.IncomingRequest} incomingRequest 9 | */ 10 | async beforeJSONDecode(incomingRequest) 11 | { 12 | incomingRequest.isAuthenticated = true; 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/Plugins/Server/AuthorizeAll.js: -------------------------------------------------------------------------------- 1 | const JSONRPC = {}; 2 | JSONRPC.ServerPluginBase = require("../../ServerPluginBase"); 3 | 4 | module.exports = 5 | class AuthenticationSkip extends JSONRPC.ServerPluginBase 6 | { 7 | /** 8 | * @param {JSONRPC.IncomingRequest} incomingRequest 9 | */ 10 | async beforeJSONDecode(incomingRequest) 11 | { 12 | incomingRequest.isAuthorized = true; 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/Plugins/Server/DebugLogger.js: -------------------------------------------------------------------------------- 1 | const JSONRPC = {}; 2 | JSONRPC.ServerPluginBase = require("../../ServerPluginBase"); 3 | 4 | module.exports = 5 | class DebugLogger extends JSONRPC.ServerPluginBase 6 | { 7 | /** 8 | * @param {number|null} nMaxMessagesCount = null 9 | */ 10 | constructor(nMaxMessagesCount = null) 11 | { 12 | super(); 13 | 14 | this.nMaxMessagesCount = nMaxMessagesCount; 15 | this.nMessagesCount = 0; 16 | } 17 | 18 | 19 | /** 20 | * Logs the received RAW request to stdout. 21 | * 22 | * @param {JSONRPC.IncomingRequest} incomingRequest 23 | */ 24 | async beforeJSONDecode(incomingRequest) 25 | { 26 | if(++this.nMessagesCount > this.nMaxMessagesCount) 27 | { 28 | return; 29 | } 30 | 31 | if(incomingRequest.requestBody.length > 1024 * 1024) 32 | { 33 | console.log("[" + process.pid + "] [" + (new Date()).toISOString() + "] Received JSONRPC request at endpoint path " + incomingRequest.endpoint.path + ", " + incomingRequest.requestObject.method + "(). Larger than 1 MB, not logging. \n"); 34 | } 35 | else 36 | { 37 | console.log("[" + process.pid + "] [" + (new Date()).toISOString() + "] Received JSONRPC request at endpoint path " + incomingRequest.endpoint.path + ": " + (typeof incomingRequest.requestBody === "string" ? incomingRequest.requestBody : JSON.stringify(incomingRequest.requestBody)) + "\n"); 38 | } 39 | } 40 | 41 | /** 42 | * Logs the RAW response to stdout. 43 | * 44 | * @param {JSONRPC.IncomingRequest} incomingRequest 45 | */ 46 | async afterSerialize(incomingRequest) 47 | { 48 | if(++this.nMessagesCount > this.nMaxMessagesCount) 49 | { 50 | return; 51 | } 52 | 53 | // @TODO: specify selected endpoint? 54 | 55 | if(incomingRequest.requestBody.length > 1024 * 1024) 56 | { 57 | console.log("[" + process.pid + "] [" + (new Date()).toISOString() + "] Sending JSONRPC response, " + incomingRequest.requestObject.method + "(). Larger than 1 MB, not logging. \n"); 58 | } 59 | else 60 | { 61 | console.log("[" + process.pid + "] [" + (new Date()).toISOString() + "] Sending JSONRPC response: " + (typeof incomingRequest.callResultSerialized === "string" ? incomingRequest.callResultSerialized : JSON.stringify(incomingRequest.callResultSerialized)) + "\n"); 62 | } 63 | } 64 | }; 65 | -------------------------------------------------------------------------------- /src/Plugins/Server/PerformanceCounters.js: -------------------------------------------------------------------------------- 1 | const JSONRPC = {}; 2 | JSONRPC.ServerPluginBase = require("../../ServerPluginBase"); 3 | 4 | module.exports = 5 | class PerformanceCounters extends JSONRPC.ServerPluginBase 6 | { 7 | /** 8 | * The bExportMethodOnEndpoint, bFakeAuthenticatedExportedMethod and bFakeAuthorizedExportedMethod may be used for convenience. 9 | * Be careful, as sometimes what functions were called and their performance metrics may be of interest to an attacker. 10 | * 11 | * If bExportMethodOnEndpoint is true, then a method named "rpc.performanceCounters" with no params 12 | * is exported on any endpoint of the Server which added this plugin. 13 | * 14 | * bFakeAuthenticatedExportedMethod will set the IncomingRequest.isAuthenticated property from this plugin directly. 15 | * bFakeAuthorizedExportedMethod will set the IncomingRequest.isAuthenticated property from this plugin directly. 16 | * 17 | * There's an UI implementation here: https://github.com/oxygen/api-performance-counters-ui 18 | * 19 | * @param {boolean} bExportMethodOnEndpoint = false 20 | * @param {boolean} bFakeAuthenticatedExportedMethod = false 21 | * @param {boolean} bFakeAuthorizedExportedMethod = false 22 | */ 23 | constructor(bExportMethodOnEndpoint = false, bFakeAuthenticatedExportedMethod = false, bFakeAuthorizedExportedMethod = false) 24 | { 25 | super(); 26 | 27 | this._mapFunctioNameToMetrics = new Map(); 28 | this._nCurrentlyRunningFunctions = 0; 29 | 30 | this._bExportMethodOnEndpoint = bExportMethodOnEndpoint; 31 | this._bFakeAuthenticatedExportedMethod = bFakeAuthenticatedExportedMethod; 32 | this._bFakeAuthorizedExportedMethod = bFakeAuthorizedExportedMethod; 33 | } 34 | 35 | 36 | /** 37 | * @param {JSONRPC.IncomingRequest} incomingRequest 38 | */ 39 | async afterJSONDecode(incomingRequest) 40 | { 41 | if(this._bExportMethodOnEndpoint) 42 | { 43 | if(["rpc.performanceCounters", "rpc.performanceCountersClear"].includes(incomingRequest.requestObject.method)) 44 | { 45 | if(this._bFakeAuthenticatedExportedMethod) 46 | { 47 | incomingRequest.isAuthenticated = true; 48 | } 49 | 50 | if(this._bFakeAuthorizedExportedMethod) 51 | { 52 | incomingRequest.isAuthorized = true; 53 | } 54 | } 55 | } 56 | 57 | 58 | incomingRequest.startDurationTimer(); 59 | 60 | if(incomingRequest.requestObject.method && !incomingRequest.isNotification) 61 | { 62 | this._nCurrentlyRunningFunctions++; 63 | } 64 | } 65 | 66 | 67 | /** 68 | * @override 69 | * 70 | * This is called after a function has been called successfully. 71 | * 72 | * @param {JSONRPC.IncomingRequest} incomingRequest 73 | */ 74 | async result(incomingRequest) 75 | { 76 | if(!incomingRequest.isNotification) 77 | { 78 | this._nCurrentlyRunningFunctions--; 79 | 80 | if(this._nCurrentlyRunningFunctions < 0) 81 | { 82 | this._nCurrentlyRunningFunctions = 0; 83 | } 84 | } 85 | 86 | const objMetrics = this._functionMappings(incomingRequest.requestObject.method); 87 | 88 | objMetrics.successCount += 1; 89 | objMetrics.successMillisecondsTotal += incomingRequest.durationMilliseconds; 90 | objMetrics.successMillisecondsAverage = parseInt(objMetrics.successMillisecondsTotal / objMetrics.successCount); 91 | } 92 | 93 | 94 | /** 95 | * @override 96 | * 97 | * This is called if a function was not called successfully. 98 | * 99 | * @param {JSONRPC.IncomingRequest} incomingRequest 100 | */ 101 | async exceptionCatch(incomingRequest) 102 | { 103 | if(incomingRequest.requestObject && incomingRequest.requestObject.method) 104 | { 105 | if(!incomingRequest.isNotification) 106 | { 107 | this._nCurrentlyRunningFunctions--; 108 | 109 | if(this._nCurrentlyRunningFunctions < 0) 110 | { 111 | this._nCurrentlyRunningFunctions = 0; 112 | } 113 | } 114 | 115 | const objMetrics = this._functionMappings(incomingRequest.requestObject.method); 116 | 117 | objMetrics.errorCount += 1; 118 | objMetrics.errorMillisecondsTotal += incomingRequest.durationMilliseconds; 119 | objMetrics.errorMillisecondsAverage = parseInt(objMetrics.errorMillisecondsTotal / objMetrics.errorCount); 120 | } 121 | } 122 | 123 | 124 | /** 125 | * If a plugin chooses to actually make the call here, 126 | * it must set the result in the incomingRequest.callResult property. 127 | * 128 | * @param {JSONRPC.IncomingRequest} incomingRequest 129 | */ 130 | async callFunction(incomingRequest) 131 | { 132 | // Useful here: 133 | // incomingRequest.requestObject.method 134 | // incomingRequest.requestObject.params 135 | 136 | // incomingRequest.callResult may be populated here with an Error class instance, or the function return. 137 | 138 | if( 139 | incomingRequest.requestObject 140 | && incomingRequest.requestObject.method 141 | 142 | && this._bExportMethodOnEndpoint 143 | ) 144 | { 145 | if(incomingRequest.requestObject.method === "rpc.performanceCounters") 146 | { 147 | incomingRequest.callResult = { 148 | metrics: this.metricsAsObject, 149 | runningCallsCount: this.runningCallsCount 150 | }; 151 | } 152 | else if(incomingRequest.requestObject.method === "rpc.performanceCountersClear") 153 | { 154 | incomingRequest.callResult = true; 155 | this._mapFunctioNameToMetrics.clear(); 156 | } 157 | } 158 | } 159 | 160 | 161 | /** 162 | * @returns {Map} 163 | */ 164 | get metrics() 165 | { 166 | return this._mapFunctioNameToMetrics; 167 | } 168 | 169 | 170 | /** 171 | * @returns {Object} 172 | */ 173 | get metricsAsObject() 174 | { 175 | const objMetrics = {}; 176 | 177 | for(const strFunctionName of this._mapFunctioNameToMetrics.keys()) 178 | { 179 | objMetrics[strFunctionName] = this._mapFunctioNameToMetrics.get(strFunctionName); 180 | } 181 | 182 | return objMetrics; 183 | } 184 | 185 | 186 | /** 187 | * @returns {number} 188 | */ 189 | get runningCallsCount() 190 | { 191 | return this._nCurrentlyRunningFunctions; 192 | } 193 | 194 | 195 | /** 196 | * @protected 197 | * 198 | * @param {string} strFunctionName 199 | * 200 | * @returns {undefined} 201 | */ 202 | _functionMappings(strFunctionName) 203 | { 204 | let objMetrics = this._mapFunctioNameToMetrics.get(strFunctionName); 205 | 206 | if(!objMetrics) 207 | { 208 | objMetrics = { 209 | successCount: 0, 210 | errorCount: 0, 211 | 212 | successMillisecondsTotal: 0, 213 | errorMillisecondsTotal: 0, 214 | 215 | successMillisecondsAverage: 0, 216 | errorMillisecondsAverage: 0 217 | }; 218 | 219 | this._mapFunctioNameToMetrics.set(strFunctionName, objMetrics); 220 | } 221 | 222 | return objMetrics; 223 | } 224 | }; 225 | -------------------------------------------------------------------------------- /src/Plugins/Server/index.js: -------------------------------------------------------------------------------- 1 | // Do not use const here, webpack/babel issues. 2 | var objExports = {}; 3 | 4 | objExports.DebugLogger = require("./DebugLogger"); 5 | objExports.AuthenticationSkip = require("./AuthenticationSkip"); 6 | objExports.AuthorizeAll = require("./AuthorizeAll"); 7 | objExports.PerformanceCounters = require("./PerformanceCounters"); 8 | objExports.URLPublic = require("./URLPublic"); 9 | 10 | module.exports = objExports; 11 | -------------------------------------------------------------------------------- /src/RouterBase.js: -------------------------------------------------------------------------------- 1 | const assert = require("assert"); 2 | 3 | const JSONRPC = { 4 | Server: require("./Server") 5 | }; 6 | 7 | const EventEmitter = require("events"); 8 | 9 | 10 | /** 11 | * @event disposed {bCallJSONRPCServerDispose, bCallEndpointDispose, bCallPluginDispose} 12 | */ 13 | class RouterBase extends EventEmitter 14 | { 15 | /** 16 | * Clients are automatically instantiated per connection and are available as a property of the first param of the exported functions, 17 | * if the JSONRPC.EndpointBase constructor param classReverseCallsClient was set to a JSONRPC.Client subclass. 18 | * 19 | * If jsonrpcServer is non-null and classReverseCallsClient is set on at least one endpoint, then bi-directional JSONRPC over the same websocket is enabled. 20 | * 21 | * @param {JSONRPC.Server|null} jsonrpcServer 22 | * @param {object} objTransportOptions = {} 23 | */ 24 | constructor(jsonrpcServer, objTransportOptions = {}) 25 | { 26 | super(); 27 | 28 | assert(jsonrpcServer === null || jsonrpcServer instanceof JSONRPC.Server, "jsonrpcServer must be either null or an instance or subclass of JSONRPC.Server."); 29 | 30 | this._jsonrpcServer = jsonrpcServer; 31 | 32 | this._nConnectionIDCounter = 0; 33 | 34 | this._objSessions = {}; 35 | this._objTransportOptions = objTransportOptions; 36 | } 37 | 38 | 39 | /** 40 | * @returns {null} 41 | */ 42 | dispose({bCallJSONRPCServerDispose = true, bCallEndpointDispose = true, bCallPluginDispose = true} = {}) 43 | { 44 | if(bCallJSONRPCServerDispose && this._jsonrpcServer) 45 | { 46 | this._jsonrpcServer.dispose({bCallEndpointDispose, bCallPluginDispose}); 47 | } 48 | 49 | this.emit("disposed", {bCallJSONRPCServerDispose, bCallEndpointDispose, bCallPluginDispose}); 50 | } 51 | 52 | 53 | /** 54 | * If the client does not exist, it will be generated and saved on the session. 55 | * Another client will not be generated automatically, regardless of the accessed endpoint's defined client class for reverse calls. 56 | * 57 | * If client is provided it will be saved and used, while ClientClass will be ignored. 58 | * 59 | * @param {number} nConnectionID 60 | * @param {Class|null} ClientClass = null 61 | * @param {JSONRPC.Client|null} client = null 62 | * 63 | * @returns {JSONRPC.Client} 64 | */ 65 | connectionIDToSingletonClient(nConnectionID, ClientClass = null, client = null) 66 | { 67 | assert(typeof nConnectionID === "number", "nConnectionID must be a number. Received this: " + JSON.stringify(nConnectionID)); 68 | assert(ClientClass === null || typeof ClientClass === "function", "Invalid ClientClass value: " + (typeof ClientClass)); 69 | 70 | if(!client && !ClientClass) 71 | { 72 | throw new Error("At least one of client or ClientClass must be non-null."); 73 | } 74 | 75 | if(!this._objSessions.hasOwnProperty(nConnectionID)) 76 | { 77 | throw new Error("Connection " + JSON.stringify(nConnectionID) + " not found in router."); 78 | } 79 | 80 | if(this._objSessions[nConnectionID].clientReverseCalls === null) 81 | { 82 | this._objSessions[nConnectionID].clientReverseCalls = client || this._makeReverseCallsClient( 83 | ClientClass, 84 | this._objSessions[nConnectionID] 85 | ); 86 | } 87 | else 88 | { 89 | assert( 90 | ClientClass === null || this._objSessions[nConnectionID].clientReverseCalls instanceof ClientClass, 91 | "clientReverseCalls already initialized with a different JSONRPC.Client subclass." 92 | ); 93 | } 94 | 95 | return this._objSessions[nConnectionID].clientReverseCalls; 96 | } 97 | 98 | 99 | /** 100 | * @param {number} nConnectionID 101 | */ 102 | onConnectionEnded(nConnectionID) 103 | { 104 | delete this._objSessions[nConnectionID]; 105 | } 106 | 107 | 108 | /** 109 | * @param {Class} ClientClass 110 | * @param {object} objSession 111 | * 112 | * @returns {JSONRPC.Client} 113 | */ 114 | _makeReverseCallsClient(ClientClass, objSession) 115 | { 116 | throw new Error("Must implement."); 117 | } 118 | }; 119 | 120 | 121 | module.exports = RouterBase; 122 | -------------------------------------------------------------------------------- /src/ServerPluginBase.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require("events"); 2 | 3 | 4 | /** 5 | * @event disposed 6 | */ 7 | class ServerPluginBase extends EventEmitter 8 | { 9 | /** 10 | * Called before JSON parsing of the JSONRPC request. 11 | * 12 | * @param {JSONRPC.IncomingRequest} incomingRequest 13 | */ 14 | async beforeJSONDecode(incomingRequest) 15 | { 16 | // incomingRequest.body has been populated or may be populated here. 17 | } 18 | 19 | 20 | /** 21 | * Called after JSON parsing of the JSONRPC request. 22 | * 23 | * @param {JSONRPC.IncomingRequest} incomingRequest 24 | */ 25 | async afterJSONDecode(incomingRequest) 26 | { 27 | // incomingRequest.requestObject has been populated. 28 | } 29 | 30 | 31 | /** 32 | * If a plugin chooses to actually make the call here, 33 | * it must set the result in the incomingRequest.callResult property. 34 | * 35 | * @param {JSONRPC.IncomingRequest} incomingRequest 36 | */ 37 | async callFunction(incomingRequest) 38 | { 39 | // Useful here: 40 | // incomingRequest.requestObject.method 41 | // incomingRequest.requestObject.params 42 | 43 | // incomingRequest.callResult may be populated here with an Error class instance, or the function return. 44 | } 45 | 46 | 47 | /** 48 | * This is called after a function has been called successfully. 49 | * 50 | * @param {JSONRPC.IncomingRequest} incomingRequest 51 | */ 52 | async result(incomingRequest) 53 | { 54 | // incomingRequest.callResult contains what the function call returned. 55 | } 56 | 57 | 58 | /** 59 | * This is called if a function was not called successfully. 60 | * 61 | * @param {JSONRPC.IncomingRequest} incomingRequest 62 | */ 63 | async exceptionCatch(incomingRequest) 64 | { 65 | // incomingRequest.callResult contains a subclass instance of Error. 66 | } 67 | 68 | 69 | /** 70 | * This is called with the actual response object. 71 | * 72 | * objResponse is a standard JSONRPC 2.0 response object. 73 | * 74 | * @param {JSONRPC.IncomingRequest} incomingRequest 75 | */ 76 | async response(incomingRequest) 77 | { 78 | // Gives a chance to modify the server response object before sending it out. 79 | 80 | // incomingRequest.callResultToBeSerialized is available here. 81 | 82 | // Normally, this allows extending the protocol. 83 | } 84 | 85 | 86 | /** 87 | * This is called with the actual response object. 88 | * 89 | * objResponse is a standard JSONRPC 2.0 response object. 90 | * 91 | * @param {JSONRPC.IncomingRequest} incomingRequest 92 | */ 93 | async afterSerialize(incomingRequest) 94 | { 95 | // Gives a chance to modify the serialized server response string (or something else) before sending it out. 96 | 97 | // incomingRequest.callResultSerialized is available here. 98 | 99 | // Normally, this allows extending the protocol. 100 | } 101 | 102 | 103 | /** 104 | * @returns {null} 105 | */ 106 | dispose() 107 | { 108 | this.emit("disposed"); 109 | } 110 | }; 111 | 112 | module.exports = ServerPluginBase; 113 | -------------------------------------------------------------------------------- /src/Utils.js: -------------------------------------------------------------------------------- 1 | const JSONRPC = {}; 2 | JSONRPC.Exception = require("./Exception"); 3 | 4 | 5 | module.exports = 6 | class Utils 7 | { 8 | constructor() 9 | { 10 | Object.seal(this); 11 | } 12 | 13 | 14 | /** 15 | * @param {string} strJSON 16 | * 17 | * @returns {null|object|Array|string|boolean|number} 18 | */ 19 | static jsonDecodeSafe(strJSON) 20 | { 21 | if(typeof strJSON !== "string") 22 | { 23 | throw new JSONRPC.Exception("JSON needs to be a string; Input: " + JSON.stringify(strJSON), JSONRPC.Exception.PARSE_ERROR); 24 | } 25 | 26 | try 27 | { 28 | return JSON.parse(strJSON); 29 | } 30 | catch(error) 31 | { 32 | // V8 doesn't have a stacktrace for JSON.parse errors. 33 | // A re-throw is absolutely necessary to enable debugging. 34 | throw new JSONRPC.Exception(error.message + "; RAW JSON string: " + strJSON, JSONRPC.Exception.PARSE_ERROR); 35 | } 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /src/WebSocketAdapters/WebSocketWrapperBase.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require("events"); 2 | 3 | module.exports = 4 | class WebSocketWrapperBase extends EventEmitter 5 | { 6 | constructor(webSocket, strURL) 7 | { 8 | super(); 9 | 10 | this._webSocket = webSocket; 11 | } 12 | 13 | 14 | /** 15 | * @returns {string|undefined} 16 | */ 17 | get url() 18 | { 19 | return this._webSocket.url; 20 | } 21 | 22 | 23 | /** 24 | * @returns {Socket} 25 | */ 26 | get socket() 27 | { 28 | return this._webSocket.socket; 29 | } 30 | 31 | 32 | /** 33 | * @returns {number} 34 | */ 35 | get readyState() 36 | { 37 | return this._webSocket.readyState; 38 | } 39 | 40 | 41 | /** 42 | * @returns {string} 43 | */ 44 | get protocol() 45 | { 46 | return this._webSocket.protocol; 47 | } 48 | 49 | 50 | /** 51 | * @returns {number} 52 | */ 53 | get protocolVersion() 54 | { 55 | return this._webSocket.protocolVersion; 56 | } 57 | 58 | 59 | /** 60 | * @returns {number} 61 | */ 62 | static get CONNECTING() 63 | { 64 | return 0; 65 | } 66 | 67 | 68 | /** 69 | * @returns {number} 70 | */ 71 | static get OPEN() 72 | { 73 | return 1; 74 | } 75 | 76 | 77 | /** 78 | * @returns {number} 79 | */ 80 | static get CLOSING() 81 | { 82 | return 2; 83 | } 84 | 85 | 86 | /** 87 | * @returns {number} 88 | */ 89 | static get CLOSED() 90 | { 91 | return 3; 92 | } 93 | 94 | 95 | /** 96 | * @param {string} strEventName 97 | * @param {Function} fnListener 98 | */ 99 | on(strEventName, fnListener) 100 | { 101 | return this._webSocket.on(strEventName, fnListener); 102 | } 103 | 104 | 105 | /** 106 | * @param {string} strEventName 107 | * @param {Function} fnListener 108 | */ 109 | superOn(strEventName, fnListener) 110 | { 111 | return super.on(strEventName, fnListener); 112 | } 113 | 114 | 115 | send(mxData, ...args) 116 | { 117 | return this._webSocket.send(mxData, ...args); 118 | } 119 | 120 | 121 | /** 122 | * @param {number} nCode 123 | * @param {string} strReason 124 | */ 125 | close(nCode, strReason) 126 | { 127 | return this._webSocket.close(nCode, strReason); 128 | } 129 | 130 | 131 | /** 132 | * 133 | */ 134 | terminate() 135 | { 136 | return this._webSocket.terminate(); 137 | } 138 | 139 | 140 | ping(...args) 141 | { 142 | return this._webSocket.ping(...args); 143 | } 144 | 145 | 146 | pong(...args) 147 | { 148 | return this._webSocket.pong(...args); 149 | } 150 | }; 151 | -------------------------------------------------------------------------------- /src/WebSocketAdapters/uws/WebSocketWrapper.js: -------------------------------------------------------------------------------- 1 | const WebSocketWrapperBase = require("../WebSocketWrapperBase"); 2 | 3 | module.exports = 4 | class WebSocketWrapper extends WebSocketWrapperBase 5 | { 6 | /** 7 | * @param {WebSocket} webSocket 8 | * @param {string|undefined} strURL 9 | */ 10 | constructor(webSocket, strURL) 11 | { 12 | super(webSocket); 13 | 14 | if(typeof strURL === "string") 15 | { 16 | this._strURL = strURL; 17 | } 18 | else 19 | { 20 | this._strURL = webSocket.url ? webSocket.url : webSocket.upgradeReq.url; 21 | } 22 | 23 | this.objListeningFor = {}; 24 | 25 | if(webSocket.upgradeReq) 26 | { 27 | // Manual at https://github.com/uWebSockets/bindings/tree/master/nodejs says: 28 | // webSocket.upgradeReq is only valid during execution of the connection handler. 29 | // If you want to keep properties of the upgradeReq for the entire lifetime of the webSocket you better attach that specific property to the webSocket at connection. 30 | this._objUpgradeReq = webSocket.upgradeReq; /*{ 31 | url: webSocket.upgradeReq.url, 32 | headers: webSocket.upgradeReq.headers, //JSON.parse(JSON.stringify(webSocket.upgradeReq.headers)), 33 | socket: { 34 | remoteAddress: webSocket.upgradeReq.socket ? webSocket.upgradeReq.socket.remoteAddress : undefined 35 | } 36 | }*/; 37 | } 38 | else 39 | { 40 | this._objUpgradeReq = undefined; 41 | } 42 | } 43 | 44 | 45 | /** 46 | * @override 47 | * 48 | * @returns {string} 49 | */ 50 | get url() 51 | { 52 | return this._strURL; 53 | } 54 | 55 | 56 | /** 57 | * @returns {object|undefined} 58 | */ 59 | get upgradeReq() 60 | { 61 | return this._objUpgradeReq; 62 | } 63 | 64 | 65 | /** 66 | * Events splitter. 67 | * 68 | * @override 69 | * 70 | * @param {string} strEventName 71 | * @param {Function} fnListener 72 | */ 73 | on(strEventName, fnListener) 74 | { 75 | super.superOn(strEventName, fnListener); 76 | 77 | // uws does not allow multiple event listeners. 78 | // Getting around that... 79 | if(!this.objListeningFor[strEventName]) 80 | { 81 | this.objListeningFor[strEventName] = true; 82 | 83 | return this._webSocket.on( 84 | strEventName, 85 | (...theArgs) => { 86 | this.emit(strEventName, ...theArgs); 87 | } 88 | ); 89 | } 90 | } 91 | }; 92 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const ChildProcess = require("child_process"); 2 | const chalk = require("chalk"); 3 | const os = require("os"); 4 | const fs = require("fs"); 5 | 6 | 7 | // Avoiding "DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code." 8 | // by actually doing the respective exit with non-zero code. 9 | // This allows the watcher to restart this process. 10 | process.on( 11 | "unhandledRejection", 12 | (reason, promise) => 13 | { 14 | console.log("[" + process.pid + "] Unhandled Rejection at: Promise", promise, "reason", reason); 15 | 16 | process.exit(1); 17 | } 18 | ); 19 | 20 | process.on( 21 | "uncaughtException", 22 | (error) => { 23 | console.log("[" + process.pid + "] Unhandled exception."); 24 | console.error(error); 25 | 26 | process.exit(1); 27 | } 28 | ); 29 | 30 | 31 | async function spawnPassthru(strExecutablePath, arrParams = []) 32 | { 33 | const childProcess = ChildProcess.spawn(strExecutablePath, arrParams, {stdio: "inherit"}); 34 | //childProcess.stdout.pipe(process.stdout); 35 | //childProcess.stderr.pipe(process.stderr); 36 | return new Promise(async(fnResolve, fnReject) => { 37 | childProcess.on("error", fnReject); 38 | childProcess.on("exit", (nCode) => { 39 | if(nCode === 0) 40 | { 41 | fnResolve(); 42 | } 43 | else 44 | { 45 | fnReject(new Error(`Exec process exited with error code ${nCode}`)); 46 | } 47 | }); 48 | }); 49 | } 50 | 51 | 52 | (async() => { 53 | process.chdir(__dirname); 54 | 55 | let bBuiltNow = false; 56 | 57 | try 58 | { 59 | if(!fs.existsSync("./builds/browser/es5/jsonrpc.min.js")) 60 | { 61 | bBuiltNow = true; 62 | console.log(chalk.bgWhite.black("npm run build")); 63 | await spawnPassthru("npm" + (os.platform() === "win32" ? ".cmd" : ""), ["run", "build"]); 64 | } 65 | 66 | console.log(chalk.bgWhite.black("npm run test_lib")); 67 | await spawnPassthru("npm" + (os.platform() === "win32" ? ".cmd" : ""), ["run", "test_lib"]); 68 | 69 | console.log(chalk.bgWhite.black("npm run test_cluster")); 70 | await spawnPassthru("npm" + (os.platform() === "win32" ? ".cmd" : ""), ["run", "test_cluster"]); 71 | 72 | console.log(chalk.bgWhite.black("npm run test_worker_threads")); 73 | await spawnPassthru("npm" + (os.platform() === "win32" ? ".cmd" : ""), ["run", "test_worker_threads"]); 74 | 75 | // @TODO: automate test_rtc using headless Chrome. 76 | // console.log(chalk.bgWhite.black("npm run test_rtc")); 77 | // await spawnPassthru("npm" + (os.platform() === "win32" ? ".cmd" : ""), ["run", "test_rtc"]); 78 | 79 | // @TODO Add CPU stress parallel process to test for race conditions. 80 | 81 | console.log(""); 82 | console.log("[" + process.pid + "] \x1b[42m\x1b[30mAll tests done (test_lib, test_cluster, test_worker_threads). No unhandled (intentional) errors encountered.\x1b[0m Which means all is good or the tests are incomplete/buggy."); 83 | console.log(""); 84 | } 85 | catch(error) 86 | { 87 | if(bBuiltNow) 88 | { 89 | if(fs.existsSync("./builds/browser/es5/jsonrpc.min.js")) 90 | { 91 | fs.unlinkSync("./builds/browser/es5/jsonrpc.min.js"); 92 | } 93 | 94 | if(fs.existsSync("./builds/browser/es7/jsonrpc.min.js")) 95 | { 96 | fs.unlinkSync("./builds/browser/es7/jsonrpc.min.js"); 97 | } 98 | } 99 | 100 | throw error; 101 | } 102 | })(); 103 | 104 | -------------------------------------------------------------------------------- /tests/Browser/TestEndpoint.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* eslint-disable */ 3 | 4 | /** 5 | * Keep everything IE10 compatible, so it can be tested there as well. 6 | * 7 | * @class 8 | */ 9 | function TestEndpoint() 10 | { 11 | JSONRPC.EndpointBase.prototype.constructor.apply( 12 | this, 13 | [ 14 | /*strName*/ "Test", 15 | /*strPath*/ location.protocol + "//" + location.host + "/api", 16 | /*objReflection*/ {}, 17 | /*classReverseCallsClient*/ JSONRPC.Client 18 | ] 19 | ); 20 | } 21 | 22 | TestEndpoint.prototype = new JSONRPC.EndpointBase("TestEndpoint", "/api", {}); 23 | TestEndpoint.prototype.constructor = JSONRPC.EndpointBase; 24 | 25 | 26 | /** 27 | * @param {JSONRPC.IncomingRequest} incomingRequest 28 | * @param {string} strReturn 29 | * @param {boolean} bRandomSleep 30 | * @param {string|null} strATeamCharacterName 31 | * 32 | * @returns {Promise.} 33 | */ 34 | TestEndpoint.prototype.ping = function(incomingRequest, strReturn, RandomSleep, strATeamCharacterName){ 35 | return new Promise(function(fnResolve, fnReject){ 36 | if(typeof strATeamCharacterName === "string") 37 | { 38 | var strReturnReverseCall; 39 | if(strATeamCharacterName === "CallMeBackOnceAgain") 40 | { 41 | strReturnReverseCall = "Calling you back once again"; 42 | } 43 | else 44 | { 45 | strReturnReverseCall = strATeamCharacterName + " called back to confirm this: " + strReturn + "."; 46 | } 47 | 48 | incomingRequest.reverseCallsClient.rpc("ping", [strReturnReverseCall, /*bRandomSleep*/ true]) 49 | .then(function(strPingResult){ 50 | window.arrErrors.push(strPingResult); 51 | 52 | fnResolve(strPingResult); 53 | }) 54 | .catch(function(error){ 55 | if(window.arrErrors) 56 | { 57 | window.arrErrors.push(error); 58 | } 59 | 60 | fnReject(error); 61 | }) 62 | ; 63 | } 64 | else 65 | { 66 | fnResolve(strReturn); 67 | } 68 | }); 69 | }; 70 | -------------------------------------------------------------------------------- /tests/Browser/Worker.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | var jsonrpcServer = new JSONRPC.Server(); 4 | jsonrpcServer.registerEndpoint(new TestEndpoint()); 5 | 6 | // By default, JSONRPC.Server rejects all requests as not authenticated and not authorized. 7 | jsonrpcServer.addPlugin(new JSONRPC.Plugins.Server.AuthenticationSkip()); 8 | jsonrpcServer.addPlugin(new JSONRPC.Plugins.Server.AuthorizeAll()); 9 | 10 | jsonrpcServer.addPlugin(new JSONRPC.Plugins.Server.DebugLogger()); 11 | 12 | var workerJSONRPCRouter = new JSONRPC.BidirectionalWorkerRouter(jsonrpcServer); 13 | 14 | 15 | 16 | workerJSONRPCRouter.addWorker(self, "/api") 17 | .then(function(nConnectionID){ 18 | var client = workerJSONRPCRouter.connectionIDToSingletonClient(nConnectionID, JSONRPC.Client); 19 | 20 | client.rpc("rpc.connectToEndpoint", ["/api"]) 21 | .then(function(mxResponse){ 22 | console.log("Sent rpc.connectToEndpoint and received ", mxResponse); 23 | }) 24 | .catch(console.error) 25 | ; 26 | }) 27 | ; 28 | -------------------------------------------------------------------------------- /tests/Browser/main_server.js: -------------------------------------------------------------------------------- 1 | // Use this CLI server to support browser development, debugging or manual testing. 2 | 3 | const AllTests = require("../Tests/AllTests"); 4 | 5 | process.on( 6 | "unhandledRejection", 7 | (reason, promise) => 8 | { 9 | console.log("[" + process.pid + "] Unhandled Rejection at: Promise", promise, "reason", reason); 10 | 11 | process.exit(1); 12 | } 13 | ); 14 | 15 | ( 16 | async() => 17 | { 18 | const allTests = new AllTests(/*bBenchmarkMode*/ false, /*bWebSocketMode*/ true, require("ws"), require("ws").Server, undefined, /*bDisableVeryLargePacket*/ false); 19 | await allTests.setupHTTPServer(); 20 | await allTests.setupWebsocketServerSiteA(); 21 | await allTests.disableServerSecuritySiteA(); 22 | 23 | console.log("Go to http://localhost:" + allTests.httpServerPort + "/tests/Browser/index.html?websocketmode=1&websocketsport=" + allTests.websocketServerPort); 24 | } 25 | )(); 26 | -------------------------------------------------------------------------------- /tests/BrowserWebRTC/TestEndpoint.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* eslint-disable */ 3 | 4 | /** 5 | * Keep everything IE10 compatible, so it can be tested there as well. 6 | * 7 | * @class 8 | */ 9 | class TestEndpoint extends JSONRPC.EndpointBase 10 | { 11 | constructor() 12 | { 13 | super( 14 | /*strName*/ "Test", 15 | /*strPath*/ "/api", 16 | /*objReflection*/ {}, 17 | /*classReverseCallsClient*/ JSONRPC.Client 18 | ); 19 | 20 | // Theoretically, this browser application should be able to hold multiple connections. 21 | // Keeping it simple for example purposes. 22 | this._nRTCConnectionID = null; 23 | this._rtcConnection = null; 24 | this._rtcDataChannel = null; 25 | } 26 | 27 | 28 | /** 29 | * @param {number} nRTCConnectionID 30 | * 31 | * @returns {RTCDataChannel} 32 | */ 33 | getRTCDataChannel(nRTCConnectionID) 34 | { 35 | if(nRTCConnectionID !== this._nRTCConnectionID) 36 | { 37 | throw new Error("Unknown connection ID."); 38 | } 39 | 40 | if(!this._rtcDataChannel) 41 | { 42 | throw new Error("Data channel does not exist yet."); 43 | } 44 | 45 | if(this._rtcDataChannel.readyState !== "open") 46 | { 47 | throw new Error("Data channel is in readyState " + this._rtcDataChannel.readyState + "."); 48 | } 49 | 50 | return this._rtcDataChannel; 51 | } 52 | 53 | 54 | /** 55 | * @param {JSONRPC.IncomingRequest} incomingRequest 56 | * @param {number} nRTCConnectionID 57 | * @param {Array} arrIceServers 58 | */ 59 | async makeOffer(incomingRequest, nRTCConnectionID, arrIceServers) 60 | { 61 | this._nRTCConnectionID = nRTCConnectionID; 62 | 63 | this._rtcConnection = new RTCPeerConnection( 64 | { 65 | iceServers: arrIceServers 66 | }, 67 | { 68 | "mandatory": { 69 | "OfferToReceiveAudio": false, 70 | "OfferToReceiveVideo": false 71 | } 72 | } 73 | ); 74 | 75 | this._rtcConnection.addEventListener( 76 | "connectionstatechange", 77 | (event) => { 78 | // @TODO: how to handle "failed" and "disconnected"? The same as "closed"? 79 | if(this._rtcConnection.connectionState === "closed") 80 | { 81 | this.breakUpRTCConnection(/*incomingRequest*/ null, nRTCConnectionID); 82 | } 83 | } 84 | ); 85 | 86 | 87 | this._rtcConnection.onicecandidate = async(event) => { 88 | await incomingRequest.reverseCallsClient.rpc("webRTCAddIceCandidate", [nRTCConnectionID, event.candidate]); 89 | }; 90 | 91 | 92 | this._rtcDataChannel = this._rtcConnection.createDataChannel(this.path, {protocol: "jsonrpc"}); 93 | //this._rtcDataChannel.onmessage = console.log; 94 | //this._rtcDataChannel.onerror = console.error; 95 | this._rtcDataChannel.addEventListener( 96 | "close", 97 | () => { 98 | this.breakUpRTCConnection(/*incomingRequest*/ null, nRTCConnectionID); 99 | } 100 | ); 101 | 102 | const offer = await this._rtcConnection.createOffer(); 103 | this._rtcConnection.setLocalDescription(new RTCSessionDescription(offer)); 104 | 105 | return offer; 106 | } 107 | 108 | 109 | /** 110 | * @param {JSONRPC.IncomingRequest} incomingRequest 111 | * @param {number} nRTCConnectionID 112 | * @param {object} objOffer 113 | * @param {Array} arrIceServers 114 | */ 115 | async makeAnswer(incomingRequest, nRTCConnectionID, objOffer, arrIceServers) 116 | { 117 | this._nRTCConnectionID = nRTCConnectionID; 118 | 119 | this._rtcConnection = new RTCPeerConnection( 120 | { 121 | iceServers: arrIceServers 122 | }, 123 | { 124 | "mandatory": { 125 | "OfferToReceiveAudio": false, 126 | "OfferToReceiveVideo": false 127 | } 128 | } 129 | ); 130 | 131 | this._rtcConnection.addEventListener( 132 | "connectionstatechange", 133 | (event) => { 134 | // @TODO: how to handle "failed" and "disconnected"? The same as "closed"? 135 | if(this._rtcConnection.connectionState === "closed") 136 | { 137 | this.breakUpRTCConnection(/*incomingRequest*/ null, nRTCConnectionID); 138 | } 139 | } 140 | ); 141 | 142 | this._rtcConnection.setRemoteDescription(new RTCSessionDescription(objOffer)); 143 | 144 | const answer = await this._rtcConnection.createAnswer(); 145 | this._rtcConnection.setLocalDescription(new RTCSessionDescription(answer)); 146 | 147 | 148 | this._rtcConnection.onicecandidate = async(event) => { 149 | await incomingRequest.reverseCallsClient.rpc("webRTCAddIceCandidate", [nRTCConnectionID, event.candidate]); 150 | }; 151 | 152 | 153 | this._rtcConnection.ondatachannel = async(event) => { 154 | if(event.channel.protocol === "jsonrpc") 155 | { 156 | if(JSONRPC.EndpointBase.normalizePath(event.channel.label) !== this.path) 157 | { 158 | throw new Error("Both ends of a RTCConnection must have the same endpoint path property value. Incoming value: " + JSONRPC.EndpointBase.normalizePath(event.channel.label) + ". This endpoint's value: " + this.path); 159 | } 160 | 161 | this._rtcDataChannel = event.channel; 162 | 163 | //this._rtcDataChannel.onmessage = console.log; 164 | //this._rtcDataChannel.onerror = console.error; 165 | this._rtcDataChannel.addEventListener( 166 | "close", 167 | () => { 168 | this.breakUpRTCConnection(/*incomingRequest*/ null, nRTCConnectionID); 169 | } 170 | ); 171 | 172 | // Init JSONRPC over WebRTC data channel here. 173 | await incomingRequest.reverseCallsClient.rpc("femaleDataChannelIsOpen", [nRTCConnectionID]); 174 | 175 | //this._rtcDataChannel.send("test from female."); 176 | } 177 | else 178 | { 179 | console.log("Ignoring event.channel.protocol: " + event.channel.protocol); 180 | } 181 | }; 182 | 183 | return answer; 184 | } 185 | 186 | 187 | /** 188 | * @param {JSONRPC.IncomingRequest} incomingRequest 189 | * @param {number} nRTCConnectionID 190 | * @param {object} objAnswer 191 | */ 192 | async thatsWhatSheSaid(incomingRequest, nRTCConnectionID, objAnswer) 193 | { 194 | if(nRTCConnectionID !== this._nRTCConnectionID) 195 | { 196 | throw new Error("Unknown connection ID."); 197 | } 198 | 199 | this._rtcConnection.setRemoteDescription(new RTCSessionDescription(objAnswer)); 200 | } 201 | 202 | 203 | /** 204 | * @param {JSONRPC.IncomingRequest} incomingRequest 205 | * @param {number} nRTCConnectionID 206 | * @param {object} objRTCIceCandidate 207 | */ 208 | async webRTCAddIceCandidate(incomingRequest, nRTCConnectionID, objRTCIceCandidate) 209 | { 210 | if(nRTCConnectionID !== this._nRTCConnectionID) 211 | { 212 | throw new Error("Unknown connection ID."); 213 | } 214 | 215 | try 216 | { 217 | await this._rtcConnection.addIceCandidate(objRTCIceCandidate); 218 | } 219 | catch(error) 220 | { 221 | if(error.message.includes("Candidate missing values for both sdpMid and sdpMLineIndex")) 222 | { 223 | // Chrome weird error, everything works fine. 224 | } 225 | else 226 | { 227 | throw error; 228 | } 229 | } 230 | } 231 | 232 | 233 | /** 234 | * @param {JSONRPC.IncomingRequest | null} incomingRequest 235 | * @param {number} nRTCConnectionID 236 | */ 237 | async breakUpRTCConnection(incomingRequest, nRTCConnectionID) 238 | { 239 | if(this._rtcConnection) 240 | { 241 | this._rtcConnection.close(); 242 | } 243 | this._rtcConnection = null; 244 | this._nRTCConnectionID = null; 245 | this._rtcDataChannel = null; 246 | } 247 | 248 | 249 | /** 250 | * @param {JSONRPC.IncomingRequest} incomingRequest 251 | * @param {number} nRTCConnectionID 252 | */ 253 | async femaleDataChannelIsOpen(incomingRequest, nRTCConnectionID) 254 | { 255 | // Init JSONRPC over WebRTC data channel here. 256 | // this._rtcDataChannel.send("test from male."); 257 | } 258 | 259 | 260 | /** 261 | * @param {JSONRPC.IncomingRequest} incomingRequest 262 | * @param {string} strReturn 263 | */ 264 | async ping(incomingRequest, strReturn) 265 | { 266 | if(strReturn !== "Yes!") 267 | { 268 | if(await incomingRequest.reverseCallsClient.rpc("ping", ["Yes!"]) !== "Yes!") 269 | { 270 | throw new Error("I think we need to see other browsers!"); 271 | } 272 | } 273 | 274 | return strReturn; 275 | } 276 | }; 277 | 278 | -------------------------------------------------------------------------------- /tests/BrowserWebRTC/WebRTC.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Tests 5 | 6 | 7 | 8 | 9 | 10 | 11 | 110 | 111 | 112 | Open the developer tools console (F12 for most browsers, CTRL+SHIFT+I in Electron) to see errors or manually make calls. 113 | 114 | 115 | -------------------------------------------------------------------------------- /tests/BrowserWebRTC/main_server.js: -------------------------------------------------------------------------------- 1 | // Use this CLI server to support browser development, debugging or manual testing. 2 | 3 | const AllTests = require("../Tests/AllTests"); 4 | 5 | process.on( 6 | "unhandledRejection", 7 | (reason, promise) => 8 | { 9 | console.log("[" + process.pid + "] Unhandled Rejection at: Promise", promise, "reason", reason); 10 | 11 | process.exit(1); 12 | } 13 | ); 14 | 15 | ( 16 | async() => 17 | { 18 | const allTests = new AllTests(/*bBenchmarkMode*/ false, /*bWebSocketMode*/ true, require("ws"), require("ws").Server, undefined, /*bDisableVeryLargePacket*/ false); 19 | await allTests.setupHTTPServer(); 20 | await allTests.setupWebsocketServerSiteA(); 21 | await allTests.disableServerSecuritySiteA(); 22 | 23 | console.log("Go to http://localhost:" + allTests.httpServerPort + "/tests/BrowserWebRTC/WebRTC.html?websocketsport=" + allTests.websocketServerPort); 24 | } 25 | )(); 26 | -------------------------------------------------------------------------------- /tests/Tests/Plugins/Client/DebugMarker.js: -------------------------------------------------------------------------------- 1 | const JSONRPC = require("../../../../index"); 2 | 3 | module.exports = 4 | class DebugMarker extends JSONRPC.ClientPluginBase 5 | { 6 | /** 7 | * @param {string} strSite 8 | */ 9 | constructor(strSite) 10 | { 11 | super(); 12 | 13 | this._strSite = strSite; 14 | } 15 | 16 | 17 | /** 18 | * Gives a chance to modify the client request object before sending it out. 19 | * 20 | * Normally, this allows extending the protocol. 21 | * 22 | * @param {JSONRPC.OutgoingRequest} outgoingRequest 23 | */ 24 | async beforeJSONEncode(outgoingRequest) 25 | { 26 | // outgoingRequest.requestObject is available here. 27 | 28 | // outgoingRequest.headers and outgoingRequest.enpointURL may be modified here. 29 | 30 | outgoingRequest.requestObject.from = this._strSite; 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /tests/Tests/Plugins/Client/InvalidRequestJSON.js: -------------------------------------------------------------------------------- 1 | const JSONRPC = require("../../../../index"); 2 | 3 | module.exports = 4 | class InvalidRequestJSON extends JSONRPC.ClientPluginBase 5 | { 6 | /** 7 | * @param {JSONRPC.OutgoingRequest} outgoingRequest 8 | */ 9 | async afterJSONEncode(outgoingRequest) 10 | { 11 | outgoingRequest.requestBody = outgoingRequest.requestBody.substr(0, outgoingRequest.requestBody.length - 2); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /tests/Tests/Plugins/Server/DebugMarker.js: -------------------------------------------------------------------------------- 1 | const JSONRPC = require("../../../../index"); 2 | 3 | module.exports = 4 | class DebugMarker extends JSONRPC.ServerPluginBase 5 | { 6 | /** 7 | * @param {string} strSite 8 | */ 9 | constructor(strSite) 10 | { 11 | super(); 12 | 13 | this._strSite = strSite; 14 | } 15 | 16 | 17 | /** 18 | * This is called with the actual response object. 19 | * 20 | * objResponse is a standard JSONRPC 2.0 response object. 21 | * 22 | * @param {JSONRPC.IncomingRequest} incomingRequest 23 | */ 24 | async response(incomingRequest) 25 | { 26 | // Gives a chance to modify the server response object before sending it out. 27 | 28 | incomingRequest.callResultToBeSerialized.from = this._strSite; 29 | 30 | // Normally, this allows extending the protocol. 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /tests/Tests/Plugins/Server/InvalidResponseJSON.js: -------------------------------------------------------------------------------- 1 | const JSONRPC = require("../../../../index"); 2 | 3 | module.exports = 4 | class InvalidResponseJSON extends JSONRPC.ServerPluginBase 5 | { 6 | /** 7 | * @param {JSONRPC.IncomingRequest} incomingRequest 8 | */ 9 | async response(incomingRequest) 10 | { 11 | for(let strKey in incomingRequest.callResultToBeSerialized) 12 | { 13 | delete incomingRequest.callResultToBeSerialized[strKey]; 14 | } 15 | 16 | incomingRequest.callResultToBeSerialized.helloFromMars = ".... . .-.. .-.. --- / ..-. .-. --- -- / -- .- .-. ..."; 17 | 18 | console.log(incomingRequest.callResultToBeSerialized); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /tests/Tests/Plugins/Server/WebSocketAuthorize.js: -------------------------------------------------------------------------------- 1 | const JSONRPC = require("../../../../index"); 2 | 3 | const assert = require("assert"); 4 | 5 | module.exports = 6 | class WebSocketAuthorize extends JSONRPC.ServerPluginBase 7 | { 8 | constructor() 9 | { 10 | super(); 11 | 12 | this._objSessions = {}; 13 | this._objATeamMemberToConnectionID = {}; 14 | 15 | Object.seal(this); 16 | } 17 | 18 | 19 | /** 20 | * @returns {null} 21 | */ 22 | dispose() 23 | { 24 | for(const nWebSocketConnectionID in this._objSessions) 25 | { 26 | this._objSessions[nWebSocketConnectionID].webSocket.close(); 27 | } 28 | 29 | for(const strTeamMember in this._objATeamMemberToConnectionID) 30 | { 31 | delete this._objATeamMemberToConnectionID[strTeamMember]; 32 | } 33 | 34 | super.dispose(); 35 | } 36 | 37 | 38 | /** 39 | * Called after JSON parsing of the JSONRPC request. 40 | * 41 | * @override 42 | * 43 | * @param {JSONRPC.IncomingRequest} incomingRequest 44 | */ 45 | async afterJSONDecode(incomingRequest) 46 | { 47 | if(incomingRequest.isAuthenticated && incomingRequest.isAuthorized) 48 | { 49 | // Nothing to do. 50 | } 51 | else if(incomingRequest.requestObject.method === "ImHereForTheParty") 52 | { 53 | incomingRequest.isAuthenticated = true; 54 | incomingRequest.isAuthorized = true; 55 | 56 | // The ImHereForTheParty is an authentication function. 57 | // It will throw if not authenticated. 58 | } 59 | else if( 60 | typeof incomingRequest.connectionID === "number" 61 | && this._objSessions.hasOwnProperty(incomingRequest.connectionID) 62 | && this._objSessions[incomingRequest.connectionID] 63 | && this._objSessions[incomingRequest.connectionID].partyMembership !== null 64 | ) 65 | { 66 | incomingRequest.isAuthenticated = true; 67 | incomingRequest.isAuthorized = this._objSessions[incomingRequest.connectionID].authorized; 68 | } 69 | } 70 | 71 | 72 | /** 73 | * This is called after a function has been called successfully. 74 | * 75 | * @param {JSONRPC.IncomingRequest} incomingRequest 76 | */ 77 | async result(incomingRequest) 78 | { 79 | if(incomingRequest.requestObject.method === "ImHereForTheParty") 80 | { 81 | assert(typeof incomingRequest.connectionID === "number"); 82 | 83 | if( 84 | this._objSessions.hasOwnProperty(incomingRequest.connectionID) 85 | && this._objSessions[incomingRequest.connectionID].partyMembership !== null 86 | ) 87 | { 88 | // Maybe race condition somewhere from alternating authenticated IDs. 89 | incomingRequest.callResult = new JSONRPC.Exception("Not authorized. Current connnection " + incomingRequest.connectionID + " was already authenticated.", JSONRPC.Exception.NOT_AUTHORIZED); 90 | 91 | return; 92 | } 93 | 94 | if(!this._objSessions.hasOwnProperty(incomingRequest.connectionID)) 95 | { 96 | throw new Error("addConnection was not called with connection id " + JSON.stringify(incomingRequest.connectionID) + "."); 97 | } 98 | 99 | assert(!(incomingRequest.callResult instanceof Error)); 100 | this._objSessions[incomingRequest.connectionID].partyMembership = incomingRequest.callResult; 101 | 102 | // bDoNotAuthorizeMe param. 103 | assert(typeof incomingRequest.requestObject.params[2] === "boolean"); 104 | this._objSessions[incomingRequest.connectionID].authorized = !incomingRequest.requestObject.params[2]; 105 | 106 | this._objATeamMemberToConnectionID[incomingRequest.callResult.teamMember] = incomingRequest.connectionID; 107 | } 108 | } 109 | 110 | 111 | /** 112 | * @param {number} nWebSocketConnectionID 113 | * @param {WebSocket} webSocket 114 | */ 115 | addConnection(nWebSocketConnectionID, webSocket) 116 | { 117 | assert(typeof nWebSocketConnectionID === "number"); 118 | 119 | this._objSessions[nWebSocketConnectionID] = { 120 | partyMembership: null, 121 | authorized: false, 122 | connectionID: nWebSocketConnectionID, 123 | webSocket 124 | }; 125 | 126 | webSocket.on( 127 | "close", 128 | (code, message) => { 129 | if( 130 | this._objSessions.hasOwnProperty(nWebSocketConnectionID) 131 | && this._objSessions[nWebSocketConnectionID].partyMembership 132 | && this._objATeamMemberToConnectionID.hasOwnProperty(this._objSessions[nWebSocketConnectionID].partyMembership.teamMember) 133 | ) 134 | { 135 | delete this._objATeamMemberToConnectionID[this._objSessions[nWebSocketConnectionID].partyMembership.teamMember]; 136 | } 137 | 138 | delete this._objSessions[nWebSocketConnectionID]; 139 | } 140 | ); 141 | } 142 | 143 | 144 | /** 145 | * @param {string} strATeamMember 146 | * 147 | * @returns {number} 148 | */ 149 | aTeamMemberToConnectionID(strATeamMember) 150 | { 151 | if(!this._objATeamMemberToConnectionID.hasOwnProperty(strATeamMember)) 152 | { 153 | throw new Error("The team member is not logged in!!!"); 154 | } 155 | 156 | return this._objATeamMemberToConnectionID[strATeamMember]; 157 | } 158 | }; 159 | -------------------------------------------------------------------------------- /tests/Tests/TestClient.js: -------------------------------------------------------------------------------- 1 | const JSONRPC = require("../../index"); 2 | 3 | module.exports = 4 | class TestClient extends JSONRPC.Client 5 | { 6 | /** 7 | * Hello world? 8 | * 9 | * @param {string} strReturn 10 | * @param {boolean} bRandomSleep 11 | * @param {string|null} strATeamCharacterName 12 | * 13 | * @returns {string} 14 | */ 15 | async ping(strReturn, bRandomSleep, strATeamCharacterName) 16 | { 17 | return this.rpc("ping", [...arguments]); 18 | } 19 | 20 | 21 | /** 22 | * Hello world? 23 | * 24 | * @returns {string} 25 | */ 26 | async throwJSONRPCException() 27 | { 28 | return this.rpc("throwJSONRPCException", [...arguments]); 29 | } 30 | 31 | 32 | /** 33 | * Hello world? 34 | * 35 | * @returns {string} 36 | */ 37 | async throwError() 38 | { 39 | return this.rpc("throwError", [...arguments]); 40 | } 41 | 42 | 43 | /** 44 | * Authentication function. 45 | * 46 | * @param {string} strTeamMember 47 | * @param {string} strSecretKnock 48 | * @param {boolean} bDoNotAuthorizeMe 49 | * 50 | * @returns {{teamMember: {string}}} 51 | */ 52 | async ImHereForTheParty(strTeamMember, strSecretKnock, bDoNotAuthorizeMe) 53 | { 54 | return this.rpc("ImHereForTheParty", [...arguments]); 55 | } 56 | 57 | 58 | /** 59 | * @param {number} nPID 60 | * @returns {never} 61 | */ 62 | async killWorker(nPID) 63 | { 64 | return this.rpc("killWorker", [...arguments], /*bNotification*/ false); 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /tests/benchmark.js: -------------------------------------------------------------------------------- 1 | const JSONRPC = require("../index"); 2 | const AllTests = require("./Tests/AllTests"); 3 | 4 | const sleep = require("sleep-promise"); 5 | 6 | const chalk = require("chalk"); 7 | 8 | let Threads; 9 | try 10 | { 11 | Threads = require("worker_threads"); 12 | } 13 | catch(error) 14 | { 15 | // console.error(error); 16 | } 17 | 18 | process.on( 19 | "unhandledRejection", 20 | async(reason, promise) => 21 | { 22 | console.log("[" + process.pid + (Threads && !Threads.isMainThread ? ` worker thread ID ${Threads.threadId}` : "") + "] Unhandled Rejection at: Promise", promise, "reason", reason); 23 | process.exitCode = 1; 24 | 25 | if(Threads && !Threads.isMainThread) 26 | { 27 | // Give time for thread to flush to stdout. 28 | await sleep(2000); 29 | } 30 | 31 | process.exit(process.exitCode); 32 | } 33 | ); 34 | 35 | process.on( 36 | "uncaughtException", 37 | async(error) => { 38 | console.log("[" + process.pid + (Threads && !Threads.isMainThread ? ` worker thread ID ${Threads.threadId}` : "") + "] Unhandled exception."); 39 | console.error(error); 40 | process.exitCode = 1; 41 | 42 | if(Threads && !Threads.isMainThread) 43 | { 44 | // Give time for thread to flush to stdout. 45 | await sleep(2000); 46 | } 47 | 48 | process.exit(process.exitCode); 49 | } 50 | ); 51 | 52 | 53 | ( 54 | async() => 55 | { 56 | const bBenchmarkMode = true; 57 | 58 | 59 | let nPasses = 5; 60 | let allTests; 61 | while(nPasses--) 62 | { 63 | console.log("heapTotal before first benchmark: " + Math.round(process.memoryUsage().heapTotal / 1024 / 1024, 2) + " MB"); 64 | 65 | //console.log("===== http (500 calls in parallel)"); 66 | //allTests = new AllTests(bBenchmarkMode, /*bWebSocketMode*/ false, undefined, undefined, undefined, /*bDisableVeryLargePacket*/ true); 67 | //await allTests.runTests(); 68 | //global.gc(); 69 | //console.log("heapTotal after gc(): " + Math.round(process.memoryUsage().heapTotal / 1024 / 1024, 2) + " MB"); 70 | //console.log(""); 71 | 72 | // uws is consistently slower than ws when benchmarking with a few open connections (2) with the same number of calls. 73 | // Most of the randomness was disabled when tested. 74 | // Tested on nodejs 7.8.0, Windows 10, 64 bit. 75 | // https://github.com/uWebSockets/uWebSockets/issues/585 76 | 77 | 78 | console.log(chalk.cyan("===== ws (RPC API calls in parallel, over as many reused connections as possible)")); 79 | allTests = new AllTests(bBenchmarkMode, /*bWebSocketMode*/ true, require("ws"), require("ws").Server, undefined, /*bDisableVeryLargePacket*/ true); 80 | await allTests.runTests(); 81 | global.gc(); 82 | console.log("heapTotal after gc(): " + Math.round(process.memoryUsage().heapTotal / 1024 / 1024, 2) + " MB"); 83 | console.log(""); 84 | 85 | 86 | let bUwsLoaded = false; 87 | try 88 | { 89 | // Requires a compilation toolset to be installed if precompiled binaries are not available. 90 | require("uws"); 91 | bUwsLoaded = true; 92 | } 93 | catch(error) 94 | { 95 | console.error(error); 96 | } 97 | 98 | if(bUwsLoaded) 99 | { 100 | console.log(chalk.cyan("===== uws (RPC API calls in parallel, over as many reused connections as possible)")); 101 | allTests = new AllTests(bBenchmarkMode, /*bWebSocketMode*/ true, require("uws"), require("uws").Server, JSONRPC.WebSocketAdapters.uws.WebSocketWrapper, /*bDisableVeryLargePacket*/ true); 102 | allTests.websocketServerPort = allTests.httpServerPort + 1; 103 | await allTests.runTests(); 104 | global.gc(); 105 | console.log("heapTotal after gc(): " + Math.round(process.memoryUsage().heapTotal / 1024 / 1024, 2) + " MB"); 106 | console.log(""); 107 | 108 | 109 | console.log(chalk.cyan("===== uws.Server, ws.Client (RPC API calls in parallel, over as many reused connections as possible)")); 110 | allTests = new AllTests(bBenchmarkMode, /*bWebSocketMode*/ true, require("ws"), require("uws").Server, JSONRPC.WebSocketAdapters.uws.WebSocketWrapper, /*bDisableVeryLargePacket*/ true); 111 | allTests.websocketServerPort = allTests.httpServerPort + 1; 112 | await allTests.runTests(); 113 | global.gc(); 114 | console.log("heapTotal after gc(): " + Math.round(process.memoryUsage().heapTotal / 1024 / 1024, 2) + " MB"); 115 | console.log(""); 116 | 117 | 118 | console.log(chalk.cyan("===== ws.Server, uws.Client (RPC API API calls in parallel, over as many reused connections as possible)")); 119 | allTests = new AllTests(bBenchmarkMode, /*bWebSocketMode*/ true, require("uws"), require("ws").Server, JSONRPC.WebSocketAdapters.uws.WebSocketWrapper, /*bDisableVeryLargePacket*/ true); 120 | allTests.websocketServerPort = allTests.httpServerPort + 1; 121 | await allTests.runTests(); 122 | global.gc(); 123 | console.log("heapTotal after gc(): " + Math.round(process.memoryUsage().heapTotal / 1024 / 1024, 2) + " MB"); 124 | console.log(""); 125 | } 126 | } 127 | 128 | console.log("[" + process.pid + "] Finished benchmarking."); 129 | 130 | process.exit(0); 131 | } 132 | )(); 133 | -------------------------------------------------------------------------------- /tests/benchmark_endless_new_websockets.js: -------------------------------------------------------------------------------- 1 | const sleep = require("sleep-promise"); 2 | 3 | // const JSONRPC = require(".."); 4 | const AllTests = require("./Tests/AllTests"); 5 | 6 | let Threads; 7 | try 8 | { 9 | Threads = require("worker_threads"); 10 | } 11 | catch(error) 12 | { 13 | // console.error(error); 14 | } 15 | 16 | process.on( 17 | "unhandledRejection", 18 | async(reason, promise) => 19 | { 20 | console.log("[" + process.pid + (Threads && !Threads.isMainThread ? ` worker thread ID ${Threads.threadId}` : "") + "] Unhandled Rejection at: Promise", promise, "reason", reason); 21 | process.exitCode = 1; 22 | 23 | if(Threads && !Threads.isMainThread) 24 | { 25 | // Give time for thread to flush to stdout. 26 | await sleep(2000); 27 | } 28 | 29 | process.exit(process.exitCode); 30 | } 31 | ); 32 | 33 | process.on( 34 | "uncaughtException", 35 | async(error) => { 36 | console.log("[" + process.pid + (Threads && !Threads.isMainThread ? ` worker thread ID ${Threads.threadId}` : "") + "] Unhandled exception."); 37 | console.error(error); 38 | process.exitCode = 1; 39 | 40 | if(Threads && !Threads.isMainThread) 41 | { 42 | // Give time for thread to flush to stdout. 43 | await sleep(2000); 44 | } 45 | 46 | process.exit(process.exitCode); 47 | } 48 | ); 49 | 50 | ( 51 | async() => 52 | { 53 | const bBenchmarkMode = true; 54 | 55 | const allTests = new AllTests(bBenchmarkMode, /*bWebSocketMode*/ true, require("ws"), require("ws").Server, undefined, /*bDisableVeryLargePacket*/ true); 56 | await allTests.runEndlessNewWebSockets(); 57 | 58 | process.exit(0); 59 | } 60 | )(); 61 | -------------------------------------------------------------------------------- /tests/bug_uws_close_await_hang.js: -------------------------------------------------------------------------------- 1 | const JSONRPC = require(".."); 2 | const AllTests = require("./Tests/AllTests"); 3 | 4 | process.on( 5 | "unhandledRejection", 6 | (reason, promise) => 7 | { 8 | console.log("[" + process.pid + "] Unhandled Rejection at: Promise", promise, "reason", reason); 9 | 10 | process.exit(1); 11 | } 12 | ); 13 | 14 | // https://github.com/uWebSockets/uWebSockets/issues/586 15 | 16 | ( 17 | async() => 18 | { 19 | const bBenchmarkMode = false; 20 | 21 | const allTests = new AllTests(bBenchmarkMode, /*bWebSocketMode*/ true, require("uws"), require("uws").Server, JSONRPC.WebSocketAdapters.uws.WebSocketWrapper, /*bDisableVeryLargePacket*/ true); 22 | allTests.bAwaitServerClose = true; 23 | allTests.websocketServerPort = allTests.httpServerPort + 1; 24 | await allTests.runTests(); 25 | 26 | console.log("[" + process.pid + "] Done!!!"); 27 | 28 | process.exit(0); 29 | } 30 | )(); 31 | -------------------------------------------------------------------------------- /tests/bug_uws_send_large_string_connection_closed.js: -------------------------------------------------------------------------------- 1 | const JSONRPC = require(".."); 2 | const AllTests = require("./Tests/AllTests"); 3 | 4 | process.on( 5 | "unhandledRejection", 6 | (reason, promise) => 7 | { 8 | console.log("[" + process.pid + "] Unhandled Rejection at: Promise", promise, "reason", reason); 9 | 10 | process.exit(1); 11 | } 12 | ); 13 | 14 | 15 | // https://github.com/uWebSockets/uWebSockets/issues/583 16 | 17 | ( 18 | async() => 19 | { 20 | const bBenchmarkMode = false; 21 | 22 | const bDisableVeryLargePacket = false; 23 | 24 | const allTests = new AllTests(bBenchmarkMode, /*bWebSocketMode*/ true, require("uws"), require("uws").Server, JSONRPC.WebSocketAdapters.uws.WebSocketWrapper, bDisableVeryLargePacket); 25 | allTests.websocketServerPort = allTests.httpServerPort + 1; 26 | await allTests.runTests(); 27 | 28 | console.log("[" + process.pid + "] Done!!!"); 29 | 30 | process.exit(0); 31 | } 32 | )(); 33 | -------------------------------------------------------------------------------- /tests/main.js: -------------------------------------------------------------------------------- 1 | const sleep = require("sleep-promise"); 2 | 3 | const JSONRPC = require("../index"); 4 | const AllTests = require("./Tests/AllTests"); 5 | 6 | const os = require("os"); 7 | const cluster = require("cluster"); 8 | let Threads; 9 | try 10 | { 11 | Threads = require("worker_threads"); 12 | } 13 | catch(error) 14 | { 15 | // console.error(error); 16 | } 17 | 18 | 19 | process.on( 20 | "unhandledRejection", 21 | async(reason, promise) => 22 | { 23 | console.log("[" + process.pid + (Threads && !Threads.isMainThread ? ` worker thread ID ${Threads.threadId}` : "") + "] Unhandled Rejection at: Promise", promise, "reason", reason); 24 | process.exitCode = 1; 25 | 26 | if(Threads && !Threads.isMainThread) 27 | { 28 | // Give time for thread to flush to stdout. 29 | await sleep(2000); 30 | } 31 | 32 | process.exit(process.exitCode); 33 | } 34 | ); 35 | 36 | process.on( 37 | "uncaughtException", 38 | async(error) => { 39 | console.log("[" + process.pid + (Threads && !Threads.isMainThread ? ` worker thread ID ${Threads.threadId}` : "") + "] Unhandled exception."); 40 | console.error(error); 41 | process.exitCode = 1; 42 | 43 | if(Threads && !Threads.isMainThread) 44 | { 45 | // Give time for thread to flush to stdout. 46 | await sleep(2000); 47 | } 48 | 49 | process.exit(process.exitCode); 50 | } 51 | ); 52 | 53 | 54 | ( 55 | async() => 56 | { 57 | let allTests; 58 | const bBenchmarkMode = false; 59 | 60 | if(cluster.isMaster) 61 | { 62 | allTests = new AllTests(bBenchmarkMode, /*bWebSocketMode*/ false); 63 | await allTests.runThreadsTests(); 64 | 65 | if(Threads.isMainThread) 66 | { 67 | allTests = new AllTests(bBenchmarkMode, /*bWebSocketMode*/ false); 68 | await allTests.runClusterTests(); 69 | 70 | allTests = new AllTests(bBenchmarkMode, /*bWebSocketMode*/ false); 71 | await allTests.runTests(); 72 | 73 | // uws "Segmentation fault" on .close() in Travis (CentOS 7). 74 | // https://github.com/uWebSockets/uWebSockets/issues/583 75 | if(os.platform() === "win32") 76 | { 77 | let bUwsLoaded = false; 78 | try 79 | { 80 | // Requires a compilation toolset to be installed if precompiled binaries are not available. 81 | require("uws"); 82 | bUwsLoaded = true; 83 | } 84 | catch(error) 85 | { 86 | console.error(error); 87 | } 88 | 89 | if(bUwsLoaded) 90 | { 91 | allTests = new AllTests(bBenchmarkMode, /*bWebSocketMode*/ true, require("uws"), require("uws").Server, JSONRPC.WebSocketAdapters.uws.WebSocketWrapper, /*bDisableVeryLargePacket*/ true); 92 | allTests.websocketServerPort = allTests.httpServerPort + 1; 93 | await allTests.runTests(); 94 | } 95 | } 96 | 97 | allTests = new AllTests(bBenchmarkMode, /*bWebSocketMode*/ true, require("ws"), require("ws").Server, undefined, /*bDisableVeryLargePacket*/ false); 98 | await allTests.runTests(); 99 | 100 | console.log(""); 101 | console.log("[" + process.pid + "] \x1b[42m\x1b[30mAll tests done. No uncaught errors encountered.\x1b[0m Which means all is good or the tests are incomplete/buggy."); 102 | console.log(""); 103 | } 104 | } 105 | else 106 | { 107 | allTests = new AllTests(bBenchmarkMode, /*bWebSocketMode*/ false); 108 | await allTests.runClusterTests(); 109 | 110 | console.log("[" + process.pid + "] \x1b[32mWorker done!!!\x1b[0m"); 111 | } 112 | 113 | 114 | process.exit(0); 115 | } 116 | )(); 117 | -------------------------------------------------------------------------------- /tests/main_NodeClusterBase.js: -------------------------------------------------------------------------------- 1 | const JSONRPC = require("../index"); 2 | 3 | const cluster = require("cluster"); 4 | const assert = require("assert"); 5 | const path = require("path"); 6 | 7 | const sleep = require("sleep-promise"); 8 | 9 | let Threads; 10 | try 11 | { 12 | Threads = require("worker_threads"); 13 | } 14 | catch(error) 15 | { 16 | // console.error(error); 17 | } 18 | 19 | 20 | process.on( 21 | "unhandledRejection", 22 | async(reason, promise) => 23 | { 24 | console.log("[" + process.pid + (Threads && !Threads.isMainThread ? ` worker thread ID ${Threads.threadId}` : "") + "] Unhandled Rejection at: Promise", promise, "reason", reason); 25 | process.exitCode = 1; 26 | 27 | if(Threads && !Threads.isMainThread) 28 | { 29 | // Give time for thread to flush to stdout. 30 | await sleep(2000); 31 | } 32 | 33 | process.exit(process.exitCode); 34 | } 35 | ); 36 | 37 | process.on( 38 | "uncaughtException", 39 | async(error) => { 40 | console.log("[" + process.pid + (Threads && !Threads.isMainThread ? ` worker thread ID ${Threads.threadId}` : "") + "] Unhandled exception."); 41 | console.error(error); 42 | process.exitCode = 1; 43 | 44 | if(Threads && !Threads.isMainThread) 45 | { 46 | // Give time for thread to flush to stdout. 47 | await sleep(2000); 48 | } 49 | 50 | process.exit(process.exitCode); 51 | } 52 | ); 53 | 54 | 55 | // Keep this process alive. 56 | setInterval(() => {}, 10000); 57 | 58 | ( 59 | async() => 60 | { 61 | try 62 | { 63 | if(cluster.isMaster) 64 | { 65 | const endpoint = new JSONRPC.NodeClusterBase.MasterEndpoint(JSONRPC.NodeClusterBase.WorkerClient); 66 | await endpoint.start(); 67 | await endpoint.watchForUpgrade(path.join(path.dirname(__dirname), "package.json")); 68 | 69 | let bNotReady; 70 | console.log("Waiting for workers to all signal they are ready."); 71 | 72 | do 73 | { 74 | bNotReady = false; 75 | for(let nID in endpoint.workerClients) 76 | { 77 | bNotReady = bNotReady || !endpoint.workerClients[nID].ready; 78 | } 79 | 80 | if(bNotReady) 81 | { 82 | await sleep(1000); 83 | } 84 | } while(bNotReady); 85 | 86 | console.log("All workers ready."); 87 | await sleep(10000000); 88 | } 89 | else 90 | { 91 | const endpoint = new JSONRPC.NodeClusterBase.WorkerEndpoint(JSONRPC.NodeClusterBase.MasterClient); 92 | await endpoint.start(); 93 | 94 | assert(await endpoint.masterClient.ping("Test") === "Test", "Calling MasterEndpoint.ping() returned the wrong thing."); 95 | 96 | console.log("Will call masterClient.gracefulExit() after sleeping for 10 seconds."); 97 | await sleep(10 * 1000); 98 | // This will call all worker's gracefulExit() methods. 99 | await endpoint.masterClient.gracefulExit(); 100 | } 101 | } 102 | catch(error) 103 | { 104 | console.error(error); 105 | 106 | // Give time for child process (like workers) or worker threads stdout and stderr to flush before exiting current process. 107 | await sleep(2000); 108 | 109 | process.exit(1); 110 | } 111 | 112 | // Give time for child process (like workers) or worker threads stdout and stderr to flush before exiting current process. 113 | await sleep(2000); 114 | process.exit(0); 115 | } 116 | )(); 117 | -------------------------------------------------------------------------------- /tests/main_NodeWorkerThreadsBase.js: -------------------------------------------------------------------------------- 1 | const JSONRPC = require("../index"); 2 | 3 | let Threads; 4 | try 5 | { 6 | Threads = require("worker_threads"); 7 | } 8 | catch(error) 9 | { 10 | // console.error(error); 11 | } 12 | 13 | const assert = require("assert"); 14 | const path = require("path"); 15 | 16 | const sleep = require("sleep-promise"); 17 | 18 | 19 | process.on( 20 | "unhandledRejection", 21 | async(reason, promise) => 22 | { 23 | console.log("[" + process.pid + (Threads && !Threads.isMainThread ? ` worker thread ID ${Threads.threadId}` : "") + "] Unhandled Rejection at: Promise", promise, "reason", reason); 24 | process.exitCode = 1; 25 | 26 | if(Threads && !Threads.isMainThread) 27 | { 28 | // Give time for thread to flush to stdout. 29 | await sleep(2000); 30 | } 31 | 32 | process.exit(process.exitCode); 33 | } 34 | ); 35 | 36 | process.on( 37 | "uncaughtException", 38 | async(error) => { 39 | console.log("[" + process.pid + (Threads && !Threads.isMainThread ? ` worker thread ID ${Threads.threadId}` : "") + "] Unhandled exception."); 40 | console.error(error); 41 | process.exitCode = 1; 42 | 43 | if(Threads && !Threads.isMainThread) 44 | { 45 | // Give time for thread to flush to stdout. 46 | await sleep(2000); 47 | } 48 | 49 | process.exit(process.exitCode); 50 | } 51 | ); 52 | 53 | 54 | // Keep this process alive. 55 | setInterval(() => {}, 10000); 56 | 57 | 58 | ( 59 | async() => 60 | { 61 | if(Threads.isMainThread) 62 | { 63 | const endpoint = new JSONRPC.NodeWorkerThreadsBase.MasterEndpoint(JSONRPC.NodeWorkerThreadsBase.WorkerClient); 64 | await endpoint.start(); 65 | await endpoint.watchForUpgrade(path.join(path.dirname(__dirname), "package.json")); 66 | 67 | let bNotReady; 68 | console.log("Waiting for workers to all signal they are ready."); 69 | 70 | do 71 | { 72 | bNotReady = false; 73 | for(let nID in endpoint.workerClients) 74 | { 75 | bNotReady = bNotReady || !endpoint.workerClients[nID].ready; 76 | } 77 | 78 | if(bNotReady) 79 | { 80 | await sleep(1000); 81 | } 82 | } while(bNotReady); 83 | 84 | console.log("All workers ready."); 85 | await sleep(10000000); 86 | } 87 | else 88 | { 89 | const endpoint = new JSONRPC.NodeWorkerThreadsBase.WorkerEndpoint(JSONRPC.NodeWorkerThreadsBase.MasterClient); 90 | await endpoint.start(); 91 | 92 | assert(await endpoint.masterClient.ping("Test") === "Test", "Calling MasterEndpoint.ping() returned the wrong thing."); 93 | 94 | let packet = Buffer.alloc(2000); 95 | await endpoint.masterClient.sendTransferListTest(packet.buffer); 96 | 97 | if(packet.length === 0) 98 | { 99 | console.log("The allocated buffer has been detached from the process because it has been added to a transferList."); 100 | } 101 | else 102 | { 103 | throw new Error("Data from transferList could not be sent to the other process."); 104 | } 105 | 106 | console.log("Will call masterClient.gracefulExit() after sleeping for 10 seconds."); 107 | await sleep(10 * 1000); 108 | // This will call all worker's gracefulExit() methods. 109 | await endpoint.masterClient.gracefulExit(); 110 | } 111 | 112 | await sleep(1000); 113 | process.exit(0); 114 | } 115 | )(); 116 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fs = require("fs"); 3 | const webpack = require("webpack"); 4 | const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin; 5 | const MinifyPlugin = require("babel-minify-webpack-plugin"); 6 | const recursiveKeys = require("recursive-keys"); 7 | 8 | const objPackageJSON = JSON.parse(fs.readFileSync("package.json")); 9 | 10 | module.exports = [ 11 | { 12 | target: "web", 13 | externals: { 14 | "electron": "null", 15 | "fs": "null", 16 | "ws": "WebSocket", 17 | "uws": "WebSocket", 18 | "node-fetch": "fetch", 19 | "cluster": "null", 20 | "fs-promise": "null", 21 | "fs-extra": "null", 22 | "node-forge": "forge", 23 | "typescript-parser": "", 24 | "worker_threads": "null", 25 | "http": "null", 26 | "https": "null" 27 | }, 28 | entry: [ 29 | // "babel-polyfill", 30 | "./index_webpack" 31 | ], 32 | output: { 33 | path: path.join(__dirname, "builds", "browser", "es5"), 34 | filename: "jsonrpc.min.js", 35 | libraryTarget: "umd" 36 | }, 37 | // devtool: "source-map", 38 | devtool: "", 39 | module: { 40 | loaders: [ 41 | { 42 | test: /\.js$/, 43 | include: [ 44 | path.resolve(__dirname, "src") 45 | ], 46 | exclude: [ 47 | path.resolve(__dirname, "node_modules"), 48 | path.resolve(__dirname, "tests") 49 | ], 50 | loader: "babel-loader", 51 | options: { 52 | presets: ["es2015", "stage-3"], 53 | plugins: [ 54 | "async-to-promises", 55 | "remove-comments" 56 | ], 57 | babelrc: false 58 | } 59 | } 60 | ] 61 | }, 62 | plugins: [ 63 | new BundleAnalyzerPlugin({ 64 | reportFilename: "./stats.html", 65 | // generateStatsFile: true, 66 | openAnalyzer: false, 67 | analyzerMode: "static" 68 | }), 69 | new webpack.optimize.OccurrenceOrderPlugin(), 70 | new webpack.DefinePlugin({ 71 | "process.env": { 72 | "NODE_ENV": "production" 73 | } 74 | }), 75 | new MinifyPlugin(/*minifyOpts*/ {}, /*pluginOpts*/ {}) 76 | // new webpack.optimize.UglifyJsPlugin({ 77 | // minimize: true, 78 | // sourceMap: true, 79 | // compress: { 80 | // screw_ie8: true, 81 | // unused: true, 82 | // dead_code: true 83 | // }, 84 | // mangle: { 85 | // screw_ie8: true, 86 | // except: recursiveKeys.dumpKeysRecursively(require("./index_webpack")).map( 87 | // (strClassName) => { 88 | // return strClassName.split(".").pop(); 89 | // } 90 | // ) 91 | // }, 92 | // output: { 93 | // screw_ie8: true, 94 | // comments: false, 95 | // preamble: `/** 96 | // ${objPackageJSON.name} v${objPackageJSON.version} 97 | // ${objPackageJSON.description} 98 | // ${objPackageJSON.homepage} 99 | // \n\n${fs.readFileSync("./LICENSE")} 100 | // */`.replace(/\t+/g, "") 101 | // } 102 | // }) 103 | ] 104 | }, 105 | { 106 | target: "web", 107 | externals: { 108 | "electron": "null", 109 | "fs": "null", 110 | "ws": "WebSocket", 111 | "uws": "WebSocket", 112 | "node-fetch": "fetch", 113 | "cluster": "", 114 | "fs-promise": "", 115 | "fs-extra": "", 116 | "node-forge": "forge", 117 | "typescript-parser": "", 118 | "worker_threads": "null", 119 | "http": "null", 120 | "https": "null" 121 | }, 122 | entry: [ 123 | "./index_webpack" 124 | ], 125 | output: { 126 | path: path.join(__dirname, "builds", "browser", "es7"), 127 | filename: "jsonrpc.min.js", 128 | libraryTarget: "umd" 129 | }, 130 | // devtool: "source-map", 131 | devtool: "", 132 | module: { 133 | loaders: [ 134 | { 135 | test: /\.js$/, 136 | include: [ 137 | path.resolve(__dirname, "src") 138 | ], 139 | exclude: [ 140 | path.resolve(__dirname, "node_modules") + "/", 141 | path.resolve(__dirname, "tests") 142 | ], 143 | loader: "babel-loader", 144 | options: { 145 | plugins: ["remove-comments"], 146 | babelrc: false 147 | } 148 | } 149 | ] 150 | }, 151 | plugins: [ 152 | new BundleAnalyzerPlugin({ 153 | reportFilename: "./stats.html", 154 | // generateStatsFile: true, 155 | openAnalyzer: false, 156 | analyzerMode: "static" 157 | }), 158 | new webpack.optimize.OccurrenceOrderPlugin(), 159 | new webpack.DefinePlugin({ 160 | "process.env": { 161 | "NODE_ENV": "production" 162 | } 163 | }), 164 | new MinifyPlugin(/*minifyOpts*/ {}, /*pluginOpts*/ {}) 165 | ] 166 | } 167 | ]; 168 | 169 | --------------------------------------------------------------------------------