@mjcctech/meteor-desktop
Version:
Build a Meteor's desktop client with hot code push.
319 lines (289 loc) • 10.4 kB
JavaScript
/* eslint-disable global-require */
// This was inspiried by
// https://github.com/electron-webapps/meteor-electron/blob/master/app/preload.js
const ipc = require('electron').ipcRenderer;
const exposedModules = [];
/**
* See https://github.com/atom/electron/issues/1753#issuecomment-104719851.
*/
/**
* Callback passed to ipc on/once methods.
*
* @callback ipcListener
* @param {string} event - event name
* @param {...*=} args - event's arguments
*/
/**
* Simple abstraction over electron's IPC. Securely wraps ipcRenderer.
* Available as `Desktop` global.
* @class
*/
const Desktop = new (class {
constructor() {
this.onceEventListeners = {};
this.eventListeners = {};
this.registeredInIpc = {};
this.fetchCallCounter = 0;
this.fetchTimeoutTimers = {};
this.fetchTimeout = 2000;
}
/**
* Just a convenience method for getting an url for a file from the local file system.
* @param {string} absolutePath - absolute path to the file
* @returns {string}
*/
getFileUrl(absolutePath) { // eslint-disable-line
return `/local-filesystem/${absolutePath}`;
}
/**
* Just a convenience method for getting an url for a file from the assets directory.
* @param {string} assetPath - file path relative to assets directory
* @returns {string}
*/
getAssetUrl(assetPath) { // eslint-disable-line
return `/___desktop/${assetPath}`;
}
/**
* Just a convenience method for getting a file from the local file system.
* Returns a promise from `fetch`.
* @param {string} absolutePath - absolute path to the file
* @returns {Promise}
*/
fetchFile(absolutePath) {
return fetch(this.getFileUrl(absolutePath, false));
}
/**
* Just a convenience method for getting a file from the assets directory.
* Returns a promise from `fetch`.
* @param {string} assetPath - file path relative to assets directory
* @returns {Promise}
*/
fetchAsset(assetPath) {
return fetch(this.getAssetUrl(assetPath, false));
}
/**
* Adds a callback to internal listeners placeholders and registers real ipc hooks.
*
* @param {string} module - module name
* @param {string} event - name of an event
* @param {ipcListener} callback - callback to fire when event arrives
* @param {boolean} once - whether this should be fired only once
* @param {boolean} response - whether we are listening for fetch response
* @private
*/
addToListeners(module, event, callback, once, response = false) {
const self = this;
const eventName = response ? this.getResponseEventName(module, event) :
this.getEventName(module, event);
function handler(...args) {
if (eventName in self.eventListeners) {
self.eventListeners[eventName].forEach(eventHandler => eventHandler(...args));
}
if (eventName in self.onceEventListeners) {
self.onceEventListeners[eventName].forEach((eventHandler) => {
eventHandler(...args);
ipc.removeListener(eventHandler, handler);
self.onceEventListeners[eventName].delete(eventHandler);
});
}
}
let listeners = 'eventListeners';
if (once) {
listeners = 'onceEventListeners';
}
if (eventName in this[listeners]) {
this[listeners][eventName].add(callback);
} else {
this[listeners][eventName] = new Set([callback]);
}
if (!(eventName in this.registeredInIpc)) {
this.registeredInIpc[eventName] = true;
ipc.on(eventName, handler);
}
}
/**
* Invokes callback when the specified IPC event is fired.
*
* @param {string} module - module name
* @param {string} event - name of an event
* @param {ipcListener} callback - function to invoke when `event` is triggered
* @public
*/
on(module, event, callback) {
this.addToListeners(module, event, callback);
}
/**
* Invokes a callback once when the specified IPC event is fired.
*
* @param {string} module - module name
* @param {string} event - name of an event
* @param {ipcListener} callback - function to invoke when `event` is triggered
* @param {boolean} response - whether we are listening for fetch response
* @public
*/
once(module, event, callback, response = false) {
this.addToListeners(module, event, callback, true, response);
}
/**
* Unregisters a callback.
*
* @param {string} module - module name
* @param {string} event - name of an event
* @param {function} callback - listener to unregister
* @public
*/
removeListener(module, event, callback) {
const eventName = this.getEventName(module, event);
['eventListeners', 'onceEventListeners'].forEach((listeners) => {
if (eventName in this[listeners]) {
if (~this[listeners][eventName].indexOf(callback)) {
this[listeners][eventName].splice(
this[listeners][eventName].indexOf(callback), 1
);
}
}
});
}
/**
* Unregisters all callbacks.
*
* @param {string} module - module name
* @param {string} event - name of an event
* @public
*/
removeAllListeners(module, event) {
const eventName = this.getEventName(module, event);
this.onceEventListeners[eventName] = new Set();
this.eventListeners[eventName] = new Set();
}
/**
* Send an event to the main Electron process.
*
* @param {string} module - module name
* @param {string} event - name of an event
* @param {...*} args - arguments to send with the event
* @public
*/
send(module, event, ...args) {
const eventName = this.getEventName(module, event);
ipc.send(eventName, ...args);
}
/**
* Sends and IPC event response for a provided fetch id.
*
* @param {string} module - module name
* @param {string} event - event name
* @param {number} fetchId - fetch id that came with then event you are
* responding to
* @param {...*=} data - data to send with the event
* @public
*/
respond(module, event, fetchId, ...data) {
ipc.send(this.getResponseEventName(module, `${event}_${fetchId}`), fetchId, ...data);
}
/**
* Fetches some data from main process by sending an IPC event and waiting for a response.
* Returns a promise that resolves when the response is received.
*
* @param {string} module - module name
* @param {string} event - name of an event
* @param {number} timeout - how long to wait for the response in milliseconds
* @param {...*} args - arguments to send with the event
* @returns {Promise}
* @public
*/
fetch(module, event, timeout = this.fetchTimeout, ...args) {
const eventName = this.getEventName(module, event);
if (this.fetchCallCounter === Number.MAX_SAFE_INTEGER) {
this.fetchCallCounter = 0;
}
this.fetchCallCounter += 1;
const fetchId = this.fetchCallCounter;
return new Promise((resolve, reject) => {
this.once(module, `${event}_${fetchId}`,
(responseEvent, id, ...responseArgs) => {
if (id === fetchId) {
clearTimeout(this.fetchTimeoutTimers[fetchId]);
delete this.fetchTimeoutTimers[fetchId];
resolve(...responseArgs);
}
}, true);
this.fetchTimeoutTimers[fetchId] = setTimeout(() => {
reject('timeout');
}, timeout);
ipc.send(eventName, fetchId, ...args);
});
}
/**
* Desktop.fetch without the need to provide a timeout value.
*
* @param {string} module - module name
* @param {string} event - name of an event
* @param {...*} args - arguments to send with the event
* @returns {Promise}
* @public
*/
call(module, event, ...args) {
return this.fetch(module, event, this.fetchTimeout, ...args);
}
/**
* Sets the default fetch timeout.
* @param {number} timeout
*/
setDefaultFetchTimeout(timeout = this.fetchTimeout) {
if (typeof timeout !== 'number') {
throw new Error('timeout must a number');
}
this.fetchTimeout = timeout;
}
/**
* Send an global event to the main Electron process.
*
* @param {...*} args - arguments to the ipc.send(event, arg1, arg2)
* @public
*/
sendGlobal(...args) { // eslint-disable-line
ipc.send(...args);
}
/**
* Concatenates module name with event name.
*
* @param {string} module - module name
* @param {string} event - event name
* @returns {string}
* @private
*/
getEventName(module, event) { // eslint-disable-line
return `${module}__${event}`;
}
/**
* Concatenates event name with response postfix.
*
* @param {string} module - module name
* @param {string} event - event name
* @returns {string}
* @private
*/
getResponseEventName(module, event) {
return `${this.getEventName(module, event)}___response`;
}
})();
process.once('loaded', () => {
let devtron = null;
try {
devtron = require('devtron'); // eslint-disable-line global-require
global.__devtron = { require, process }; // eslint-disable-line no-underscore-dangle
} catch (e) {
// If that fails, then this is a production build and devtron is not available.
}
if (process.env.NODE_ENV === 'test') {
global.electronRequire = require;
global.process = process;
}
Desktop.devtron = devtron;
Desktop.electron = [];
exposedModules.forEach((module) => {
Desktop.electron[module] = require('electron')[module];
});
global.Desktop = Desktop;
});