concurrent-worker
Version:
Multithreading for javascript
265 lines (260 loc) • 9.92 kB
JavaScript
var noop = function () { return []; };
/**
* Calling functions through strings makes sure that aggressive minification
* does not break the call. The same applies to the apply call, this ensures no polyfill
* is assumed for a rest spread which would also break the worker script.
*
* Tested with Webpack dev and prod mode, Closure Compiler ADVANCED mode with ES3, ES5 and ES6 target.
* @param message
*/
var onmessage = function (message) {
return new Promise(function (resolve) {
resolve(self["run"].apply(null, message.data[1]));
})
.then(function (result) {
var transferrable = self["getTransferrables"](result);
postMessage([message.data[0], result, false], transferrable);
})
.catch(function (e) {
var error = self["getError"](e);
postMessage([message.data[0], error, true]);
})
.finally(function () {
if (self["terminateOnCompletion"]) {
close();
}
});
};
var getError = function (e) {
if (typeof e === "string") {
return e;
}
if (typeof e === "object") {
var props = Object.getOwnPropertyNames(e);
return props.reduce(function (acc, prop) {
acc[prop] = e[prop];
return acc;
}, {});
}
return "Unknown error in Worker";
};
var getContextDeclaration = function (contextItem) {
switch (typeof contextItem) {
case "boolean":
case "number":
return contextItem;
case "string":
return "'" + contextItem + "'";
case "function":
return "" + contextItem;
case "object":
return JSON.stringify(contextItem, null, 2);
}
};
var getContextString = function (context) {
return context
? Object.keys(context)
.map(function (key) { return "this." + key + " = " + getContextDeclaration(context[key]); })
.join(";\r\n\r\n")
: "";
};
var isUrlRelative = function (url) {
return url.indexOf("://") === -1 && url.indexOf("//") === -1;
};
var getScriptImport = function (scripts, rootUrl) {
if (rootUrl === void 0) { rootUrl = ""; }
return scripts != null && scripts.length > 0
? "importScripts(" + scripts
.map(function (script) {
return isUrlRelative(script) ? "\"" + rootUrl + script + "\"" : "\"" + script + "\"";
})
.join(",") + ");"
: "";
};
var getScript = function (execute, config, terminateOnCompletion) {
var _a;
return ("\n" + getScriptImport(config.scripts, config.rootUrl) + "\n" + getContextString(config.context) + "\nself.terminateOnCompletion = " + terminateOnCompletion + ";\nself.rootUrl = '" + config.rootUrl + "';\nself.getError = " + getError + ";\nself.getTransferrables = " + ((_a = config.outTransferable) !== null && _a !== void 0 ? _a : noop) + ";\nself.run = " + execute + ";\nself.onmessage = " + onmessage + ";\n").trim();
};
var createWorkerUrl = function (execute, config, terminateOnCompletion) {
if (terminateOnCompletion === void 0) { terminateOnCompletion = false; }
var script = getScript(execute, config, terminateOnCompletion);
var blob = new Blob([script], { type: "application/javascript" });
return URL.createObjectURL(blob);
};
/**
* Create a worker onmessage callback that resolves or rejects if the sync id matches.
*/
var createWorkerCallback = function (worker, syncIdPing, resolve, reject) {
return function cb(message) {
var syncIdPong = message.data[0];
var dataOrError = message.data[1];
var hasError = message.data[2];
if (syncIdPing === syncIdPong) {
if (hasError) {
reject(dataOrError);
}
else {
resolve(dataOrError);
}
worker.removeEventListener("message", cb);
}
};
};
/**
* Call worker with given arguments, returns a promise that resolves when onmessage is called
* with matching syncId.
*/
var executePromiseWorker = function (worker, syncId, args, transferrable) {
if (transferrable === void 0) { transferrable = []; }
return new Promise(function (resolve, reject) {
worker.addEventListener("message", createWorkerCallback(worker, syncId, resolve, reject));
worker.postMessage([syncId, args], transferrable);
});
};
/**
* Creates a task that can be run in a webworker. If you want to use functions and variables from
* the outer scope you must pass them in via the context parameter, else they will not be available.
* This creation method creates a new web worker for each call to it allowing multiple calls to run in paralel.
*
* @param task Function to execute off the main thread
* @param config Worker configuration
*/
var concurrent = function (task, config) {
var _a;
if (config === void 0) { config = {}; }
var url = typeof task === "string" ? task : createWorkerUrl(task, config, true);
var getTransferable = (_a = config.inTransferable) !== null && _a !== void 0 ? _a : noop;
var run = (function (args) {
var worker = new Worker(url);
var transferable = getTransferable(args);
return executePromiseWorker(worker, -1, args, transferable);
});
var kill = function () {
URL.revokeObjectURL(url);
};
/**
* Returns an identical copy that runs on it's own workers
*/
var clone = function () { return concurrent(url, config); };
return {
clone: clone,
kill: kill,
run: run,
};
};
/**
* Creates a task that can be run in a webworker. If you want to use functions and variables from
* the outer scope you must pass them in via the context parameter, else they will not be available.
* This creation method uses a single web worker for all calls to it, calls will be processed synchronously
* in that worker. Has les overhead than `create` but does not run multiple calls in paralel.
*
* @param task Function to execute off the main thread, or object url pointing to worker script
* @param config Worker configuration
*/
var serial = function (task, config) {
var _a;
if (config === void 0) { config = {}; }
var url = typeof task === "string" ? task : createWorkerUrl(task, config);
var worker = new Worker(url);
var getTransferable = (_a = config.inTransferable) !== null && _a !== void 0 ? _a : noop;
var syncId = 0;
var run = (function (args) {
var transferable = getTransferable(args);
return executePromiseWorker(worker, syncId++, args, transferable);
});
var kill = function () {
worker.terminate();
URL.revokeObjectURL(url);
};
/**
* Returns an identical copy that runs on it's own worker
*/
var clone = function () { return serial(url, config); };
return {
clone: clone,
kill: kill,
run: run,
};
};
var _a, _b;
var defaultConcurrency = (_b = (_a = self === null || self === void 0 ? void 0 : self.navigator) === null || _a === void 0 ? void 0 : _a.hardwareConcurrency) !== null && _b !== void 0 ? _b : 4;
var defaultTimeout = 1000 * 60;
var WorkerPool = /** @class */ (function () {
function WorkerPool(task, config) {
if (config === void 0) { config = {}; }
var _a;
this.task = task;
this.config = config;
this.workers = [];
this.busy = [];
this.waiting = [];
var url = typeof task === "string" ? task : createWorkerUrl(task, config);
var workers = (_a = config === null || config === void 0 ? void 0 : config.workers) !== null && _a !== void 0 ? _a : defaultConcurrency;
for (var i = 0; i < workers; i++) {
this.workers.push(serial(url, config));
}
}
WorkerPool.prototype.get = function () {
var _this = this;
var worker = this.workers.pop();
if (worker != null) {
this.busy.push(worker);
return Promise.resolve(worker);
}
return new Promise(function (resolve, reject) {
var _a, _b;
var timeout = setTimeout(function () {
var index = _this.waiting.indexOf(cb);
if (index !== -1) {
_this.waiting.splice(index, 1);
}
reject("No workers available, timeout of " + timeout + "ms exceeded.");
}, (_b = (_a = _this.config) === null || _a === void 0 ? void 0 : _a.timeout) !== null && _b !== void 0 ? _b : defaultTimeout);
var cb = function (w) {
clearTimeout(timeout);
resolve(w);
};
_this.waiting.unshift(cb);
});
};
WorkerPool.prototype.release = function (worker) {
var next = this.waiting.pop();
if (next) {
next(worker);
}
else {
var index = this.busy.indexOf(worker);
if (index !== -1) {
this.busy.splice(index, 1);
}
this.workers.push(worker);
}
};
WorkerPool.prototype.kill = function () {
this.workers.forEach(function (worker) { return worker.kill(); });
this.busy.forEach(function (worker) { return worker.kill(); });
};
return WorkerPool;
}());
var pool = function (task, config) {
if (config === void 0) { config = {}; }
var workerPool = new WorkerPool(task, config);
var run = (function (args) {
return workerPool.get().then(function (worker) {
return worker.run(args).then(function (res) {
workerPool.release(worker);
return res;
});
});
});
var kill = function () {
workerPool.kill();
};
var clone = function () { return pool(task, config); };
return {
clone: clone,
kill: kill,
run: run,
};
};
export { concurrent, pool, serial };