Skip to content

Commit

Permalink
Refactor to base class
Browse files Browse the repository at this point in the history
  • Loading branch information
sibnerian committed Apr 14, 2019
1 parent ddc2dd9 commit fbeeec1
Show file tree
Hide file tree
Showing 7 changed files with 115 additions and 152 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"mkdirp": "^0.5.1",
"mocha": "^5.2.0",
"nyc": "^13.3.0",
"proxyquire": "^1.7.11",
"proxyquire": "2.1.0",
"rimraf": "^2.6.3",
"sinon": "^7.2.5",
"sinon-chai": "^3.3.0"
Expand Down
80 changes: 80 additions & 0 deletions src/base.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import uuid from 'uuid/v4';
import Promise from 'bluebird';
import serializeError from 'serialize-error';
import Map from 'es6-map';
import { ipcMain } from 'electron'; // eslint-disable-line

export default class PromiseIpcBase {
constructor(opts, eventEmitter) {
if (opts) {
this.maxTimeoutMs = opts.maxTimeoutMs;
}
// either ipcRenderer or ipcMain
this.eventEmitter = eventEmitter;
this.listenerMap = new Map();
}

send(route, sender, ...dataArgs) {
return new Promise((resolve, reject) => {
const replyChannel = `${route}#${uuid()}`;
let timeout;
let didTimeOut = false;

// ipcRenderer will send a message back to replyChannel when it finishes calculating
this.eventEmitter.once(replyChannel, (event, status, returnData) => {
clearTimeout(timeout);
if (didTimeOut) {
return null;
}
switch (status) {
case 'success':
return resolve(returnData);
case 'failure':
return reject(returnData);
default:
return reject(new Error(`Unexpected IPC call status "${status}" in ${route}`));
}
});
sender.send(route, replyChannel, ...dataArgs);

if (this.maxTimeoutMs) {
timeout = setTimeout(() => {
didTimeOut = true;
reject(new Error(`${route} timed out.`));
}, this.maxTimeoutMs);
}
});
}

on(route, listener) {
// If listener has already been added, don't add it again.
if (this.listenerMap.has(listener)) {
return this;
}
// This function _wraps_ the listener argument. We maintain a map of
// listener -> wrapped listener in order to implement #off().
const wrappedListener = (event, replyChannel, ...dataArgs) => {
// Chaining off of Promise.resolve() means that listener can return a promise, or return
// synchronously -- it can even throw. The end result will still be handled promise-like.
Promise.resolve()
.then(() => listener(...dataArgs))
.then((results) => {
event.sender.send(replyChannel, 'success', results);
})
.catch((e) => {
event.sender.send(replyChannel, 'failure', serializeError(e));
});
};
this.listenerMap.set(listener, wrappedListener);
this.eventEmitter.on(route, wrappedListener);
return this;
}

off(route, listener) {
const wrappedListener = this.listenerMap.get(listener);
if (wrappedListener) {
this.eventEmitter.removeListener(route, wrappedListener);
this.listenerMap.delete(listener);
}
}
}
76 changes: 5 additions & 71 deletions src/mainProcess.js
Original file line number Diff line number Diff line change
@@ -1,80 +1,14 @@
import { ipcMain } from 'electron'; // eslint-disable-line
import uuid from 'uuid/v4';
import Promise from 'bluebird';
import serializeError from 'serialize-error';
import Map from 'es6-map';
import { ipcMain } from 'electron';
import PromiseIpcBase from './base';

export class PromiseIpcMain {
export class PromiseIpcMain extends PromiseIpcBase {
constructor(opts) {
if (opts) {
this.maxTimeoutMs = opts.maxTimeoutMs;
}
this.listenerMap = new Map();
super(opts, ipcMain);
}

// Send requires webContents -- see http://electron.atom.io/docs/api/ipc-main/
send(route, webContents, ...dataArgs) {
return new Promise((resolve, reject) => {
const replyChannel = `${route}#${uuid()}`;
let timeout;
let didTimeOut = false;

// ipcRenderer will send a message back to replyChannel when it finishes calculating
ipcMain.once(replyChannel, (event, status, returnData) => {
clearTimeout(timeout);
if (didTimeOut) {
return null;
}
switch (status) {
case 'success':
return resolve(returnData);
case 'failure':
return reject(returnData);
default:
return reject(new Error(`Unexpected IPC call status "${status}" in ${route}`));
}
});
webContents.send(route, replyChannel, ...dataArgs);

if (this.maxTimeoutMs) {
timeout = setTimeout(() => {
didTimeOut = true;
reject(new Error(`${route} timed out.`));
}, this.maxTimeoutMs);
}
});
}

on(route, listener) {
// If listener has already been added, don't add it again.
if (this.listenerMap.has(listener)) {
return this;
}
// This function _wraps_ the listener argument. We maintain a map of
// listener -> wrapped listener in order to implement #off().
const wrappedListener = (event, replyChannel, ...dataArgs) => {
// Chaining off of Promise.resolve() means that listener can return a promise, or return
// synchronously -- it can even throw. The end result will still be handled promise-like.
Promise.resolve()
.then(() => listener(...dataArgs))
.then((results) => {
event.sender.send(replyChannel, 'success', results);
})
.catch((e) => {
event.sender.send(replyChannel, 'failure', serializeError(e));
});
};
this.listenerMap.set(listener, wrappedListener);
ipcMain.on(route, wrappedListener);
return this;
}

off(route, listener) {
const wrappedListener = this.listenerMap.get(listener);
if (wrappedListener) {
ipcMain.removeListener(route, wrappedListener);
this.listenerMap.delete(listener);
}
return super.send(route, webContents, ...dataArgs);
}
}

Expand Down
72 changes: 4 additions & 68 deletions src/renderer.js
Original file line number Diff line number Diff line change
@@ -1,77 +1,13 @@
import { ipcRenderer } from 'electron'; // eslint-disable-line
import uuid from 'uuid/v4';
import Promise from 'bluebird';
import serializeError from 'serialize-error';
import Map from 'es6-map';
import PromiseIpcBase from './base';

export class PromiseIpcRenderer {
export class PromiseIpcRenderer extends PromiseIpcBase {
constructor(opts) {
if (opts) {
this.maxTimeoutMs = opts.maxTimeoutMs;
}
this.listenerMap = new Map();
super(opts, ipcRenderer);
}

send(route, ...dataArgs) {
return new Promise((resolve, reject) => {
const replyChannel = `${route}#${uuid()}`;
let timeout;
let didTimeOut = false;

// ipcMain will send a message back to replyChannel when it finishes calculating
ipcRenderer.once(replyChannel, (event, status, returnData) => {
clearTimeout(timeout);
if (didTimeOut) {
return null;
}
switch (status) {
case 'success':
return resolve(returnData);
case 'failure':
return reject(returnData);
default:
return reject(new Error(`Unexpected IPC call status "${status}" in ${route}`));
}
});
ipcRenderer.send(route, replyChannel, ...dataArgs);

if (this.maxTimeoutMs) {
timeout = setTimeout(() => {
didTimeOut = true;
reject(new Error(`${route} timed out.`));
}, this.maxTimeoutMs);
}
});
}

on(route, listener) {
// If listener has already been added, don't add it again.
if (this.listenerMap.has(listener)) {
return this;
}
const wrappedListener = (event, replyChannel, ...dataArgs) => {
// Chaining off of Promise.resolve() means that listener can return a promise, or return
// synchronously -- it can even throw. The end result will still be handled promise-like.
Promise.resolve()
.then(() => listener(...dataArgs))
.then((results) => {
ipcRenderer.send(replyChannel, 'success', results);
})
.catch((e) => {
ipcRenderer.send(replyChannel, 'failure', serializeError(e));
});
};
this.listenerMap.set(listener, wrappedListener);
ipcRenderer.on(route, wrappedListener);
return this;
}

off(route, listener) {
const wrappedListener = this.listenerMap.get(listener);
if (wrappedListener) {
ipcRenderer.removeListener(route, wrappedListener);
this.listenerMap.delete(listener);
}
return super.send(route, ipcRenderer, ...dataArgs);
}
}

Expand Down
7 changes: 6 additions & 1 deletion test/mainProcess-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,14 @@ const { ipcRenderer, ipcMain } = require('electron-ipc-mock')();
chai.use(chaiAsPromised);
const uuid = 'totally_random_uuid';

// Need a 2-layer proxyquire now because of the base class dependencies.
const Base = proxyquire('../src/base', {
'uuid/v4': () => uuid,
});

const mainProcessDefault = proxyquire('../src/mainProcess', {
electron: { ipcMain },
'uuid/v4': () => uuid,
'./base': Base,
});
const { PromiseIpc } = mainProcessDefault;

Expand Down
8 changes: 7 additions & 1 deletion test/renderer-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,16 @@ const generateRoute = (function generateRoute() {
return () => i++; // eslint-disable-line no-plusplus
})();

// Need a 2-layer proxyquire now because of the base class dependencies.
const Base = proxyquire('../src/base', {
'uuid/v4': () => uuid,
});

const renderer = proxyquire('../src/renderer', {
electron: { ipcRenderer },
'uuid/v4': () => uuid,
'./base': Base,
});

const { PromiseIpc } = renderer;

describe('renderer', () => {
Expand Down
22 changes: 12 additions & 10 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3490,7 +3490,7 @@ path-key@^2.0.0, path-key@^2.0.1:
resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=

path-parse@^1.0.6:
path-parse@^1.0.5, path-parse@^1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==
Expand Down Expand Up @@ -3638,14 +3638,14 @@ prop-types@^15.6.2:
object-assign "^4.1.1"
react-is "^16.8.1"

proxyquire@^1.7.11:
version "1.8.0"
resolved "https://registry.yarnpkg.com/proxyquire/-/proxyquire-1.8.0.tgz#02d514a5bed986f04cbb2093af16741535f79edc"
integrity sha1-AtUUpb7ZhvBMuyCTrxZ0FTX3ntw=
proxyquire@2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/proxyquire/-/proxyquire-2.1.0.tgz#c2263a38bf0725f2ae950facc130e27510edce8d"
integrity sha512-kptdFArCfGRtQFv3Qwjr10lwbEV0TBJYvfqzhwucyfEXqVgmnAkyEw/S3FYzR5HI9i5QOq4rcqQjZ6AlknlCDQ==
dependencies:
fill-keys "^1.0.2"
module-not-found-error "^1.0.0"
resolve "~1.1.7"
resolve "~1.8.1"

pseudomap@^1.0.2:
version "1.0.2"
Expand Down Expand Up @@ -3934,10 +3934,12 @@ resolve@^1.10.0, resolve@^1.3.2, resolve@^1.5.0, resolve@^1.9.0:
dependencies:
path-parse "^1.0.6"

resolve@~1.1.7:
version "1.1.7"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=
resolve@~1.8.1:
version "1.8.1"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.8.1.tgz#82f1ec19a423ac1fbd080b0bab06ba36e84a7a26"
integrity sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==
dependencies:
path-parse "^1.0.5"

restore-cursor@^2.0.0:
version "2.0.0"
Expand Down

0 comments on commit fbeeec1

Please sign in to comment.