Merge pull request #1258 from thecodingmachine/query-answer-mechanism

Add new "query/answer" utility functions for the scripting API
This commit is contained in:
David Négrier 2021-07-02 17:29:41 +02:00 committed by GitHub
commit a7ced533c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 249 additions and 114 deletions

View File

@ -81,7 +81,7 @@ WA.room.getCurrentRoom(): Promise<Room>
``` ```
Return a promise that resolves to a `Room` object with the following attributes : Return a promise that resolves to a `Room` object with the following attributes :
* **id (string) :** ID of the current room * **id (string) :** ID of the current room
* **map (ITiledMap) :** contains the JSON map file with the properties that were setted by the script if `setProperty` was called. * **map (ITiledMap) :** contains the JSON map file with the properties that were set by the script if `setProperty` was called.
* **mapUrl (string) :** Url of the JSON map file * **mapUrl (string) :** Url of the JSON map file
* **startLayer (string | null) :** Name of the layer where the current user started, only if different from `start` layer * **startLayer (string | null) :** Name of the layer where the current user started, only if different from `start` layer

View File

@ -23,6 +23,9 @@ export interface TypedMessageEvent<T> extends MessageEvent {
data: T; data: T;
} }
/**
* List event types sent from an iFrame to WorkAdventure
*/
export type IframeEventMap = { export type IframeEventMap = {
loadPage: LoadPageEvent; loadPage: LoadPageEvent;
chat: ChatEvent; chat: ChatEvent;
@ -62,7 +65,6 @@ export interface IframeResponseEventMap {
enterEvent: EnterLeaveEvent; enterEvent: EnterLeaveEvent;
leaveEvent: EnterLeaveEvent; leaveEvent: EnterLeaveEvent;
buttonClickedEvent: ButtonClickedEvent; buttonClickedEvent: ButtonClickedEvent;
gameState: GameStateEvent;
hasPlayerMoved: HasPlayerMovedEvent; hasPlayerMoved: HasPlayerMovedEvent;
dataLayer: DataLayerEvent; dataLayer: DataLayerEvent;
menuItemClicked: MenuItemClickedEvent; menuItemClicked: MenuItemClickedEvent;
@ -76,3 +78,46 @@ export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> {
export const isIframeResponseEventWrapper = (event: { export const isIframeResponseEventWrapper = (event: {
type?: string; type?: string;
}): event is IframeResponseEvent<keyof IframeResponseEventMap> => typeof event.type === "string"; }): event is IframeResponseEvent<keyof IframeResponseEventMap> => typeof event.type === "string";
/**
* List event types sent from an iFrame to WorkAdventure that expect a unique answer from WorkAdventure along the type for the answer from WorkAdventure to the iFrame
*/
export type IframeQueryMap = {
getState: {
query: undefined,
answer: GameStateEvent
},
}
export interface IframeQuery<T extends keyof IframeQueryMap> {
type: T;
data: IframeQueryMap[T]['query'];
}
export interface IframeQueryWrapper<T extends keyof IframeQueryMap> {
id: number;
query: IframeQuery<T>;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isIframeQuery = (event: any): event is IframeQuery<keyof IframeQueryMap> => typeof event.type === 'string';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isIframeQueryWrapper = (event: any): event is IframeQueryWrapper<keyof IframeQueryMap> => typeof event.id === 'number' && isIframeQuery(event.query);
export interface IframeAnswerEvent<T extends keyof IframeQueryMap> {
id: number;
type: T;
data: IframeQueryMap[T]['answer'];
}
export const isIframeAnswerEvent = (event: { type?: string, id?: number }): event is IframeAnswerEvent<keyof IframeQueryMap> => typeof event.type === 'string' && typeof event.id === 'number';
export interface IframeErrorAnswerEvent {
id: number;
type: keyof IframeQueryMap;
error: string;
}
export const isIframeErrorAnswerEvent = (event: { type?: string, id?: number, error?: string }): event is IframeErrorAnswerEvent => typeof event.type === 'string' && typeof event.id === 'number' && typeof event.error === 'string';

View File

@ -10,11 +10,13 @@ import { scriptUtils } from "./ScriptUtils";
import { GoToPageEvent, isGoToPageEvent } from "./Events/GoToPageEvent"; import { GoToPageEvent, isGoToPageEvent } from "./Events/GoToPageEvent";
import { isOpenCoWebsite, OpenCoWebSiteEvent } from "./Events/OpenCoWebSiteEvent"; import { isOpenCoWebsite, OpenCoWebSiteEvent } from "./Events/OpenCoWebSiteEvent";
import { import {
IframeErrorAnswerEvent,
IframeEvent, IframeEvent,
IframeEventMap, IframeEventMap, IframeQueryMap,
IframeResponseEvent, IframeResponseEvent,
IframeResponseEventMap, IframeResponseEventMap,
isIframeEventWrapper, isIframeEventWrapper,
isIframeQueryWrapper,
TypedMessageEvent, TypedMessageEvent,
} from "./Events/IframeEvent"; } from "./Events/IframeEvent";
import type { UserInputChatEvent } from "./Events/UserInputChatEvent"; import type { UserInputChatEvent } from "./Events/UserInputChatEvent";
@ -31,6 +33,8 @@ import { isLoadPageEvent } from "./Events/LoadPageEvent";
import { handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent } from "./Events/ui/MenuItemRegisterEvent"; import { handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent } from "./Events/ui/MenuItemRegisterEvent";
import { SetTilesEvent, isSetTilesEvent } from "./Events/SetTilesEvent"; import { SetTilesEvent, isSetTilesEvent } from "./Events/SetTilesEvent";
type AnswererCallback<T extends keyof IframeQueryMap> = (query: IframeQueryMap[T]['query']) => IframeQueryMap[T]['answer']|Promise<IframeQueryMap[T]['answer']>;
/** /**
* Listens to messages from iframes and turn those messages into easy to use observables. * Listens to messages from iframes and turn those messages into easy to use observables.
* Also allows to send messages to those iframes. * Also allows to send messages to those iframes.
@ -81,9 +85,6 @@ class IframeListener {
private readonly _setPropertyStream: Subject<SetPropertyEvent> = new Subject(); private readonly _setPropertyStream: Subject<SetPropertyEvent> = new Subject();
public readonly setPropertyStream = this._setPropertyStream.asObservable(); public readonly setPropertyStream = this._setPropertyStream.asObservable();
private readonly _gameStateStream: Subject<void> = new Subject();
public readonly gameStateStream = this._gameStateStream.asObservable();
private readonly _dataLayerChangeStream: Subject<void> = new Subject(); private readonly _dataLayerChangeStream: Subject<void> = new Subject();
public readonly dataLayerChangeStream = this._dataLayerChangeStream.asObservable(); public readonly dataLayerChangeStream = this._dataLayerChangeStream.asObservable();
@ -110,6 +111,10 @@ class IframeListener {
private readonly scripts = new Map<string, HTMLIFrameElement>(); private readonly scripts = new Map<string, HTMLIFrameElement>();
private sendPlayerMove: boolean = false; private sendPlayerMove: boolean = false;
private answerers: {
[key in keyof IframeQueryMap]?: AnswererCallback<key>
} = {};
init() { init() {
window.addEventListener( window.addEventListener(
"message", "message",
@ -119,7 +124,7 @@ class IframeListener {
// Note: maybe we could restrict on the domain too for additional security (in case the iframe goes to another domain). // Note: maybe we could restrict on the domain too for additional security (in case the iframe goes to another domain).
let foundSrc: string | undefined; let foundSrc: string | undefined;
let iframe: HTMLIFrameElement; let iframe: HTMLIFrameElement | undefined;
for (iframe of this.iframes) { for (iframe of this.iframes) {
if (iframe.contentWindow === message.source) { if (iframe.contentWindow === message.source) {
foundSrc = iframe.src; foundSrc = iframe.src;
@ -129,7 +134,7 @@ class IframeListener {
const payload = message.data; const payload = message.data;
if (foundSrc === undefined) { if (foundSrc === undefined || iframe === undefined) {
if (isIframeEventWrapper(payload)) { if (isIframeEventWrapper(payload)) {
console.warn( console.warn(
"It seems an iFrame is trying to communicate with WorkAdventure but was not explicitly granted the permission to do so. " + "It seems an iFrame is trying to communicate with WorkAdventure but was not explicitly granted the permission to do so. " +
@ -143,65 +148,101 @@ class IframeListener {
foundSrc = this.getBaseUrl(foundSrc, message.source); foundSrc = this.getBaseUrl(foundSrc, message.source);
if (isIframeEventWrapper(payload)) { if (isIframeQueryWrapper(payload)) {
if (payload.type === "showLayer" && isLayerEvent(payload.data)) { const queryId = payload.id;
this._showLayerStream.next(payload.data); const query = payload.query;
} else if (payload.type === "hideLayer" && isLayerEvent(payload.data)) {
this._hideLayerStream.next(payload.data); const answerer = this.answerers[query.type];
} else if (payload.type === "setProperty" && isSetPropertyEvent(payload.data)) { if (answerer === undefined) {
this._setPropertyStream.next(payload.data); const errorMsg = 'The iFrame sent a message of type "'+query.type+'" but there is no service configured to answer these messages.';
} else if (payload.type === "chat" && isChatEvent(payload.data)) { console.error(errorMsg);
this._chatStream.next(payload.data); iframe.contentWindow?.postMessage({
} else if (payload.type === "openPopup" && isOpenPopupEvent(payload.data)) { id: queryId,
this._openPopupStream.next(payload.data); type: query.type,
} else if (payload.type === "closePopup" && isClosePopupEvent(payload.data)) { error: errorMsg
this._closePopupStream.next(payload.data); } as IframeErrorAnswerEvent, '*');
} else if (payload.type === "openTab" && isOpenTabEvent(payload.data)) { return;
scriptUtils.openTab(payload.data.url); }
} else if (payload.type === "goToPage" && isGoToPageEvent(payload.data)) {
scriptUtils.goToPage(payload.data.url); Promise.resolve(answerer(query.data)).then((value) => {
} else if (payload.type === "loadPage" && isLoadPageEvent(payload.data)) { iframe?.contentWindow?.postMessage({
this._loadPageStream.next(payload.data.url); id: queryId,
} else if (payload.type === "playSound" && isPlaySoundEvent(payload.data)) { type: query.type,
this._playSoundStream.next(payload.data); data: value
} else if (payload.type === "stopSound" && isStopSoundEvent(payload.data)) { }, '*');
this._stopSoundStream.next(payload.data); }).catch(reason => {
} else if (payload.type === "loadSound" && isLoadSoundEvent(payload.data)) { console.error('An error occurred while responding to an iFrame query.', reason);
this._loadSoundStream.next(payload.data); let reasonMsg: string;
} else if (payload.type === "openCoWebSite" && isOpenCoWebsite(payload.data)) { if (reason instanceof Error) {
scriptUtils.openCoWebsite( reasonMsg = reason.message;
payload.data.url, } else {
foundSrc, reasonMsg = reason.toString();
payload.data.allowApi, }
payload.data.allowPolicy
); iframe?.contentWindow?.postMessage({
} else if (payload.type === "closeCoWebSite") { id: queryId,
scriptUtils.closeCoWebSite(); type: query.type,
} else if (payload.type === "disablePlayerControls") { error: reasonMsg
this._disablePlayerControlStream.next(); } as IframeErrorAnswerEvent, '*');
} else if (payload.type === "restorePlayerControls") { });
this._enablePlayerControlStream.next();
} else if (payload.type === "displayBubble") { } else if (isIframeEventWrapper(payload)) {
this._displayBubbleStream.next(); if (payload.type === "showLayer" && isLayerEvent(payload.data)) {
} else if (payload.type === "removeBubble") { this._showLayerStream.next(payload.data);
this._removeBubbleStream.next(); } else if (payload.type === "hideLayer" && isLayerEvent(payload.data)) {
} else if (payload.type == "getState") { this._hideLayerStream.next(payload.data);
this._gameStateStream.next(); } else if (payload.type === "setProperty" && isSetPropertyEvent(payload.data)) {
} else if (payload.type == "onPlayerMove") { this._setPropertyStream.next(payload.data);
this.sendPlayerMove = true; } else if (payload.type === "chat" && isChatEvent(payload.data)) {
} else if (payload.type == "getDataLayer") { this._chatStream.next(payload.data);
this._dataLayerChangeStream.next(); } else if (payload.type === "openPopup" && isOpenPopupEvent(payload.data)) {
} else if (isMenuItemRegisterIframeEvent(payload)) { this._openPopupStream.next(payload.data);
const data = payload.data.menutItem; } else if (payload.type === "closePopup" && isClosePopupEvent(payload.data)) {
// @ts-ignore this._closePopupStream.next(payload.data);
this.iframeCloseCallbacks.get(iframe).push(() => { } else if (payload.type === "openTab" && isOpenTabEvent(payload.data)) {
this._unregisterMenuCommandStream.next(data); scriptUtils.openTab(payload.data.url);
}); } else if (payload.type === "goToPage" && isGoToPageEvent(payload.data)) {
handleMenuItemRegistrationEvent(payload.data); scriptUtils.goToPage(payload.data.url);
} else if (payload.type == "setTiles" && isSetTilesEvent(payload.data)) { } else if (payload.type === "loadPage" && isLoadPageEvent(payload.data)) {
this._setTilesStream.next(payload.data); this._loadPageStream.next(payload.data.url);
} else if (payload.type === "playSound" && isPlaySoundEvent(payload.data)) {
this._playSoundStream.next(payload.data);
} else if (payload.type === "stopSound" && isStopSoundEvent(payload.data)) {
this._stopSoundStream.next(payload.data);
} else if (payload.type === "loadSound" && isLoadSoundEvent(payload.data)) {
this._loadSoundStream.next(payload.data);
} else if (payload.type === "openCoWebSite" && isOpenCoWebsite(payload.data)) {
scriptUtils.openCoWebsite(
payload.data.url,
foundSrc,
payload.data.allowApi,
payload.data.allowPolicy
);
} else if (payload.type === "closeCoWebSite") {
scriptUtils.closeCoWebSite();
} else if (payload.type === "disablePlayerControls") {
this._disablePlayerControlStream.next();
} else if (payload.type === "restorePlayerControls") {
this._enablePlayerControlStream.next();
} else if (payload.type === "displayBubble") {
this._displayBubbleStream.next();
} else if (payload.type === "removeBubble") {
this._removeBubbleStream.next();
} else if (payload.type == "onPlayerMove") {
this.sendPlayerMove = true;
} else if (payload.type == "getDataLayer") {
this._dataLayerChangeStream.next();
} else if (isMenuItemRegisterIframeEvent(payload)) {
const data = payload.data.menutItem;
// @ts-ignore
this.iframeCloseCallbacks.get(iframe).push(() => {
this._unregisterMenuCommandStream.next(data);
});
handleMenuItemRegistrationEvent(payload.data);
} else if (payload.type == "setTiles" && isSetTilesEvent(payload.data)) {
this._setTilesStream.next(payload.data);
}
} }
}
}, },
false false
); );
@ -214,13 +255,6 @@ class IframeListener {
}); });
} }
sendGameStateEvent(gameStateEvent: GameStateEvent) {
this.postMessage({
type: "gameState",
data: gameStateEvent,
});
}
/** /**
* Allows the passed iFrame to send/receive messages via the API. * Allows the passed iFrame to send/receive messages via the API.
*/ */
@ -368,6 +402,22 @@ class IframeListener {
iframe.contentWindow?.postMessage(message, "*"); iframe.contentWindow?.postMessage(message, "*");
} }
} }
/**
* Registers a callback that can be used to respond to some query (as defined in the IframeQueryMap type).
*
* Important! There can be only one "answerer" so registering a new one will unregister the old one.
*
* @param key The "type" of the query we are answering
* @param callback
*/
public registerAnswerer<T extends keyof IframeQueryMap>(key: T, callback: (query: IframeQueryMap[T]['query']) => IframeQueryMap[T]['answer']|Promise<IframeQueryMap[T]['answer']> ): void {
this.answerers[key] = callback;
}
public unregisterAnswerer(key: keyof IframeQueryMap): void {
delete this.answerers[key];
}
} }
export const iframeListener = new IframeListener(); export const iframeListener = new IframeListener();

View File

@ -1,9 +1,40 @@
import type * as tg from "generic-type-guard"; import type * as tg from "generic-type-guard";
import type { IframeEvent, IframeEventMap, IframeResponseEventMap } from '../Events/IframeEvent'; import type {
IframeEvent,
IframeEventMap, IframeQuery,
IframeQueryMap,
IframeResponseEventMap
} from '../Events/IframeEvent';
import type {IframeQueryWrapper} from "../Events/IframeEvent";
export function sendToWorkadventure(content: IframeEvent<keyof IframeEventMap>) { export function sendToWorkadventure(content: IframeEvent<keyof IframeEventMap>) {
window.parent.postMessage(content, "*") window.parent.postMessage(content, "*")
} }
let queryNumber = 0;
export const answerPromises = new Map<number, {
resolve: (value: (IframeQueryMap[keyof IframeQueryMap]['answer'] | PromiseLike<IframeQueryMap[keyof IframeQueryMap]['answer']>)) => void,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
reject: (reason?: any) => void
}>();
export function queryWorkadventure<T extends keyof IframeQueryMap>(content: IframeQuery<T>): Promise<IframeQueryMap[T]['answer']> {
return new Promise<IframeQueryMap[T]['answer']>((resolve, reject) => {
window.parent.postMessage({
id: queryNumber,
query: content
} as IframeQueryWrapper<T>, "*");
answerPromises.set(queryNumber, {
resolve,
reject
});
queryNumber++;
});
}
type GuardedType<Guard extends tg.TypeGuard<unknown>> = Guard extends tg.TypeGuard<infer T> ? T : never type GuardedType<Guard extends tg.TypeGuard<unknown>> = Guard extends tg.TypeGuard<infer T> ? T : never
export interface IframeCallback<Key extends keyof IframeResponseEventMap, T = IframeResponseEventMap[Key], Guard = tg.TypeGuard<T>> { export interface IframeCallback<Key extends keyof IframeResponseEventMap, T = IframeResponseEventMap[Key], Guard = tg.TypeGuard<T>> {

View File

@ -4,7 +4,7 @@ import { isDataLayerEvent } from "../Events/DataLayerEvent";
import { EnterLeaveEvent, isEnterLeaveEvent } from "../Events/EnterLeaveEvent"; import { EnterLeaveEvent, isEnterLeaveEvent } from "../Events/EnterLeaveEvent";
import { isGameStateEvent } from "../Events/GameStateEvent"; import { isGameStateEvent } from "../Events/GameStateEvent";
import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution"; import {IframeApiContribution, queryWorkadventure, sendToWorkadventure} from "./IframeApiContribution";
import { apiCallback } from "./registeredCallbacks"; import { apiCallback } from "./registeredCallbacks";
import type { ITiledMap } from "../../Phaser/Map/ITiledMap"; import type { ITiledMap } from "../../Phaser/Map/ITiledMap";
@ -16,7 +16,7 @@ const leaveStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subj
const dataLayerResolver = new Subject<DataLayerEvent>(); const dataLayerResolver = new Subject<DataLayerEvent>();
const stateResolvers = new Subject<GameStateEvent>(); const stateResolvers = new Subject<GameStateEvent>();
let immutableData: GameStateEvent; let immutableDataPromise: Promise<GameStateEvent> | undefined = undefined;
interface Room { interface Room {
id: string; id: string;
@ -39,14 +39,10 @@ interface TileDescriptor {
} }
function getGameState(): Promise<GameStateEvent> { function getGameState(): Promise<GameStateEvent> {
if (immutableData) { if (immutableDataPromise === undefined) {
return Promise.resolve(immutableData); immutableDataPromise = queryWorkadventure({ type: "getState", data: undefined });
} else {
return new Promise<GameStateEvent>((resolver, thrower) => {
stateResolvers.subscribe(resolver);
sendToWorkadventure({ type: "getState", data: null });
});
} }
return immutableDataPromise;
} }
function getDataLayer(): Promise<DataLayerEvent> { function getDataLayer(): Promise<DataLayerEvent> {
@ -72,13 +68,6 @@ export class WorkadventureRoomCommands extends IframeApiContribution<Workadventu
leaveStreams.get(payloadData.name)?.next(); leaveStreams.get(payloadData.name)?.next();
}, },
}), }),
apiCallback({
type: "gameState",
typeChecker: isGameStateEvent,
callback: (payloadData) => {
stateResolvers.next(payloadData);
},
}),
apiCallback({ apiCallback({
type: "dataLayer", type: "dataLayer",
typeChecker: isDataLayerEvent, typeChecker: isDataLayerEvent,

View File

@ -44,7 +44,6 @@ export class TextUtils {
options.align = object.text.halign; options.align = object.text.halign;
} }
console.warn(options);
const textElem = scene.add.text(object.x, object.y, object.text.text, options); const textElem = scene.add.text(object.x, object.y, object.text.text, options);
textElem.setAngle(object.rotation); textElem.setAngle(object.rotation);
} }

View File

@ -1044,18 +1044,16 @@ ${escapedMessage}
}) })
); );
this.iframeSubscriptionList.push( iframeListener.registerAnswerer('getState', () => {
iframeListener.gameStateStream.subscribe(() => { return {
iframeListener.sendGameStateEvent({ mapUrl: this.MapUrlFile,
mapUrl: this.MapUrlFile, startLayerName: this.startPositionCalculator.startLayerName,
startLayerName: this.startPositionCalculator.startLayerName, uuid: localUserStore.getLocalUser()?.uuid,
uuid: localUserStore.getLocalUser()?.uuid, nickname: localUserStore.getName(),
nickname: localUserStore.getName(), roomId: this.RoomId,
roomId: this.RoomId, tags: this.connection ? this.connection.getAllTags() : [],
tags: this.connection ? this.connection.getAllTags() : [], };
}); });
})
);
this.iframeSubscriptionList.push( this.iframeSubscriptionList.push(
iframeListener.setTilesStream.subscribe((eventTiles) => { iframeListener.setTilesStream.subscribe((eventTiles) => {
for (const eventTile of eventTiles) { for (const eventTile of eventTiles) {
@ -1149,6 +1147,7 @@ ${escapedMessage}
this.emoteManager.destroy(); this.emoteManager.destroy();
this.peerStoreUnsubscribe(); this.peerStoreUnsubscribe();
this.biggestAvailableAreaStoreUnsubscribe(); this.biggestAvailableAreaStoreUnsubscribe();
iframeListener.unregisterAnswerer('getState');
mediaManager.hideGameOverlay(); mediaManager.hideGameOverlay();

View File

@ -1,7 +1,7 @@
import { registeredCallbacks } from "./Api/iframe/registeredCallbacks"; import { registeredCallbacks } from "./Api/iframe/registeredCallbacks";
import { import {
IframeResponseEvent, IframeResponseEvent,
IframeResponseEventMap, IframeResponseEventMap, isIframeAnswerEvent, isIframeErrorAnswerEvent,
isIframeResponseEventWrapper, isIframeResponseEventWrapper,
TypedMessageEvent, TypedMessageEvent,
} from "./Api/Events/IframeEvent"; } from "./Api/Events/IframeEvent";
@ -16,7 +16,7 @@ import player from "./Api/iframe/player";
import type { ButtonDescriptor } from "./Api/iframe/Ui/ButtonDescriptor"; import type { ButtonDescriptor } from "./Api/iframe/Ui/ButtonDescriptor";
import type { Popup } from "./Api/iframe/Ui/Popup"; import type { Popup } from "./Api/iframe/Ui/Popup";
import type { Sound } from "./Api/iframe/Sound/Sound"; import type { Sound } from "./Api/iframe/Sound/Sound";
import { sendToWorkadventure } from "./Api/iframe/IframeApiContribution"; import { answerPromises, sendToWorkadventure} from "./Api/iframe/IframeApiContribution";
const wa = { const wa = {
ui, ui,
@ -164,16 +164,38 @@ declare global {
window.WA = wa; window.WA = wa;
window.addEventListener( window.addEventListener(
"message", "message", <T extends keyof IframeResponseEventMap>(message: TypedMessageEvent<IframeResponseEvent<T>>) => {
<T extends keyof IframeResponseEventMap>(message: TypedMessageEvent<IframeResponseEvent<T>>) => { if (message.source !== window.parent) {
if (message.source !== window.parent) { return; // Skip message in this event listener
return; // Skip message in this event listener }
} const payload = message.data;
const payload = message.data;
console.debug(payload);
if (isIframeResponseEventWrapper(payload)) { console.debug(payload);
const payloadData = payload.data;
if (isIframeAnswerEvent(payload)) {
const queryId = payload.id;
const payloadData = payload.data;
const resolver = answerPromises.get(queryId);
if (resolver === undefined) {
throw new Error('In Iframe API, got an answer for a question that we have no track of.');
}
resolver.resolve(payloadData);
answerPromises.delete(queryId);
} else if (isIframeErrorAnswerEvent(payload)) {
const queryId = payload.id;
const payloadError = payload.error;
const resolver = answerPromises.get(queryId);
if (resolver === undefined) {
throw new Error('In Iframe API, got an error answer for a question that we have no track of.');
}
resolver.reject(payloadError);
answerPromises.delete(queryId);
} else if (isIframeResponseEventWrapper(payload)) {
const payloadData = payload.data;
const callback = registeredCallbacks[payload.type] as IframeCallback<T> | undefined; const callback = registeredCallbacks[payload.type] as IframeCallback<T> | undefined;
if (callback?.typeChecker(payloadData)) { if (callback?.typeChecker(payloadData)) {