UNPKG

promise-worker-bi

Version:

Promise-based messaging for Web Workers and Shared Workers

259 lines (244 loc) 8.42 kB
(function () { 'use strict'; 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 PWBWorker extends PWBBase { constructor() { super(); // Only actually used for SharedWorker this._hosts = new Map(); this._maxHostID = -1; if ( // @ts-expect-error typeof SharedWorkerGlobalScope !== "undefined" && // @ts-expect-error self instanceof SharedWorkerGlobalScope) { this._workerType = "SharedWorker"; self.addEventListener("connect", e => { // @ts-expect-error const port = e.ports[0]; port.addEventListener("message", e2 => this._onMessage(e2)); port.start(); this._maxHostID += 1; const hostID = this._maxHostID; this._hosts.set(hostID, { port }); // Send back hostID to this host, otherwise it has no way to know it this._postMessage([MSGTYPE_HOST_ID, -1, hostID], hostID); }); self.addEventListener("error", e => { logError(e.error); // Just send to first host, so as to not duplicate error tracking const hostID = this._hosts.keys().next().value; if (hostID !== undefined) { this._postMessage([MSGTYPE_WORKER_ERROR, -1, toFakeError(e.error)], hostID); } }); } else { this._workerType = "Worker"; self.addEventListener("message", this._onMessage); // Since this is not a Shared Worker, hostID is always 0 so it's not strictly required to // send this back, but it makes the API a bit more consistent if there is the same // initialization handshake in both cases. this._postMessage([MSGTYPE_HOST_ID, -1, 0], 0); self.addEventListener("error", e => { logError(e.error); this._postMessage([MSGTYPE_WORKER_ERROR, -1, toFakeError(e.error)]); }); } } _postMessage(obj, targetHostID, transfer) { // console.log('_postMessage', obj, targetHostID); if (this._workerType === "SharedWorker") { // If targetHostID has been deleted, this will do nothing, which is fine I think this._hosts.forEach(({ port }, hostID) => { if (targetHostID === undefined || targetHostID === hostID) { // @ts-expect-error TypeScript thinks transfer can't be undefined port.postMessage(obj, transfer); } }); } else if (this._workerType === "Worker") { // @ts-expect-error TypeScript thinks self is window, which has a different postMessage call signature. In a worker, this is correct. self.postMessage(obj, transfer); } else { throw new Error("WTF"); } } postMessage(userMessage, targetHostID, transfer) { // console.log('postMessage', userMessage, targetHostID); const actuallyPostMessage = (resolve, reject) => { const messageID = messageIDs; messageIDs += 1; const messageToSend = [MSGTYPE_QUERY, messageID, userMessage]; this._callbacks.set(messageID, (error, result) => { if (error) { reject(error); } else { resolve(result); } }); this._postMessage(messageToSend, targetHostID, transfer); }; return new Promise((resolve, reject) => { actuallyPostMessage(resolve, reject); }); } _onMessage(e) { const common = this._onMessageCommon(e); if (!common) { return; } const { message, type } = common; if (type === MSGTYPE_HOST_CLOSE) { if (typeof message[2] !== "number") { throw new Error("Invalid hostID"); } const hostID = message[2]; this._hosts.delete(hostID); } } } const promiseWorker = new PWBWorker(); promiseWorker.register(() => { throw new Error("busted!"); }); })();