Skip to content

Commit

Permalink
docs(frameMessenger): clarify advanced use cases (#2885)
Browse files Browse the repository at this point in the history
* 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 <danielbj@microsoft.com>

Co-authored-by: Steven Lambert <2433219+straker@users.noreply.github.com>
Co-authored-by: Dan Bjorge <danielbj@microsoft.com>
  • Loading branch information
3 people committed Jun 22, 2021
1 parent 2d5506f commit a10e2f8
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 17 deletions.
18 changes: 9 additions & 9 deletions axe.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
76 changes: 70 additions & 6 deletions doc/frame-messenger.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
},
Expand All @@ -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.
4 changes: 2 additions & 2 deletions lib/core/utils/frame-messenger/channel-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down

0 comments on commit a10e2f8

Please sign in to comment.