promise-worker-bi
Version:
Promise-based messaging for Web Workers and Shared Workers
1,816 lines (1,592 loc) • 50.4 kB
JavaScript
(function () {
'use strict';
var global$1 = (typeof global !== "undefined" ? global :
typeof self !== "undefined" ? self :
typeof window !== "undefined" ? window : {});
// the following is from is-buffer, also by Feross Aboukhadijeh and with same lisence
// The _isBuffer check is for Safari 5-7 support, because it's missing
// Object.prototype.constructor. Remove this eventually
function isBuffer(obj) {
return obj != null && (!!obj._isBuffer || isFastBuffer(obj) || isSlowBuffer(obj))
}
function isFastBuffer (obj) {
return !!obj.constructor && typeof obj.constructor.isBuffer === 'function' && obj.constructor.isBuffer(obj)
}
// For Node v0.10 support. Remove this eventually.
function isSlowBuffer (obj) {
return typeof obj.readFloatLE === 'function' && typeof obj.slice === 'function' && isFastBuffer(obj.slice(0, 0))
}
// from https://github.com/kumavis/browser-process-hrtime/blob/master/index.js
var performance = global$1.performance || {};
performance.now ||
performance.mozNow ||
performance.msNow ||
performance.oNow ||
performance.webkitNow ||
function(){ return (new Date()).getTime() };
var inherits;
if (typeof Object.create === 'function'){
inherits = function inherits(ctor, superCtor) {
// implementation from standard node.js 'util' module
ctor.super_ = superCtor;
ctor.prototype = Object.create(superCtor.prototype, {
constructor: {
value: ctor,
enumerable: false,
writable: true,
configurable: true
}
});
};
} else {
inherits = function inherits(ctor, superCtor) {
ctor.super_ = superCtor;
var TempCtor = function () {};
TempCtor.prototype = superCtor.prototype;
ctor.prototype = new TempCtor();
ctor.prototype.constructor = ctor;
};
}
var inherits$1 = inherits;
/**
* Echos the value of a value. Trys to print the value out
* in the best way possible given the different types.
*
* @param {Object} obj The object to print out.
* @param {Object} opts Optional options object that alters the output.
*/
/* legacy: obj, showHidden, depth, colors*/
function inspect$1(obj, opts) {
// default options
var ctx = {
seen: [],
stylize: stylizeNoColor
};
// legacy...
if (arguments.length >= 3) ctx.depth = arguments[2];
if (arguments.length >= 4) ctx.colors = arguments[3];
if (isBoolean(opts)) {
// legacy...
ctx.showHidden = opts;
} else if (opts) {
// got an "options" object
_extend(ctx, opts);
}
// set default options
if (isUndefined(ctx.showHidden)) ctx.showHidden = false;
if (isUndefined(ctx.depth)) ctx.depth = 2;
if (isUndefined(ctx.colors)) ctx.colors = false;
if (isUndefined(ctx.customInspect)) ctx.customInspect = true;
if (ctx.colors) ctx.stylize = stylizeWithColor;
return formatValue(ctx, obj, ctx.depth);
}
// http://en.wikipedia.org/wiki/ANSI_escape_code#graphics
inspect$1.colors = {
'bold' : [1, 22],
'italic' : [3, 23],
'underline' : [4, 24],
'inverse' : [7, 27],
'white' : [37, 39],
'grey' : [90, 39],
'black' : [30, 39],
'blue' : [34, 39],
'cyan' : [36, 39],
'green' : [32, 39],
'magenta' : [35, 39],
'red' : [31, 39],
'yellow' : [33, 39]
};
// Don't use 'blue' not visible on cmd.exe
inspect$1.styles = {
'special': 'cyan',
'number': 'yellow',
'boolean': 'yellow',
'undefined': 'grey',
'null': 'bold',
'string': 'green',
'date': 'magenta',
// "name": intentionally not styling
'regexp': 'red'
};
function stylizeWithColor(str, styleType) {
var style = inspect$1.styles[styleType];
if (style) {
return '\u001b[' + inspect$1.colors[style][0] + 'm' + str +
'\u001b[' + inspect$1.colors[style][1] + 'm';
} else {
return str;
}
}
function stylizeNoColor(str, styleType) {
return str;
}
function arrayToHash(array) {
var hash = {};
array.forEach(function(val, idx) {
hash[val] = true;
});
return hash;
}
function formatValue(ctx, value, recurseTimes) {
// Provide a hook for user-specified inspect functions.
// Check that value is an object with an inspect function on it
if (ctx.customInspect &&
value &&
isFunction(value.inspect) &&
// Filter out the util module, it's inspect function is special
value.inspect !== inspect$1 &&
// Also filter out any prototype objects using the circular check.
!(value.constructor && value.constructor.prototype === value)) {
var ret = value.inspect(recurseTimes, ctx);
if (!isString(ret)) {
ret = formatValue(ctx, ret, recurseTimes);
}
return ret;
}
// Primitive types cannot have properties
var primitive = formatPrimitive(ctx, value);
if (primitive) {
return primitive;
}
// Look up the keys of the object.
var keys = Object.keys(value);
var visibleKeys = arrayToHash(keys);
if (ctx.showHidden) {
keys = Object.getOwnPropertyNames(value);
}
// IE doesn't make error fields non-enumerable
// http://msdn.microsoft.com/en-us/library/ie/dww52sbt(v=vs.94).aspx
if (isError(value)
&& (keys.indexOf('message') >= 0 || keys.indexOf('description') >= 0)) {
return formatError(value);
}
// Some type of object without properties can be shortcutted.
if (keys.length === 0) {
if (isFunction(value)) {
var name = value.name ? ': ' + value.name : '';
return ctx.stylize('[Function' + name + ']', 'special');
}
if (isRegExp(value)) {
return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp');
}
if (isDate(value)) {
return ctx.stylize(Date.prototype.toString.call(value), 'date');
}
if (isError(value)) {
return formatError(value);
}
}
var base = '', array = false, braces = ['{', '}'];
// Make Array say that they are Array
if (isArray(value)) {
array = true;
braces = ['[', ']'];
}
// Make functions say that they are functions
if (isFunction(value)) {
var n = value.name ? ': ' + value.name : '';
base = ' [Function' + n + ']';
}
// Make RegExps say that they are RegExps
if (isRegExp(value)) {
base = ' ' + RegExp.prototype.toString.call(value);
}
// Make dates with properties first say the date
if (isDate(value)) {
base = ' ' + Date.prototype.toUTCString.call(value);
}
// Make error with message first say the error
if (isError(value)) {
base = ' ' + formatError(value);
}
if (keys.length === 0 && (!array || value.length == 0)) {
return braces[0] + base + braces[1];
}
if (recurseTimes < 0) {
if (isRegExp(value)) {
return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp');
} else {
return ctx.stylize('[Object]', 'special');
}
}
ctx.seen.push(value);
var output;
if (array) {
output = formatArray(ctx, value, recurseTimes, visibleKeys, keys);
} else {
output = keys.map(function(key) {
return formatProperty(ctx, value, recurseTimes, visibleKeys, key, array);
});
}
ctx.seen.pop();
return reduceToSingleString(output, base, braces);
}
function formatPrimitive(ctx, value) {
if (isUndefined(value))
return ctx.stylize('undefined', 'undefined');
if (isString(value)) {
var simple = '\'' + JSON.stringify(value).replace(/^"|"$/g, '')
.replace(/'/g, "\\'")
.replace(/\\"/g, '"') + '\'';
return ctx.stylize(simple, 'string');
}
if (isNumber(value))
return ctx.stylize('' + value, 'number');
if (isBoolean(value))
return ctx.stylize('' + value, 'boolean');
// For some reason typeof null is "object", so special case here.
if (isNull(value))
return ctx.stylize('null', 'null');
}
function formatError(value) {
return '[' + Error.prototype.toString.call(value) + ']';
}
function formatArray(ctx, value, recurseTimes, visibleKeys, keys) {
var output = [];
for (var i = 0, l = value.length; i < l; ++i) {
if (hasOwnProperty(value, String(i))) {
output.push(formatProperty(ctx, value, recurseTimes, visibleKeys,
String(i), true));
} else {
output.push('');
}
}
keys.forEach(function(key) {
if (!key.match(/^\d+$/)) {
output.push(formatProperty(ctx, value, recurseTimes, visibleKeys,
key, true));
}
});
return output;
}
function formatProperty(ctx, value, recurseTimes, visibleKeys, key, array) {
var name, str, desc;
desc = Object.getOwnPropertyDescriptor(value, key) || { value: value[key] };
if (desc.get) {
if (desc.set) {
str = ctx.stylize('[Getter/Setter]', 'special');
} else {
str = ctx.stylize('[Getter]', 'special');
}
} else {
if (desc.set) {
str = ctx.stylize('[Setter]', 'special');
}
}
if (!hasOwnProperty(visibleKeys, key)) {
name = '[' + key + ']';
}
if (!str) {
if (ctx.seen.indexOf(desc.value) < 0) {
if (isNull(recurseTimes)) {
str = formatValue(ctx, desc.value, null);
} else {
str = formatValue(ctx, desc.value, recurseTimes - 1);
}
if (str.indexOf('\n') > -1) {
if (array) {
str = str.split('\n').map(function(line) {
return ' ' + line;
}).join('\n').substr(2);
} else {
str = '\n' + str.split('\n').map(function(line) {
return ' ' + line;
}).join('\n');
}
}
} else {
str = ctx.stylize('[Circular]', 'special');
}
}
if (isUndefined(name)) {
if (array && key.match(/^\d+$/)) {
return str;
}
name = JSON.stringify('' + key);
if (name.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)) {
name = name.substr(1, name.length - 2);
name = ctx.stylize(name, 'name');
} else {
name = name.replace(/'/g, "\\'")
.replace(/\\"/g, '"')
.replace(/(^"|"$)/g, "'");
name = ctx.stylize(name, 'string');
}
}
return name + ': ' + str;
}
function reduceToSingleString(output, base, braces) {
var length = output.reduce(function(prev, cur) {
if (cur.indexOf('\n') >= 0) ;
return prev + cur.replace(/\u001b\[\d\d?m/g, '').length + 1;
}, 0);
if (length > 60) {
return braces[0] +
(base === '' ? '' : base + '\n ') +
' ' +
output.join(',\n ') +
' ' +
braces[1];
}
return braces[0] + base + ' ' + output.join(', ') + ' ' + braces[1];
}
// NOTE: These type checking functions intentionally don't use `instanceof`
// because it is fragile and can be easily faked with `Object.create()`.
function isArray(ar) {
return Array.isArray(ar);
}
function isBoolean(arg) {
return typeof arg === 'boolean';
}
function isNull(arg) {
return arg === null;
}
function isNumber(arg) {
return typeof arg === 'number';
}
function isString(arg) {
return typeof arg === 'string';
}
function isUndefined(arg) {
return arg === void 0;
}
function isRegExp(re) {
return isObject(re) && objectToString(re) === '[object RegExp]';
}
function isObject(arg) {
return typeof arg === 'object' && arg !== null;
}
function isDate(d) {
return isObject(d) && objectToString(d) === '[object Date]';
}
function isError(e) {
return isObject(e) &&
(objectToString(e) === '[object Error]' || e instanceof Error);
}
function isFunction(arg) {
return typeof arg === 'function';
}
function isPrimitive(arg) {
return arg === null ||
typeof arg === 'boolean' ||
typeof arg === 'number' ||
typeof arg === 'string' ||
typeof arg === 'symbol' || // ES6 symbol
typeof arg === 'undefined';
}
function objectToString(o) {
return Object.prototype.toString.call(o);
}
function _extend(origin, add) {
// Don't do anything if add isn't an object
if (!add || !isObject(add)) return origin;
var keys = Object.keys(add);
var i = keys.length;
while (i--) {
origin[keys[i]] = add[keys[i]];
}
return origin;
}
function hasOwnProperty(obj, prop) {
return Object.prototype.hasOwnProperty.call(obj, prop);
}
function compare(a, b) {
if (a === b) {
return 0;
}
var x = a.length;
var y = b.length;
for (var i = 0, len = Math.min(x, y); i < len; ++i) {
if (a[i] !== b[i]) {
x = a[i];
y = b[i];
break;
}
}
if (x < y) {
return -1;
}
if (y < x) {
return 1;
}
return 0;
}
var hasOwn = Object.prototype.hasOwnProperty;
var objectKeys = Object.keys || function (obj) {
var keys = [];
for (var key in obj) {
if (hasOwn.call(obj, key)) keys.push(key);
}
return keys;
};
var pSlice = Array.prototype.slice;
var _functionsHaveNames;
function functionsHaveNames() {
if (typeof _functionsHaveNames !== 'undefined') {
return _functionsHaveNames;
}
return _functionsHaveNames = (function () {
return function foo() {}.name === 'foo';
}());
}
function pToString (obj) {
return Object.prototype.toString.call(obj);
}
function isView(arrbuf) {
if (isBuffer(arrbuf)) {
return false;
}
if (typeof global$1.ArrayBuffer !== 'function') {
return false;
}
if (typeof ArrayBuffer.isView === 'function') {
return ArrayBuffer.isView(arrbuf);
}
if (!arrbuf) {
return false;
}
if (arrbuf instanceof DataView) {
return true;
}
if (arrbuf.buffer && arrbuf.buffer instanceof ArrayBuffer) {
return true;
}
return false;
}
// 1. The assert module provides functions that throw
// AssertionError's when particular conditions are not met. The
// assert module must conform to the following interface.
function assert(value, message) {
if (!value) fail(value, true, message, '==', ok);
}
// 2. The AssertionError is defined in assert.
// new assert.AssertionError({ message: message,
// actual: actual,
// expected: expected })
var regex = /\s*function\s+([^\(\s]*)\s*/;
// based on https://github.com/ljharb/function.prototype.name/blob/adeeeec8bfcc6068b187d7d9fb3d5bb1d3a30899/implementation.js
function getName(func) {
if (!isFunction(func)) {
return;
}
if (functionsHaveNames()) {
return func.name;
}
var str = func.toString();
var match = str.match(regex);
return match && match[1];
}
assert.AssertionError = AssertionError;
function AssertionError(options) {
this.name = 'AssertionError';
this.actual = options.actual;
this.expected = options.expected;
this.operator = options.operator;
if (options.message) {
this.message = options.message;
this.generatedMessage = false;
} else {
this.message = getMessage(this);
this.generatedMessage = true;
}
var stackStartFunction = options.stackStartFunction || fail;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, stackStartFunction);
} else {
// non v8 browsers so we can have a stacktrace
var err = new Error();
if (err.stack) {
var out = err.stack;
// try to strip useless frames
var fn_name = getName(stackStartFunction);
var idx = out.indexOf('\n' + fn_name);
if (idx >= 0) {
// once we have located the function frame
// we need to strip out everything before it (and its line)
var next_line = out.indexOf('\n', idx + 1);
out = out.substring(next_line + 1);
}
this.stack = out;
}
}
}
// assert.AssertionError instanceof Error
inherits$1(AssertionError, Error);
function truncate(s, n) {
if (typeof s === 'string') {
return s.length < n ? s : s.slice(0, n);
} else {
return s;
}
}
function inspect(something) {
if (functionsHaveNames() || !isFunction(something)) {
return inspect$1(something);
}
var rawname = getName(something);
var name = rawname ? ': ' + rawname : '';
return '[Function' + name + ']';
}
function getMessage(self) {
return truncate(inspect(self.actual), 128) + ' ' +
self.operator + ' ' +
truncate(inspect(self.expected), 128);
}
// At present only the three keys mentioned above are used and
// understood by the spec. Implementations or sub modules can pass
// other keys to the AssertionError's constructor - they will be
// ignored.
// 3. All of the following functions must throw an AssertionError
// when a corresponding condition is not met, with a message that
// may be undefined if not provided. All assertion methods provide
// both the actual and expected values to the assertion error for
// display purposes.
function fail(actual, expected, message, operator, stackStartFunction) {
throw new AssertionError({
message: message,
actual: actual,
expected: expected,
operator: operator,
stackStartFunction: stackStartFunction
});
}
// EXTENSION! allows for well behaved errors defined elsewhere.
assert.fail = fail;
// 4. Pure assertion tests whether a value is truthy, as determined
// by !!guard.
// assert.ok(guard, message_opt);
// This statement is equivalent to assert.equal(true, !!guard,
// message_opt);. To test strictly for the value true, use
// assert.strictEqual(true, guard, message_opt);.
function ok(value, message) {
if (!value) fail(value, true, message, '==', ok);
}
assert.ok = ok;
// 5. The equality assertion tests shallow, coercive equality with
// ==.
// assert.equal(actual, expected, message_opt);
assert.equal = equal;
function equal(actual, expected, message) {
if (actual != expected) fail(actual, expected, message, '==', equal);
}
// 6. The non-equality assertion tests for whether two objects are not equal
// with != assert.notEqual(actual, expected, message_opt);
assert.notEqual = notEqual;
function notEqual(actual, expected, message) {
if (actual == expected) {
fail(actual, expected, message, '!=', notEqual);
}
}
// 7. The equivalence assertion tests a deep equality relation.
// assert.deepEqual(actual, expected, message_opt);
assert.deepEqual = deepEqual;
function deepEqual(actual, expected, message) {
if (!_deepEqual(actual, expected, false)) {
fail(actual, expected, message, 'deepEqual', deepEqual);
}
}
assert.deepStrictEqual = deepStrictEqual;
function deepStrictEqual(actual, expected, message) {
if (!_deepEqual(actual, expected, true)) {
fail(actual, expected, message, 'deepStrictEqual', deepStrictEqual);
}
}
function _deepEqual(actual, expected, strict, memos) {
// 7.1. All identical values are equivalent, as determined by ===.
if (actual === expected) {
return true;
} else if (isBuffer(actual) && isBuffer(expected)) {
return compare(actual, expected) === 0;
// 7.2. If the expected value is a Date object, the actual value is
// equivalent if it is also a Date object that refers to the same time.
} else if (isDate(actual) && isDate(expected)) {
return actual.getTime() === expected.getTime();
// 7.3 If the expected value is a RegExp object, the actual value is
// equivalent if it is also a RegExp object with the same source and
// properties (`global`, `multiline`, `lastIndex`, `ignoreCase`).
} else if (isRegExp(actual) && isRegExp(expected)) {
return actual.source === expected.source &&
actual.global === expected.global &&
actual.multiline === expected.multiline &&
actual.lastIndex === expected.lastIndex &&
actual.ignoreCase === expected.ignoreCase;
// 7.4. Other pairs that do not both pass typeof value == 'object',
// equivalence is determined by ==.
} else if ((actual === null || typeof actual !== 'object') &&
(expected === null || typeof expected !== 'object')) {
return strict ? actual === expected : actual == expected;
// If both values are instances of typed arrays, wrap their underlying
// ArrayBuffers in a Buffer each to increase performance
// This optimization requires the arrays to have the same type as checked by
// Object.prototype.toString (aka pToString). Never perform binary
// comparisons for Float*Arrays, though, since e.g. +0 === -0 but their
// bit patterns are not identical.
} else if (isView(actual) && isView(expected) &&
pToString(actual) === pToString(expected) &&
!(actual instanceof Float32Array ||
actual instanceof Float64Array)) {
return compare(new Uint8Array(actual.buffer),
new Uint8Array(expected.buffer)) === 0;
// 7.5 For all other Object pairs, including Array objects, equivalence is
// determined by having the same number of owned properties (as verified
// with Object.prototype.hasOwnProperty.call), the same set of keys
// (although not necessarily the same order), equivalent values for every
// corresponding key, and an identical 'prototype' property. Note: this
// accounts for both named and indexed properties on Arrays.
} else if (isBuffer(actual) !== isBuffer(expected)) {
return false;
} else {
memos = memos || {actual: [], expected: []};
var actualIndex = memos.actual.indexOf(actual);
if (actualIndex !== -1) {
if (actualIndex === memos.expected.indexOf(expected)) {
return true;
}
}
memos.actual.push(actual);
memos.expected.push(expected);
return objEquiv(actual, expected, strict, memos);
}
}
function isArguments(object) {
return Object.prototype.toString.call(object) == '[object Arguments]';
}
function objEquiv(a, b, strict, actualVisitedObjects) {
if (a === null || a === undefined || b === null || b === undefined)
return false;
// if one is a primitive, the other must be same
if (isPrimitive(a) || isPrimitive(b))
return a === b;
if (strict && Object.getPrototypeOf(a) !== Object.getPrototypeOf(b))
return false;
var aIsArgs = isArguments(a);
var bIsArgs = isArguments(b);
if ((aIsArgs && !bIsArgs) || (!aIsArgs && bIsArgs))
return false;
if (aIsArgs) {
a = pSlice.call(a);
b = pSlice.call(b);
return _deepEqual(a, b, strict);
}
var ka = objectKeys(a);
var kb = objectKeys(b);
var key, i;
// having the same number of owned properties (keys incorporates
// hasOwnProperty)
if (ka.length !== kb.length)
return false;
//the same set of keys (although not necessarily the same order),
ka.sort();
kb.sort();
//~~~cheap key test
for (i = ka.length - 1; i >= 0; i--) {
if (ka[i] !== kb[i])
return false;
}
//equivalent values for every corresponding key, and
//~~~possibly expensive deep test
for (i = ka.length - 1; i >= 0; i--) {
key = ka[i];
if (!_deepEqual(a[key], b[key], strict, actualVisitedObjects))
return false;
}
return true;
}
// 8. The non-equivalence assertion tests for any deep inequality.
// assert.notDeepEqual(actual, expected, message_opt);
assert.notDeepEqual = notDeepEqual;
function notDeepEqual(actual, expected, message) {
if (_deepEqual(actual, expected, false)) {
fail(actual, expected, message, 'notDeepEqual', notDeepEqual);
}
}
assert.notDeepStrictEqual = notDeepStrictEqual;
function notDeepStrictEqual(actual, expected, message) {
if (_deepEqual(actual, expected, true)) {
fail(actual, expected, message, 'notDeepStrictEqual', notDeepStrictEqual);
}
}
// 9. The strict equality assertion tests strict equality, as determined by ===.
// assert.strictEqual(actual, expected, message_opt);
assert.strictEqual = strictEqual;
function strictEqual(actual, expected, message) {
if (actual !== expected) {
fail(actual, expected, message, '===', strictEqual);
}
}
// 10. The strict non-equality assertion tests for strict inequality, as
// determined by !==. assert.notStrictEqual(actual, expected, message_opt);
assert.notStrictEqual = notStrictEqual;
function notStrictEqual(actual, expected, message) {
if (actual === expected) {
fail(actual, expected, message, '!==', notStrictEqual);
}
}
function expectedException(actual, expected) {
if (!actual || !expected) {
return false;
}
if (Object.prototype.toString.call(expected) == '[object RegExp]') {
return expected.test(actual);
}
try {
if (actual instanceof expected) {
return true;
}
} catch (e) {
// Ignore. The instanceof check doesn't work for arrow functions.
}
if (Error.isPrototypeOf(expected)) {
return false;
}
return expected.call({}, actual) === true;
}
function _tryBlock(block) {
var error;
try {
block();
} catch (e) {
error = e;
}
return error;
}
function _throws(shouldThrow, block, expected, message) {
var actual;
if (typeof block !== 'function') {
throw new TypeError('"block" argument must be a function');
}
if (typeof expected === 'string') {
message = expected;
expected = null;
}
actual = _tryBlock(block);
message = (expected && expected.name ? ' (' + expected.name + ').' : '.') +
(message ? ' ' + message : '.');
if (shouldThrow && !actual) {
fail(actual, expected, 'Missing expected exception' + message);
}
var userProvidedMessage = typeof message === 'string';
var isUnwantedException = !shouldThrow && isError(actual);
var isUnexpectedException = !shouldThrow && actual && !expected;
if ((isUnwantedException &&
userProvidedMessage &&
expectedException(actual, expected)) ||
isUnexpectedException) {
fail(actual, expected, 'Got unwanted exception' + message);
}
if ((shouldThrow && actual && expected &&
!expectedException(actual, expected)) || (!shouldThrow && actual)) {
throw actual;
}
}
// 11. Expected to throw an error:
// assert.throws(block, Error_opt, message_opt);
assert.throws = throws;
function throws(block, /*optional*/error, /*optional*/message) {
_throws(true, block, error, message);
}
// EXTENSION! This is annoying to write outside this module.
assert.doesNotThrow = doesNotThrow;
function doesNotThrow(block, /*optional*/error, /*optional*/message) {
_throws(false, block, error, message);
}
assert.ifError = ifError;
function ifError(err) {
if (err) throw err;
}
let messageIDs = 0;
const MSGTYPE_QUERY = 0;
const MSGTYPE_RESPONSE = 1;
const MSGTYPE_HOST_ID = 2;
const MSGTYPE_HOST_CLOSE = 3;
const MSGTYPE_WORKER_ERROR = 4;
const MSGTYPES = [MSGTYPE_QUERY, MSGTYPE_RESPONSE, MSGTYPE_HOST_ID, MSGTYPE_HOST_CLOSE, MSGTYPE_WORKER_ERROR];
// Inlined from https://github.com/then/is-promise
const isPromise = obj => !!obj && (typeof obj === "object" || typeof obj === "function") && typeof obj.then === "function";
const toFakeError = error => {
const fakeError = {
name: error.name,
message: error.message
};
if (typeof error.stack === "string") {
fakeError.stack = error.stack;
}
// These are non-standard properties, I think only in some versions of Firefox
// @ts-expect-error
if (typeof error.fileName === "string") {
// @ts-expect-error
fakeError.fileName = error.fileName;
}
// @ts-expect-error
if (typeof error.columnNumber === "number") {
// @ts-expect-error
fakeError.columnNumber = error.columnNumber;
}
// @ts-expect-error
if (typeof error.lineNumber === "number") {
// @ts-expect-error
fakeError.lineNumber = error.lineNumber;
}
return fakeError;
};
// Object rather than FakeError for convenience
const fromFakeError = fakeError => {
const error = new Error();
return Object.assign(error, fakeError);
};
const logError = err => {
// Logging in the console makes debugging in the worker easier
console.error("Error in Worker:");
console.error(err); // Safari needs it on new line
};
class PWBBase {
constructor() {
// console.log('constructor', worker);
this._callbacks = new Map();
this._queryCallback = () => {};
// @ts-expect-error
this._onMessage = this._onMessage.bind(this);
}
register(cb) {
// console.log('register', cb);
this._queryCallback = cb;
}
// From worker, 2nd param could be hostID if sending to specific host. From UI, 2nd param could be an array of transferable objects
_postResponse(messageID, error, result, hostID) {
// console.log('_postResponse', messageID, error, result);
if (error) {
logError(error);
this._postMessage([MSGTYPE_RESPONSE, messageID, toFakeError(error)], hostID);
} else {
// Hackily identify when message contains transferable objects
if (typeof result === "object" && result !== null && Object.hasOwn(result, "message") && Object.hasOwn(result, "_PWB_TRANSFER")) {
this._postMessage([MSGTYPE_RESPONSE, messageID, null, result.message], hostID, result._PWB_TRANSFER);
} else {
this._postMessage([MSGTYPE_RESPONSE, messageID, null, result], hostID);
}
}
}
_handleQuery(messageID, query, hostID) {
// console.log('_handleQuery', messageID, query);
try {
const result = this._queryCallback(query, hostID);
if (!isPromise(result)) {
this._postResponse(messageID, null, result, hostID);
} else {
result.then(finalResult => {
this._postResponse(messageID, null, finalResult, hostID);
}, finalError => {
this._postResponse(messageID, finalError, hostID);
});
}
} catch (err) {
this._postResponse(messageID, err);
}
}
// Either return messageID and type if further processing is needed, or undefined otherwise
_onMessageCommon(e) {
// console.log('_onMessage', e.data);
const message = e.data;
if (!Array.isArray(message) || message.length < 3 || message.length > 4) {
return; // Ignore - this message is not for us
}
if (typeof message[0] !== "number" || MSGTYPES.indexOf(message[0]) < 0) {
throw new Error("Invalid messageID");
}
const type = message[0];
if (typeof message[1] !== "number") {
throw new Error("Invalid messageID");
}
const messageID = message[1];
if (type === MSGTYPE_QUERY) {
const query = message[2];
if (typeof message[3] !== "number" && message[3] !== undefined) {
throw new Error("Invalid hostID");
}
const hostID = message[3];
this._handleQuery(messageID, query, hostID);
return;
}
if (type === MSGTYPE_RESPONSE) {
if (message[2] !== null && typeof message[2] !== "object") {
throw new Error("Invalid error");
}
const error = message[2] === null ? null : fromFakeError(message[2]);
const result = message[3];
const callback = this._callbacks.get(messageID);
if (callback === undefined) {
// Ignore - user might have created multiple PromiseWorkers.
// This message is not for us.
return;
}
this._callbacks.delete(messageID);
callback(error, result);
return;
}
return {
message,
type
};
}
}
class PWBHost extends PWBBase {
// Only defined on host
constructor(worker) {
super();
// The following if statement used to check `worker instanceof Worker` but I have recieved
// reports that in some weird cases, Safari will inappropriately return false for that, even
// in obvious cases like:
//
// blob = new Blob(["self.onmessage = function() {};"], { type: "text/javascript" });
// worker = new Worker(window.URL.createObjectURL(blob));
// console.log(worker instanceof Worker);
//
// So instead, let's do this test for worker.port which only exists on shared workers.
// @ts-expect-error
if (worker.port === undefined) {
this._workerType = "Worker";
// @ts-expect-error
worker.addEventListener("message", this._onMessage);
} else {
this._workerType = "SharedWorker";
// @ts-expect-error - it doesn't know if _worker is Worker or SharedWorker, but I do
worker.port.addEventListener("message", this._onMessage);
// @ts-expect-error - it doesn't know if _worker is Worker or SharedWorker, but I do
worker.port.start();
// Handle tab close. This isn't perfect, but there is no perfect method
// http://stackoverflow.com/q/13662089/786644 and this should work like
// 99% of the time. It is a memory leak if it fails, but for most use
// cases, it shouldn't be noticeable.
window.addEventListener("beforeunload", () => {
// Prevent firing if we don't know hostID yet
if (this._hostID !== undefined) {
this._postMessage([MSGTYPE_HOST_CLOSE, -1, this._hostID]);
}
});
}
this._worker = worker;
this._hostIDQueue = [];
}
registerError(cb) {
// console.log('registerError', cb);
this._errorCallback = cb;
// Some browsers (Firefox) call onerror on every host, while others
// (Chrome) do nothing. Let's disable that everywhere, for consistency.
this._worker.addEventListener("error", e => {
e.preventDefault();
e.stopPropagation();
});
}
_postMessage(obj, _hostID, transfer) {
// console.log('_postMessage', obj, _hostID, transfer);
if (this._workerType === "Worker") {
// @ts-expect-error - it doesn't know if _worker is Worker or SharedWorker, but I do
this._worker.postMessage(obj, transfer);
} else if (this._workerType === "SharedWorker") {
// @ts-expect-error - it doesn't know if _worker is Worker or SharedWorker, but I do
this._worker.port.postMessage(obj, transfer);
} else {
throw new Error("WTF");
}
}
postMessage(userMessage, _hostID, transfer) {
// console.log('postMessage', userMessage, _hostID, transfer);
const actuallyPostMessage = (resolve, reject) => {
const messageID = messageIDs;
messageIDs += 1;
const messageToSend = [MSGTYPE_QUERY, messageID, userMessage, this._hostID];
this._callbacks.set(messageID, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
this._postMessage(messageToSend, undefined, transfer);
};
return new Promise((resolve, reject) => {
// Don't send a message until hostID is known, otherwise it's a race
// condition and sometimes hostID will be undefined.
if (this._hostIDQueue !== undefined) {
this._hostIDQueue.push(() => {
actuallyPostMessage(resolve, reject);
});
} else {
actuallyPostMessage(resolve, reject);
}
});
}
_onMessage(e) {
const common = this._onMessageCommon(e);
if (!common) {
return;
}
const {
message,
type
} = common;
if (type === MSGTYPE_HOST_ID) {
if (message[2] !== undefined && typeof message[2] !== "number") {
throw new Error("Invalid hostID");
}
const hostID = message[2];
this._hostID = hostID;
if (this._hostIDQueue !== undefined) {
this._hostIDQueue.forEach(func => {
// Not entirely sure why setTimeout is needed, might be just for unit tests
setTimeout(() => {
func();
}, 0);
});
this._hostIDQueue = undefined; // Never needed again after initial setup
}
} else if (type === MSGTYPE_WORKER_ERROR) {
if (message[2] !== undefined && message[2] !== null && typeof message[2] === "object") {
const error = fromFakeError(message[2]);
if (this._errorCallback !== undefined) {
this._errorCallback(error);
}
}
}
}
}
const pathPrefix = "/base/dist/test/";
// Only run in browser
const testSharedWorker = typeof SharedWorker !== "undefined" ? it : it.skip;
describe("host -> worker", function () {
this.timeout(120000);
it("sends a message back and forth", () => {
const worker = new Worker(`${pathPrefix}worker-pong.js`);
const promiseWorker = new PWBHost(worker);
return promiseWorker.postMessage("ping").then((res) => {
assert.equal(res, "pong");
});
});
it("echoes a message", () => {
const worker = new Worker(`${pathPrefix}worker-echo.js`);
const promiseWorker = new PWBHost(worker);
return promiseWorker.postMessage("ping").then((res) => {
assert.equal(res, "ping");
});
});
it("pongs a message with a promise", () => {
const worker = new Worker(`${pathPrefix}worker-pong-promise.js`);
const promiseWorker = new PWBHost(worker);
return promiseWorker.postMessage("ping").then((res) => {
assert.equal(res, "pong");
});
});
it("pongs a message with a promise, again", () => {
const worker = new Worker(`${pathPrefix}worker-pong-promise.js`);
const promiseWorker = new PWBHost(worker);
return promiseWorker.postMessage("ping").then((res) => {
assert.equal(res, "pong");
});
});
it("echoes a message multiple times", () => {
const worker = new Worker(`${pathPrefix}worker-echo.js`);
const promiseWorker = new PWBHost(worker);
const words = [
"foo",
"bar",
"baz",
"quux",
"toto",
"bongo",
"haha",
"flim",
"foob",
"foobar",
"bazzy",
"fifi",
"kiki",
];
return Promise.all(
words.map((word) => {
return promiseWorker.postMessage(word).then((res) => {
assert.equal(res, word);
});
}),
);
});
it("can have multiple PromiseWorkers", () => {
const worker = new Worker(`${pathPrefix}worker-echo.js`);
const promiseWorker1 = new PWBHost(worker);
const promiseWorker2 = new PWBHost(worker);
return promiseWorker1
.postMessage("foo")
.then((res) => {
assert.equal(res, "foo");
})
.then(() => {
return promiseWorker2.postMessage("bar");
})
.then((res) => {
assert.equal(res, "bar");
});
});
it("can have multiple PromiseWorkers 2", () => {
const worker = new Worker(`${pathPrefix}worker-echo.js`);
const promiseWorkers = [
new PWBHost(worker),
new PWBHost(worker),
new PWBHost(worker),
new PWBHost(worker),
new PWBHost(worker),
];
return Promise.all(
promiseWorkers.map((promiseWorker, i) => {
return promiseWorker
.postMessage(`foo${i}`)
.then((res) => {
assert.equal(res, `foo${i}`);
})
.then(() => {
return promiseWorker.postMessage(`bar${i}`);
})
.then((res) => {
assert.equal(res, `bar${i}`);
});
}),
);
});
it("handles synchronous errors", () => {
const worker = new Worker(`${pathPrefix}worker-error-sync.js`);
const promiseWorker = new PWBHost(worker);
return promiseWorker.postMessage("foo").then(
() => {
throw new Error("expected an error here");
},
(err) => {
assert.equal(err.message, "busted!");
// Either have the file name or error message in the stack. Chrome has both, Firefox has just the file name, Node has just the error message.
assert(
err.stack.includes("worker-error-sync") ||
err.stack.includes("busted!"),
);
},
);
});
it("handles asynchronous errors", () => {
const worker = new Worker(`${pathPrefix}worker-error-async.js`);
const promiseWorker = new PWBHost(worker);
return promiseWorker.postMessage("foo").then(
() => {
throw new Error("expected an error here");
},
(err) => {
assert.equal(err.message, "oh noes");
// Firefox stack does not include the error message here, for some reason
if (!navigator || !navigator.userAgent.includes("Firefox")) {
assert(err.stack.indexOf("oh noes") >= 0);
}
},
);
});
it("handles unregistered callbacks", () => {
const worker = new Worker(`${pathPrefix}worker-empty.js`);
const promiseWorker = new PWBHost(worker);
return promiseWorker.postMessage("ping").then(
() => {
throw new Error("expected an error here");
},
(err) => {
assert(err);
},
);
});
it("allows custom additional behavior", () => {
const worker = new Worker(`${pathPrefix}worker-echo-custom.js`);
const promiseWorker = new PWBHost(worker);
return Promise.all([
promiseWorker.postMessage("ping"),
new Promise((resolve, reject) => {
function cleanup() {
worker.removeEventListener("message", onMessage);
worker.removeEventListener("error", onError);
}
function onMessage(e) {
if (Array.isArray(e.data)) {
return;
}
cleanup();
resolve(e.data);
}
function onError(e) {
cleanup();
reject(e);
}
worker.addEventListener("error", onError);
worker.addEventListener("message", onMessage);
worker.postMessage({ hello: "world" });
}).then((data) => {
assert.equal(data.hello, "world");
}),
]);
});
it("allows custom additional behavior 2", () => {
const worker = new Worker(`${pathPrefix}worker-echo-custom-2.js`);
const promiseWorker = new PWBHost(worker);
return Promise.all([
promiseWorker.postMessage("ping"),
new Promise((resolve, reject) => {
function cleanup() {
worker.removeEventListener("message", onMessage);
worker.removeEventListener("error", onError);
}
function onMessage(e) {
if (e.data !== "[2]") {
return;
}
cleanup();
resolve(e.data);
}
function onError(e) {
cleanup();
reject(e);
}
worker.addEventListener("error", onError);
worker.addEventListener("message", onMessage);
worker.postMessage("[2]");
}).then((data) => {
assert.equal(data, "[2]");
}),
]);
});
it("makes hostID immediately available", () => {
const worker = new Worker(`${pathPrefix}worker-hostid.js`);
const promiseWorker = new PWBHost(worker);
return promiseWorker
.postMessage("ping")
.then((res) => {
assert.equal(res, 0);
})
.then(() => {
return new Promise((resolve, reject) => {
setTimeout(() => {
return promiseWorker
.postMessage("ping")
.then((res) => {
assert.equal(res, 0);
resolve();
})
.catch(reject);
}, 500);
});
});
});
});
describe("worker -> host", function () {
this.timeout(120000);
it("sends a message from worker to host", (done) => {
const worker = new Worker(`${pathPrefix}worker-host-ping.js`);
const promiseWorker = new PWBHost(worker);
let i = 0;
promiseWorker.register((msg) => {
if (i === 0) {
assert.equal(msg, "ping");
} else if (i === 1) {
assert.equal(msg, "pong");
done();
} else {
throw new Error("Extra message");
}
i += 1;
return "pong";
});
});
it("echoes a message", (done) => {
const worker = new Worker(`${pathPrefix}worker-host-echo.js`);
const promiseWorker = new PWBHost(worker);
let i = 0;
promiseWorker.register((msg) => {
if (i === 0) {
assert.equal(msg, "ping");
} else if (i === 1) {
assert.equal(msg, "ping");
done();
} else {
throw new Error("Extra message");
}
i += 1;
return msg;
});
});
it("pongs a message with a promise", (done) => {
const worker = new Worker(`${pathPrefix}worker-host-ping.js`);
const promiseWorker = new PWBHost(worker);
let i = 0;
promiseWorker.register((msg) => {
if (i === 0) {
assert.equal(msg, "ping");
} else if (i === 1) {
assert.equal(msg, "pong");
done();
} else {
throw new Error("Extra message");
}
i += 1;
return Promise.resolve("pong");
});
});
it("pongs a message with a promise, again", (done) => {
const worker = new Worker(`${pathPrefix}worker-host-ping.js`);
const promiseWorker = new PWBHost(worker);
let i = 0;
promiseWorker.register((msg) => {
if (i === 0) {
assert.equal(msg, "ping");
} else if (i === 1) {
assert.equal(msg, "pong");
done();
} else {
throw new Error("Extra message");
}
i += 1;
return Promise.resolve("pong");
});
});
it("echoes a message multiple times", (done) => {
const worker = new Worker(`${pathPrefix}worker-host-echo-multiple.js`);
const promiseWorker = new PWBHost(worker);
const words = [
"foo",
"bar",
"baz",
"quux",
"toto",
"bongo",
"haha",
"flim",
"foob",
"foobar",
"bazzy",
"fifi",
"kiki",
];
let i = 0;
promiseWorker.register((msg) => {
assert.equal(msg, words[i % words.length]);
i += 1;
if (i === words.length * 2) {
done();
}
return msg;
});
});
it("can have multiple PromiseWorkers", (done) => {
const worker = new Worker(`${pathPrefix}worker-host-echo.js`);
const promiseWorker1 = new PWBHost(worker);
const promiseWorker2 = new PWBHost(worker);
let i = 0;
let j = 0;
promiseWorker1.register((msg) => {
if (i === 0) {
assert.equal(msg, "ping");
} else if (i === 1) {
assert.equal(msg, "ping");
} else {
throw new Error("Extra message");
}
if (i === 1 && j === 1) {
done();
}
i += 1;
return msg;
});
promiseWorker2.register((msg) => {
if (j === 0) {
assert.equal(msg, "ping");
} else if (j === 1) {
assert.equal(msg, "ping");
} else {
throw new Error("Extra message");
}
if (i === 1 && j === 1) {
done();
}
j += 1;
return msg;
});
});
it("handles synchronous errors", (done) => {
const worker = new Worker(`${pathPrefix}worker-host-error-sync.js`);
const promiseWorker = new PWBHost(worker);
let i = 0;
promiseWorker.register((msg) => {
if (i === 0) {
i += 1;
throw new Error("busted!");
} else if (i === 1) {
i += 1;
assert.equal(msg, "done");
done();
} else {
throw new Error("Extra message");
}
});
});
it("handles asynchronous errors", (done) => {
const worker = new Worker(`${pathPrefix}worker-host-error-async.js`);
const promiseWorker = new PWBHost(worker);
let i = 0;
promiseWorker.register((msg) => {
if (i === 0) {
i += 1;
return Promise.resolve().then(() => {
throw new Error("oh noes");
});
}
if (i === 1) {
i += 1;
assert.equal(msg, "done");
done();
} else {
throw new Error("Extra message");
}
});
});
testSharedWorker("handles errors outside of responses", (done) => {
const worker = new Worker(
`${pathPrefix}worker-host-error-outside-response.js`,
);
const promiseWorker = new PWBHost(worker);
promiseWorker.registerError((e) => {
assert(e.message.indexOf("error-outside-response") >= 0);
assert(e.stack.indexOf("error-outside-response") >= 0);
done();
});
});
// This test is a little dicey, relies on setTimeout timing across host and worker
it("handles unregistered callbacks", (done) => {
const worker = new Worker(`${pathPrefix}worker-host-empty.js`);
const promiseWorker = new PWBHost(worker);
promiseWorker.register("mistake!");
setTimeout(() => {
promiseWorker.register((msg) => {
assert.equal(msg, "done");
done();
});
}, 50);
});
it("allows custom additional behavior", (done) => {
const worker = new Worker(`${pathPrefix}worker-host-echo-custom.js`);
const promiseWorker = new PWBHost(worker);
let i = 0;
promiseWorker.register((msg) => {
if (i === 0) {
assert.equal(msg, "ping");
} else if (i === 1) {
assert.equal(msg, "done");
done();
} else {
throw new Error("Extra message");
}
i += 1;
return msg;
});
worker.addEventListener("message", (e) => {
if (!Array.isArray(e.data)) {
// custom message
worker.postMessage(e.data);
}
});
});
});
describe("bidirectional communication", function () {
this.timeout(120000);
it("echoes a message", (done) => {
const worker = new Worker(`${pathPrefix}worker-bidirectional-echo.js`);
const promiseWorker = new PWBHost(worker);
let i = 0;
promiseWorker.register((msg) => {
if (i === 0) {
assert.equal(msg, "ping");
} else if (i === 1) {
assert.equal(msg, "ping");
promiseWorker.postMessage("pong").then((res) => {
assert.equal(res, "pong");
done();
});
} else {
throw new Error("Extra message");
}
i += 1;
return msg;
});
});
});
// This is a shitty test, not sure how to simulate a real multi-tab test
describe("Shared Worker", function () {
this.timeout(120000);
testSharedWorker("works", (done) => {
const worker = new SharedWorker(`${pathPrefix}worker-shared.js`);
const promiseWorker = new PWBHost(worker);
let i = 0;
const NUM_MESSAGES = 4; // 2 from broadcast, 1 from non-broadcast, and 2 from individual message responses
function gotMessage() {
i += 1;
if (i === NUM_MESSAGES) {
done();
}
}
const expected = ["to all hosts", "to just one host"];
promiseWorker.register((msg) => {
const expectedMsg = expected.shift();
assert.equal(msg, expectedMsg);
gotMessage();
});
promiseWorker
.postMessage("broadcast")
.then((res) => {
assert.equal(res, "broadcast");
gotMessage();
})
.then(() => {
return promiseWorker.postMessage("foo").then((res) => {
assert.equal(res, "foo");
gotMessage();
});
});
});
testSharedWorker("handles errors outside of responses", (done) => {
const worker = new SharedWorker(
`${pathPrefix}worker-host-error-outside-response.js`,
);
const promiseWorker = new PWBHost(worker);
promiseWorker.registerError((e) => {
assert(e.message.indexOf("error-outside-response") >= 0);
assert(e.stack.indexOf("error-outside-response") >= 0);
done();
});
});
});
describe("transferable", function () {
this.timeout(120000);
it("from host to worker", async () => {
const worker = new Worker(`${pathPrefix}worker-transferable.js`);
const promiseWorker = new PWBHost(worker);
const buffer = new ArrayBuffer(1);
const response1 = await promiseWorker.postMessage(buffer);
assert.equal(buffer.byteLength, 1);
assert.equal(response1.byteLength, 1);
// byteLength goes to 0 when transfered https://developer.chrome.com/blog/transferable-objects-lightning-fast/
const response2 = await promiseWorker.postMessage(buffer, undefined, [
buffer,
]);
assert.equal(buffer.byteLength, 0);
assert.equal(response2.byteLength, 1);
// Wait for async errors in worker to happen
await new Promise((resolve) => {
setTimeout(() => {
resolve();
}, 500);
});
});
it("from worker to host", async () => {
const worker = new Worker(`${pathPrefix}worker-host-transferable.js`);
const promiseWorker = new PWBHost(worker);
promiseWorker.register((buffer) => {
return buffer;
});
const buffers = [];