From a10e2f873c645f04fd73551102d27f12e4c3888f Mon Sep 17 00:00:00 2001 From: Wilco Fiers Date: Thu, 17 Jun 2021 10:33:32 +0200 Subject: [PATCH] docs(frameMessenger): clarify advanced use cases (#2885) * docs(frameMessenger): clarify advanced use cases * chore: address feedback * Update doc/frame-messenger.md Co-authored-by: Steven Lambert <2433219+straker@users.noreply.github.com> * Update axe.d.ts Co-authored-by: Dan Bjorge Co-authored-by: Steven Lambert <2433219+straker@users.noreply.github.com> Co-authored-by: Dan Bjorge --- axe.d.ts | 18 ++--- doc/frame-messenger.md | 76 +++++++++++++++++-- .../utils/frame-messenger/channel-store.js | 4 +- 3 files changed, 81 insertions(+), 17 deletions(-) diff --git a/axe.d.ts b/axe.d.ts index 69a9e1fc0c..66aeb9baed 100644 --- a/axe.d.ts +++ b/axe.d.ts @@ -318,20 +318,20 @@ declare namespace axe { open: (topicHandler: TopicHandler) => Close | void; post: ( frameWindow: Window, - data: TopicData | ReplyData, + data: TopicData, replyHandler: ReplyHandler - ) => void; + ) => boolean | void; }; type Close = Function; - type TopicHandler = (data: TopicData, responder?: Responder) => void; - type ReplyHandler = (data: ReplyData, responder?: Responder) => void; + type TopicHandler = (data: TopicData, responder: Responder) => void; + type ReplyHandler = (message: any | Error, keepalive: boolean, responder: Responder) => void; type Responder = ( - message: any, - keepalive: boolean, - replyHandler: ReplyHandler + message: any | Error, + keepalive?: boolean, + replyHandler?: ReplyHandler ) => void; - type TopicData = { topic: String } & ReplyData; - type ReplyData = { channelId: String; message: any; keepAlive: Boolean }; + type TopicData = { topic: string } & ReplyData; + type ReplyData = { channelId: string; message: any; keepalive: boolean }; } export = axe; diff --git a/doc/frame-messenger.md b/doc/frame-messenger.md index 5dd2420867..34384def03 100644 --- a/doc/frame-messenger.md +++ b/doc/frame-messenger.md @@ -8,10 +8,13 @@ Tools like browser extensions and testing environments often have different chan axe.frameMessenger({ // Called to initialize message handling open(topicHandler) { + // Map data from the bridge to topicHandler + function subscriber(frameWin, data, response) { + // Data deserializations / validation / etc. here + topicHandler(data, response); + } // Start listening for "axe-core" events - const unsubscribe = bridge.subscribe('axe-core', data => { - topicHandler(data); - }); + const unsubscribe = bridge.subscribe('axe-core', subscriber); // Tell axe how to close the connection if it needs to return unsubscribe; }, @@ -34,10 +37,71 @@ axe.frameMessenger({ The `topicHandler` function takes two arguments: the `data` object and a callback function that is called when the subscribed listener completes. The `data` object is exclusively passed data that can be serialized with `JSON.stringify()`, which depending on the system may need to be used. -The `open` function can `return` an optional cleanup function, which is called when another frameMessenger is registered. +The `open` function can `return` an optional `close` function. Axe-core will only ever have one frameMessenger open at a time. The `close` function is called when another frameMessenger is registered. ## axe.frameMessenger({ post }) -`post` is a function that dictates how axe-core communicates with frames. It is passed three arguments: `frameWindow`, which is the frames `contentWindow`, the `data` object, and a `replyHandler` that must be called when responses are received. +`post` is a function that dictates how axe-core communicates with frames. It is passed three arguments: `frameWindow`, which is the frame's `contentWindow`, the `data` object, and a `replyHandler` that must be called when responses are received. To inform axe-core that no message was sent, return `false`. This informs axe-core not to await for the ping to time out. + +Currently, axe-core will only require `replyHandler` to be called once, so promises can also be used here. This may change in the future, so it is preferable to make it possible for `replyHandler` to be called multiple times. Some axe-core [plugins](plugins.md) may rely on this feature. + +A second frameMessenger feature available to plugins, but not used in axe-core by default is to reply to a reply. This works by passing `replyHandler` a `responder` callback as a second argument. This requires a different setup, in which callbacks are stored based on their `channelId` property. + +```js +// store handlers based on channelId +const channels = {}; + +axe.frameMessenger({ + post(frameWindow, data, replyHandler) { + // Store the handler so it can be called later + channels[data.channelId] = replyHandler; + // Send a message to the frame + bridge.send(frameWindow, data); + }, + + open(topicHandler) { + function subscriber(frameWin, data) { + const { channelId, message, keepalive } = data; + // Create a callback to invoke on a reply. + const responder = createResponder(frameWin, channelId); + + // If there is a topic, pass it to the axe supplied topic-handler + if (data.topic) { + topicHandler(data, responder); + + // If there is a replyHandler stored, invoke it + } else if (channels[channelId]) { + const replyHandler = channels[channelId]; + replyHandler(message, keepalive, responder); + + // Clean up replyHandler, as no further messages are expected + if (!keepalive) delete channels[channelId]; + } + } + + // Start listening for "axe-core" events + const unsubscribe = bridge.subscribe('axe-core', subscriber); + // Tell axe how to close the connection if it needs to + return unsubscribe; + } +}); + +// Return a function to be called when a reply is received +function createResponder(frameWin, channelId) { + return function responder(message, keepalive, replyHandler) { + // Store the new reply handler, possibly replacing a previous one + // to avoid receiving a message twice. + channels[channelId] = replyHandler; + // Send a message to the frame + bridge.send(frameWin, { channelId, message, keepalive }); + }; +} +``` + +## Error handling & Timeouts + +If for some reason the frameMessenger fails to open, post, or close you should not throw an error. Axe-core will handle missing results by reporting on them in the `frame-tested` rule. It should not be possible for the `topicHandler` and `replyHandler` callbacks to throw an error. If this happens, please file an issue. + +Axe-core has a timeout mechanism built in, which pings frames to see if they respond before instructing them to run. There is no retry behavior in axe-core, which assumes that whatever channel is used is stable. If this isn't the case, this will need to be built into frameMessenger. -**note**: Currently, axe-core will only call `replyHandler` once, so promises can also be used here. This may change in the future, so it is preferable to make it possible for `replyHandler` to be called multiple times. +The `message` passed to responder may be an `Error`. If axe-core passes an `Error`, this should be propagated "as is". If this is not possible because the message needs to be serialized, a new `Error` object must be constructed as part of deserialization. diff --git a/lib/core/utils/frame-messenger/channel-store.js b/lib/core/utils/frame-messenger/channel-store.js index 17d625539d..59ba07878f 100644 --- a/lib/core/utils/frame-messenger/channel-store.js +++ b/lib/core/utils/frame-messenger/channel-store.js @@ -14,8 +14,8 @@ export function storeReplyHandler( channels[channelId] = { replyHandler, sendToParent }; } -export function getReplyHandler(topic) { - return channels[topic]; +export function getReplyHandler(channelId) { + return channels[channelId]; } export function deleteReplyHandler(channelId) {