@o3r/third-party
Version:
This module provides a bridge to communicate with third parties via an iFrame solution.
1 lines • 16.9 kB
Source Map (JSON)
{"version":3,"file":"o3r-third-party.mjs","sources":["../../src/bridge/ab-testing/ab-testing-bridge.ts","../../src/bridge/iframe/helpers.ts","../../src/bridge/iframe/bridge.ts","../../src/o3r-third-party.ts"],"sourcesContent":["import type {\n Logger,\n} from '@o3r/core';\nimport {\n BehaviorSubject,\n distinctUntilChanged,\n Observable,\n} from 'rxjs';\n\n/**\n * Shared interface with the A/B testing provider\n */\nexport interface AbTestBridgeInterface<T> {\n /**\n * Start an AB testing experiment\n */\n start(experiments: T | T[]): void;\n /**\n * Stop an AB testing experiment\n */\n stop(experiments?: T | T[]): void;\n}\n\n/**\n * Configure the A/B testing script interfaces with the application\n */\nexport interface AbTestBridgeConfig {\n /**\n * Reference to communicate with the bridge from the window\n * @default 'abTestBridge'\n */\n bridgeName: string;\n /**\n * Debug logger\n * @default console\n */\n logger: Logger;\n /**\n * Event a third party can subscribe to before starting the communication with the bridge\n * @default 'ab-test-ready'\n */\n readyEventName: string;\n}\n\n/**\n * Default options that will represent the interface\n */\nconst defaultOptions = {\n bridgeName: 'abTestBridge',\n readyEventName: 'ab-test-ready',\n logger: console\n} as const satisfies AbTestBridgeConfig;\n\n/**\n * Bridge between the application and a third party A/B testing provider.\n * Exposes a start and stop methods to allow the external script to set the list of experiments to run over the\n * application.\n *\n * Share the resulting list of experiments with the rest of the application via an observable.\n */\nexport class AbTestBridge<T> implements AbTestBridgeInterface<T> {\n /**\n * Behaviour subject to control the experiments via the exposed interface\n */\n private readonly experimentSubject$: BehaviorSubject<T[]> = new BehaviorSubject<T[]>([]);\n /**\n * Options to configure the communication between the AB Testing bridge and third parties\n */\n private readonly options: AbTestBridgeConfig;\n /**\n * Observable with the list of AB testing experiments currently applied\n */\n public experiments$: Observable<T[]>;\n\n /**\n * AbTestBridge constructor\n * @param isExperimentEqual check two different experiments match to identify an experiment to start or to stop\n * @param options configure the communication with the A/B testing third party provider\n */\n constructor(private readonly isExperimentEqual: (value1?: T, value2?: T) => boolean, options?: Partial<AbTestBridgeConfig>) {\n this.experiments$ = this.experimentSubject$.pipe(\n distinctUntilChanged((experimentsA: T[], experimentsB: T[]) =>\n experimentsB.length === experimentsA.length && experimentsA.every((eA) => experimentsB.find((eB) => isExperimentEqual(eA, eB))\n ))\n );\n this.options = {\n ...defaultOptions,\n ...options\n };\n if ((window as any)[this.options.bridgeName]) {\n this.log(`An instance of ${this.options.bridgeName} already exists. This AbTestBridge instance will be ignored`);\n } else {\n (window as any)[this.options.bridgeName] = { start: this.start.bind(this), stop: this.stop.bind(this) };\n }\n document.dispatchEvent(new CustomEvent(this.options.readyEventName));\n }\n\n /**\n * Use configured logger to log AB testing related information\n * @param args\n */\n private log(...args: any[]) {\n (this.options.logger.debug || this.options.logger.log)('A/B Test', ...args);\n }\n\n /**\n * @inheritDoc\n */\n public start(experiments: T | T[]) {\n this.log('Start experiment', experiments);\n const currentProfile = this.experimentSubject$.getValue();\n this.experimentSubject$.next([\n ...currentProfile,\n ...(Array.isArray(experiments)\n ? experiments\n : [experiments]).filter((exp) => !currentProfile.some((expB: T) => this.isExperimentEqual(exp, expB))\n )\n ]);\n }\n\n /**\n * @inheritDoc\n */\n public stop(experiments?: T | T[]) {\n this.log('Stop experiment', experiments);\n const currentExperiments = this.experimentSubject$.getValue();\n if (experiments) {\n // Stop the mentioned experiment\n this.experimentSubject$.next(currentExperiments.filter((expB: T) => !(Array.isArray(experiments) ? experiments : [experiments]).some((expA) => this.isExperimentEqual(expB, expA)))\n );\n } else {\n // Stop all the experiment\n this.experimentSubject$.next([]);\n }\n }\n}\n","import {\n IFrameBridgeOptions,\n InternalIframeMessage,\n} from './contracts';\n\n/**\n * Default options for an IFrameBridge\n */\nexport const IFRAME_BRIDGE_DEFAULT_OPTIONS: Readonly<IFrameBridgeOptions> = {\n handshakeTries: 10,\n handshakeTimeout: 200,\n messageWithResponseTimeout: 1000\n} as const;\n\n/**\n * Verifies if a message respects the format expected by an IFrameBridge\n * @param message\n */\nexport function isSupportedMessage(message: any): message is InternalIframeMessage {\n return typeof message === 'object'\n && !!message.action\n && !!message.version\n && !!message.channelId;\n}\n\n/**\n * Generates the html content of an iframe\n * @param scriptUrl script to be executed inside the iframe\n * @param additionalHeader custom html headers stringified\n */\nexport function generateIFrameContent(scriptUrl: string, additionalHeader = '') {\n return `<html>\n <head>\n <script>\n class Bridge {\n handshakeDone = false;\n\n queuedMessages = [];\n\n channelId;\n\n messagesBuffer = [];\n\n listener;\n\n constructor() {\n if (window.parent) {\n window.addEventListener('message', (event) => {\n const message = event.data;\n if (this.isValidMessage(message)) {\n if (message.action === 'HANDSHAKE_PARENT') {\n this.channelId = message.channelId;\n this.sendMessage({action: 'HANDSHAKE_CHILD', version: '1.0', id: message.id});\n this.handshakeDone = true;\n this.queuedMessages.forEach((queuedMessage) => this.sendMessage(queuedMessage));\n this.queuedMessages = [];\n } else if (this.channelId && this.channelId === message.channelId) {\n // actual message\n if (this.listener) {\n this.listener(message);\n } else {\n this.messagesBuffer.push(message);\n }\n }\n }\n });\n } else {\n throw new Error('Error in child frame bridge: can\\\\'t access parent window.');\n }\n }\n\n register(handlerFunction, replayMissedMessages) {\n if (!this.listener) {\n this.listener = handlerFunction;\n if (replayMissedMessages) {\n this.messagesBuffer.forEach((message) => handlerFunction(message));\n }\n this.messagesBuffer = [];\n }\n }\n\n isValidMessage(message) {\n return !!message.action && !!message.version && !!message.channelId;\n }\n\n sendMessage(message) {\n if(this.handshakeDone) {\n window.parent.postMessage({...message, channelId: this.channelId}, '*');\n } else {\n this.queuedMessages.push(message);\n }\n }\n\n uuid() {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {\n var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);\n return v.toString(16);\n });\n }\n }\n\n const BRIDGE = new Bridge();\n </script>\n <script src='${scriptUrl}'></script>\n ${additionalHeader}\n </head>\n <body></body>\n</html>`;\n}\n","import {\n firstValueFrom,\n fromEvent,\n Observable,\n} from 'rxjs';\nimport {\n filter,\n map,\n share,\n timeout,\n} from 'rxjs/operators';\nimport {\n v4,\n} from 'uuid';\nimport {\n IFrameBridgeOptions,\n IframeMessage,\n InternalIframeMessage,\n} from './contracts';\nimport {\n IFRAME_BRIDGE_DEFAULT_OPTIONS,\n isSupportedMessage,\n} from './helpers';\n\n/**\n * Bridge that exposes an easy abstraction layer to communicate between a Host and an IFrame using the\n * postMessage API.\n */\nexport class IframeBridge {\n /** ID used to ensure that the Bridge only processes messages meant for this instance, since postMessage is global to the window. */\n private readonly channelId: string;\n\n /** Observable that emits all the messages received from the IFrame. */\n private readonly internalMessages$: Observable<InternalIframeMessage>;\n\n /** Promise that will resolve once the handshake has been completed, undefined if it's already done. */\n private readonly handshakePromise?: Promise<void>;\n\n /** Options to configure the behaviour of the Bridge. */\n private readonly options: IFrameBridgeOptions;\n\n /**\n * Observable that emits all the messages received from the IFrame and that are\n * not a direct response to a request.\n */\n public readonly messages$: Observable<InternalIframeMessage>;\n\n constructor(parent: Window, private readonly child: HTMLIFrameElement, options: Partial<IFrameBridgeOptions> = {}) {\n this.options = { ...IFRAME_BRIDGE_DEFAULT_OPTIONS, ...options };\n this.channelId = v4();\n this.internalMessages$ = fromEvent(parent, 'message').pipe(\n filter((event): event is MessageEvent<InternalIframeMessage> => {\n const messageEvent = event as MessageEvent<InternalIframeMessage>;\n return isSupportedMessage(messageEvent.data) && messageEvent.data.channelId === this.channelId;\n }),\n map((event) => event.data),\n share()\n );\n this.messages$ = this.internalMessages$.pipe(\n // Here we remove all the messages having an \"ID\" because they are bound to their corresponding request and\n // are returned directly by the function sendMessageAndWaitForResponse\n filter((message) => !message.id)\n );\n\n this.handshakePromise = this.handshake();\n }\n\n private async handshake() {\n for (let i = 0; i < this.options.handshakeTries; i++) {\n try {\n await this._sendMessageAndWaitForResponse({\n action: 'HANDSHAKE_PARENT',\n version: '1.0'\n }, this.options.handshakeTimeout);\n return;\n } catch {}\n }\n throw new Error('Handshake failed.');\n }\n\n private _sendMessage(message: IframeMessage, messageId?: string) {\n if (this.child.contentWindow) {\n this.child.contentWindow.postMessage({ ...message, channelId: this.channelId, id: messageId }, '*');\n }\n }\n\n private _sendMessageAndWaitForResponse(message: IframeMessage, timeoutMilliseconds: number = this.options.messageWithResponseTimeout) {\n const id = v4();\n const promise = firstValueFrom(\n this.internalMessages$.pipe(\n filter((response) => response.id === id),\n timeout(timeoutMilliseconds)\n )\n );\n void this._sendMessage(message, id);\n return promise;\n }\n\n /**\n * Method to send a message to the script run in the iframe\n * @param message message object\n * @param messageId message identifier\n */\n public async sendMessage(message: IframeMessage, messageId?: string) {\n await this.handshakePromise;\n this._sendMessage(message, messageId);\n }\n\n /**\n * Method to send a message to the script run in the iframe and wait for an answer\n * @param message\n * @param timeoutMilliseconds\n */\n public async sendMessageAndWaitForResponse(message: IframeMessage, timeoutMilliseconds: number = this.options.messageWithResponseTimeout) {\n await this.handshakePromise;\n return this._sendMessageAndWaitForResponse(message, timeoutMilliseconds);\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;AA4CA;;AAEG;AACH,MAAM,cAAc,GAAG;AACrB,IAAA,UAAU,EAAE,cAAc;AAC1B,IAAA,cAAc,EAAE,eAAe;AAC/B,IAAA,MAAM,EAAE;CAC6B;AAEvC;;;;;;AAMG;MACU,YAAY,CAAA;AAcvB;;;;AAIG;IACH,WAA6B,CAAA,iBAAsD,EAAE,OAAqC,EAAA;QAA7F,IAAiB,CAAA,iBAAA,GAAjB,iBAAiB;AAlB9C;;AAEG;AACc,QAAA,IAAA,CAAA,kBAAkB,GAAyB,IAAI,eAAe,CAAM,EAAE,CAAC;QAgBtF,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAC9C,oBAAoB,CAAC,CAAC,YAAiB,EAAE,YAAiB,KACxD,YAAY,CAAC,MAAM,KAAK,YAAY,CAAC,MAAM,IAAI,YAAY,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,YAAY,CAAC,IAAI,CAAC,CAAC,EAAE,KAAK,iBAAiB,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAC7H,CAAC,CACL;QACD,IAAI,CAAC,OAAO,GAAG;AACb,YAAA,GAAG,cAAc;AACjB,YAAA,GAAG;SACJ;QACD,IAAK,MAAc,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE;YAC5C,IAAI,CAAC,GAAG,CAAC,CAAkB,eAAA,EAAA,IAAI,CAAC,OAAO,CAAC,UAAU,CAA6D,2DAAA,CAAA,CAAC;;aAC3G;AACJ,YAAA,MAAc,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;;AAEzG,QAAA,QAAQ,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;;AAGtE;;;AAGG;IACK,GAAG,CAAC,GAAG,IAAW,EAAA;QACxB,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,EAAE,UAAU,EAAE,GAAG,IAAI,CAAC;;AAG7E;;AAEG;AACI,IAAA,KAAK,CAAC,WAAoB,EAAA;AAC/B,QAAA,IAAI,CAAC,GAAG,CAAC,kBAAkB,EAAE,WAAW,CAAC;QACzC,MAAM,cAAc,GAAG,IAAI,CAAC,kBAAkB,CAAC,QAAQ,EAAE;AACzD,QAAA,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC;AAC3B,YAAA,GAAG,cAAc;AACjB,YAAA,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,WAAW;AAC3B,kBAAE;AACF,kBAAE,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC,CAAC,GAAG,KAAK,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,IAAO,KAAK,IAAI,CAAC,iBAAiB,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;AAExG,SAAA,CAAC;;AAGJ;;AAEG;AACI,IAAA,IAAI,CAAC,WAAqB,EAAA;AAC/B,QAAA,IAAI,CAAC,GAAG,CAAC,iBAAiB,EAAE,WAAW,CAAC;QACxC,MAAM,kBAAkB,GAAG,IAAI,CAAC,kBAAkB,CAAC,QAAQ,EAAE;QAC7D,IAAI,WAAW,EAAE;;YAEf,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAC,IAAO,KAAK,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC,GAAG,WAAW,GAAG,CAAC,WAAW,CAAC,EAAE,IAAI,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,iBAAiB,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAClL;;aACI;;AAEL,YAAA,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAAC;;;AAGrC;;AClID;;AAEG;AACU,MAAA,6BAA6B,GAAkC;AAC1E,IAAA,cAAc,EAAE,EAAE;AAClB,IAAA,gBAAgB,EAAE,GAAG;AACrB,IAAA,0BAA0B,EAAE;;AAG9B;;;AAGG;AACG,SAAU,kBAAkB,CAAC,OAAY,EAAA;IAC7C,OAAO,OAAO,OAAO,KAAK;WACrB,CAAC,CAAC,OAAO,CAAC;WACV,CAAC,CAAC,OAAO,CAAC;AACV,WAAA,CAAC,CAAC,OAAO,CAAC,SAAS;AAC1B;AAEA;;;;AAIG;SACa,qBAAqB,CAAC,SAAiB,EAAE,gBAAgB,GAAG,EAAE,EAAA;IAC5E,OAAO,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAwEY,SAAS,CAAA;QACtB,gBAAgB;;;QAGhB;AACR;;ACpFA;;;AAGG;MACU,YAAY,CAAA;AAmBvB,IAAA,WAAA,CAAY,MAAc,EAAmB,KAAwB,EAAE,UAAwC,EAAE,EAAA;QAApE,IAAK,CAAA,KAAA,GAAL,KAAK;QAChD,IAAI,CAAC,OAAO,GAAG,EAAE,GAAG,6BAA6B,EAAE,GAAG,OAAO,EAAE;AAC/D,QAAA,IAAI,CAAC,SAAS,GAAG,EAAE,EAAE;AACrB,QAAA,IAAI,CAAC,iBAAiB,GAAG,SAAS,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,IAAI,CACxD,MAAM,CAAC,CAAC,KAAK,KAAkD;YAC7D,MAAM,YAAY,GAAG,KAA4C;AACjE,YAAA,OAAO,kBAAkB,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,YAAY,CAAC,IAAI,CAAC,SAAS,KAAK,IAAI,CAAC,SAAS;AAChG,SAAC,CAAC,EACF,GAAG,CAAC,CAAC,KAAK,KAAK,KAAK,CAAC,IAAI,CAAC,EAC1B,KAAK,EAAE,CACR;AACD,QAAA,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,iBAAiB,CAAC,IAAI;;;AAG1C,QAAA,MAAM,CAAC,CAAC,OAAO,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC,CACjC;AAED,QAAA,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC,SAAS,EAAE;;AAGlC,IAAA,MAAM,SAAS,GAAA;AACrB,QAAA,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,cAAc,EAAE,CAAC,EAAE,EAAE;AACpD,YAAA,IAAI;gBACF,MAAM,IAAI,CAAC,8BAA8B,CAAC;AACxC,oBAAA,MAAM,EAAE,kBAAkB;AAC1B,oBAAA,OAAO,EAAE;AACV,iBAAA,EAAE,IAAI,CAAC,OAAO,CAAC,gBAAgB,CAAC;gBACjC;;YACA,MAAM;;AAEV,QAAA,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC;;IAG9B,YAAY,CAAC,OAAsB,EAAE,SAAkB,EAAA;AAC7D,QAAA,IAAI,IAAI,CAAC,KAAK,CAAC,aAAa,EAAE;YAC5B,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,WAAW,CAAC,EAAE,GAAG,OAAO,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,EAAE,EAAE,SAAS,EAAE,EAAE,GAAG,CAAC;;;IAI/F,8BAA8B,CAAC,OAAsB,EAAE,mBAAA,GAA8B,IAAI,CAAC,OAAO,CAAC,0BAA0B,EAAA;AAClI,QAAA,MAAM,EAAE,GAAG,EAAE,EAAE;AACf,QAAA,MAAM,OAAO,GAAG,cAAc,CAC5B,IAAI,CAAC,iBAAiB,CAAC,IAAI,CACzB,MAAM,CAAC,CAAC,QAAQ,KAAK,QAAQ,CAAC,EAAE,KAAK,EAAE,CAAC,EACxC,OAAO,CAAC,mBAAmB,CAAC,CAC7B,CACF;QACD,KAAK,IAAI,CAAC,YAAY,CAAC,OAAO,EAAE,EAAE,CAAC;AACnC,QAAA,OAAO,OAAO;;AAGhB;;;;AAIG;AACI,IAAA,MAAM,WAAW,CAAC,OAAsB,EAAE,SAAkB,EAAA;QACjE,MAAM,IAAI,CAAC,gBAAgB;AAC3B,QAAA,IAAI,CAAC,YAAY,CAAC,OAAO,EAAE,SAAS,CAAC;;AAGvC;;;;AAIG;IACI,MAAM,6BAA6B,CAAC,OAAsB,EAAE,sBAA8B,IAAI,CAAC,OAAO,CAAC,0BAA0B,EAAA;QACtI,MAAM,IAAI,CAAC,gBAAgB;QAC3B,OAAO,IAAI,CAAC,8BAA8B,CAAC,OAAO,EAAE,mBAAmB,CAAC;;AAE3E;;ACrHD;;AAEG;;;;"}