├── .gitignore ├── package-lock.json ├── package.json ├── readme.md ├── src ├── adapters │ ├── Adapter.ts │ └── WindowAdapter.ts ├── bus │ └── Bus.ts ├── config │ └── index.ts ├── index.ts ├── protocols │ └── WindowProtocol.ts └── utils │ ├── UniqPrimitiveCollection.ts │ ├── console │ └── index.ts │ ├── index.ts │ └── utils │ └── index.ts ├── stand ├── iframe.html └── parent.html ├── test ├── Bus.test.ts ├── WindowAdapter.test.ts ├── mock │ ├── MockAdapter.ts │ └── Win.ts └── tsconfig.json └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | dist 4 | coverage 5 | **/*.js 6 | **/*.js.map 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@waves/waves-browser-bus", 3 | "version": "0.2.7", 4 | "main": "dist/index.js", 5 | "types": "dist/index.d.ts", 6 | "repository": { 7 | "type": "git", 8 | "url": "git@github.com:wavesplatform/waves-browser-bus.git" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/wavesplatform/waves-browser-bus/issues" 12 | }, 13 | "devDependencies": { 14 | "@types/jest": "^24.0.4", 15 | "browserify": "^16.2.3", 16 | "jest": "^24.1.0", 17 | "ts-jest": "^23.10.5", 18 | "ts-utils": "^6.0.7", 19 | "typescript": "^3.3.3", 20 | "uglifyjs": "^2.4.11" 21 | }, 22 | "files": [ 23 | "dist" 24 | ], 25 | "jest": { 26 | "collectCoverage": true, 27 | "coveragePathIgnorePatterns": [ 28 | "/node_modules/", 29 | "/test/" 30 | ], 31 | "moduleFileExtensions": [ 32 | "ts", 33 | "tsx", 34 | "js" 35 | ], 36 | "transform": { 37 | "^.+\\.(ts|tsx)$": "ts-jest" 38 | }, 39 | "testMatch": [ 40 | "**/test/*test.+(ts)" 41 | ] 42 | }, 43 | "scripts": { 44 | "patch": "npm version patch && npm publish && git push", 45 | "prepare": "npm run build", 46 | "preversion": "npm run test", 47 | "postversion": "npm publish", 48 | "postpublish": "git push", 49 | "build": "tsc --build ./ && npm run _build-full", 50 | "test": "jest", 51 | "_build-full": "browserify ./dist/index.js -s bus -o ./dist/browser-bus.js && uglifyjs ./dist/browser-bus.js -o ./dist/browser-bus.min.js" 52 | }, 53 | "dependencies": { 54 | "@types/node": "^11.9.4", 55 | "typed-ts-events": "1.1.1" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Browser Bus 2 | 3 | Библиотека для работы над текстовым протоколом. 4 | Позволяет реализовать связь например для: 5 | * двух различных окон браузера 6 | * окно браузера с iframe 7 | 8 | ## Browser Bus API 9 | 10 | В библиотеке содержится классы Bus, Adapter и WindowAdapter, кроме того есть конфигурирование 11 | уровня логирования. 12 | Для работы библиотеки должны быть 2 13 | стороны между которыми можно отправлять сообщения любого формата. 14 | С каждой из этих сторон нужно создать по экземпляру класса Bus для отправки и получения 15 | запросов и событий. 16 | 17 | ## Bus 18 | 19 | Позволяет отправлять и подписываться на события. 20 | 21 | Принимает экземпляр класса Adapter отвечающий за реализацию протокола отправки сообщений 22 | и время ожидания ответа по умолчанию (в миллисекундах). 23 | Время ответа не обязательный параметр и по умолчанию равен 5 секунд. 24 | 25 | Пример связи iframe и родительского окна: 26 | 27 | На стороне родительского окна: 28 | ```javascript 29 | import { Bus, WindowAdapter } from '@waves/waves-browser-bus'; 30 | 31 | const url = 'https://some-iframe-content-url.com'; 32 | const iframe = document.createElement('iframe'); 33 | 34 | WindowAdapter.createSimpleWindowAdapter(iframe).then(adapter => { 35 | const bus = new Bus(adapter); 36 | 37 | bus.once('ready', () => { 38 | // Получено сообщение от iframe 39 | }); 40 | }); 41 | iframe.src = url; // Предпочтительно присваивать url после вызова WindowAdapter.createSimpleWindowAdapter 42 | document.body.appendChild(iframe); 43 | ``` 44 | На стороне iframe: 45 | ```javascript 46 | import { Bus, WindowAdapter } from '@waves/waves-browser-bus'; 47 | WindowAdapter.createSimpleWindowAdapter().then(adapter => { 48 | const bus = new Bus(adapter); 49 | 50 | bus.dispatchEvent('ready', null); // Отправили сообщение в родительское окно 51 | }); 52 | 53 | ``` 54 | 55 | ### dispatchEvent 56 | 57 | Отправляет событие. Все экземпляры Bus, с которыми установлена связь и есть подписка на это событие, получат это сообщение. 58 | Вторым аргументом передаются данные для обработчиков. 59 | Везде, кроме IE, допустимы объекты, которые клонируются, а в IE – только строка. 60 | 61 | ```javascript 62 | 63 | bus.dispatchEvent('some-event-name', jsonLikeData); 64 | 65 | ``` 66 | 67 | ### request 68 | 69 | Параметры: 70 | + name - метод запроса котоый вызовится на другом экземпляре Bus 71 | + [data] - данные которые будут переданы в метод 72 | + [timeout] - время ожидания ответа (default = 5000) 73 | 74 | Если за время `timeout` ответа не последует - будет сгенерирована ошибка по таймауту. 75 | Если другой экземпляр Bus не имеет обработчика с именем `name` будет сгенерирована ошибка 76 | (см. `registerRequestHandler`) 77 | Если во время выполнения метода произойдёт ошибка - она вернётся в Promise.reject. 78 | 79 | Отправляет запрос к другому экземпляру Bus. 80 | 81 | ```javascript 82 | 83 | bus.request('some-event-name', jsonLikeData, 100).then(data => { 84 | // data - ответ от Bus 85 | }); 86 | 87 | ``` 88 | 89 | ### on 90 | Позволяет подписаться на события из Bus. 91 | 92 | Пример: 93 | ```javascript 94 | bus.on('some-event', data => { 95 | //data - данные пришедшие в событии 96 | }); 97 | ``` 98 | 99 | 100 | ### once 101 | Позволяет однократно подписаться на события из Bus. 102 | 103 | Пример: 104 | ```javascript 105 | bus.once('some-event', data => { 106 | //data - данные пришедшие в событии 107 | }); 108 | ``` 109 | 110 | ### off 111 | Позволяет отписаться от событий другого Bus. 112 | 113 | Параметры: 114 | + [eventName] - имя события. Если не передано отпишется от всех событий с переданным `handler`. 115 | + [handler] - обработчик событий. Если не передан - отпишется от всех обработчиков с данным `eventName`. 116 | 117 | Если параметры не переданы - отпишется от всех событий. 118 | 119 | Пример: 120 | ```javascript 121 | bus.off('some-event', handler); // Отпишется от `some-event` с обработчиком `handler` 122 | bus.off('some-event'); // Отпишется от всех обработчиков на имя `some-event` 123 | bus.off(null, handler); // Отпишется во всех именах от обработчика `handler` 124 | bus.off(); // Отпишется от всех событий 125 | ``` 126 | 127 | 128 | 129 | ### registerRequestHandler 130 | Метод для обработки запроса из другого экземпляра bus. 131 | 132 | Параметры: 133 | + name - имя метода который будет доступен для вызова из другого bus 134 | + handler - обработчик который будет отвечать в другой bus 135 | Если обработчик возвращает `Promise`, то bus дождется окончания и отправит результат. 136 | 137 | Пример: 138 | ```javascript 139 | // В коде c одним из bus (например в iframe) 140 | bus.registerRequestHandler('get-random', () => Math.random()); 141 | 142 | // В основном коде приложения 143 | bus.request('get-random').then(num => { 144 | // Получили ответ из окна в iframe 145 | }) 146 | 147 | 148 | ``` 149 | 150 | или 151 | 152 | ```javascript 153 | // В коде c одним из bus (например в iframe) 154 | bus.registerRequestHandler('get-random', () => Promise.resolve(Math.random())); 155 | 156 | // В основном коде приложения 157 | bus.request('get-random').then(num => { 158 | // Получили ответ из окна в iframe 159 | }) 160 | 161 | 162 | ``` 163 | 164 | -------------------------------------------------------------------------------- /src/adapters/Adapter.ts: -------------------------------------------------------------------------------- 1 | import { IOneArgFunction, TMessageContent } from '../bus/Bus'; 2 | 3 | 4 | export abstract class Adapter { 5 | 6 | public abstract send(data: TMessageContent): this; 7 | public abstract addListener(cb: IOneArgFunction): this; 8 | public abstract destroy(): void; 9 | } -------------------------------------------------------------------------------- /src/adapters/WindowAdapter.ts: -------------------------------------------------------------------------------- 1 | import { Adapter } from './Adapter'; 2 | import { console, IOneArgFunction, TChanelId, TMessageContent, toArray, UniqPrimitiveCollection, uniqueId } from '..'; 3 | import { WindowProtocol } from '../protocols/WindowProtocol'; 4 | import { pipe } from '../utils/utils'; 5 | import IOptions = WindowAdapter.IOptions; 6 | import IWindow = WindowProtocol.IWindow; 7 | 8 | const EMPTY_OPTIONS: IOptions, TOrList> = { origins: [], availableChanelId: [] }; 9 | type TOrList = T | Array; 10 | type TContent = HTMLIFrameElement | WindowProtocol.IWindow; 11 | 12 | 13 | export class WindowAdapter extends Adapter { 14 | 15 | public readonly id: string = uniqueId('wa'); 16 | private readonly dispatch: Array>; 17 | private readonly listen: Array>; 18 | private readonly options: WindowAdapter.IOptions, UniqPrimitiveCollection>; 19 | private readonly callbacks: Array> = []; 20 | 21 | 22 | constructor(listen: Array>, dispatch: Array>, options?: Partial, TOrList>>) { 23 | super(); 24 | 25 | this.options = WindowAdapter.prepareOptions(options); 26 | this.listen = listen; 27 | this.dispatch = dispatch; 28 | this.listen.forEach(protocol => protocol.on('message', this.onMessage, this)); 29 | } 30 | 31 | public addListener(cb: IOneArgFunction): this { 32 | this.callbacks.push(cb); 33 | console.info('WindowAdapter: Add iframe message listener'); 34 | return this; 35 | } 36 | 37 | public send(data: TMessageContent): this { 38 | const message = { ...data, chanelId: this.options.chanelId }; 39 | this.dispatch.forEach(protocol => protocol.dispatch(message)); 40 | console.info('WindowAdapter: Send message', message); 41 | return this; 42 | } 43 | 44 | public destroy(): void { 45 | this.listen.forEach(protocol => protocol.destroy()); 46 | this.dispatch.forEach(protocol => protocol.destroy()); 47 | console.info('WindowAdapter: Destroy'); 48 | } 49 | 50 | private onMessage(event: WindowProtocol.IMessageEvent): void { 51 | if (this.accessEvent(event)) { 52 | this.callbacks.forEach(cb => { 53 | try { 54 | cb(event.data); 55 | } catch (e) { 56 | console.warn('WindowAdapter: Unhandled exception!', e); 57 | } 58 | }); 59 | } 60 | } 61 | 62 | private accessEvent(event: WindowProtocol.IMessageEvent): boolean { 63 | if (typeof event.data !== 'object' || event.data.type == null) { 64 | console.info('WindowAdapter: Block event. Wrong event format!', event.data); 65 | return false; 66 | } 67 | 68 | if (!this.options.origins.has('*') && !this.options.origins.has(event.origin)) { 69 | console.info(`SimpleWindowAdapter: Block event by origin "${event.origin}"`); 70 | return false; 71 | } 72 | 73 | if (!this.options.availableChanelId.size) { 74 | return true; 75 | } 76 | 77 | const access = !!(event.data.chanelId && this.options.availableChanelId.has(event.data.chanelId)); 78 | 79 | if (!access) { 80 | console.info(`SimpleWindowAdapter: Block event by chanel id "${event.data.chanelId}"`); 81 | } 82 | 83 | return access; 84 | } 85 | 86 | public static createSimpleWindowAdapter(iframe?: TContent, options?: Partial, TOrList>>): Promise { 87 | const origin = this.getContentOrigin(iframe); 88 | const myOptions = this.prepareOptions(options); 89 | const events: Array> = []; 90 | 91 | if (origin) { 92 | myOptions.origins.add(origin); 93 | } 94 | 95 | const listen = new WindowProtocol(window, WindowProtocol.PROTOCOL_TYPES.LISTEN); 96 | const handler: (e: WindowProtocol.IMessageEvent) => void = event => { 97 | events.push(event); 98 | }; 99 | 100 | listen.on('message', handler); 101 | 102 | return this.getIframeContent(iframe) 103 | .then(win => { 104 | const dispatch = new WindowProtocol(win.win, WindowProtocol.PROTOCOL_TYPES.DISPATCH); 105 | const adapter = new WindowAdapter([listen], [dispatch], this.unPrepareOptions(myOptions)); 106 | 107 | events.forEach(event => { 108 | adapter.onMessage(event); 109 | }); 110 | listen.off('message', handler); 111 | 112 | return adapter; 113 | }); 114 | } 115 | 116 | private static prepareOptions(options: Partial, TOrList>> = EMPTY_OPTIONS): WindowAdapter.IOptions, UniqPrimitiveCollection> { 117 | const concat = (initialValue: UniqPrimitiveCollection) => (list: Array) => list.reduce((set, item) => set.add(item), initialValue); 118 | const getCollection = (data: TOrList, initial: UniqPrimitiveCollection) => pipe, Array, UniqPrimitiveCollection>(toArray, concat(initial))(data); 119 | 120 | const origins = getCollection(options.origins || [], new UniqPrimitiveCollection([window.location.origin])); 121 | const chanelId = getCollection(options.availableChanelId || [], new UniqPrimitiveCollection()); 122 | 123 | return { ...options, origins, availableChanelId: chanelId }; 124 | } 125 | 126 | private static unPrepareOptions(options: WindowAdapter.IOptions, UniqPrimitiveCollection>): WindowAdapter.IOptions, TOrList> { 127 | return { 128 | origins: options.origins.toArray(), 129 | availableChanelId: options.availableChanelId.toArray(), 130 | chanelId: options.chanelId 131 | }; 132 | } 133 | 134 | private static getIframeContent(content?: TContent): Promise<{ win: IWindow }> { 135 | if (!content) { 136 | return Promise.resolve({ win: window.opener || window.parent }); 137 | } 138 | if (!(content instanceof HTMLIFrameElement)) { 139 | return Promise.resolve({ win: content }); 140 | } 141 | if (content.contentWindow) { 142 | return Promise.resolve({ win: content.contentWindow }); 143 | } 144 | return new Promise((resolve, reject) => { 145 | content.addEventListener('load', () => resolve({ win: content.contentWindow as IWindow }), false); 146 | content.addEventListener('error', reject, false); 147 | }); 148 | } 149 | 150 | private static getContentOrigin(content?: TContent): string | null { 151 | if (!content) { 152 | try { 153 | return new URL(document.referrer).origin; 154 | } catch (e) { 155 | return null; 156 | } 157 | } 158 | 159 | if (!(content instanceof HTMLIFrameElement)) { 160 | try { 161 | return window.top.origin; 162 | } catch (e) { 163 | return null; 164 | } 165 | } 166 | 167 | try { 168 | return new URL(content.src).origin || null; 169 | } catch (e) { 170 | return null; 171 | } 172 | } 173 | } 174 | 175 | 176 | export namespace WindowAdapter { 177 | 178 | export interface IOptions { 179 | origins: ORIGINS; 180 | availableChanelId: CHANEL_ID; 181 | chanelId?: TChanelId; 182 | } 183 | 184 | } -------------------------------------------------------------------------------- /src/bus/Bus.ts: -------------------------------------------------------------------------------- 1 | import { Adapter } from '../adapters/Adapter'; 2 | import { uniqueId, console } from '../utils'; 3 | 4 | 5 | export const enum EventType { 6 | Event, 7 | Action, 8 | Response 9 | } 10 | 11 | export const enum ResponseStatus { 12 | Success, 13 | Error 14 | } 15 | 16 | export class Bus = any, H extends Record any> = any> { 17 | 18 | public id: string = uniqueId('bus'); 19 | private _adapter: Adapter; 20 | private readonly _activeRequestHash: Record; 21 | private readonly _timeout: number; 22 | private readonly _eventHandlers: Record; 23 | private readonly _requestHandlers: H; 24 | 25 | 26 | constructor(adapter: Adapter, defaultTimeout?: number) { 27 | this._timeout = defaultTimeout || 5000; 28 | this._adapter = adapter; 29 | this._adapter.addListener((data) => this._onMessage(data)); 30 | this._eventHandlers = Object.create(null); 31 | this._activeRequestHash = Object.create(null); 32 | this._requestHandlers = Object.create(null); 33 | 34 | console.info(`Create Bus with id "${this.id}"`); 35 | } 36 | 37 | public dispatchEvent(name: K, data: T[K]): this { 38 | this._adapter.send(Bus._createEvent(name as string, data)); 39 | console.info(`Dispatch event "${name}"`, data); 40 | return this; 41 | } 42 | 43 | public request(name: E, data?: Parameters[0], timeout?: number): Promise extends Promise ? P : ReturnType> { 44 | return new Promise((resolve, reject) => { 45 | const id = uniqueId(`${this.id}-action`); 46 | const wait = timeout || this._timeout; 47 | 48 | let timer: number | NodeJS.Timeout; 49 | 50 | if ((timeout || this._timeout) !== -1) { 51 | timer = setTimeout(() => { 52 | delete this._activeRequestHash[id]; 53 | const error = new Error(`Timeout error for request with name "${name}" and timeout ${wait}!`); 54 | console.error(error); 55 | reject(error); 56 | }, wait); 57 | } 58 | 59 | const cancelTimeout = () => { 60 | if (timer) { 61 | clearTimeout(timer as number); 62 | } 63 | }; 64 | 65 | this._activeRequestHash[id] = { 66 | reject: (error: any) => { 67 | cancelTimeout(); 68 | console.error(`Error request with name "${name}"`, error); 69 | reject(error); 70 | }, 71 | resolve: (data: T) => { 72 | cancelTimeout(); 73 | console.info(`Request with name "${name}" success resolved!`, data); 74 | resolve(data); 75 | } 76 | }; 77 | 78 | this._adapter.send({ id, type: EventType.Action, name, data }); 79 | console.info(`Request with name "${name}"`, data); 80 | }); 81 | } 82 | 83 | public on(name: K, handler: IOneArgFunction, context?: any): this { 84 | return this._addEventHandler(name as string, handler, context, false); 85 | } 86 | 87 | public once(name: K, handler: IOneArgFunction, context?: any): this { 88 | return this._addEventHandler(name as string, handler, context, true); 89 | } 90 | 91 | public off(name?: string, handler?: IOneArgFunction): this 92 | public off(name?: K, handler?: IOneArgFunction): this 93 | public off(name?: string, handler?: IOneArgFunction): this { 94 | if (!name) { 95 | Object.keys(this._eventHandlers).forEach((name) => this.off(name, handler)); 96 | return this; 97 | } 98 | 99 | if (!this._eventHandlers[name]) { 100 | return this; 101 | } 102 | 103 | if (!handler) { 104 | this._eventHandlers[name].slice().forEach((info) => { 105 | this.off(name, info.handler); 106 | }); 107 | return this; 108 | } 109 | 110 | this._eventHandlers[name] = this._eventHandlers[name].filter((info) => info.handler !== handler); 111 | 112 | if (!this._eventHandlers[name].length) { 113 | delete this._eventHandlers[name]; 114 | } 115 | 116 | return this; 117 | } 118 | 119 | public registerRequestHandler(name: E, handler: H[E]): this { 120 | if (this._requestHandlers[name]) { 121 | throw new Error('Duplicate request handler!'); 122 | } 123 | 124 | this._requestHandlers[name] = handler; 125 | 126 | return this; 127 | } 128 | 129 | public unregisterHandler(name: E): this { 130 | if (this._requestHandlers[name]) { 131 | delete this._requestHandlers[name]; 132 | } 133 | return this; 134 | } 135 | 136 | public changeAdapter(adapter: Adapter): Bus { 137 | const bus = new Bus(adapter, this._timeout); 138 | 139 | Object.keys(this._eventHandlers).forEach((name) => { 140 | this._eventHandlers[name].forEach((info) => { 141 | if (info.once) { 142 | bus.once(name, info.handler, info.context); 143 | } else { 144 | bus.on(name, info.handler, info.context); 145 | } 146 | }); 147 | }); 148 | 149 | Object.keys(this._requestHandlers).forEach((name) => { 150 | bus.registerRequestHandler(name, this._requestHandlers[name]); 151 | }); 152 | 153 | return bus; 154 | } 155 | 156 | public destroy(): void { 157 | console.info('Destroy Bus'); 158 | this.off(); 159 | this._adapter.destroy(); 160 | } 161 | 162 | private _addEventHandler(name: string, handler: IOneArgFunction, context: any, once: boolean): this { 163 | if (!this._eventHandlers[name]) { 164 | this._eventHandlers[name] = []; 165 | } 166 | 167 | this._eventHandlers[name].push({ handler, once, context }); 168 | 169 | return this; 170 | } 171 | 172 | private _onMessage(message: TMessageContent): void { 173 | switch (message.type) { 174 | case EventType.Event: 175 | console.info(`Has event with name "${String(message.name)}"`, message.data); 176 | this._fireEvent(String(message.name), message.data); 177 | break; 178 | case EventType.Action: 179 | console.info(`Start action with id "${message.id}" and name "${String(message.name)}"`, message.data); 180 | this._createResponse(message); 181 | break; 182 | case EventType.Response: 183 | console.info(`Start response with name "${message.id}" and status "${message.status}"`, message.content); 184 | this._fireEndAction(message); 185 | break; 186 | } 187 | } 188 | 189 | private _createResponse(message: IRequestData): void { 190 | const sendError = (error: Error) => { 191 | console.error(error); 192 | this._adapter.send({ 193 | id: message.id, 194 | type: EventType.Response, 195 | status: ResponseStatus.Error, 196 | content: Bus._dataToMessage(error) 197 | }); 198 | }; 199 | 200 | if (!this._requestHandlers[String(message.name)]) { 201 | sendError(new Error(`Has no handler for "${String(message.name)}" action!`)); 202 | return void 0; 203 | } 204 | 205 | try { 206 | const result = this._requestHandlers[String(message.name)](message.data); 207 | 208 | if (Bus._isPromise(result)) { 209 | result.then((data) => { 210 | this._adapter.send({ 211 | id: message.id, 212 | type: EventType.Response, 213 | status: ResponseStatus.Success, 214 | content: Bus._dataToMessage(data) 215 | }); 216 | }, sendError); 217 | } else { 218 | this._adapter.send({ 219 | id: message.id, 220 | type: EventType.Response, 221 | status: ResponseStatus.Success, 222 | content: Bus._dataToMessage(result) 223 | }); 224 | } 225 | } catch (e) { 226 | sendError(e); 227 | } 228 | } 229 | 230 | private _fireEndAction(message: IResponseData) { 231 | if (this._activeRequestHash[message.id]) { 232 | switch (message.status) { 233 | case ResponseStatus.Error: 234 | this._activeRequestHash[message.id].reject(Bus._messageToData(message.content)); 235 | break; 236 | case ResponseStatus.Success: 237 | this._activeRequestHash[message.id].resolve(Bus._messageToData(message.content)); 238 | break; 239 | } 240 | delete this._activeRequestHash[message.id]; 241 | } 242 | } 243 | 244 | private _fireEvent(name: string, value: any): void { 245 | if (!this._eventHandlers[name]) { 246 | return void 0; 247 | } 248 | 249 | this._eventHandlers[name] = this._eventHandlers[name] 250 | .slice() 251 | .filter((handlerInfo) => { 252 | try { 253 | handlerInfo.handler.call(handlerInfo.context, value); 254 | } catch (e) { 255 | console.warn(e); 256 | } 257 | return !handlerInfo.once; 258 | }); 259 | 260 | if (!this._eventHandlers[name].length) { 261 | delete this._eventHandlers[name]; 262 | } 263 | } 264 | 265 | private static _createEvent(eventName: string, data: any): IEventData { 266 | return { 267 | type: EventType.Event, 268 | name: eventName, 269 | data 270 | }; 271 | } 272 | 273 | private static _isPromise(some: any): some is Promise { 274 | return some && some.then && typeof some.then === 'function'; 275 | } 276 | 277 | private static _dataToMessage(data: any): IInternalMessage { 278 | const type = data instanceof Error ? 'error' : 'data'; 279 | const content = type === 'error' 280 | ? data.message 281 | : data; 282 | return { type, content }; 283 | } 284 | 285 | private static _messageToData(message: IInternalMessage): any { 286 | if (!message.type || !['error', 'data'].includes(message.type) || !('content' in message)) { 287 | return message; 288 | } 289 | if (message.type === 'error') { 290 | return new Error(message.content); 291 | } 292 | return message.content; 293 | } 294 | } 295 | 296 | export interface IOneArgFunction { 297 | (data: T): R; 298 | } 299 | 300 | export type TMessageContent = IEventData | IRequestData | IResponseData; 301 | export type TChanelId = string | number; 302 | 303 | export interface IEventData { 304 | type: EventType.Event; 305 | chanelId?: TChanelId | undefined; 306 | name: keyof any; 307 | data?: any; 308 | } 309 | 310 | export interface IRequestData { 311 | id: string | number; 312 | chanelId?: TChanelId | undefined; 313 | type: EventType.Action; 314 | name: keyof any; 315 | data?: any; 316 | } 317 | 318 | export interface IResponseData { 319 | id: string | number; 320 | chanelId?: TChanelId | undefined; 321 | type: EventType.Response; 322 | status: ResponseStatus; 323 | content: any; 324 | } 325 | 326 | interface ISentActionData { 327 | resolve: Function; 328 | reject: Function; 329 | } 330 | 331 | interface IEventHandlerData { 332 | context: any; 333 | once: boolean; 334 | handler: IOneArgFunction; 335 | } 336 | 337 | interface IInternalMessage { 338 | type: 'data' | 'error'; 339 | content: any 340 | } 341 | -------------------------------------------------------------------------------- /src/config/index.ts: -------------------------------------------------------------------------------- 1 | export namespace config { 2 | 3 | export namespace console { 4 | export type TConsoleMethods = 'log' | 'info' | 'warn' | 'error'; 5 | 6 | export const LOG_LEVEL = { 7 | PRODUCTION: 0, 8 | ERRORS: 1, 9 | VERBOSE: 2 10 | }; 11 | export let logLevel: number = LOG_LEVEL.PRODUCTION; 12 | 13 | export const methodsData: Record = { 14 | log: { save: false, logLevel: LOG_LEVEL.VERBOSE }, 15 | info: { save: false, logLevel: LOG_LEVEL.VERBOSE }, 16 | warn: { save: false, logLevel: LOG_LEVEL.VERBOSE }, 17 | error: { save: true, logLevel: LOG_LEVEL.ERRORS } 18 | }; 19 | } 20 | } 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './bus/Bus'; 2 | export * from './adapters/Adapter'; 3 | export * from './adapters/WindowAdapter'; 4 | export * from './protocols/WindowProtocol'; 5 | export * from './config'; 6 | export * from './utils'; 7 | -------------------------------------------------------------------------------- /src/protocols/WindowProtocol.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'typed-ts-events'; 2 | 3 | 4 | export class WindowProtocol extends EventEmitter> { 5 | 6 | private win: WindowProtocol.IWindow; 7 | private readonly handler: (event: WindowProtocol.IMessageEvent) => any; 8 | private readonly type: WindowProtocol.TProtocolType; 9 | 10 | 11 | constructor(win: WindowProtocol.IWindow, type: WindowProtocol.TProtocolType) { 12 | super(); 13 | 14 | this.win = win; 15 | this.type = type; 16 | 17 | this.handler = event => { 18 | this.trigger('message', event); 19 | }; 20 | 21 | if (type === WindowProtocol.PROTOCOL_TYPES.LISTEN) { 22 | this.win.addEventListener('message', this.handler, false); 23 | } 24 | } 25 | 26 | public dispatch(data: R): this { 27 | this.win.postMessage(data, '*'); 28 | return this; 29 | } 30 | 31 | public destroy(): void { 32 | if (this.type === WindowProtocol.PROTOCOL_TYPES.LISTEN) { 33 | this.win.removeEventListener('message', this.handler, false); 34 | } 35 | this.win = WindowProtocol._fakeWin; 36 | } 37 | 38 | private static _fakeWin: WindowProtocol.IWindow = (function () { 39 | const empty = () => null; 40 | return { 41 | postMessage: empty, 42 | addEventListener: empty, 43 | removeEventListener: empty 44 | }; 45 | })(); 46 | } 47 | 48 | /* istanbul ignore next */ 49 | export namespace WindowProtocol { 50 | 51 | export const PROTOCOL_TYPES = { 52 | LISTEN: 'listen' as 'listen', 53 | DISPATCH: 'dispatch' as 'dispatch' 54 | }; 55 | 56 | export interface IWindow { 57 | postMessage: typeof window['postMessage']; 58 | addEventListener: typeof window['addEventListener'] 59 | removeEventListener: typeof window['removeEventListener']; 60 | } 61 | 62 | export interface IMessageEvent extends MessageEvent { 63 | data: T; 64 | } 65 | 66 | export interface IEvents { 67 | message: IMessageEvent; 68 | } 69 | 70 | export type TProtocolType = 'listen' | 'dispatch'; 71 | } 72 | -------------------------------------------------------------------------------- /src/utils/UniqPrimitiveCollection.ts: -------------------------------------------------------------------------------- 1 | export class UniqPrimitiveCollection { 2 | 3 | public size: number = 0; 4 | private hash: Record = Object.create(null); 5 | 6 | constructor(list?: Array) { 7 | if (list) { 8 | list.forEach(this.add, this); 9 | } 10 | } 11 | 12 | public add(item: T): this { 13 | this.hash[item] = true; 14 | this.size = Object.keys(this.hash).length; 15 | return this; 16 | } 17 | 18 | public has(key: T): boolean { 19 | return key in this.hash; 20 | } 21 | 22 | public toArray(): Array { 23 | return Object.keys(this.hash) as Array; 24 | } 25 | } -------------------------------------------------------------------------------- /src/utils/console/index.ts: -------------------------------------------------------------------------------- 1 | import { config } from '../../config'; 2 | import { keys } from '../utils'; 3 | 4 | /* istanbul ignore next */ 5 | const consoleModule = (function (root: { console: Console }) { 6 | return root.console; 7 | })(typeof self !== 'undefined' ? self : global); 8 | 9 | const storage: Record>> = Object.create(null); 10 | 11 | function addNamespace(type: string) { 12 | if (!storage[type]) { 13 | storage[type] = []; 14 | } 15 | } 16 | 17 | function saveEvent(type: string, args: Array) { 18 | storage[type].push(args); 19 | } 20 | 21 | function generateConsole(): Record) => void> { 22 | return keys(config.console.methodsData).reduce((api, method) => { 23 | api[method] = (...args: Array) => { 24 | if (config.console.logLevel < config.console.methodsData[method].logLevel) { 25 | if (config.console.methodsData[method].save) { 26 | addNamespace(method); 27 | saveEvent(method, args); 28 | } 29 | } else { 30 | consoleModule[method](...args); 31 | } 32 | }; 33 | return api; 34 | }, Object.create(null)); 35 | } 36 | 37 | export const console = { 38 | ...generateConsole(), 39 | getSavedMessages(type: config.console.TConsoleMethods): Array> { 40 | return storage[type] || []; 41 | } 42 | }; 43 | 44 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './utils'; 2 | export * from './console'; 3 | export * from './UniqPrimitiveCollection'; -------------------------------------------------------------------------------- /src/utils/utils/index.ts: -------------------------------------------------------------------------------- 1 | export function keys>(o: T): Array { 2 | return Object.keys(o) as Array; 3 | } 4 | 5 | const salt = Math.floor(Date.now() * Math.random()); 6 | let counter = 0; 7 | 8 | export function uniqueId(prefix: string): string { 9 | return `${prefix}-${salt}-${counter++}`; 10 | } 11 | 12 | export function toArray(some: T | T[]): T[] { 13 | return Array.isArray(some) ? some : [some]; 14 | } 15 | 16 | export function pipe(a: (data: T) => R): (data: T) => R; 17 | export function pipe(a: (data: T) => U, b: (data: U) => R): (data: T) => R; 18 | export function pipe(a: (data: T) => U, b: (data: U) => E, c: (data: E) => R): (data: T) => R; 19 | export function pipe(...args: Array<(a: any) => any>): (data: any) => any { 20 | return data => args.reduce((acc, cb) => cb(acc), data); 21 | } -------------------------------------------------------------------------------- /stand/iframe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Iframe 7 | 8 | 9 | 10 | 11 | 12 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /stand/parent.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Parent 7 | 8 | 9 | 10 | 11 | 12 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /test/Bus.test.ts: -------------------------------------------------------------------------------- 1 | import { Bus, EventType, TMessageContent, console, config } from '../src'; 2 | import { MockAdapter } from './mock/MockAdapter'; 3 | import { Signal } from 'ts-utils'; 4 | 5 | 6 | describe('Bus', () => { 7 | 8 | let adapter: MockAdapter; 9 | let bus: Bus; 10 | 11 | beforeEach(() => { 12 | config.console.logLevel = config.console.LOG_LEVEL.PRODUCTION; 13 | adapter = new MockAdapter(); 14 | bus = new Bus(adapter); 15 | }); 16 | 17 | it('destroy', () => { 18 | let counter = 0; 19 | adapter.destroy = () => { 20 | counter++; 21 | }; 22 | bus.destroy(); 23 | expect(counter).toBe(1); 24 | }); 25 | 26 | it('bus id is unique', () => { 27 | const first = new Bus(new MockAdapter()); 28 | const second = new Bus(new MockAdapter()); 29 | 30 | expect(first.id).not.toBe(second.id); 31 | }); 32 | 33 | it('bus dispatch event', () => { 34 | const adapter = new MockAdapter(); 35 | const bus = new Bus(adapter); 36 | const eventName = 'test'; 37 | const eventData = { some: true }; 38 | let wasCall = 0; 39 | 40 | adapter.onSend.once((eventData: TMessageContent) => { 41 | 42 | if (eventData.type !== EventType.Event) { 43 | throw new Error('Wrong event type!'); 44 | } 45 | 46 | expect(eventData.type).toBe(EventType.Event); 47 | expect(eventData.name).toBe(eventName); 48 | expect(eventData.data).toBe(undefined); 49 | wasCall++; 50 | }); 51 | 52 | bus.dispatchEvent(eventName, void 0); 53 | 54 | adapter.onSend.once((event: TMessageContent) => { 55 | if (event.type !== EventType.Event) { 56 | throw new Error('Wrong event type!'); 57 | } 58 | wasCall++; 59 | expect(event.data).toBe(eventData); 60 | }); 61 | 62 | bus.dispatchEvent(eventName, eventData); 63 | 64 | expect(wasCall).toBe(2); 65 | }); 66 | 67 | describe('console', () => { 68 | 69 | const consoleModule = (function (root: { console: Console }) { 70 | return root.console; 71 | })(self || global); 72 | 73 | const originError = consoleModule.error; 74 | const originInfo = consoleModule.info; 75 | 76 | let info: Signal>; 77 | let error: Signal>; 78 | 79 | beforeEach(() => { 80 | info = new Signal>(); 81 | error = new Signal>(); 82 | consoleModule.info = (...args: Array) => { 83 | info.dispatch(args); 84 | }; 85 | consoleModule.error = (...args: Array) => { 86 | error.dispatch(args); 87 | }; 88 | }); 89 | 90 | it('Check production level', done => { 91 | 92 | let counter = 0; 93 | info.on(() => { 94 | counter++; 95 | }); 96 | error.on(() => { 97 | counter++; 98 | }); 99 | 100 | new MockAdapter(); 101 | new Bus(adapter).request('some', null, 10) 102 | .catch(() => { 103 | expect(counter).toBe(0); 104 | const message = console.getSavedMessages('error'); 105 | const info = console.getSavedMessages('info'); 106 | expect(info).toHaveLength(0); 107 | expect(String(message[0][0])).toBe('Error: Timeout error for request with name "some" and timeout 10!'); 108 | done(); 109 | }); 110 | }); 111 | 112 | it('Check errors level', done => { 113 | 114 | config.console.logLevel = config.console.LOG_LEVEL.ERRORS; 115 | let counter = 0; 116 | info.on(() => { 117 | counter++; 118 | }); 119 | error.on(e => { 120 | expect(String(e)).toBe('Error: Timeout error for request with name "some" and timeout 10!'); 121 | counter++; 122 | }); 123 | 124 | new MockAdapter(); 125 | new Bus(adapter).request('some', null, 10) 126 | .catch(() => { 127 | expect(counter).toBe(1); 128 | done(); 129 | }); 130 | }); 131 | 132 | it('Check verbose level', done => { 133 | 134 | config.console.logLevel = config.console.LOG_LEVEL.VERBOSE; 135 | let counter = 0; 136 | info.on(() => { 137 | counter++; 138 | }); 139 | error.on(e => { 140 | expect(String(e)).toBe('Error: Timeout error for request with name "some" and timeout 10!'); 141 | counter++; 142 | }); 143 | 144 | new MockAdapter(); 145 | new Bus(adapter).request('some', null, 10) 146 | .catch(() => { 147 | expect(counter).toBe(3); 148 | done(); 149 | }); 150 | }); 151 | 152 | afterAll(() => { 153 | consoleModule.error = originError; 154 | consoleModule.info = originInfo; 155 | }); 156 | 157 | }); 158 | 159 | it('change adapter', () => { 160 | let count = 0; 161 | 162 | bus.once('some-event', () => { 163 | count++; 164 | }); 165 | 166 | bus.on('some-event', () => { 167 | count++; 168 | }); 169 | 170 | bus.registerRequestHandler('some-request', () => { 171 | count++; 172 | }); 173 | 174 | const newAdapter = new MockAdapter(); 175 | bus.changeAdapter(newAdapter); 176 | 177 | newAdapter.dispatchAdapterEvent({ name: 'some-request', id: 0, type: EventType.Action }); 178 | newAdapter.dispatchAdapterEvent({ name: 'some-event', type: EventType.Event }); 179 | 180 | expect(count).toBe(3); 181 | }); 182 | 183 | describe('event emitter', () => { 184 | const event = { 185 | type: EventType.Event, 186 | name: 'test-event', 187 | data: { someData: true } 188 | }; 189 | 190 | it('on', () => { 191 | let count = 0; 192 | 193 | bus.on(event.name, function (data) { 194 | count++; 195 | expect(data).toBe(event.data); 196 | }); 197 | 198 | bus.on(event.name, function (data) { 199 | count++; 200 | expect(data).toBe(event.data); 201 | }); 202 | 203 | adapter.dispatchAdapterEvent(event as any); 204 | adapter.dispatchAdapterEvent({ ...event, name: 'new' } as any); 205 | adapter.dispatchAdapterEvent(event as any); 206 | 207 | expect(count).toBe(4); 208 | }); 209 | 210 | it('once', () => { 211 | let count = 0; 212 | 213 | bus.once(event.name, function (data) { 214 | count++; 215 | expect(data).toBe(event.data); 216 | }); 217 | 218 | adapter.dispatchAdapterEvent(event as any); 219 | adapter.dispatchAdapterEvent({ ...event, name: 'new' } as any); 220 | adapter.dispatchAdapterEvent(event as any); 221 | 222 | expect(count).toBe(1); 223 | }); 224 | 225 | it('off', () => { 226 | let count = 0; 227 | 228 | const handlers = [() => count++, () => count++]; 229 | 230 | handlers.forEach((handler) => { 231 | bus.on(event.name, handler); 232 | }); 233 | bus.off(event.name, handlers[0]).off('some-event'); 234 | 235 | adapter.dispatchAdapterEvent(event as any); 236 | expect(count).toBe(1); 237 | 238 | bus.off(); 239 | 240 | adapter.dispatchAdapterEvent(event as any); 241 | expect(count).toBe(1); 242 | }); 243 | 244 | it('should call second handler', () => { 245 | let count = 0; 246 | const eventName = 'some-event'; 247 | [ 248 | () => { 249 | throw new Error('Some error!'); 250 | }, 251 | () => count++ 252 | ].forEach(f => { 253 | bus.on(eventName, f); 254 | }); 255 | 256 | adapter.dispatchAdapterEvent({ 257 | name: eventName, 258 | type: EventType.Event 259 | }); 260 | 261 | expect(count).toBe(1); 262 | }); 263 | 264 | }); 265 | 266 | describe('request api', () => { 267 | 268 | it('timeout error', (done) => { 269 | const adapter = new MockAdapter(); 270 | const bus = new Bus(adapter, 50); 271 | 272 | bus.request('some-event').catch((e) => { 273 | expect(e.message).toBe('Timeout error for request with name "some-event" and timeout 50!'); 274 | done(); 275 | }); 276 | }); 277 | 278 | it('response without request', () => { 279 | adapter.dispatchAdapterEvent({ 280 | type: EventType.Response, 281 | id: 'some' 282 | } as any); 283 | }); 284 | 285 | it('request', (done) => { 286 | const requestData = { 287 | count: 0, 288 | name: 'getRequestCount', 289 | handler: (c: number) => { 290 | requestData.count++; 291 | return requestData.count + c; 292 | } 293 | }; 294 | 295 | const secondAdapter = new MockAdapter(); 296 | const secondBus = new Bus(secondAdapter); 297 | 298 | secondBus.registerRequestHandler(requestData.name, requestData.handler); 299 | 300 | adapter.onSend.once((data) => { 301 | secondAdapter.onSend.once(d => adapter.dispatchAdapterEvent(d)); 302 | secondAdapter.dispatchAdapterEvent(data); 303 | }); 304 | 305 | bus.request(requestData.name, 10, 100) 306 | .then((r) => { 307 | expect(r).toBe(11); 308 | done(); 309 | }); 310 | }); 311 | 312 | it('request async', (done) => { 313 | const requestData = { 314 | count: 0, 315 | name: 'getRequestCount', 316 | handler: (c: number) => { 317 | requestData.count++; 318 | return Promise.resolve(requestData.count + c); 319 | } 320 | }; 321 | 322 | const secondAdapter = new MockAdapter(); 323 | const secondBus = new Bus(secondAdapter); 324 | 325 | secondBus.registerRequestHandler(requestData.name, requestData.handler); 326 | 327 | adapter.onSend.once((data) => { 328 | secondAdapter.onSend.once(d => adapter.dispatchAdapterEvent(d)); 329 | secondAdapter.dispatchAdapterEvent(data); 330 | }); 331 | 332 | bus.request(requestData.name, 10, 100) 333 | .then((r) => { 334 | expect(r).toBe(11); 335 | 336 | secondBus.unregisterHandler(requestData.name); 337 | 338 | bus.request(requestData.name, 10, 100).catch(() => { 339 | done(); 340 | }); 341 | }); 342 | }); 343 | 344 | it('has no handler for request', done => { 345 | const requestData = { 346 | name: 'getRequestCount', 347 | handler: () => null 348 | }; 349 | 350 | const secondAdapter = new MockAdapter(); 351 | new Bus(secondAdapter); 352 | 353 | adapter.onSend.once((data) => { 354 | secondAdapter.onSend.once(d => adapter.dispatchAdapterEvent(d)); 355 | secondAdapter.dispatchAdapterEvent(data); 356 | }); 357 | 358 | bus.request(requestData.name, null, 100) 359 | .catch((e) => { 360 | expect(String(e)).toBe('Error: Has no handler for "getRequestCount" action!'); 361 | done(); 362 | }); 363 | }); 364 | 365 | it('handler with exception', done => { 366 | const requestData = { 367 | count: 0, 368 | name: 'getRequestCount', 369 | handler: () => { 370 | throw new Error('Test error!'); 371 | } 372 | }; 373 | 374 | const secondAdapter = new MockAdapter(); 375 | const secondBus = new Bus(secondAdapter); 376 | 377 | secondBus.registerRequestHandler(requestData.name, requestData.handler); 378 | 379 | adapter.onSend.once((data) => { 380 | secondAdapter.onSend.once(d => adapter.dispatchAdapterEvent(d)); 381 | secondAdapter.dispatchAdapterEvent(data); 382 | }); 383 | 384 | bus.request(requestData.name, 10, 100) 385 | .catch((e) => { 386 | console.log(e); 387 | expect(String(e)).toBe('Error: Test error!'); 388 | done(); 389 | }); 390 | }); 391 | 392 | it('duplicate handler', () => { 393 | const f = () => null, name = 'test'; 394 | 395 | bus.registerRequestHandler(name, f); 396 | expect(() => bus.registerRequestHandler(name, f)).toThrow('Duplicate request handler!'); 397 | }); 398 | 399 | }); 400 | 401 | }); 402 | -------------------------------------------------------------------------------- /test/WindowAdapter.test.ts: -------------------------------------------------------------------------------- 1 | import { EventType, IEventData, TMessageContent, WindowAdapter } from '../src'; 2 | import { IMockWindow, mockWindow } from './mock/Win'; 3 | import { EventEmitter } from 'typed-ts-events'; 4 | import { WindowProtocol } from '../src/protocols/WindowProtocol'; 5 | 6 | 7 | describe('Window adapter', () => { 8 | 9 | const eventData: IEventData = { 10 | type: EventType.Event, 11 | name: 'test', 12 | data: 'some data for event', 13 | chanelId: undefined 14 | }; 15 | 16 | let listen: Array>; 17 | let dispatch: Array>; 18 | let adapter: WindowAdapter; 19 | let listenWin: IMockWindow = mockWindow(); 20 | let dispatchWin: IMockWindow = mockWindow(); 21 | 22 | beforeEach(() => { 23 | listenWin = mockWindow(); 24 | dispatchWin = mockWindow(); 25 | listen = [new WindowProtocol(listenWin, WindowProtocol.PROTOCOL_TYPES.LISTEN)]; 26 | dispatch = [new WindowProtocol(dispatchWin, WindowProtocol.PROTOCOL_TYPES.DISPATCH)]; 27 | adapter = new WindowAdapter(listen, dispatch, {}); 28 | }); 29 | 30 | describe('check connect by chanel id', () => { 31 | 32 | it('with same chain id', () => { 33 | 34 | let ok = false; 35 | 36 | adapter = new WindowAdapter(listen, dispatch, { 37 | chanelId: 1, 38 | origins: ['*'], 39 | availableChanelId: [2] 40 | }); 41 | 42 | adapter.addListener(event => { 43 | ok = event.type === EventType.Event && event.data === 1 && event.name === 'test'; 44 | }); 45 | 46 | listenWin.runEventListeners('message', { 47 | origin: 'https://some-origin.com', 48 | data: { 49 | type: EventType.Event, 50 | data: 1, 51 | name: 'test' 52 | } 53 | }); 54 | 55 | expect(ok).toBe(false); 56 | 57 | listenWin.runEventListeners('message', { 58 | origin: 'https://some-origin.com', 59 | data: { 60 | type: EventType.Event, 61 | data: 1, 62 | name: 'test', 63 | chanelId: 2 64 | } 65 | }); 66 | 67 | expect(ok).toBe(true); 68 | }); 69 | 70 | }); 71 | 72 | it('all origin', () => { 73 | listenWin = mockWindow(); 74 | listen = [new WindowProtocol(listenWin, WindowProtocol.PROTOCOL_TYPES.LISTEN)]; 75 | dispatch = [new WindowProtocol(mockWindow(), WindowProtocol.PROTOCOL_TYPES.DISPATCH)]; 76 | adapter = new WindowAdapter(listen, dispatch, { origins: '*' }); 77 | 78 | let count = 0; 79 | 80 | adapter.addListener(() => { 81 | count++; 82 | }); 83 | 84 | listenWin.runEventListeners('message', { 85 | origin: 'https://dispatch-origin.com', 86 | data: { ...eventData } 87 | }); 88 | 89 | expect(count).toBe(1); 90 | }); 91 | 92 | it('Exception in handler', () => { 93 | let ok = false; 94 | 95 | adapter.addListener(() => { 96 | throw new Error('Some error'); 97 | }); 98 | adapter.addListener(() => { 99 | ok = true; 100 | }); 101 | 102 | listenWin.runEventListeners('message', { 103 | origin: window.location.origin, 104 | data: { ...eventData } 105 | }); 106 | expect(ok).toBe(true); 107 | }); 108 | 109 | it('Wrong event format', () => { 110 | let ok = true; 111 | adapter.addListener(() => { 112 | ok = false; 113 | }); 114 | 115 | listenWin.runEventListeners('message', { 116 | origin: window.location.origin, 117 | data: null 118 | }); 119 | listenWin.runEventListeners('message', { 120 | origin: window.location.origin, 121 | data: {} 122 | }); 123 | expect(ok).toBe(true); 124 | }); 125 | 126 | it('send', () => { 127 | let wasEvent = false; 128 | 129 | dispatchWin.onPostMessageRun.once(message => { 130 | wasEvent = true; 131 | expect(message.data).toEqual(eventData); 132 | }); 133 | const sendResult = adapter.send(eventData); 134 | 135 | expect(sendResult).toBe(adapter); 136 | expect(wasEvent).toBe(true); 137 | }); 138 | 139 | it('listen with origin', () => { 140 | let count = 0; 141 | const data = [{ ...eventData, data: 'test 1' }, { ...eventData, data: 'test 2' }]; 142 | 143 | const addListenerResult = adapter.addListener((eventData: any) => { 144 | if (eventData !== data[count]) { 145 | throw new Error('Wrong data in event!'); 146 | } 147 | count++; 148 | }); 149 | 150 | listenWin.runEventListeners('message', { 151 | origin: window.location.origin, 152 | data: data[0] 153 | }); 154 | 155 | listenWin.runEventListeners('message', { 156 | origin: window.location.origin, 157 | data: data[1] 158 | }); 159 | 160 | listenWin.runEventListeners('message', { 161 | origin: 'some-origin', 162 | data: eventData 163 | }); 164 | 165 | expect(addListenerResult).toBe(adapter); 166 | expect(count).toBe(2); 167 | }); 168 | 169 | it('destroy', () => { 170 | let wasPostMessage = false; 171 | let wasListenEvent = false; 172 | 173 | dispatchWin.onPostMessageRun.once(() => { 174 | wasPostMessage = true; 175 | }); 176 | 177 | adapter.addListener(() => { 178 | wasListenEvent = true; 179 | }); 180 | 181 | const destroyResult = adapter.destroy(); 182 | adapter.destroy(); 183 | 184 | adapter.send(eventData); 185 | listenWin.runEventListeners('message', { 186 | origin: 'listen.origin', 187 | data: 'some data' 188 | }); 189 | 190 | expect(destroyResult).toBe(undefined); 191 | expect(wasPostMessage).toBe(false); 192 | expect(wasListenEvent).toBe(false); 193 | }); 194 | 195 | describe('SimpleWindowAdapter', () => { 196 | 197 | const addEventListener = window.addEventListener; 198 | const removeEventListener = window.removeEventListener; 199 | const postMessage = window.postMessage; 200 | const emitter = new EventEmitter(); 201 | 202 | beforeEach(() => { 203 | (window as any).origin = window.location.origin; 204 | emitter.off(); 205 | window.addEventListener = (event: string, handler: any) => { 206 | emitter.on(event, handler); 207 | }; 208 | window.removeEventListener = (event: string, handler: any) => { 209 | emitter.off(event, handler); 210 | }; 211 | window.postMessage = (data: any, origin: string) => { 212 | emitter.trigger('message', { data, origin }); 213 | }; 214 | }); 215 | 216 | afterAll(() => { 217 | window.addEventListener = addEventListener; 218 | window.removeEventListener = removeEventListener; 219 | window.postMessage = postMessage; 220 | }); 221 | 222 | it('Create', done => { 223 | WindowAdapter.createSimpleWindowAdapter() 224 | .then(() => { 225 | done(); 226 | }); 227 | }); 228 | 229 | it('Add Listener', done => { 230 | WindowAdapter.createSimpleWindowAdapter().then(adapter => { 231 | let ok = false; 232 | 233 | adapter.addListener(() => { 234 | ok = true; 235 | }); 236 | 237 | window.postMessage({ type: EventType.Event, name: 'test' }, window.origin); 238 | expect(ok).toBe(true); 239 | done(); 240 | }); 241 | }); 242 | 243 | it('Destroy', done => { 244 | const win = mockWindow(); 245 | (window as any).opener = win; 246 | 247 | WindowAdapter.createSimpleWindowAdapter() 248 | .then(adapter => { 249 | let listenerCount = 0; 250 | let sendCount = 0; 251 | 252 | win.onPostMessageRun.on(() => { 253 | sendCount++; 254 | }); 255 | 256 | adapter.addListener(() => { 257 | listenerCount++; 258 | }); 259 | 260 | 261 | window.postMessage({ type: EventType.Event, name: 'test' }, window.origin); 262 | adapter.send({ type: EventType.Event, data: '', name: 'test' }); 263 | adapter.destroy(); 264 | adapter.send({ type: EventType.Event, data: '', name: 'test' }); 265 | window.postMessage({ type: EventType.Event, name: 'test' }, window.origin); 266 | 267 | expect(listenerCount).toBe(1); 268 | expect(sendCount).toBe(1); 269 | done(); 270 | }); 271 | }); 272 | 273 | }); 274 | 275 | }); 276 | -------------------------------------------------------------------------------- /test/mock/MockAdapter.ts: -------------------------------------------------------------------------------- 1 | import { Adapter, TMessageContent, IOneArgFunction } from '../../src'; 2 | import { Signal } from 'ts-utils'; 3 | 4 | 5 | export class MockAdapter extends Adapter { 6 | 7 | public onSend: Signal = new Signal(); 8 | public onDestroy: Signal<{}> = new Signal(); 9 | private listeners: Array = []; 10 | 11 | 12 | public send(data: TMessageContent): this { 13 | this.onSend.dispatch(data); 14 | return this; 15 | } 16 | 17 | public addListener(cb: IOneArgFunction): this { 18 | this.listeners.push(cb); 19 | return this; 20 | } 21 | 22 | public dispatchAdapterEvent(e: TMessageContent): void { 23 | this.listeners.forEach(cb => cb(e)); 24 | } 25 | 26 | public destroy(): void { 27 | this.onDestroy.dispatch({}); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /test/mock/Win.ts: -------------------------------------------------------------------------------- 1 | import { Signal } from 'ts-utils'; 2 | import { WindowProtocol } from '../../src/protocols/WindowProtocol'; 3 | 4 | 5 | class Win { 6 | 7 | public onPostMessageRun: Signal = new Signal(); 8 | private _handlers: Record> = Object.create(null); 9 | 10 | 11 | public postMessage(data: any, origin: string): void { 12 | this.onPostMessageRun.dispatch({ data, origin }); 13 | } 14 | 15 | public removeEventListener(event: string, handler: any): void { 16 | if (!this._handlers[event]) { 17 | return void 0; 18 | } 19 | this._handlers[event] = this._handlers[event].filter(cb => cb !== handler); 20 | } 21 | 22 | public addEventListener(event: string, handler: Function): void { 23 | if (!this._handlers[event]) { 24 | this._handlers[event] = []; 25 | } 26 | this._handlers[event].push(handler); 27 | } 28 | 29 | public runEventListeners(event: string, eventData: any): void { 30 | if (!this._handlers[event]) { 31 | return void 0; 32 | } 33 | 34 | this._handlers[event].forEach(cb => cb(eventData)); 35 | } 36 | 37 | } 38 | 39 | export function mockWindow(): IMockWindow { 40 | return new Win() as any; 41 | } 42 | 43 | export interface IMockWindow extends WindowProtocol.IWindow { 44 | onPostMessageRun: Signal>; 45 | runEventListeners(event: string, eventData: any): void; 46 | } 47 | 48 | export interface IPostMessageEvent { 49 | data: T; 50 | origin: string; 51 | } 52 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "module": "commonjs", 5 | "target": "es5", 6 | "sourceMap": true, 7 | "lib": [ 8 | "dom", 9 | "es2015", 10 | "es2015.promise" 11 | ] 12 | }, 13 | "exclude": [ 14 | "node_modules" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "strict": true, 6 | "preserveConstEnums": true, 7 | "noImplicitReturns": true, 8 | "noUnusedLocals": true, 9 | "outDir": "./dist", 10 | "declaration": true 11 | }, 12 | "files": [ 13 | "src/index.ts" 14 | ], 15 | "exclude": [ 16 | "node_modules", "dist" 17 | ] 18 | } --------------------------------------------------------------------------------