UNPKG

@actualwave/messageport-dispatcher

Version:

Cross-domain EventDispatcher for MessagePort interface

549 lines (432 loc) 14.5 kB
(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'; class MessagePortTarget { constructor(sender, receiver) { this.sender = sender || []; this.receiver = receiver || []; if (!(this.sender instanceof Array)) { this.sender = [this.sender]; } if (!(this.receiver instanceof Array)) { this.receiver = [this.receiver]; } } /* @param data @param origin */ 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)); } } function unwrapExports (x) { return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; } function createCommonjsModule(fn, module) { return module = { exports: {} }, fn(module, module.exports), module.exports; } var hasOwn_1 = createCommonjsModule(function (module, exports) { Object.defineProperty(exports, '__esModule', { value: true }); const hasOwn = ( (has) => (target, property) => Boolean(target && has.call(target, property)) )(Object.prototype.hasOwnProperty); exports.hasOwn = hasOwn; exports.default = hasOwn; }); var hasOwn = unwrapExports(hasOwn_1); var hasOwn_2 = hasOwn_1.hasOwn; var eventDispatcher = createCommonjsModule(function (module, exports) { Object.defineProperty(exports, '__esModule', { value: true }); function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } var hasOwn = _interopDefault(hasOwn_1); /** * */ /* eslint-disable import/prefer-default-export */ const isObject = value => typeof value === 'object' && value !== null; /** * Created by Oleg Galaburda on 09.02.16. * */ class Event { constructor(type, data = null) { this.type = type; this.data = data; this.defaultPrevented = false; } toJSON() { return { type: this.type, data: this.data }; } isDefaultPrevented() { return this.defaultPrevented; } preventDefault() { this.defaultPrevented = true; } } const getEvent = (eventOrType, optionalData) => { let event = eventOrType; if (!isObject(eventOrType)) { event = new Event(String(eventOrType), optionalData); } return event; }; /** * Created by Oleg Galaburda on 09.02.16. * */ 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, target) { let listener; const { listeners } = this; this.augmentEvent(event); // TODO this has to be handled in separate object ListenersRunner that should be // created for each call() call and asked for index validation on each listener remove. for (this.index = 0; this.index < listeners.length; this.index++) { if (this.immediatelyStopped) break; listener = listeners[this.index]; listener.call(target, event); } this.clearEvent(event); this.onComplete(this); } augmentEvent(eventObject) { const event = eventObject; event.stopPropagation = this.onStopped; event.stopImmediatePropagation = this.stopImmediatePropagation; } /* eslint class-methods-use-this: "off" */ clearEvent(eventObject) { const event = eventObject; delete event.stopPropagation; delete event.stopImmediatePropagation; } listenerRemoved(listeners, index) { if (listeners === this.listeners && index <= this.index) { this.index--; } } } /** * Created by Oleg Galaburda on 09.02.16. * */ class EventListeners { constructor() { this._listeners = {}; this._runners = []; this.removeRunner = runner => { this._runners.splice(this._runners.indexOf(runner), 1); }; } createList(eventType, priorityOpt) { const priority = parseInt(priorityOpt, 10); const target = this.getPrioritiesByKey(eventType); const key = String(priority); let value; if (hasOwn(target, key)) { value = target[key]; } else { value = []; target[key] = value; } return value; } getPrioritiesByKey(key) { let value; if (hasOwn(this._listeners, key)) { value = this._listeners[key]; } else { 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) { let priority; let result = false; const priorities = this.getPrioritiesByKey(eventType); if (priorities) { for (priority in priorities) { if (hasOwn(priorities, priority)) { result = true; break; } } } return result; } remove(eventType, handler) { const priorities = this.getPrioritiesByKey(eventType); if (priorities) { const list = Object.getOwnPropertyNames(priorities); const { length } = list; for (let index = 0; index < length; index++) { const priority = list[index]; 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, target) { const priorities = this.getPrioritiesByKey(event.type); let stopped = false; const stopPropagation = () => { stopped = true; }; if (priorities) { // getOwnPropertyNames() or keys()? const list = Object.getOwnPropertyNames(priorities).sort((a, b) => a - b); const { length } = list; for (let index = 0; index < length; index++) { if (stopped) break; const handlers = priorities[list[index]]; // in case if all handlers of priority were removed while event // was dispatched and handlers become undefined. if (handlers) { const runner = this.createRunner(handlers, stopPropagation); runner.run(event, target); if (runner.immediatelyStopped) break; } } } } } /** * Created by Oleg Galaburda on 09.02.16. * */ class EventDispatcher { constructor(eventPreprocessor = null) { this._eventPreprocessor = eventPreprocessor; this._listeners = new EventListeners(); } addEventListener(eventType, listener, priority = 0) { 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); exports.default = EventDispatcher; exports.Event = Event; exports.EventDispatcher = EventDispatcher; exports.createEventDispatcher = createEventDispatcher; exports.getEvent = getEvent; exports.isObject = isObject; }); unwrapExports(eventDispatcher); var eventDispatcher_1 = eventDispatcher.Event; var eventDispatcher_2 = eventDispatcher.EventDispatcher; var eventDispatcher_3 = eventDispatcher.createEventDispatcher; var eventDispatcher_4 = eventDispatcher.getEvent; var eventDispatcher_5 = eventDispatcher.isObject; /** * Created by Oleg Galaburda on 09.02.16. */ const createId = () => `MP/${Math.ceil(Math.random() * 10000)}/${Date.now()}`; /** * If toJSON method implemented on object, it will be called instead of converting to JSON string. * This was made to utilize structured cloning algorithm for raw objects. * https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm * In this case developer is responsible for converting linked objects. * @param object * @returns {Object} */ const toRawData = object => { if (typeof object.toJSON === 'function') { return object.toJSON(); } return JSON.stringify(object); }; /** * * @param data {Object|String} * @returns {Object} */ const parseRawData = data => { let object; // keep it undefined in case of error if (eventDispatcher_5(data)) { return data; } try { return JSON.parse(data); } catch (error) {// this isn't an event we are waiting for. } return object; }; /** * Created by Oleg Galaburda on 09.02.16. */ class MessagePortEvent { constructor(event, dispatcherId) { this.event = event; this.dispatcherId = dispatcherId; } toJSON() { return { event: toRawData(this.event), dispatcherId: this.dispatcherId }; } } const isMessagePortEvent = object => eventDispatcher_5(object) && hasOwn(object, 'dispatcherId') && 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; }; /** * Created by Oleg Galaburda on 09.02.16. */ 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 = eventDispatcher_3(); this.receiver = eventDispatcher_3(receiverEventPreprocessor); 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 = eventDispatcher_4(eventType, data); if (this.senderEventPreprocessor) { event = this.senderEventPreprocessor.call(this, event); } const eventJson = toRawData(new MessagePortEvent(event, this.dispatcherId)); return this._postMessageHandler(eventJson, transferList); } /** * @private */ _postMessageHandler(data, transferList) { const handler = this.customPostMessageHandler; if (handler) { return handler.call(this, data, this.targetOrigin, transferList); } return this.target.postMessage(data, this.targetOrigin, transferList); } /** * @private */ _postMessageListener(event) { // INFO .nativeEvent react-native thing, it contains event object coming from WebView event = event.nativeEvent || event; const message = parseMessagePortEvent(event.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, customPostMessageHandler, receiverEventPreprocessor, senderEventPreprocessor); const factory = (getTarget, dispatcher = null) => () => { 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