ember-web-workers
Version:
Service to communicate your application with browser web workers
269 lines (226 loc) • 6.31 kB
JavaScript
import { assert } from '@ember/debug';
import { A } from '@ember/array';
import RSVP from 'rsvp';
import { get } from '@ember/object';
import Service, { inject as service } from '@ember/service';
import Evented, { on } from '@ember/object/evented';
import { isPresent } from '@ember/utils';
function messageListener(meta, event) {
const ping = event.data === true;
// Check if the worker has been instantiated via event listener:
// worker.on('name', data, callback);
if (get(meta, 'keepAlive')) {
const fn = get(meta, 'callback');
if (ping) {
// A 'true' message tell us that the worker has been created correctly,
// then resolve the promise returned by the event listener.
this.trigger('resolve', meta);
} else if (fn) {
// The worker is sending data, call the fn this the event data.
fn(event.data);
} else {
// Receiving data from a worker created via 'open'.
this.trigger('resolve', meta, event.data);
}
// If the response is equals to 'true' we should ignore it because
// the worker is pinging us to tell that everything is correct.
} else if (!ping) {
// Resolve the promise returned by the method 'postMessage' with the event data.
this.trigger('resolve', meta, event.data);
}
}
function errorListener(meta, error) {
// An error has ocurrect, reject the promise and kill the worker.
this.trigger('reject', meta, error.message);
}
export default Service.extend(Evented, {
assetMap: service('asset-map'),
/**
* Check if workers are enabled.
*
* @property isEnabled
* @type Boolean
*/
get isEnabled() {
return Boolean(window.Worker);
},
/**
* Static workers file path.
*
* @property webWorkersPath
* @type String
*/
webWorkersPath: 'assets/web-workers/',
/**
* Initialize metadata array.
*
* @method init
*/
init() {
this._super(...arguments);
// Initialize metadata array, it will store all running workers with their own promises/callbacks.
this.set('_cache', A([]));
},
/**
* Start a worker and attach the events given a name.
*
* @method _wakeup
* @param String name
* @param Function callback
* @return Object
*/
_wakeUp(name, callback, keepAlive = false) {
assert('You must provide the worker name', isPresent(name));
// 'keepAlive' will store if the worker should still alive after sending a message.
const workerUrl = this.get('assetMap').resolve(`${this.get('webWorkersPath')}${name}.js`);
const worker = new window.Worker(workerUrl);
const deferred = RSVP.defer('Worker: sending message');
const meta = {
keepAlive,
worker,
name,
deferred,
callback
};
// Attach the worker events.
worker.addEventListener('message', messageListener.bind(this, meta));
worker.addEventListener('error', errorListener.bind(this, meta));
return meta;
},
/**
* Resolve pending promise.
*
* @method _onResolve
* @param Object data
*/
_onResolve: on('resolve', function(meta, data) {
const deferred = get(meta, 'deferred');
if (!get(meta, 'keepAlive')) {
this._cleanMeta(meta);
}
deferred.resolve(data);
}),
/**
* Reject pending promise.
*
* @method _onReject
* @param Object meta
*/
_onReject: on('reject', function(meta, error) {
this._cleanMeta(meta);
get(meta, 'deferred').reject(error);
}),
/**
* Clean request metadata & kill worker if neccessary.
*
* @method _cleanMeta
* @param Object meta
*/
_cleanMeta(meta) {
this._sleep(get(meta, 'worker'));
this.get('_cache').removeObject(meta);
},
/**
* Kill worker.
*
* @method _sleep
* @param Worker worker
*/
_sleep(worker) {
worker.terminate();
},
/**
* Cancel pending promise.
*
* @method terminate
* @param Ember.RSVP promise
*/
terminate(promise) {
const _cache = this.get('_cache');
let index = _cache.length;
// Reverse loop to prevent errors (this loop iterates a collection while deletes its items)
while (index--) {
const meta = _cache[index];
const deferred = get(meta, 'deferred');
// If promise exists reject it, if not reject all.
if ((deferred.promise === promise) || !promise) {
this.trigger('reject', meta);
}
}
},
/**
* Send event to the worker and terminate it when responses.
*
* @method postMessage
* @param String name
* @param Object data
* @return Mixed
*/
postMessage(name, data) {
assert('Workers are disabled', this.get('isEnabled'));
const meta = this._wakeUp(name);
this.get('_cache').pushObject(meta);
get(meta, 'worker').postMessage(data);
return get(meta, 'deferred.promise');
},
/**
* Suscribe to a worker.
*
* @method on
* @param String name
* @param Object data
* @param Function callback
*/
on(name, callback) {
assert('Cannot register an event with no callback', typeof callback === 'function');
const meta = this._wakeUp(name, callback, true);
this.get('_cache').pushObject(meta);
return get(meta, 'deferred.promise');
},
/**
* Suscribe to a worker.
*
* @method off
* @param String name
* @param Function callback
*/
off(name, callback) {
let metaArray;
if (callback) {
assert('Callback should be a function', typeof callback === 'function');
const matchingWorker = this.get('_cache').find((meta) => (name === meta.name && callback === meta.callback));
metaArray = matchingWorker ? [matchingWorker] : [];
} else {
metaArray = this.get('_cache').filter((meta) => name === meta.name);
}
if (metaArray.length) {
metaArray.forEach((meta) => this._cleanMeta(meta));
return RSVP.resolve();
}
return RSVP.reject('Worker: event does not exist');
},
/**
* Start a worker.
*
* @method open
* @param String name
*/
open(name) {
const meta = this._wakeUp(name, null, true);
const promise = get(meta, 'deferred.promise').then(() => ({
postMessage: (data) => {
const deferred = RSVP.defer();
const channel = new MessageChannel();
channel.port2.onmessage = (e) => deferred.resolve(e.data);
get(meta, 'worker').postMessage(data, [channel.port1]);
return deferred.promise;
},
terminate: () => {
this._cleanMeta(meta);
return RSVP.resolve();
}
}));
this.get('_cache').pushObject(meta);
return promise;
}
});