@jitsi/js-utils
Version:
Utilities for Jitsi JS projects
137 lines (136 loc) • 6.02 kB
JavaScript
/**
* 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;
}
}
}