UNPKG

promise-worker-bi

Version:

Promise-based messaging for Web Workers and Shared Workers

1,816 lines (1,592 loc) 50.4 kB
(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 = [];