UNPKG

@jitsi/js-utils

Version:

Utilities for Jitsi JS projects

137 lines (136 loc) 6.02 kB
/** * Implements message transport using the MessageChannel API. * * When shouldCreateChannel is true, a new MessageChannel is created and port2 is * transferred to the target window via postMessage. When false, the backend listens * for an incoming init_channel message carrying the port. * * Outgoing messages sent before the port is established (receiver side) are buffered and * flushed automatically once the handshake completes. Incoming messages are buffered * natively by the MessagePort until a receive callback is wired via setReceiveCallback, * since assigning onmessage implicitly starts the port. * * Re-handshake: on the receiver side the init_channel listener stays attached for the * lifetime of the backend. If the creator (e.g. an iframe) self-navigates or reloads * and posts a fresh init_channel, the receiver closes the previous (now-dead) port and * adopts the new one transparently. MessageChannel exposes no liveness signal, so this * "always re-adopt the latest matching port" policy is the only way to keep working * across in-creator reloads without cooperation from the creator side. */ export default class MessageChannelTransportBackend { /** * Creates a new MessageChannelTransportBackend instance. * * @param {IMessageChannelTransportBackendOptions} options - Configuration options for the transport backend. */ constructor({ shouldCreateChannel = false, targetWindow = window.parent, origin, scope }) { /** * Forwards messages from the MessagePort to the current receive callback. * * @param {MessageEvent} event - The incoming port message event. */ this.handlePortMessage = (event) => { var _a; (_a = this.receiveCallback) === null || _a === void 0 ? void 0 : _a.call(this, event.data); }; this.initialMessageHandler = this.initialMessageHandler.bind(this); this.origin = origin; this.scope = scope; this.pendingMessages = []; if (shouldCreateChannel) { this.channel = new MessageChannel(); this.port = this.channel.port1; targetWindow.postMessage({ type: 'init_channel', scope: this.scope }, origin !== null && origin !== void 0 ? origin : '*', [this.channel.port2]); } else { window.addEventListener('message', this.initialMessageHandler); } } /** * Handles the initial postMessage event to receive the MessagePort from the channel creator. * * The listener remains attached for the lifetime of the backend so that if the creator * self-reloads (its previous document and port1 are gone, leaving the receiver with an * inert port2 that the platform never signals), a fresh init_channel can replace the * dead port without cooperation from the creator. * * @param {MessageEvent} event - The incoming message event. */ initialMessageHandler(event) { var _a; if (this.origin && event.origin !== this.origin) { return; } const { data, ports } = event; if ((data === null || data === void 0 ? void 0 : data.type) === 'init_channel' && data.scope === this.scope && (ports === null || ports === void 0 ? void 0 : ports.length)) { // Release the previous port (if any) before adopting the new one. Closing also // detaches its onmessage handler, so no stale messages can still be dispatched. (_a = this.port) === null || _a === void 0 ? void 0 : _a.close(); this.port = event.ports[0]; // Only start the port (via onmessage assignment) if the consumer has already wired // a receive callback. Otherwise let the port buffer incoming messages natively until // setReceiveCallback is called. if (this.receiveCallback) { this.port.onmessage = this.handlePortMessage; } this.flushPendingMessages(); } } /** * Flushes any messages that were buffered before the port was established. */ flushPendingMessages() { var _a; for (const { message, transfer } of this.pendingMessages) { (_a = this.port) === null || _a === void 0 ? void 0 : _a.postMessage(message, transfer ? { transfer } : undefined); } this.pendingMessages = []; } /** * Disposes the allocated resources. * * @returns {void} */ dispose() { var _a; (_a = this.port) === null || _a === void 0 ? void 0 : _a.close(); this.port = undefined; this.channel = undefined; this.pendingMessages = []; this.receiveCallback = undefined; window.removeEventListener('message', this.initialMessageHandler); } /** * Sends the passed message. If the port has not yet been established (receiver side), * the message is buffered and will be sent once the handshake completes. * * @param {any} message - The message to be sent. * @param {Transferable[]} [transfer] - Optional array of transferable objects. * @returns {void} */ send(message, transfer) { if (this.port) { this.port.postMessage(message, transfer ? { transfer } : undefined); } else { this.pendingMessages.push({ message, transfer }); } } /** * Sets the callback for receiving data. If the MessagePort is available and has not * yet been started, this also starts it (by assigning onmessage), flushing any messages * buffered natively by the port since the handshake completed. * * @param {Function} callback - The new callback. * @returns {void} */ setReceiveCallback(callback) { this.receiveCallback = callback; if (this.port && !this.port.onmessage) { this.port.onmessage = this.handlePortMessage; } } }