Skip to content

Commit

Permalink
Various typescript fixes
Browse files Browse the repository at this point in the history
* Tests are now in typescript
* Fix various types
* Get NYC coverage working with typescript
* Properly configure the linter
  • Loading branch information
sibnerian committed Nov 3, 2019
1 parent dddba3a commit 4f0dc80
Show file tree
Hide file tree
Showing 14 changed files with 546 additions and 226 deletions.
11 changes: 9 additions & 2 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
{
"extends": "airbnb",
"parser": "@typescript-eslint/parser",
"extends": ["plugin:@typescript-eslint/recommended", "airbnb", "prettier"],
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module"
},
"env": {
"node": true,
"es6": true
Expand All @@ -8,6 +13,8 @@
"global-require": 0,
"comma-dangle": 0,
"wrap-iife": ["error", "inside"],
"implicit-arrow-linebreak": "warn"
"implicit-arrow-linebreak": "warn",
"@typescript-eslint/no-explicit-any": 2,
"import/no-unresolved": 0
}
}
403 changes: 330 additions & 73 deletions package-lock.json

Large diffs are not rendered by default.

37 changes: 25 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"coverage": "nyc mocha",
"coveralls": "nyc report --reporter=text-lcov | coveralls",
"test": "cross-env NODE_ENV=test npm run coverage",
"lint": "eslint .",
"lint": "eslint . --ext=.ts,.js",
"build": "mkdirp build && tsc && babel src --out-dir build --source-maps",
"prepublish": "npm run build",
"clean": "rimraf build"
Expand All @@ -23,12 +23,9 @@
"electron": "^1.4.15"
},
"dependencies": {
"@babel/plugin-proposal-class-properties": "^7.5.5",
"@babel/plugin-proposal-object-rest-spread": "^7.6.2",
"es6-map": "^0.1.5",
"is-electron-renderer": "^2.0.1",
"serialize-error": "^3.0.0",
"typescript": "^3.6.4",
"uuid": "^3.0.1"
},
"devDependencies": {
Expand All @@ -37,7 +34,15 @@
"@babel/polyfill": "^7.2.5",
"@babel/preset-env": "^7.3.4",
"@babel/preset-typescript": "^7.6.0",
"typescript": "^3.6.4",
"@babel/register": "^7.0.0",
"@types/chai": "^4.2.4",
"@types/chai-as-promised": "^7.1.2",
"@types/mocha": "^5.2.7",
"@babel/plugin-proposal-class-properties": "^7.5.5",
"@babel/plugin-proposal-object-rest-spread": "^7.6.2",
"@typescript-eslint/eslint-plugin": "^2.6.0",
"@typescript-eslint/parser": "^2.6.0",
"babel-plugin-istanbul": "^5.1.1",
"chai": "^4.2.0",
"chai-as-promised": "^7.1.1",
Expand All @@ -47,28 +52,36 @@
"electron-ipc-mock": "^0.0.3",
"eslint": "^5.14.1",
"eslint-config-airbnb": "^17.1.0",
"eslint-config-prettier": "^6.5.0",
"eslint-plugin-import": "^2.16.0",
"eslint-plugin-jsx-a11y": "^6.2.1",
"eslint-plugin-prettier": "^3.1.1",
"eslint-plugin-react": "^7.12.4",
"lolex": "^1.5.2",
"mkdirp": "^0.5.1",
"mocha": "^5.2.0",
"nyc": "^13.3.0",
"prettier": "^1.18.2",
"proxyquire": "2.1.0",
"rimraf": "^2.6.3",
"sinon": "^7.2.5",
"sinon-chai": "^3.3.0"
"sinon-chai": "^3.3.0",
"source-map-support": "^0.5.16",
"ts-node": "^8.4.1"
},
"nyc": {
"include": [
"src/**/*.ts"
],
"extension": [
".ts",
".tsx"
],
"require": [
"@babel/register"
"ts-node/register"
],
"sourceMap": false,
"instrument": false,
"exclude": [
"test",
"build"
]
"sourceMap": true,
"instrument": true
},
"directories": {
"test": "test"
Expand Down
212 changes: 118 additions & 94 deletions src/base.ts
Original file line number Diff line number Diff line change
@@ -1,105 +1,129 @@
 import uuid from 'uuid/v4';
 import serializeError from 'serialize-error';
 import Map from 'es6-map';
 import { IpcMain, IpcRenderer, WebContents, IpcMessageEvent } from 'electron';
import uuid from 'uuid/v4';
import serializeError from 'serialize-error';
import Map from 'es6-map';
import { IpcMain, IpcRenderer, WebContents, IpcMessageEvent } from 'electron';

export type Listener = (event?: IpcMessageEvent, ...dataArgs: any) => void
export type Options = { maxTimeoutMs?: number }
/**
* For backwards compatibility, event is the (optional) LAST argument to a listener function.
* This leads to the following verbose overload type for a listener function.
*/
export type Listener =
| { (event?: IpcMessageEvent): void }
| { (arg1?: unknown, event?: IpcMessageEvent): void }
| { (arg1?: unknown, arg2?: unknown, event?: IpcMessageEvent): void }
| { (arg1?: unknown, arg2?: unknown, arg3?: unknown, event?: IpcMessageEvent): void }
| {
(
arg1?: unknown,
arg2?: unknown,
arg3?: unknown,
arg4?: unknown,
event?: IpcMessageEvent,
): void;
}
| {
(
arg1?: unknown,
arg2?: unknown,
arg3?: unknown,
arg4?: unknown,
arg5?: unknown,
event?: IpcMessageEvent,
): void;
};
export type Options = { maxTimeoutMs?: number };

 export default class PromiseIpcBase {
   private eventEmitterIpcMain | IpcRenderer;
   private maxTimeoutMsnumber;
   private routeListenerMapMap;
   private listenerMapMap;
export default class PromiseIpcBase {
private eventEmitter: IpcMain | IpcRenderer;

   constructor(opts{ maxTimeoutMs?: number} | undefined, eventEmitterIpcMain | IpcRenderer) {
private maxTimeoutMs: number;

  if (opts && opts.maxTimeoutMs) {
this.maxTimeoutMs = opts.maxTimeoutMs;
    }
private routeListenerMap: Map;

     // either ipcRenderer or ipcMain
     this.eventEmitter = eventEmitter;
     this.routeListenerMap = new Map();
     this.listenerMap = new Map();
   }
private listenerMap: Map;

   public send(routestring, senderWebContents | IpcRenderer, ...dataArgsany)Promise<unknown> {
     return new Promise((resolve, reject) => {
       const replyChannelstring = `${route}#${uuid()}`;
       let timeout: any;
       let didTimeOut: boolean = false;
constructor(opts: { maxTimeoutMs?: number } | undefined, eventEmitter: IpcMain | IpcRenderer) {
if (opts && opts.maxTimeoutMs) {
this.maxTimeoutMs = opts.maxTimeoutMs;
} // either ipcRenderer or ipcMain

       // 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);
       }
     });
   }
this.eventEmitter = eventEmitter;
this.routeListenerMap = new Map();
this.listenerMap = new Map();
}

public on(route: string, listener: Listener): WebContents | PromiseIpcBase {
     const prevListener = this.routeListenerMap.get(route);
     // If listener has already been added for this route, don't add it again.
     if (prevListener === listener) {
       return this;
     }
     // Only one listener may be active for a given route.
     // If two are active promises it won't work correctly - that's a race condition.
     if (this.routeListenerMap.has(route)) {
       this.off(route, prevListener);
     }
     // 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, event))
         .then((results) => {
           event.sender.send(replyChannel, 'success', results);
         })
         .catch((e) => {

           event.sender.send(replyChannel, 'failure', serializeError(e));
         });
     };
     this.routeListenerMap.set(route, listener);
     this.listenerMap.set(listener, wrappedListener);
     this.eventEmitter.on(route, wrappedListener);
     return this;
   }
public send(
route: string,
sender: WebContents | IpcRenderer,
...dataArgs: [unknown]
): Promise<unknown> {
return new Promise((resolve, reject) => {
const replyChannel = `${route}#${uuid()}`;
let timeout: NodeJS.Timeout;
let didTimeOut = false; // ipcRenderer will send a message back to replyChannel when it finishes calculating

   public off(route: string, listener: Listener): void {
     const registeredListener = this.routeListenerMap.get(route);
     if (listener && listener !== registeredListener) {
       return; // trying to remove the wrong listener, so do nothing.
     }
     const wrappedListener = this.listenerMap.get(registeredListener);
     this.eventEmitter.removeListener(route, wrappedListener);
     this.listenerMap.delete(registeredListener);
     this.routeListenerMap.delete(route);
   }
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);
}
});
}

   public removeListener(route: string, listener: Listener): void {
     this.off(route, listener);
   }
public on(route: string, listener: Listener): WebContents | PromiseIpcBase {
const prevListener = this.routeListenerMap.get(route); // If listener has already been added for this route, don't add it again.
if (prevListener === listener) {
return this;
} // Only one listener may be active for a given route. // If two are active promises it won't work correctly - that's a race condition.
if (this.routeListenerMap.has(route)) {
this.off(route, prevListener);
} // This function _wraps_ the listener argument. We maintain a map of // listener -> wrapped listener in order to implement #off().
const wrappedListener = (event, replyChannel, ...dataArgs): void => {
// 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, event))
.then((results) => {
event.sender.send(replyChannel, 'success', results);
})
.catch((e) => {
event.sender.send(replyChannel, 'failure', serializeError(e));
});
};
this.routeListenerMap.set(route, listener);
this.listenerMap.set(listener, wrappedListener);
this.eventEmitter.on(route, wrappedListener);
return this;
}

 }
public off(route: string, listener: Listener): void {
const registeredListener = this.routeListenerMap.get(route);
if (listener && listener !== registeredListener) {
return; // trying to remove the wrong listener, so do nothing.
}
const wrappedListener = this.listenerMap.get(registeredListener);
this.eventEmitter.removeListener(route, wrappedListener);
this.listenerMap.delete(registeredListener);
this.routeListenerMap.delete(route);
}

public removeListener(route: string, listener: Listener): void {
this.off(route, listener);
}
}
16 changes: 9 additions & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import isRenderer from 'is-electron-renderer';
import renderer from "./renderer";
import mainProcess from "./mainProcess";
import renderer, { RendererProcessType } from './renderer';
import mainProcess, { MainProcessType } from './mainProcess';

if (isRenderer) {
module.exports = renderer;
} else {
module.exports = mainProcess;
}
const exportedModule: RendererProcessType | MainProcessType = isRenderer ? renderer : mainProcess;
export default exportedModule;
module.exports = exportedModule;

// Re-export the renderer and main process types for consumer modules to access
export { RendererProcessType } from './renderer';
export { MainProcessType } from './mainProcess';
13 changes: 5 additions & 8 deletions src/mainProcess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,24 @@ import { ipcMain, WebContents } from 'electron';
import PromiseIpcBase, { Options } from './base';

export class PromiseIpcMain extends PromiseIpcBase {
public PromiseIpc?: any;
public PromiseIpcMain?: any;

constructor(opts?: Options) {
super(opts, ipcMain);
}

// Send requires webContents -- see http://electron.atom.io/docs/api/ipc-main/
public send(route: string, webContents: WebContents, ...dataArgs: any): Promise<unknown> {
public send(route: string, webContents: WebContents, ...dataArgs: [unknown]): Promise<unknown> {
return super.send(route, webContents, ...dataArgs);
}
}

type MainProcessExportType = PromiseIpcMain & {
PromiseIpc?: PromiseIpcMain,
PromiseIpcMain?: PromiseIpcMain
export type MainProcessType = PromiseIpcMain & {
PromiseIpc?: typeof PromiseIpcMain;
PromiseIpcMain?: typeof PromiseIpcMain;
};

export const PromiseIpc = PromiseIpcMain;

const mainExport: MainProcessExportType = new PromiseIpcMain();
const mainExport: MainProcessType = new PromiseIpcMain();
mainExport.PromiseIpc = PromiseIpcMain;
mainExport.PromiseIpcMain = PromiseIpcMain;

Expand Down

0 comments on commit 4f0dc80

Please sign in to comment.