├── .babelrc ├── .compilerc ├── .eslintrc ├── .gitignore ├── .npmignore ├── CODE_OF_CONDUCT.md ├── COPYING ├── README.md ├── build.cmd ├── build.sh ├── esdoc.json ├── package-lock.json ├── package.json ├── remote-ajax.js ├── src ├── custom-operators.js ├── execute-js-func.js ├── index.js ├── remote-ajax.js ├── remote-event-browser.js ├── remote-event.js ├── renderer-require-preload.html ├── renderer-require-preload.js ├── renderer-require.js └── rx-dom.js └── test ├── .eslintrc ├── asserttest.js ├── dummy-module.js ├── recursive-proxy-handler.js ├── remote-event.js ├── renderer-require.js └── support.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2016-node5"], 3 | "plugins": ["transform-async-to-generator"] 4 | } 5 | -------------------------------------------------------------------------------- /.compilerc: -------------------------------------------------------------------------------- 1 | { 2 | "application/javascript": { 3 | "presets": ["es2016-node5"], 4 | "plugins": ["transform-async-to-generator", "transform-runtime"], 5 | "sourceMaps": "inline" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "rules": { 4 | "strict": 0, 5 | "indent": [ 6 | 2, 7 | 2 8 | ], 9 | "semi": [ 10 | 2, 11 | "always" 12 | ], 13 | "no-console": 0 14 | }, 15 | "env": { 16 | "es6": true, 17 | "node": true, 18 | "browser": true 19 | }, 20 | "extends": "eslint:recommended" 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 27 | node_modules 28 | 29 | lib 30 | test-dist 31 | docs 32 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 27 | node_modules 28 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating 6 | documentation, submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in this project a harassment-free 9 | experience for everyone, regardless of level of experience, gender, gender 10 | identity and expression, sexual orientation, disability, personal appearance, 11 | body size, race, ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | * The use of sexualized language or imagery 16 | * Personal attacks 17 | * Trolling or insulting/derogatory comments 18 | * Public or private harassment 19 | * Publishing other's private information, such as physical or electronic 20 | addresses, without explicit permission 21 | * Other unethical or unprofessional conduct 22 | 23 | Project maintainers have the right and responsibility to remove, edit, or 24 | reject comments, commits, code, wiki edits, issues, and other contributions 25 | that are not aligned to this Code of Conduct, or to ban temporarily or 26 | permanently any contributor for other behaviors that they deem inappropriate, 27 | threatening, offensive, or harmful. 28 | 29 | By adopting this Code of Conduct, project maintainers commit themselves to 30 | fairly and consistently applying these principles to every aspect of managing 31 | this project. Project maintainers who do not follow or enforce the Code of 32 | Conduct may be permanently removed from the project team. 33 | 34 | This Code of Conduct applies both within project spaces and in public spaces 35 | when an individual is representing the project or its community. 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 38 | reported by contacting a project maintainer at paul@paulbetts.org. All 39 | complaints will be reviewed and investigated and will result in a response that 40 | is deemed necessary and appropriate to the circumstances. Maintainers are 41 | obligated to maintain confidentiality with regard to the reporter of an 42 | incident. 43 | 44 | 45 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 46 | version 1.3.0, available at 47 | [http://contributor-covenant.org/version/1/3/0/][version] 48 | 49 | [homepage]: http://contributor-covenant.org 50 | [version]: http://contributor-covenant.org/version/1/3/0/ 51 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Paul Betts 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED: electron-remote: an asynchronous 'remote', and more 2 | 3 | [![No Maintenance Intended](http://unmaintained.tech/badge.svg)](http://unmaintained.tech/) 4 | 5 | This project is no longer maintained, pull requests are no longer being reviewed or merged and issues are no longer being responded to. 6 | 7 | --- 8 | 9 | ![](https://img.shields.io/npm/dm/electron-remote.svg) ![](http://paulcbetts.github.io/electron-remote/docs/badge.svg) 10 | 11 | 12 | electron-remote provides an alternative to Electron's `remote` module based around Promises instead of synchronous execution. It also provides an automatic way to use BrowserWindows as "background processes" that auto-scales based on usage, similar to Grand Central Dispatch or the .NET TPL Taskpool. 13 | 14 | ## The Quickest of Quick Starts 15 | 16 | ###### Calling main process modules from a renderer 17 | 18 | ```js 19 | import { createProxyForMainProcessModule } from 'electron-remote'; 20 | 21 | // app is now a proxy for the app module in the main process 22 | const app = createProxyForMainProcessModule('app'); 23 | 24 | // The difference is all methods return a Promise instead of blocking 25 | const memoryInfo = await app.getAppMemoryInfo(); 26 | ``` 27 | 28 | ###### Calling code in other windows 29 | 30 | ```js 31 | import { createProxyForRemote } from 'electron-remote'; 32 | 33 | // myWindowJs is now a proxy object for myWindow's `window` global object 34 | const myWindowJs = createProxyForRemote(myWindow); 35 | 36 | // Functions suffixed with _get will read a value 37 | userAgent = await myWindowJs.navigator.userAgent_get() 38 | ``` 39 | 40 | ###### Renderer Taskpool 41 | 42 | ```js 43 | import { requireTaskPool } from 'electron-remote'; 44 | 45 | const myCoolModule = requireTaskPool(require.resolve('./my-cool-module')); 46 | 47 | // This method will run synchronously, but in a background BrowserWindow process 48 | // so that your app will not block 49 | let result = await myCoolModule.calculateDigitsOfPi(100000); 50 | ``` 51 | 52 | ## But I like Remote! 53 | 54 | Remote is super convenient! But it also has some downsides - its main downside is that its action is **synchronous**. This means that both the main and window processes will _wait_ for a method to finish running. Even for quick methods, calling it too often can introduce scroll jank and generally cause performance problems. 55 | 56 | electron-remote is a version of remote that, while less ergonomic, guarantees that it won't block the calling thread. 57 | 58 | ## Using createProxyForRemote 59 | 60 | `createProxyForRemote` is a replacement for places where you would use Electron's `executeJavaScript` method on BrowserWindow or WebView instances - however, it works a little differently. Using a new feature in ES2015 called [proxy objects](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy), we create an object which represents the `window` object on a remote context, and all method calls get sent as messages to that remote instead of being run immediately, which feels very similar to the `remote` Electron module. 61 | 62 | This provides a number of very important advantages: 63 | 64 | * `createProxyForRemote` uses asynchronous IPC instead of blocking 65 | * Parameters are serialized directly, so you don't have to try to build strings that can be `eval`d, which is a dangerous endeavor at best. 66 | * Calling methods on objects is far more convenient than trying to poke at things via a remote eval. 67 | 68 | #### How do I get properties if everything is a Promise tho??? 69 | 70 | Astute observers will note, that getting the value of a property is always a synchronous operation - to facilitate that, any method with `_get()` appended to it will let you fetch the value for the property. 71 | 72 | ```js 73 | import { createProxyForRemote } from 'electron-remote'; 74 | 75 | // myWindowJs is now a proxy object for myWindow's `window` global object 76 | const myWindowJs = createProxyForRemote(myWindow); 77 | 78 | // Functions suffixed with _get will read a value 79 | myWindowJs.navigator.userAgent_get() 80 | .then((agent) => console.log(`The user agent is ${agent}`)); 81 | ``` 82 | 83 | #### But do this first! 84 | 85 | Before you use `createProxyForRemote`, you **must** call `initializeEvalHandler()` in the target window on startup. This sets up the listeners that electron-remote will use. 86 | 87 | #### Bringing it all together 88 | 89 | ```js 90 | // In my window's main.js 91 | initializeEvalHandler(); 92 | window.addNumbers = (a,b) => a + b; 93 | 94 | 95 | // In my main process 96 | let myWindowProxy = createProxyForRemote(myWindow); 97 | myWindowProxy.addNumbers(5, 5) 98 | .then((x) => console.log(x)); 99 | 100 | >>> 10 101 | ``` 102 | 103 | #### Using createProxyForMainProcessModule 104 | This is meant to be a drop-in replacement for places you would have used `remote` in a renderer process. It's almost identical to `createProxyForRemote`, but instead of `eval`ing JavaScript it can only call methods on main process modules. It still has all the same benefits: asynchronous IPC instead of an `ipc.sendSync`. 105 | 106 | ## Here Be Dragons 107 | 108 | electron-remote has a number of significant caveats versus the remote module that you should definitely be aware of: 109 | 110 | * Remote values must be Serializable 111 | 112 | Objects that you return to the calling process must be serializable (i.e. you can call `JSON.stringify` on it and get a valid thing)- this means that creating Classes won't work, nor will return objects like BrowserWindows or other Electron objects. For example: 113 | 114 | ```js 115 | let myWindowProxy = createProxyForRemote(myWindow); 116 | 117 | // XXX: BAD - HTML elements aren't serializable 118 | let obj = myWindowProxy.document.createElement('h1'); 119 | ``` 120 | 121 | * Remote event listeners aren't supported 122 | 123 | Anything that involves an event handler isn't going to work: 124 | 125 | ```js 126 | // XXX: BAD - You can't add event handlers 127 | myWindowProxy.document.addEventListener('onBlur', (e) => console.log("Blur!")); 128 | ``` 129 | 130 | ## The Renderer Taskpool 131 | 132 | Renderer Taskpools provide an automatic way to use BrowserWindows as "background processes" that auto-scales based on usage, similar to Grand Central Dispatch or the .NET TPL Taskpool. This works by allowing you to provide a Module that you'd like to load in the remote processes, which will be loaded and unloaded on the fly according to demand. 133 | 134 | Let's look at the example again: 135 | 136 | ```js 137 | import { requireTaskPool } from 'electron-remote'; 138 | 139 | const myCoolModule = requireTaskPool(require.resolve('./my-cool-module')); 140 | 141 | // This method will run synchronously, but in a background BrowserWindow process 142 | // so that your app will not block 143 | let result = await myCoolModule.calculateDigitsOfPi(100000); 144 | ``` 145 | 146 | By default, `requireTaskPool` will create up to four background processes to concurrently run JS code on. As these processes become busy, requests will be queued to different processes and wait in line implicitly. 147 | 148 | ##### More Dragons 149 | 150 | Since `requireTaskPool` will create and destroy processes as needed, this means that global variables or other state will be destroyed as well. You can't rely on setting a global variable and having it persist for a period of time longer than one method call. 151 | 152 | ## The remote-ajax module 153 | 154 | One module that is super useful to have from the main process is a way to make network requests using Chromium's networking stack, which correctly does things such as respecting the system proxy settings. To this end, electron-remote comes with a convenient wrapper around Rx-DOM's AJAX methods called `remote-ajax`. 155 | 156 | ```js 157 | import { requireTaskPool } from 'electron-remote'; 158 | 159 | const remoteAjax = requireTaskPool(require.resolve('electron-remote/remote-ajax')); 160 | 161 | // Result is the object that XmlHttpRequest gives you 162 | let result = await remoteAjax.get('https://httpbin.org/get'); 163 | console.log(result.url) 164 | 165 | >>> 'https://httpbin.org/get' 166 | ``` 167 | 168 | See the documentation for [Rx-DOM](https://github.com/Reactive-Extensions/RxJS-DOM/blob/master/modules/main-ajax/readme.md) for how these methods work. 169 | 170 | Another method that is included is `downloadFileOrUrl`, which lets you download a file to a target: 171 | 172 | ```js 173 | /** 174 | * Downloads a path as either a file path or a HTTP URL to a specific place 175 | * 176 | * @param {string} pathOrUrl Either an HTTP URL or a file path. 177 | * @return {string} The contents as a UTF-8 decoded string. 178 | */ 179 | function downloadFileOrUrl(pathOrUrl, target) 180 | ``` 181 | -------------------------------------------------------------------------------- /build.cmd: -------------------------------------------------------------------------------- 1 | npm i && npm t 2 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | npm i && npm t 3 | -------------------------------------------------------------------------------- /esdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "./src", 3 | "destination": "./docs", 4 | "includes": ["\\.(js|es6)$"], 5 | "excludes": ["\\.config\\.(js|es6)$"], 6 | "access": ["public", "protected"], 7 | "autoPrivate": true, 8 | "unexportIdentifier": false, 9 | "undocumentIdentifier": true, 10 | "builtinExternal": true, 11 | "index": "./README.md", 12 | "package": "./package.json", 13 | "coverage": true, 14 | "includeSource": true, 15 | "title": "electron-compilers", 16 | "plugins": [ 17 | {"name": "esdoc-es7-plugin"}, 18 | {"name": "esdoc-plugin-async-to-sync"} 19 | ], 20 | "test": { 21 | "type": "mocha", 22 | "source": "./test", 23 | "includes": ["\\.(js|es6)$"] 24 | }, 25 | "lint": true 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-remote", 3 | "version": "1.3.0", 4 | "description": "Execute JavaScript in remote Electron processes, but more betterer", 5 | "scripts": { 6 | "doc": "esdoc -c ./esdoc.json", 7 | "compile": "git clean -xdf ./lib && babel -d lib/ src/ && cp ./src/*.html ./lib/", 8 | "prepublish": "npm run compile", 9 | "test-renderer": "electron-mocha --renderer --require ./test/support.js ./test", 10 | "test-browser": "electron-mocha --require ./test/support.js ./test/renderer-require", 11 | "test": "npm run test-renderer && npm run test-browser", 12 | "node": "cross-env ELECTRON_RUN_AS_NODE=1 ./node_modules/electron-prebuilt-compile/node_modules/.bin/electron" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/paulcbetts/electron-remote" 17 | }, 18 | "keywords": [ 19 | "remote", 20 | "electron", 21 | "rx" 22 | ], 23 | "author": "Paul Betts ", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/paulcbetts/electron-remote/issues" 27 | }, 28 | "main": "lib/index.js", 29 | "homepage": "https://github.com/paulcbetts/electron-remote", 30 | "dependencies": { 31 | "debug": "^2.5.1", 32 | "hashids": "^1.1.1", 33 | "lodash.get": "^4.4.2", 34 | "pify": "^2.3.0", 35 | "rxjs": "^5.0.0-beta.12", 36 | "xmlhttprequest": "^1.8.0" 37 | }, 38 | "devDependencies": { 39 | "babel-cli": "^6.16.0", 40 | "babel-eslint": "^7.0.0", 41 | "babel-plugin-transform-async-to-generator": "^6.16.0", 42 | "babel-plugin-transform-runtime": "^6.15.0", 43 | "babel-preset-es2016-node5": "^1.1.2", 44 | "babel-register": "^6.16.3", 45 | "chai": "^3.5.0", 46 | "chai-as-promised": "^6.0.0", 47 | "cross-env": "^3.0.0", 48 | "electron-mocha": "^6.0.3", 49 | "electron-prebuilt-compile": "4.0.0", 50 | "esdoc": "^1.1.0", 51 | "esdoc-es7-plugin": "0.0.3", 52 | "esdoc-plugin-async-to-sync": "^0.5.0", 53 | "eslint": "^3.7.1" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /remote-ajax.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/remote-ajax'); 2 | -------------------------------------------------------------------------------- /src/custom-operators.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rxjs/Observable'; 2 | import {Scheduler} from 'rxjs/Scheduler'; 3 | 4 | import 'rxjs/add/operator/map'; 5 | import 'rxjs/add/operator/switch'; 6 | import 'rxjs/add/observable/timer'; 7 | 8 | const newCoolOperators = { 9 | guaranteedThrottle: function (time, scheduler=Scheduler.timeout) { 10 | return this 11 | .map((x) => Observable.timer(time, scheduler).map(() => x)) 12 | .switch(); 13 | } 14 | }; 15 | 16 | for (let key of Object.keys(newCoolOperators)) { 17 | Observable.prototype[key] = newCoolOperators[key]; 18 | } 19 | -------------------------------------------------------------------------------- /src/execute-js-func.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rxjs/Observable'; 2 | import {Subscription} from 'rxjs/Subscription'; 3 | import Hashids from 'hashids'; 4 | import get from 'lodash.get'; 5 | 6 | import 'rxjs/add/observable/of'; 7 | import 'rxjs/add/observable/throw'; 8 | 9 | import 'rxjs/add/operator/catch'; 10 | import 'rxjs/add/operator/do'; 11 | import 'rxjs/add/operator/filter'; 12 | import 'rxjs/add/operator/take'; 13 | import 'rxjs/add/operator/mergeMap'; 14 | import 'rxjs/add/operator/timeout'; 15 | import 'rxjs/add/operator/toPromise'; 16 | 17 | const requestChannel = 'execute-javascript-request'; 18 | const responseChannel = 'execute-javascript-response'; 19 | const rootEvalProxyName = 'electron-remote-eval-proxy'; 20 | const requireElectronModule = '__requireElectronModule__'; 21 | 22 | const electron = require('electron'); 23 | const isBrowser = (process.type === 'browser'); 24 | const ipc = electron[isBrowser ? 'ipcMain' : 'ipcRenderer']; 25 | 26 | const d = require('debug')('electron-remote:execute-js-func'); 27 | const webContents = isBrowser ? 28 | electron.webContents : 29 | electron.remote.webContents; 30 | 31 | let nextId = 1; 32 | const hashIds = new Hashids(); 33 | 34 | function getNextId() { 35 | return hashIds.encode(process.pid, nextId++); 36 | } 37 | 38 | /** 39 | * Determines the identifier for the current process (i.e. the thing we can use 40 | * to route messages to it) 41 | * 42 | * @return {object} An object with either a `guestInstanceId` or a `webContentsId` 43 | */ 44 | export function getSenderIdentifier() { 45 | if (isBrowser) return {}; 46 | 47 | if (process.guestInstanceId) { 48 | return { guestInstanceId: process.guestInstanceId }; 49 | } 50 | 51 | return { 52 | webContentsId: require('electron').remote.getCurrentWebContents().id 53 | }; 54 | } 55 | 56 | /** 57 | * Determines a way to send a reply back from an incoming eval request. 58 | * 59 | * @param {Object} request An object returned from {getSenderIdentifier} 60 | * 61 | * @return {Function} A function that act like ipc.send, but to a 62 | * particular process. 63 | * 64 | * @private 65 | */ 66 | function getReplyMethod(request) { 67 | let target = findTargetFromParentInfo(request); 68 | 69 | if (target) { 70 | return (...a) => { 71 | if ('isDestroyed' in target && target.isDestroyed()) return; 72 | target.send(...a); 73 | }; 74 | } else { 75 | d("Using reply to main process"); 76 | return (...a) => ipc.send(...a); 77 | } 78 | } 79 | 80 | /** 81 | * Turns an IPC channel into an Observable 82 | * 83 | * @param {String} channel The IPC channel to listen to via `ipc.on` 84 | * 85 | * @return {Observable} An Observable which sends IPC args via `onNext` 86 | * 87 | * @private 88 | */ 89 | function listenToIpc(channel) { 90 | return Observable.create((subj) => { 91 | let listener = (event, ...args) => { 92 | d(`Got an event for ${channel}: ${JSON.stringify(args)}`); 93 | subj.next(args); 94 | }; 95 | 96 | d(`Setting up listener! ${channel}`); 97 | ipc.on(channel, listener); 98 | 99 | return new Subscription(() => 100 | ipc.removeListener(channel, listener)); 101 | }); 102 | } 103 | 104 | /** 105 | * Returns a method that will act like `ipc.send` depending on the parameter 106 | * passed to it, so you don't have to check for `webContents`. 107 | * 108 | * @param {BrowserWindow|WebView} windowOrWebView The renderer to send to. 109 | * 110 | * @return {Function} A function that behaves like 111 | * `ipc.send`. 112 | * 113 | * @private 114 | */ 115 | function getSendMethod(windowOrWebView) { 116 | if (!windowOrWebView) return (...a) => ipc.send(...a); 117 | 118 | if ('webContents' in windowOrWebView) { 119 | return (...a) => { 120 | d(`webContents send: ${JSON.stringify(a)}`); 121 | if (!windowOrWebView.webContents.isDestroyed()) { 122 | windowOrWebView.webContents.send(...a); 123 | } else { 124 | throw new Error(`WebContents has been destroyed`); 125 | } 126 | }; 127 | } else { 128 | return (...a) => { 129 | d(`webView send: ${JSON.stringify(a)}`); 130 | windowOrWebView.send(...a); 131 | }; 132 | } 133 | } 134 | 135 | /** 136 | * This method creates an Observable Promise that represents a future response 137 | * to a remoted call. It filters on ID, then cancels itself once either a 138 | * response is returned, or it times out. 139 | * 140 | * @param {Guid} id The ID of the sent request 141 | * @param {Number} timeout The timeout in milliseconds 142 | * 143 | * @return {Observable} An Observable Promise 144 | * representing the result, or 145 | * an OnError with the error. 146 | * 147 | * @private 148 | */ 149 | function listenerForId(id, timeout) { 150 | return listenToIpc(responseChannel) 151 | .do(([x]) => d(`Got IPC! ${x.id} === ${id}; ${JSON.stringify(x)}`)) 152 | .filter(([receive]) => receive.id === id && id) 153 | .take(1) 154 | .mergeMap(([receive]) => { 155 | if (receive.error) { 156 | let e = new Error(receive.error.message); 157 | e.stack = receive.error.stack; 158 | return Observable.throw(e); 159 | } 160 | 161 | return Observable.of(receive.result); 162 | }) 163 | .timeout(timeout); 164 | } 165 | 166 | 167 | /** 168 | * Given the parentInfo returned from {getSenderIdentifier}, returns the actual 169 | * WebContents that it represents. 170 | * 171 | * @param {object} parentInfo The renderer process identifying info. 172 | * 173 | * @return {WebContents} An actual Renderer Process object. 174 | * 175 | * @private 176 | */ 177 | function findTargetFromParentInfo(parentInfo=window.parentInfo) { 178 | if (!parentInfo) return null; 179 | if ('guestInstanceId' in parentInfo) { 180 | return require('electron').remote.getGuestWebContents(parentInfo.guestInstanceId); 181 | } 182 | 183 | if ('webContentsId' in parentInfo) { 184 | return webContents.fromId(parentInfo.webContentsId); 185 | } 186 | 187 | return null; 188 | } 189 | 190 | /** 191 | * Configures a child renderer process who to send replies to. Call this method 192 | * when you want child windows to be able to use their parent as an implicit 193 | * target. 194 | * 195 | * @param {BrowserWindow|WebView} windowOrWebView The child to configure 196 | */ 197 | export function setParentInformation(windowOrWebView) { 198 | let info = getSenderIdentifier(); 199 | let ret; 200 | 201 | if (info.guestInstanceId) { 202 | ret = remoteEval(windowOrWebView, `window.parentInfo = { guestInstanceId: ${info.guestInstanceId} }`); 203 | } else if (info.webContentsId) { 204 | ret = remoteEval(windowOrWebView, `window.parentInfo = { webContentsId: ${info.webContentsId} }`); 205 | } else { 206 | ret = remoteEval(windowOrWebView, `window.parentInfo = {}`); 207 | } 208 | 209 | return ret.catch((err) => d(`Unable to set parentInfo: ${err.stack || err.message}`)); 210 | } 211 | 212 | /** 213 | * Evaluates a string `eval`-style in a remote renderer process. 214 | * 215 | * @param {BrowserWindow|WebView} windowOrWebView The child to execute code in. 216 | * @param {string} str The code to execute. 217 | * @param {Number} timeout The timeout in milliseconds 218 | * 219 | * @return {Observable} The result of the evaluation. 220 | * Must be JSON-serializable. 221 | */ 222 | export function remoteEvalObservable(windowOrWebView, str, timeout=5*1000) { 223 | let send = getSendMethod(windowOrWebView || findTargetFromParentInfo()); 224 | if (!send) { 225 | return Observable.throw(new Error(`Unable to find a target for: ${JSON.stringify(window.parentInfo)}`)); 226 | } 227 | 228 | if (!str || str.length < 1) { 229 | return Observable.throw(new Error("RemoteEval called with empty or null code")); 230 | } 231 | 232 | let toSend = Object.assign({ id: getNextId(), eval: str }, getSenderIdentifier()); 233 | let ret = listenerForId(toSend.id, timeout); 234 | 235 | d(`Sending: ${JSON.stringify(toSend)}`); 236 | send(requestChannel, toSend); 237 | return ret; 238 | } 239 | 240 | /** 241 | * Evaluates a string `eval`-style in a remote renderer process. 242 | * 243 | * @param {BrowserWindow|WebView} windowOrWebView The child to execute code in. 244 | * @param {string} str The code to execute. 245 | * @param {Number} timeout The timeout in milliseconds 246 | * 247 | * @return {Promise} The result of the evaluation. 248 | * Must be JSON-serializable. 249 | */ 250 | export function remoteEval(windowOrWebView, str, timeout=5*1000) { 251 | return remoteEvalObservable(windowOrWebView, str, timeout).toPromise(); 252 | } 253 | 254 | /** 255 | * Evaluates a JavaScript method on a remote object and returns the result. this 256 | * method can be used to either execute Functions in remote renderers, or return 257 | * values from objects. For example: 258 | * 259 | * let userAgent = await executeJavaScriptMethod(wnd, 'navigator.userAgent'); 260 | * 261 | * executeJavaScriptMethod will also be smart enough to recognize when methods 262 | * themselves return Promises and await them: 263 | * 264 | * let fetchResult = await executeJavaScriptMethod('window.fetchHtml', 'https://google.com'); 265 | * 266 | * @param {BrowserWindow|WebView} windowOrWebView The child to execute code 267 | * in. If this parameter is 268 | * null, this will reference 269 | * the browser process. 270 | * @param {Number} timeout Timeout in milliseconds 271 | * @param {string} pathToObject A path to the object to execute, in dotted 272 | * form i.e. 'document.querySelector'. 273 | * @param {Array} args The arguments to pass to the method 274 | * 275 | * @return {Observable} The result of evaluating the method or 276 | * property. Must be JSON serializable. 277 | */ 278 | export function executeJavaScriptMethodObservable(windowOrWebView, timeout, pathToObject, ...args) { 279 | let send = getSendMethod(windowOrWebView || findTargetFromParentInfo()); 280 | if (!send) { 281 | return Observable.throw(new Error(`Unable to find a target for: ${JSON.stringify(window.parentInfo)}`)); 282 | } 283 | 284 | if (Array.isArray(pathToObject)) { 285 | pathToObject = pathToObject.join('.'); 286 | } 287 | 288 | if (!pathToObject.match(/^[a-zA-Z0-9\._]+$/)) { 289 | return Observable.throw(new Error(`pathToObject must be of the form foo.bar.baz (got ${pathToObject})`)); 290 | } 291 | 292 | let toSend = Object.assign({ args, id: getNextId(), path: pathToObject }, getSenderIdentifier()); 293 | let ret = listenerForId(toSend.id, timeout); 294 | 295 | d(`Sending: ${JSON.stringify(toSend)}`); 296 | send(requestChannel, toSend); 297 | return ret; 298 | } 299 | 300 | 301 | /** 302 | * Evaluates a JavaScript method on a remote object and returns the result. this 303 | * method can be used to either execute Functions in remote renderers, or return 304 | * values from objects. For example: 305 | * 306 | * let userAgent = await executeJavaScriptMethod(wnd, 'navigator.userAgent'); 307 | * 308 | * executeJavaScriptMethod will also be smart enough to recognize when methods 309 | * themselves return Promises and await them: 310 | * 311 | * let fetchResult = await executeJavaScriptMethod('window.fetchHtml', 'https://google.com'); 312 | * 313 | * @param {BrowserWindow|WebView} windowOrWebView The child to execute code 314 | * in. If this parameter is 315 | * null, this will reference 316 | * the browser process. 317 | * @param {string} pathToObject A path to the object to execute, in dotted 318 | * form i.e. 'document.querySelector'. 319 | * @param {Array} args The arguments to pass to the method 320 | * 321 | * @return {Promise} The result of evaluating the method or 322 | * property. Must be JSON serializable. 323 | */ 324 | export function executeJavaScriptMethod(windowOrWebView, pathToObject, ...args) { 325 | return executeJavaScriptMethodObservable(windowOrWebView, 5*1000, pathToObject, ...args).toPromise(); 326 | } 327 | 328 | /** 329 | * Creates an object that is a representation of the remote process's 'window' 330 | * object that allows you to remotely invoke methods. 331 | * 332 | * @param {BrowserWindow|WebView} windowOrWebView The child to execute code 333 | * in. If this parameter is 334 | * null, this will reference 335 | * the browser process. 336 | * @param {number} timeout The timeout to use, defaults to 240sec 337 | * 338 | * @return {Object} A Proxy object that will invoke methods remotely. 339 | * Similar to {executeJavaScriptMethod}, methods will return 340 | * a Promise even if the target method returns a normal 341 | * value. 342 | */ 343 | export function createProxyForRemote(windowOrWebView, timeout=240*1000) { 344 | return RecursiveProxyHandler.create(rootEvalProxyName, (methodChain, args) => { 345 | let chain = methodChain.splice(1); 346 | 347 | d(`Invoking ${chain.join('.')}(${JSON.stringify(args)})`); 348 | return executeJavaScriptMethodObservable(windowOrWebView, timeout, chain, ...args).toPromise(); 349 | }); 350 | } 351 | 352 | /** 353 | * Creates an object that is a representation of a module in the main process, 354 | * but with all of its methods Promisified. 355 | * 356 | * @param {String} moduleName The name of the main process module to proxy 357 | * @returns {Object} A Proxy object that will invoke methods remotely. 358 | * All methods will return a Promise. 359 | */ 360 | export function createProxyForMainProcessModule(moduleName) { 361 | return createProxyForRemote(null)[requireElectronModule][moduleName]; 362 | } 363 | 364 | /** 365 | * Walks the global object hierarchy to resolve the actual object that a dotted 366 | * object path refers to. 367 | * 368 | * @param {string} path A path to the object to execute, in dotted 369 | * form i.e. 'document.querySelector'. 370 | * 371 | * @return {Array} Returns the actual method object and its parent, 372 | * usually a Function and its `this` parameter, as 373 | * `[parent, obj]` 374 | * 375 | * @private 376 | */ 377 | function objectAndParentGivenPath(path) { 378 | let obj = global || window; 379 | let parent = obj; 380 | 381 | for (let part of path.split('.')) { 382 | parent = obj; 383 | obj = obj[part]; 384 | } 385 | 386 | d(`parent: ${parent}, obj: ${obj}`); 387 | if (typeof(parent) !== 'object') { 388 | throw new Error(`Couldn't access part of the object window.${path}`); 389 | } 390 | 391 | return [parent, obj]; 392 | } 393 | 394 | /** 395 | * Given an object path and arguments, actually invokes the method and returns 396 | * the result. This method is run on the target side (i.e. not the one doing 397 | * the invoking). This method tries to figure out the return value of an object 398 | * and do the right thing, including awaiting Promises to get their values. 399 | * 400 | * @param {string} path A path to the object to execute, in dotted 401 | * form i.e. 'document.querySelector'. 402 | * @param {Array} args The arguments to pass to the method 403 | * 404 | * @return {Promise} The result of evaluating path(...args) 405 | * 406 | * @private 407 | */ 408 | async function evalRemoteMethod(path, args) { 409 | let [parent, obj] = objectAndParentGivenPath(path); 410 | 411 | let result = obj; 412 | if (obj && typeof(obj) === 'function') { 413 | d("obj is function!"); 414 | let res = obj.apply(parent, args); 415 | 416 | result = res; 417 | if (typeof(res) === 'object' && res && 'then' in res) { 418 | d("result is Promise!"); 419 | result = await res; 420 | } 421 | } 422 | 423 | return result; 424 | } 425 | 426 | /** 427 | * Invokes a method on a module in the main process. 428 | * 429 | * @param {string} moduleName The name of the module to require 430 | * @param {Array} methodChain The path to the module, e.g., ['dock', 'bounce'] 431 | * @param {Array} args The arguments to pass to the method 432 | * 433 | * @returns The result of calling the method 434 | * 435 | * @private 436 | */ 437 | function executeMainProcessMethod(moduleName, methodChain, args) { 438 | const theModule = electron[moduleName]; 439 | const path = methodChain.join('.'); 440 | return get(theModule, path).apply(theModule, args); 441 | } 442 | 443 | /** 444 | * Initializes the IPC listener that {executeJavaScriptMethod} will send IPC 445 | * messages to. You need to call this method in any process that you want to 446 | * execute remote methods on. 447 | * 448 | * @return {Subscription} An object that you can call `unsubscribe` on to clean up 449 | * the listener early. Usually not necessary. 450 | */ 451 | export function initializeEvalHandler() { 452 | let listener = async function(e, receive) { 453 | d(`Got Message! ${JSON.stringify(receive)}`); 454 | let send = getReplyMethod(receive); 455 | 456 | try { 457 | if (receive.eval) { 458 | receive.result = eval(receive.eval); 459 | } else { 460 | const parts = receive.path.split('.'); 461 | if (parts.length > 1 && parts[0] === requireElectronModule) { 462 | receive.result = executeMainProcessMethod(parts[1], parts.splice(2), receive.args); 463 | } else { 464 | receive.result = await evalRemoteMethod(receive.path, receive.args); 465 | } 466 | } 467 | 468 | d(`Replying! ${JSON.stringify(receive)} - ID is ${e.sender}`); 469 | send(responseChannel, receive); 470 | } catch(err) { 471 | receive.error = { 472 | message: err.message, 473 | stack: err.stack 474 | }; 475 | 476 | d(`Failed! ${JSON.stringify(receive)}`); 477 | send(responseChannel, receive); 478 | } 479 | }; 480 | 481 | d("Set up listener!"); 482 | ipc.on('execute-javascript-request', listener); 483 | 484 | return new Subscription(() => ipc.removeListener('execute-javascript-request', listener)); 485 | } 486 | 487 | const emptyFn = function() {}; 488 | 489 | /** 490 | * RecursiveProxyHandler is a ES6 Proxy Handler object that intercepts method 491 | * invocations and returns the full object that was invoked. So this means, if you 492 | * get a proxy, then execute `foo.bar.bamf(5)`, you'll recieve a callback with 493 | * the parameters "foo.bar.bamf" as a string, and [5]. 494 | */ 495 | export class RecursiveProxyHandler { 496 | /** 497 | * Creates a new RecursiveProxyHandler. Don't use this, use `create` 498 | * 499 | * @private 500 | */ 501 | constructor(name, methodHandler, parent=null, overrides=null) { 502 | this.name = name; 503 | this.proxies = {}; 504 | this.methodHandler = methodHandler; 505 | this.parent = parent; 506 | this.overrides = overrides; 507 | } 508 | 509 | /** 510 | * Creates an ES6 Proxy which is handled by RecursiveProxyHandler. 511 | * 512 | * @param {string} name The root object name 513 | * @param {Function} methodHandler The Function to handle method invocations - 514 | * this method will receive an Array of 515 | * object names which will point to the Function 516 | * on the Proxy being invoked. 517 | * 518 | * @param {Object} overrides An optional object that lets you directly 519 | * include functions on the top-level object, its 520 | * keys are key names for the property, and 521 | * the values are what the key on the property 522 | * should return. 523 | * 524 | * @return {Proxy} An ES6 Proxy object that uses 525 | * RecursiveProxyHandler. 526 | */ 527 | static create(name, methodHandler, overrides=null) { 528 | return new Proxy(emptyFn, new RecursiveProxyHandler(name, methodHandler, null, overrides)); 529 | } 530 | 531 | /** 532 | * The {get} ES6 Proxy handler. 533 | * 534 | * @private 535 | */ 536 | get(target, prop) { 537 | if (this.overrides && prop in this.overrides) { 538 | return this.overrides[prop]; 539 | } 540 | 541 | return new Proxy(emptyFn, this.getOrCreateProxyHandler(prop)); 542 | } 543 | 544 | /** 545 | * The {apply} ES6 Proxy handler. 546 | * 547 | * @private 548 | */ 549 | apply(target, thisArg, argList) { 550 | let methodChain = [this.replaceGetterWithName(this.name)]; 551 | let iter = this.parent; 552 | 553 | while (iter) { 554 | methodChain.unshift(iter.name); 555 | iter = iter.parent; 556 | } 557 | 558 | return this.methodHandler(methodChain, argList); 559 | } 560 | 561 | /** 562 | * Creates a proxy for a returned `get` call. 563 | * 564 | * @param {string} name The property name 565 | * @return {RecursiveProxyHandler} 566 | * 567 | * @private 568 | */ 569 | getOrCreateProxyHandler(name) { 570 | let ret = this.proxies[name]; 571 | if (ret) return ret; 572 | 573 | ret = new RecursiveProxyHandler(name, this.methodHandler, this); 574 | this.proxies[name] = ret; 575 | return ret; 576 | } 577 | 578 | /** 579 | * Because we don't support directly getting values by-name, we convert any 580 | * call of the form "getXyz" into a call for the value 'xyz' 581 | * 582 | * @return {string} The name of the actual method or property to evaluate. 583 | * @private 584 | */ 585 | replaceGetterWithName(name) { 586 | return name.replace(/_get$/, ''); 587 | } 588 | } 589 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import * as executeJsFunc from './execute-js-func'; 2 | import * as rendererRequire from './renderer-require'; 3 | import * as remoteEvent from './remote-event'; 4 | 5 | const executeJsFuncExports = [ 6 | 'createProxyForRemote', 7 | 'createProxyForMainProcessModule', 8 | 'getSenderIdentifier', 9 | 'executeJavaScriptMethodObservable', 10 | 'executeJavaScriptMethod', 11 | 'initializeEvalHandler', 12 | 'remoteEvalObservable', 13 | 'remoteEval', 14 | 'setParentInformation', 15 | 'RecursiveProxyHandler' 16 | ]; 17 | 18 | module.exports = Object.assign( 19 | executeJsFuncExports.reduce((acc, x) => { acc[x] = executeJsFunc[x]; return acc; }, {}), 20 | rendererRequire, 21 | remoteEvent 22 | ); 23 | -------------------------------------------------------------------------------- /src/remote-ajax.js: -------------------------------------------------------------------------------- 1 | import promisify from 'pify'; 2 | 3 | import 'rxjs/add/operator/toPromise'; 4 | 5 | const rx = require('./rx-dom'); 6 | 7 | const toInclude = ['ajax', 'get', 'getJSON', 'post']; 8 | const fs = promisify(require('fs')); 9 | 10 | if (!('type' in process)) { 11 | global.XMLHttpRequest = require('xmlhttprequest').XMLHttpRequest; 12 | } 13 | 14 | module.exports = toInclude.reduce((acc, k) => { 15 | acc[k] = (...args) => { 16 | let stall = Promise.resolve(true); 17 | if (!root.window || !root.window.document) { 18 | stall = new Promise((res) => setTimeout(res, 100)); 19 | } 20 | 21 | return stall.then(() => rx[k](...args).toPromise()); 22 | }; 23 | 24 | return acc; 25 | }, {}); 26 | 27 | let isHttpUrl = (pathOrUrl) => pathOrUrl.match(/^https?:\/\//i); 28 | 29 | /** 30 | * Fetches a path as either a file path or a HTTP URL. 31 | * 32 | * @param {string} pathOrUrl Either an HTTP URL or a file path. 33 | * @return {string} The contents as a UTF-8 decoded string. 34 | */ 35 | module.exports.fetchFileOrUrl = async function(pathOrUrl) { 36 | if (!isHttpUrl(pathOrUrl)) { 37 | return await fs.readFile(pathOrUrl, 'utf8'); 38 | } 39 | 40 | let ret = await module.exports.get(pathOrUrl); 41 | return ret.response; 42 | }; 43 | 44 | /** 45 | * Downloads a path as either a file path or a HTTP URL to a specific place 46 | * 47 | * @param {string} pathOrUrl Either an HTTP URL or a file path. 48 | * @return {string} The contents as a UTF-8 decoded string. 49 | */ 50 | module.exports.downloadFileOrUrl = async function(pathOrUrl, target) { 51 | if (!isHttpUrl(pathOrUrl)) { 52 | try { 53 | let buf = await fs.readFile(pathOrUrl); 54 | await fs.writeFile(target, buf); 55 | 56 | return buf.length; 57 | } catch (e) { 58 | return rx.Observable.throw(e); 59 | } 60 | } 61 | 62 | let response = await window.fetch(pathOrUrl, { 63 | method: 'GET', 64 | cache: 'no-store', 65 | redirect: 'follow' 66 | }); 67 | 68 | let fd = await fs.open(target, 'w'); 69 | let length = 0; 70 | try { 71 | let reader = await response.body.getReader(); 72 | let chunk = await reader.read(); 73 | 74 | while (!chunk.done) { 75 | let buf = new Buffer(new Uint8Array(chunk.value)); 76 | await fs.write(fd, buf, 0, buf.length); 77 | length += buf.length; 78 | 79 | chunk = await reader.read(); 80 | } 81 | 82 | // Write out the last chunk 83 | if (chunk.value && chunk.value.length > 0) { 84 | let buf = new Buffer(new Uint8Array(chunk.value)); 85 | await fs.write(fd, buf, 0, buf.length); 86 | length += buf.length; 87 | } 88 | } finally { 89 | await fs.close(fd); 90 | } 91 | 92 | if (!response.ok) { 93 | throw new Error(`HTTP request returned error: ${response.status}: ${response.statusText}`); 94 | } 95 | 96 | return length; 97 | }; 98 | -------------------------------------------------------------------------------- /src/remote-event-browser.js: -------------------------------------------------------------------------------- 1 | import {BrowserWindow, webContents, ipcMain} from 'electron'; 2 | import {Observable} from 'rxjs/Observable'; 3 | 4 | import 'rxjs/add/observable/fromEvent'; 5 | 6 | import 'rxjs/add/operator/do'; 7 | import 'rxjs/add/operator/takeUntil'; 8 | 9 | const eventListenerTable = {}; 10 | const d = require('debug')('remote-event-browser'); 11 | 12 | function initialize() { 13 | d('Initializing browser-half of remote-event'); 14 | 15 | ipcMain.on('electron-remote-event-subscribe', (e, x) => { 16 | const {type, id, event, onWebContents} = x; 17 | let target = null; 18 | 19 | switch(type) { 20 | case 'window': 21 | target = BrowserWindow.fromId(id); 22 | break; 23 | case 'webcontents': 24 | target = webContents.fromId(id); 25 | break; 26 | default: 27 | target = null; 28 | } 29 | 30 | if (!target) { 31 | e.returnValue = {error: `Failed to find ${type} with ID ${id}`}; 32 | d(e.returnValue.error); 33 | return; 34 | } 35 | 36 | const key = `electron-remote-event-${type}-${id}-${event}-${e.sender.id}`; 37 | if (eventListenerTable[key]) { 38 | d(`Using existing key ${key} in eventListenerTable`); 39 | eventListenerTable[key].refCount++; 40 | e.returnValue = {error: null}; 41 | return; 42 | } 43 | 44 | let targetWebContents = e.sender; 45 | 46 | d(`Creating new event subscription with key ${key}: ${event}`); 47 | d(JSON.stringify(Object.keys(target))); 48 | 49 | eventListenerTable[key] = { 50 | refCount: 1, 51 | subscription: Observable.fromEvent(onWebContents ? target.webContents : target, event, (...args) => [args]) 52 | .do(() => d(`Got event on browser side: ${key}`)) 53 | .takeUntil(Observable.fromEvent(targetWebContents, 'destroyed')) 54 | .subscribe((args) => targetWebContents.send(key, args)) 55 | }; 56 | 57 | e.returnValue = {error: null}; 58 | }); 59 | 60 | ipcMain.on('electron-remote-event-unsubscribe', (e, key) => { 61 | let k = eventListenerTable[key]; 62 | if (!k) { 63 | d(`*** Tried to release missing key! ${key}`); 64 | return; 65 | } 66 | 67 | k.refCount--; 68 | if (k.refCount <= 0) { 69 | d(`Disposing key: ${key}`); 70 | 71 | delete eventListenerTable[key]; 72 | k.subscription.unsubscribe(); 73 | } 74 | }); 75 | } 76 | 77 | initialize(); 78 | -------------------------------------------------------------------------------- /src/remote-event.js: -------------------------------------------------------------------------------- 1 | import {remote, ipcRenderer} from 'electron'; 2 | 3 | import {Observable} from 'rxjs/Observable'; 4 | import {Subscription} from 'rxjs/Subscription'; 5 | 6 | import 'rxjs/add/observable/throw'; 7 | import 'rxjs/add/observable/fromEvent'; 8 | 9 | import 'rxjs/add/operator/do'; 10 | import 'rxjs/add/operator/publish'; 11 | 12 | const isBrowser = process.type === 'browser'; 13 | 14 | if (!isBrowser) { 15 | remote.require(require.resolve('./remote-event-browser')); 16 | } 17 | 18 | const d = require('debug')('remote-event'); 19 | 20 | /** 21 | * Safely subscribes to an event on a BrowserWindow or its WebContents. This 22 | * method avoids the "remote event listener" Electron issue. 23 | * 24 | * @param browserWindow BrowserWindow - the window to listen to 25 | * @param event String - The event to listen to 26 | * @param onWebContents Boolean - If true, the event is on the window's 27 | * WebContents, not on the window itself. 28 | * 29 | * @returns Observable - an Observable representing the event. 30 | * Unsubscribing from the Observable will 31 | * remove the event listener. 32 | */ 33 | export function fromRemoteWindow(browserWindowOrWebView, event, onWebContents=false) { 34 | let ctorName = Object.getPrototypeOf(browserWindowOrWebView).constructor.name; 35 | 36 | if (isBrowser) { 37 | if (onWebContents) { 38 | let wc; 39 | if (ctorName === 'WebContents') { 40 | wc = browserWindowOrWebView; 41 | } else { 42 | wc = ('webContents' in browserWindowOrWebView ? browserWindowOrWebView.webContents : browserWindowOrWebView.getWebContents()); 43 | } 44 | return Observable.fromEvent(wc, event, (...args) => args); 45 | } else { 46 | Observable.fromEvent(browserWindowOrWebView, event, (...args) => args); 47 | } 48 | } 49 | 50 | 51 | if ((ctorName === 'webview' || ctorName === 'WebContents') && !onWebContents) { 52 | throw new Error("WebViews and WebContents can only be used with onWebContents=true"); 53 | } 54 | 55 | let type = onWebContents ? 'webcontents' : 'window'; 56 | let id; 57 | if (onWebContents) { 58 | if (ctorName === 'WebContents') { 59 | id = browserWindowOrWebView.id; 60 | } else { 61 | id = ('webContents' in browserWindowOrWebView ? browserWindowOrWebView.webContents : browserWindowOrWebView.getWebContents()).id; 62 | } 63 | } else { 64 | id = browserWindowOrWebView.id; 65 | } 66 | 67 | const key = `electron-remote-event-${type}-${id}-${event}-${remote.getCurrentWebContents().id}`; 68 | 69 | d(`Subscribing to event with key: ${key}`); 70 | let {error} = ipcRenderer.sendSync( 71 | 'electron-remote-event-subscribe', 72 | {type, id, event, onWebContents}); 73 | 74 | if (error) { 75 | d(`Failed with error: ${error}`); 76 | return Observable.throw(new Error(error)); 77 | } 78 | 79 | let ret = Observable.create((subj) => { 80 | let disp = new Subscription(); 81 | disp.add( 82 | Observable.fromEvent(ipcRenderer, key, (e,arg) => arg) 83 | .do(() => d(`Got event: ${key}`)) 84 | .subscribe(subj)); 85 | 86 | disp.add(new Subscription(() => { 87 | d(`Got event: ${key}`); 88 | ipcRenderer.send('electron-remote-event-unsubscribe', key); 89 | })); 90 | 91 | return disp; 92 | }); 93 | 94 | return ret.publish().refCount(); 95 | } 96 | -------------------------------------------------------------------------------- /src/renderer-require-preload.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /src/renderer-require-preload.js: -------------------------------------------------------------------------------- 1 | import {initializeEvalHandler} from './execute-js-func'; 2 | import url from 'url'; 3 | 4 | initializeEvalHandler(); 5 | 6 | url.parse(window.location.href); 7 | let escapedModule = url.parse(window.location.href).query.split('=')[1]; 8 | try { 9 | window.requiredModule = require(decodeURIComponent(escapedModule)); 10 | } catch (e) { 11 | window.moduleLoadFailure = e; 12 | } 13 | -------------------------------------------------------------------------------- /src/renderer-require.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import {fromRemoteWindow} from './remote-event'; 3 | 4 | import {AsyncSubject} from 'rxjs/AsyncSubject'; 5 | import {Observable} from 'rxjs/Observable'; 6 | import {Subject} from 'rxjs/Subject'; 7 | 8 | import 'rxjs/add/observable/merge'; 9 | import 'rxjs/add/observable/throw'; 10 | import 'rxjs/add/observable/fromPromise'; 11 | import 'rxjs/add/observable/of'; 12 | import 'rxjs/add/observable/defer'; 13 | 14 | import 'rxjs/add/operator/map'; 15 | import 'rxjs/add/operator/mergeAll'; 16 | import 'rxjs/add/operator/mergeMap'; 17 | import 'rxjs/add/operator/multicast'; 18 | import 'rxjs/add/operator/take'; 19 | import 'rxjs/add/operator/toPromise'; 20 | 21 | import {createProxyForRemote, executeJavaScriptMethod, executeJavaScriptMethodObservable, RecursiveProxyHandler} from './execute-js-func'; 22 | 23 | import './custom-operators'; 24 | 25 | const d = require('debug')('electron-remote:renderer-require'); 26 | 27 | const BrowserWindow = process.type === 'renderer' ? 28 | require('electron').remote.BrowserWindow : 29 | require('electron').BrowserWindow; 30 | 31 | /** 32 | * Creates a BrowserWindow, requires a module in it, then returns a Proxy 33 | * object that will call into it. You probably want to use {requireTaskPool} 34 | * instead. 35 | * 36 | * @param {string} modulePath The path of the module to include. 37 | * @param {number} timeout The timeout to use, defaults to 240sec 38 | * @return {Object} Returns an Object with a `module` which is a Proxy 39 | * object, and a `unsubscribe` method that will clean up 40 | * the window. 41 | */ 42 | export async function rendererRequireDirect(modulePath, timeout=240*1000) { 43 | let bw = new BrowserWindow({width: 500, height: 500, show: false}); 44 | let fullPath = require.resolve(modulePath); 45 | 46 | let ready = Observable.merge( 47 | fromRemoteWindow(bw, 'did-finish-load', true), 48 | fromRemoteWindow(bw, 'did-fail-load', true) 49 | .filter(([, errCode]) => errCode !== 0) 50 | .mergeMap(([, , errMsg]) => Observable.throw(new Error(errMsg))) 51 | ).take(1).toPromise(); 52 | 53 | /* Uncomment for debugging! 54 | bw.show(); 55 | bw.openDevTools(); 56 | */ 57 | 58 | let preloadFile = path.join(__dirname, 'renderer-require-preload.html'); 59 | bw.loadURL(`file:///${preloadFile}?module=${encodeURIComponent(fullPath)}`); 60 | await ready; 61 | 62 | let fail = await executeJavaScriptMethod(bw, 'window.moduleLoadFailure'); 63 | if (fail) { 64 | let msg = await executeJavaScriptMethod(bw, 'window.moduleLoadFailure.message'); 65 | throw new Error(msg); 66 | } 67 | 68 | return { 69 | module: createProxyForRemote(bw).requiredModule, 70 | executeJavaScriptMethod: (chain, ...args) => executeJavaScriptMethodObservable(bw, timeout, chain, ...args).toPromise(), 71 | executeJavaScriptMethodObservable: (chain, ...args) => executeJavaScriptMethodObservable(bw, timeout, chain, ...args), 72 | unsubscribe: () => bw.isDestroyed() ? bw.destroy() : bw.close() 73 | }; 74 | } 75 | 76 | /** 77 | * requires a module in BrowserWindows that are created/destroyed as-needed, and 78 | * returns a Proxy object that will secretly marshal invocations to other processes 79 | * and marshal back the result. This is the cool method in this library. 80 | * 81 | * Note that since the global context is created / destroyed, you *cannot* rely 82 | * on module state (i.e. global variables) to be consistent 83 | * 84 | * @param {string} modulePath The path to the module. You may have to 85 | * `require.resolve` it. 86 | * @param {Number} maxConcurrency The maximum number of concurrent processes 87 | * to run. Defaults to 4. 88 | * @param {Number} idleTimeout The amount of time to wait before closing 89 | * a BrowserWindow as idle, in ms 90 | * @param {Number} methodTimeout The amount of time to wait before a method 91 | * fails, in ms 92 | * 93 | * @return {Proxy} An ES6 Proxy object representing the module. 94 | */ 95 | export function requireTaskPool(modulePath, maxConcurrency=4, idleTimeout=5*1000, methodTimeout=240*1000) { 96 | return new RendererTaskpoolItem(modulePath, maxConcurrency, idleTimeout, methodTimeout).moduleProxy; 97 | } 98 | 99 | /** 100 | * This class implements the scheduling logic for queuing and dispatching method 101 | * invocations to various background windows. It is complicated. But in like, 102 | * a cool way. 103 | */ 104 | class RendererTaskpoolItem { 105 | constructor(modulePath, maxConcurrency, idleTimeout, methodTimeout) { 106 | const freeWindowList = []; 107 | const invocationQueue = new Subject(); 108 | const completionQueue = new Subject(); 109 | 110 | // This method will find a window that is currently idle or if it doesn't 111 | // exist, create one. 112 | const getOrCreateWindow = () => { 113 | let item = freeWindowList.pop(); 114 | if (item) return Observable.of(item); 115 | 116 | return Observable.fromPromise(rendererRequireDirect(modulePath, methodTimeout)); 117 | }; 118 | 119 | // Here, we set up a pipeline that maps a stream of invocations (i.e. 120 | // something we can pass to executeJavaScriptMethod) => stream of Future 121 | // Results from various windows => Stream of completed results, for which we 122 | // throw the Window that completed the result back onto the free window stack. 123 | invocationQueue 124 | .map(({chain, args, retval}) => Observable.defer(() => { 125 | return getOrCreateWindow() 126 | .mergeMap((wnd) => { 127 | d(`Actually invoking ${chain.join('.')}(${JSON.stringify(args)})`); 128 | let ret = wnd.executeJavaScriptMethodObservable(chain, ...args); 129 | 130 | ret.multicast(retval).connect(); 131 | return ret.map(() => wnd).catch(() => Observable.of(wnd)); 132 | }); 133 | })) 134 | .mergeAll(maxConcurrency) 135 | .subscribe((wnd) => { 136 | if (!wnd || !wnd.unsubscribe) throw new Error("Bogus!"); 137 | freeWindowList.push(wnd); 138 | completionQueue.next(true); 139 | }); 140 | 141 | // Here, we create a version of RecursiveProxyHandler that will turn method 142 | // invocations into something we can push onto our invocationQueue pipeline. 143 | // This is the object that ends up being returned to the caller of 144 | // requireTaskPool. 145 | this.moduleProxy = RecursiveProxyHandler.create('__removeme__', (methodChain, args) => { 146 | let chain = methodChain.splice(1); 147 | 148 | d(`Queuing ${chain.join('.')}(${JSON.stringify(args)})`); 149 | let retval = new AsyncSubject(); 150 | 151 | invocationQueue.next({ chain: ['requiredModule'].concat(chain), args, retval }); 152 | return retval.toPromise(); 153 | }); 154 | 155 | // If we haven't received any invocations within a certain idle timeout 156 | // period, burn all of our BrowserWindow instances 157 | completionQueue.guaranteedThrottle(idleTimeout).subscribe(() => { 158 | d(`Freeing ${freeWindowList.length} taskpool processes`); 159 | while (freeWindowList.length > 0) { 160 | let wnd = freeWindowList.pop(); 161 | if (wnd) wnd.unsubscribe(); 162 | } 163 | }); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/rx-dom.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rxjs/Observable'; 2 | import 'rxjs/add/operator/map'; 3 | 4 | // Gets the proper XMLHttpRequest for support for older IE 5 | function getXMLHttpRequest() { 6 | if (root.XMLHttpRequest) { 7 | return new root.XMLHttpRequest(); 8 | } else { 9 | var progId; 10 | try { 11 | var progIds = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0']; 12 | for(var i = 0; i < 3; i++) { 13 | try { 14 | progId = progIds[i]; 15 | if (new root.ActiveXObject(progId)) { 16 | break; 17 | } 18 | } catch(e) { } 19 | } 20 | return new root.ActiveXObject(progId); 21 | } catch (e) { 22 | throw new Error('XMLHttpRequest is not supported by your browser'); 23 | } 24 | } 25 | } 26 | 27 | // Get CORS support even for older IE 28 | function getCORSRequest() { 29 | var xhr = new root.XMLHttpRequest(); 30 | if ('withCredentials' in xhr) { 31 | xhr.withCredentials = this.withCredentials ? true : false; 32 | return xhr; 33 | } else if (!!root.XDomainRequest) { 34 | return new XDomainRequest(); 35 | } else { 36 | throw new Error('CORS is not supported by your browser'); 37 | } 38 | } 39 | 40 | function normalizeAjaxSuccessEvent(e, xhr, settings) { 41 | var response = ('response' in xhr) ? xhr.response : xhr.responseText; 42 | response = settings.responseType === 'json' ? JSON.parse(response) : response; 43 | return { 44 | response: response, 45 | status: xhr.status, 46 | responseType: xhr.responseType, 47 | xhr: xhr, 48 | originalEvent: e 49 | }; 50 | } 51 | 52 | function normalizeAjaxErrorEvent(e, xhr, type) { 53 | return { 54 | type: type, 55 | status: xhr.status, 56 | xhr: xhr, 57 | originalEvent: e 58 | }; 59 | } 60 | 61 | function AjaxDisposable(state, xhr) { 62 | this._state = state; 63 | this._xhr = xhr; 64 | this.isDisposed = false; 65 | } 66 | 67 | AjaxDisposable.prototype.unsubscribe = function () { 68 | if (!this.isDisposed) { 69 | this.isDisposed = true; 70 | if (!this._state.isDone && this._xhr.readyState !== 4) { this._xhr.abort(); } 71 | } 72 | }; 73 | 74 | function createAjaxObservable(settings) { 75 | return Observable.create((o) => { 76 | var state = { isDone: false }; 77 | var xhr; 78 | 79 | var normalizeError = settings.normalizeError; 80 | var normalizeSuccess = settings.normalizeSuccess; 81 | 82 | var processResponse = function(xhr, e){ 83 | var status = xhr.status === 1223 ? 204 : xhr.status; 84 | if ((status >= 200 && status <= 300) || status === 0 || status === '') { 85 | o.next(normalizeSuccess(e, xhr, settings)); 86 | o.complete(); 87 | } else { 88 | o.error(settings.normalizeError(e, xhr, 'error')); 89 | } 90 | state.isDone = true; 91 | }; 92 | 93 | try { 94 | xhr = settings.createXHR(); 95 | } catch (err) { 96 | return o.error(err); 97 | } 98 | 99 | try { 100 | if (settings.user) { 101 | xhr.open(settings.method, settings.url, settings.async, settings.user, settings.password); 102 | } else { 103 | xhr.open(settings.method, settings.url, settings.async); 104 | } 105 | 106 | if (settings.responseType === 'blob') { 107 | xhr.responseType = 'blob'; 108 | } 109 | 110 | var headers = settings.headers; 111 | for (var header in headers) { 112 | if (hasOwnProperty.call(headers, header)) { 113 | xhr.setRequestHeader(header, headers[header]); 114 | } 115 | } 116 | 117 | xhr.timeout = settings.timeout; 118 | xhr.ontimeout = function (e) { 119 | settings.progressObserver && settings.progressObserver.error(e); 120 | o.error(normalizeError(e, xhr, 'timeout')); 121 | }; 122 | 123 | if(!!xhr.upload || (!('withCredentials' in xhr) && !!root.XDomainRequest)) { 124 | xhr.onload = function(e) { 125 | if(settings.progressObserver) { 126 | settings.progressObserver.next(e); 127 | settings.progressObserver.complete(); 128 | } 129 | processResponse(xhr, e); 130 | }; 131 | 132 | if(settings.progressObserver) { 133 | xhr.onprogress = function(e) { 134 | settings.progressObserver.next(e); 135 | }; 136 | } 137 | 138 | xhr.onerror = function(e) { 139 | settings.progressObserver && settings.progressObserver.error(e); 140 | o.error(normalizeError(e, xhr, 'error')); 141 | state.isDone = true; 142 | }; 143 | 144 | xhr.onabort = function(e) { 145 | settings.progressObserver && settings.progressObserver.error(e); 146 | o.error(normalizeError(e, xhr, 'abort')); 147 | state.isDone = true; 148 | }; 149 | } else { 150 | xhr.onreadystatechange = function (e) { 151 | xhr.readyState === 4 && processResponse(xhr, e); 152 | }; 153 | } 154 | 155 | var contentType = settings.headers['Content-Type'] || 156 | settings.headers['Content-type'] || 157 | settings.headers['content-type']; 158 | if (settings.hasContent && contentType === 'application/x-www-form-urlencoded' && typeof settings.body !== 'string') { 159 | var newBody = []; 160 | for (var prop in settings.body) { 161 | if (hasOwnProperty.call(settings.body, prop)) { 162 | newBody.push(prop + '=' + settings.body[prop]); 163 | } 164 | } 165 | settings.body = newBody.join('&'); 166 | } 167 | 168 | xhr.send(settings.hasContent && settings.body || null); 169 | } catch (e) { 170 | o.error(e); 171 | } 172 | 173 | return new AjaxDisposable(state, xhr); 174 | }); 175 | } 176 | 177 | /** 178 | * Creates an observable for an Ajax request with either a settings object with url, headers, etc or a string for a URL. 179 | * 180 | * @example 181 | * source = Rx.DOM.ajax('/products'); 182 | * source = Rx.DOM.ajax( url: 'products', method: 'GET' }); 183 | * 184 | * @param {Object} settings Can be one of the following: 185 | * 186 | * A string of the URL to make the Ajax call. 187 | * An object with the following properties 188 | * - url: URL of the request 189 | * - body: The body of the request 190 | * - method: Method of the request, such as GET, POST, PUT, PATCH, DELETE 191 | * - async: Whether the request is async 192 | * - headers: Optional headers 193 | * - crossDomain: true if a cross domain request, else false 194 | * 195 | * @returns {Observable} An observable sequence containing the XMLHttpRequest. 196 | */ 197 | export function ajax(options) { 198 | var settings = { 199 | method: 'GET', 200 | crossDomain: false, 201 | async: true, 202 | headers: {}, 203 | responseType: 'text', 204 | timeout: 0, 205 | withCredentials: false, 206 | createXHR: function() { 207 | return this.crossDomain ? getCORSRequest.call(this) : getXMLHttpRequest(); 208 | }, 209 | normalizeError: normalizeAjaxErrorEvent, 210 | normalizeSuccess: normalizeAjaxSuccessEvent 211 | }; 212 | 213 | if(typeof options === 'string') { 214 | settings.url = options; 215 | } else { 216 | for(var prop in options) { 217 | if(hasOwnProperty.call(options, prop)) { 218 | settings[prop] = options[prop]; 219 | } 220 | } 221 | } 222 | 223 | if (!settings.crossDomain && !settings.headers['X-Requested-With']) { 224 | settings.headers['X-Requested-With'] = 'XMLHttpRequest'; 225 | } 226 | settings.hasContent = settings.body !== undefined; 227 | 228 | return createAjaxObservable(settings); 229 | } 230 | 231 | var ajaxRequest = ajax; 232 | 233 | /** 234 | * Creates an observable sequence from an Ajax POST Request with the body. 235 | * 236 | * @param {String} url The URL to POST 237 | * @param {Object} body The body to POST 238 | * @returns {Observable} The observable sequence which contains the response from the Ajax POST. 239 | */ 240 | export function post(url, body) { 241 | var settings; 242 | if (typeof url === 'string') { 243 | settings = {url: url, body: body, method: 'POST' }; 244 | } else if (typeof url === 'object') { 245 | settings = url; 246 | settings.method = 'POST'; 247 | } 248 | return ajaxRequest(settings); 249 | } 250 | 251 | /** 252 | * Creates an observable sequence from an Ajax GET Request with the body. 253 | * 254 | * @param {String} url The URL to GET 255 | * @returns {Observable} The observable sequence which contains the response from the Ajax GET. 256 | */ 257 | export function get(url) { 258 | var settings; 259 | if (typeof url === 'string') { 260 | settings = {url: url }; 261 | } else if (typeof url === 'object') { 262 | settings = url; 263 | } 264 | return ajaxRequest(settings); 265 | } 266 | 267 | /** 268 | * Creates an observable sequence from JSON from an Ajax request 269 | * 270 | * @param {String} url The URL to GET 271 | * @returns {Observable} The observable sequence which contains the parsed JSON. 272 | */ 273 | export function getJSON(url) { 274 | if (!root.JSON && typeof root.JSON.parse !== 'function') { throw new TypeError('JSON is not supported in your runtime.'); } 275 | return ajaxRequest({url: url, responseType: 'json'}).map(function (x) { 276 | return x.response; 277 | }); 278 | } 279 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "rules": { 4 | "strict": 0, 5 | "indent": [ 6 | 2, 7 | 2 8 | ], 9 | "semi": [ 10 | 2, 11 | "always" 12 | ], 13 | "no-console": 0 14 | }, 15 | "env": { 16 | "es6": true, 17 | "node": true, 18 | "browser": true, 19 | "mocha": true 20 | }, 21 | "globals": { 22 | "expect": true, 23 | "chai": true, 24 | }, 25 | "extends": "eslint:recommended" 26 | } 27 | -------------------------------------------------------------------------------- /test/asserttest.js: -------------------------------------------------------------------------------- 1 | import './support'; 2 | 3 | function delay(ms) { 4 | return new Promise((resolve) => { 5 | setTimeout(resolve, ms); 6 | }); 7 | } 8 | 9 | describe('The test runner', function() { 10 | it('should pass this test', async function() { 11 | await delay(1000); 12 | expect(true).to.be.ok; 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /test/dummy-module.js: -------------------------------------------------------------------------------- 1 | export const dummyVal = 42; 2 | 3 | export function dummyFunc() { 4 | return 42; 5 | } 6 | 7 | export function dummyBuffer() { 8 | return new Buffer([1,2,3,4,5,6,7,8]); 9 | } 10 | -------------------------------------------------------------------------------- /test/recursive-proxy-handler.js: -------------------------------------------------------------------------------- 1 | import {RecursiveProxyHandler} from '../src/execute-js-func'; 2 | 3 | describe('RecursiveProxyHandler', function() { 4 | it('should let me apply a function', function() { 5 | var proxy = RecursiveProxyHandler.create('proxy', (names, args) => { 6 | console.log(`${JSON.stringify(names)} - ${JSON.stringify(args)}`); 7 | }); 8 | 9 | let baz = proxy.foo.bar.baz(1,2,3); 10 | }); 11 | 12 | it('tests ES6 proxies', function() { 13 | let createHandler = () => { 14 | return { 15 | get: function(target, prop) { 16 | console.log('Get!'); 17 | return new Proxy(function() {}, createHandler()); 18 | }, 19 | 20 | apply: function(target, thisArg, argList) { 21 | console.log("Apply!"); 22 | console.log(JSON.stringify(argList)); 23 | } 24 | } 25 | }; 26 | 27 | var foo = new Proxy({}, createHandler()); 28 | foo.bar.baz(1,2,3,4); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test/remote-event.js: -------------------------------------------------------------------------------- 1 | import {remote} from 'electron'; 2 | import {fromRemoteWindow} from '../src/remote-event'; 3 | 4 | const {BrowserWindow} = remote; 5 | 6 | describe('fromRemoteWindow', function() { 7 | this.timeout(10*1000); 8 | 9 | it('should get the ready-to-show event', async function() { 10 | let bw = new BrowserWindow({width: 500, height: 500, show: false}); 11 | 12 | let finished = fromRemoteWindow(bw, 'ready-to-show').take(1).toPromise(); 13 | bw.loadURL('https://www.google.com'); 14 | 15 | await finished; 16 | 17 | bw.close(); 18 | }); 19 | 20 | it('should get the dom-ready event', async function() { 21 | let bw = new BrowserWindow({width: 500, height: 500, show: false}); 22 | 23 | let finished = fromRemoteWindow(bw, 'dom-ready', true).take(1).toPromise(); 24 | bw.loadURL('https://www.google.com'); 25 | 26 | await finished; 27 | bw.close(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /test/renderer-require.js: -------------------------------------------------------------------------------- 1 | import {rendererRequireDirect, requireTaskPool} from '../src/renderer-require'; 2 | 3 | describe('the requireTaskPool method', function() { 4 | this.timeout(30*1000); 5 | 6 | it('can make a bunch of requests at once', async function() { 7 | const { getJSON } = requireTaskPool( 8 | require.resolve('../src/remote-ajax'), 9 | 10, // Allow 10 windows open at a time 10 | 200); // Close idle windows after 500ms 11 | 12 | const emptyArray = Array.apply(null, Array(20)).map(() => 0); 13 | const result = await Promise.all(emptyArray.map(() => getJSON('https://httpbin.org/get'))); 14 | 15 | expect(result.length).to.equal(20); 16 | result.forEach(({ url }) => expect(url).to.equal('https://httpbin.org/get')); 17 | 18 | // Give the windows some time to close. 19 | await new Promise((res) => setTimeout(res, 400)); 20 | }); 21 | }); 22 | 23 | describe('the rendererRequireDirect method', function() { 24 | this.timeout(10*1000); 25 | 26 | it('makes a request using remote-ajax', async function() { 27 | let { module, unsubscribe } = await rendererRequireDirect(require.resolve('../src/remote-ajax')); 28 | 29 | try { 30 | let result = await module.getJSON('https://httpbin.org/get'); 31 | expect(result.url).to.equal('https://httpbin.org/get'); 32 | } finally { 33 | unsubscribe(); 34 | } 35 | }); 36 | 37 | it('marshals errors correctly', async function() { 38 | let { module, unsubscribe } = await rendererRequireDirect(require.resolve('../src/remote-ajax')); 39 | 40 | let shouldDie = true; 41 | try { 42 | await module.getJSON('https://httpbin.org/status/500').toPromise(); 43 | } catch (e) { 44 | shouldDie = false; 45 | } finally { 46 | unsubscribe(); 47 | } 48 | 49 | expect(shouldDie).to.equal(false); 50 | }); 51 | 52 | it('marshals simple values via getters', async function() { 53 | let { module, unsubscribe } = await rendererRequireDirect(require.resolve('./dummy-module')); 54 | 55 | try { 56 | let result = await module.dummyVal_get(); 57 | expect(result).to.equal(42); 58 | } finally { 59 | unsubscribe(); 60 | } 61 | }); 62 | 63 | it('marshals simple functions', async function() { 64 | let { module, unsubscribe } = await rendererRequireDirect(require.resolve('./dummy-module')); 65 | 66 | try { 67 | let result = await module.dummyFunc(); 68 | expect(result).to.equal(42); 69 | } finally { 70 | unsubscribe(); 71 | } 72 | }); 73 | 74 | it('marshals Buffers', async function() { 75 | let { module, unsubscribe } = await rendererRequireDirect(require.resolve('./dummy-module')); 76 | 77 | try { 78 | let result = await module.dummyBuffer(); 79 | expect(result.length).to.equal(8); 80 | expect(result[0]).to.equal(1); 81 | expect(result[7]).to.equal(8); 82 | } finally { 83 | unsubscribe(); 84 | } 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /test/support.js: -------------------------------------------------------------------------------- 1 | let chai = require("chai"); 2 | let chaiAsPromised = require("chai-as-promised"); 3 | 4 | chai.should(); 5 | chai.use(chaiAsPromised); 6 | 7 | global.chai = chai; 8 | global.chaiAsPromised = chaiAsPromised; 9 | global.expect = chai.expect; 10 | global.AssertionError = chai.AssertionError; 11 | global.Assertion = chai.Assertion; 12 | global.assert = chai.assert; 13 | --------------------------------------------------------------------------------