Add new "query/answer" utility functions for the scripting API

So far, the scripting API was using events to communicate between WA and the iFrame.
But often, the scripting API might actually want to "ask" WA a question and wait for an answer.

We dealt with this by using 2 unrelated events (in a mostly painful way).

This commit adds a "queryWorkadventure" utility function in the iFrame API that allows us
to send a query, and to wait for an answer. The query and answer events have a unique ID to be
sure the answer matches the correct query.

On the WA side, a new `IframeListener.registerAnswerer` method can be used to register a possible answer.
This commit is contained in:
David Négrier 2021-07-02 16:41:24 +02:00
parent d29c0cc99f
commit 5b4a72ea1f
7 changed files with 248 additions and 113 deletions

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']) => 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); 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']) => 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 Promise.resolve({
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)) {