@actualwave/messageport-dispatcher
Version:
Cross-domain EventDispatcher for MessagePort interface
355 lines (335 loc) • 13.9 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.MessagePortDispatcher = {}));
})(this, (function (exports) { 'use strict';
/* eslint-disable @typescript-eslint/no-explicit-any */
class MessagePortTarget {
constructor(sender, receiver) {
this.sender = sender ? (Array.isArray(sender) ? sender : [sender]) : [];
this.receiver = receiver ? (Array.isArray(receiver) ? receiver : [receiver]) : [];
}
postMessage(...args) {
this.sender.forEach((item) => item.postMessage(...args));
}
addEventListener(type, handler) {
this.receiver.forEach((item) => item.addEventListener(type, handler));
}
removeEventListener(type, handler) {
this.receiver.forEach((item) => item.removeEventListener(type, handler));
}
}
var eventDispatcher = {};
var hasRequiredEventDispatcher;
function requireEventDispatcher () {
if (hasRequiredEventDispatcher) return eventDispatcher;
hasRequiredEventDispatcher = 1;
Object.defineProperty(eventDispatcher, '__esModule', { value: true });
const isObject = (value) => typeof value === 'object' && value !== null;
class Event {
constructor(type, data = null) {
this.defaultPrevented = false;
this.type = type;
this.data = data;
}
toJSON() {
return { type: this.type, data: this.data };
}
isDefaultPrevented() {
return this.defaultPrevented;
}
preventDefault() {
this.defaultPrevented = true;
}
}
const getEvent = (eventOrType, optionalData) => {
if (!isObject(eventOrType)) {
return new Event(String(eventOrType), optionalData);
}
return eventOrType;
};
class ListenersRunner {
constructor(listeners, onStopped, onComplete) {
this.index = -1;
this.immediatelyStopped = false;
this.stopImmediatePropagation = () => {
this.immediatelyStopped = true;
};
this.listeners = listeners;
this.onStopped = onStopped;
this.onComplete = onComplete;
}
run(event) {
const dispatched = event;
dispatched.stopPropagation = this.onStopped;
dispatched.stopImmediatePropagation = this.stopImmediatePropagation;
for (this.index = 0; this.index < this.listeners.length; this.index++) {
if (this.immediatelyStopped)
break;
this.listeners[this.index](dispatched);
}
delete event.stopPropagation;
delete event.stopImmediatePropagation;
this.onComplete(this);
}
listenerRemoved(listeners, index) {
if (listeners === this.listeners && index <= this.index) {
this.index--;
}
}
}
class EventListeners {
constructor() {
this._listeners = {};
this._runners = [];
this.removeRunner = (runner) => {
this._runners.splice(this._runners.indexOf(runner), 1);
};
}
createList(eventType, priority) {
const target = this.getPrioritiesByKey(eventType);
const key = String(priority);
if (Object.hasOwn(target, key)) {
return target[key];
}
const value = [];
target[key] = value;
return value;
}
getPrioritiesByKey(key) {
if (Object.hasOwn(this._listeners, key)) {
return this._listeners[key];
}
const value = {};
this._listeners[key] = value;
return value;
}
add(eventType, handler, priority) {
const handlers = this.createList(eventType, priority);
if (handlers.indexOf(handler) < 0) {
handlers.push(handler);
}
}
has(eventType) {
if (!Object.hasOwn(this._listeners, eventType)) {
return false;
}
return Object.keys(this._listeners[eventType]).length > 0;
}
remove(eventType, handler) {
if (!Object.hasOwn(this._listeners, eventType)) {
return;
}
const priorities = this._listeners[eventType];
const list = Object.getOwnPropertyNames(priorities);
for (const priority of list) {
const handlers = priorities[priority];
const handlerIndex = handlers.indexOf(handler);
if (handlerIndex >= 0) {
handlers.splice(handlerIndex, 1);
if (!handlers.length) {
delete priorities[priority];
}
this._runners.forEach((runner) => {
runner.listenerRemoved(handlers, handlerIndex);
});
}
}
}
removeAll(eventType) {
delete this._listeners[eventType];
}
createRunner(handlers, onStopped) {
const runner = new ListenersRunner(handlers, onStopped, this.removeRunner);
this._runners.push(runner);
return runner;
}
call(event) {
if (!Object.hasOwn(this._listeners, event.type)) {
return;
}
const priorities = this._listeners[event.type];
let stopped = false;
const stopPropagation = () => {
stopped = true;
};
const list = Object.getOwnPropertyNames(priorities).sort((a, b) => Number(a) - Number(b));
for (const key of list) {
if (stopped)
break;
const handlers = priorities[key];
if (handlers) {
const runner = this.createRunner(handlers, stopPropagation);
runner.run(event);
if (runner.immediatelyStopped)
break;
}
}
}
}
class EventDispatcher {
constructor(eventPreprocessor = null) {
this._eventPreprocessor = eventPreprocessor;
this._listeners = new EventListeners();
}
addEventListener(eventType, listener, priority = 0) {
// Negate priority so higher values sort first (ascending sort order in EventListeners)
this._listeners.add(eventType, listener, -priority || 0);
}
hasEventListener(eventType) {
return this._listeners.has(eventType);
}
removeEventListener(eventType, listener) {
this._listeners.remove(eventType, listener);
}
removeAllEventListeners(eventType) {
this._listeners.removeAll(eventType);
}
dispatchEvent(event, data) {
let eventObject = getEvent(event, data);
if (this._eventPreprocessor) {
eventObject = this._eventPreprocessor.call(this, eventObject);
}
this._listeners.call(eventObject);
}
}
const createEventDispatcher = (eventPreprocessor) => new EventDispatcher(eventPreprocessor ?? null);
eventDispatcher.Event = Event;
eventDispatcher.EventDispatcher = EventDispatcher;
eventDispatcher.createEventDispatcher = createEventDispatcher;
eventDispatcher.default = EventDispatcher;
eventDispatcher.getEvent = getEvent;
eventDispatcher.isObject = isObject;
return eventDispatcher;
}
var eventDispatcherExports = requireEventDispatcher();
const isObject = (value) => typeof value === 'object' && value !== null;
const createId = () => `MP/${Math.ceil(Math.random() * 10000)}/${Date.now()}`;
/**
* If toJSON method is implemented on the object, it will be called instead of converting to a
* JSON string. This utilises the structured cloning algorithm for raw objects.
* https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
* In this case the developer is responsible for converting nested objects.
*/
const toRawData = (object) => {
if (typeof object.toJSON === 'function') {
return object.toJSON();
}
return JSON.stringify(object);
};
const parseRawData = (data) => {
if (isObject(data)) {
return data;
}
try {
return JSON.parse(data);
}
catch {
// not a valid JSON event
}
return undefined;
};
class MessagePortEvent {
constructor(event, dispatcherId) {
this.event = event;
this.dispatcherId = dispatcherId;
}
toJSON() {
return {
event: toRawData(this.event),
dispatcherId: this.dispatcherId,
};
}
}
const isMessagePortEvent = (object) => typeof object === 'object' &&
object !== null &&
Object.hasOwn(object, 'dispatcherId') &&
Object.hasOwn(object, 'event');
const parseMessagePortEvent = (object) => {
const result = parseRawData(object);
if (result && isMessagePortEvent(result)) {
const { event, dispatcherId } = result;
return new MessagePortEvent(parseRawData(event), dispatcherId);
}
return null;
};
/* eslint-disable no-restricted-globals, @typescript-eslint/no-explicit-any */
class MessagePortDispatcher {
constructor(target = null, customPostMessageHandler = null, receiverEventPreprocessor = null, senderEventPreprocessor = null) {
this.dispatcherId = createId();
this.targetOrigin = '*';
this.target = target ?? self;
this.customPostMessageHandler = customPostMessageHandler;
this.senderEventPreprocessor = senderEventPreprocessor;
this.sender = eventDispatcherExports.createEventDispatcher();
this.receiver = eventDispatcherExports.createEventDispatcher(receiverEventPreprocessor ?? undefined);
this.target.addEventListener('message', (event) => this._postMessageListener(event));
}
addEventListener(eventType, listener, priority) {
this.receiver.addEventListener(eventType, listener, priority);
}
hasEventListener(eventType) {
return this.receiver.hasEventListener(eventType);
}
removeEventListener(eventType, listener) {
this.receiver.removeEventListener(eventType, listener);
}
removeAllEventListeners(eventType) {
this.receiver.removeAllEventListeners(eventType);
}
dispatchEvent(eventType, data, transferList) {
let event = typeof eventType === 'string' ? { type: eventType, data } : eventType;
if (this.senderEventPreprocessor) {
event = this.senderEventPreprocessor(event);
}
const eventJson = toRawData(new MessagePortEvent(event, this.dispatcherId));
this._postMessageHandler(eventJson, transferList);
}
_postMessageHandler(data, transferList) {
const handler = this.customPostMessageHandler;
if (handler) {
handler.call(this, data, this.targetOrigin, transferList);
return;
}
this.target.postMessage(data, this.targetOrigin, transferList);
}
_postMessageListener(event) {
// .nativeEvent is a React Native property containing the event from WebView
const nativeEvent = event.nativeEvent ?? event;
const message = parseMessagePortEvent(nativeEvent.data);
if (message) {
if (message.dispatcherId === this.dispatcherId) {
this.sender.dispatchEvent(message.event);
}
else {
this.receiver.dispatchEvent(message.event);
}
}
}
}
const createMessagePortDispatcher = (target, customPostMessageHandler, receiverEventPreprocessor, senderEventPreprocessor) => new MessagePortDispatcher(target ?? null, customPostMessageHandler ?? null, receiverEventPreprocessor ?? null, senderEventPreprocessor ?? null);
const factory = (getTarget) => {
let dispatcher = null;
return () => {
if (!dispatcher) {
dispatcher = createMessagePortDispatcher(getTarget());
}
return dispatcher;
};
};
const getForSelf = factory(() => self);
const getForParent = factory(() => parent);
const getForTop = factory(() => top);
exports.MessagePortDispatcher = MessagePortDispatcher;
exports.MessagePortEvent = MessagePortEvent;
exports.MessagePortTarget = MessagePortTarget;
exports.createMessagePortDispatcher = createMessagePortDispatcher;
exports.default = MessagePortDispatcher;
exports.getForParent = getForParent;
exports.getForSelf = getForSelf;
exports.getForTop = getForTop;
exports.isMessagePortEvent = isMessagePortEvent;
exports.parseMessagePortEvent = parseMessagePortEvent;
Object.defineProperty(exports, '__esModule', { value: true });
}));
//# sourceMappingURL=messageport-dispatcher.js.map