-
Notifications
You must be signed in to change notification settings - Fork 11
How React/Redux codebases integrate with Fronts/Clients #58
Description
Context: Fission.
For fission we will have to handle multiple targets at once. One per iframe in regular toolbox. One per process in the browser toolbox.
In order to handle this, typically in redux's actions, we will have to call the right target's front method. For now we had only one front, but now we will have many which will be specific to each resource.
I started looking into how we would handle this in all panels and saw that every panel was having a different way to interact with Fronts/Clients.
I think it is a good time to discuss between all panels and try to agree on a unified way to integrate a React/Redux codebase with Fronts and Clients.
Before suggesting anything I would like to first go over the codebase and correctly describe the current architecture of each panel using React and Redux.
I'm especially interested in describing:
- In which layer(s) (React, Redux, Reducers, middleware or other abstraction layer) we are using the Fronts/Client.
- Where do we process Front responses and if we convert/map/translate them.
Debugger
- React components pass around actor IDs of each source. The thread actor ID is stored in the Redux state as PausedState.currentThread.
- Actions are calling "commands" which are the browser specific action implementations (debugger supports both chrome and firefox) .
- Commands receive a thread actor ID coming from the state object and maps it to a thread client via a Map caching clients by actor IDs.
- It looks like there is very little data mapping between what Front returns and what we save in Redux store (There is this function setting default values)
Click to expand
Source object type definition saved in store
type BaseSource = {|
+id: string, // <== Actor ID
+url: string,
+thread: string,
...export function command(type: Command) {
return async ({ dispatch, getState, client }: ThunkArgs) => {
const thread = getCurrentThread(getState()); // <== Get the current thread/target from the state
return dispatch({
type: "COMMAND",
command: type,
thread,
[PROMISE]: client[type](thread) // <== Call the command with the thread actor ID
});
};
}A command calling a thread client method
function resume(thread: string): Promise<*> {
return new Promise(resolve => {
lookupThreadClient(thread).resume(resolve); // <== Call the thread client method after looking up for client instance by an actor ID
});
}
```js
</details>
### New about:debugging
- In most cases React components only manipulate actor IDs (for everything but SW registration front). Components are then calling actions with actor IDs and actions are using multiple ways, each time specific to each actor type, to get the front out of an actor ID.
- Actions are calling front methods and passing the result over to reducers which pass the data as-is, but a middleware is mapping the data to a custom representation.
- about:debugging is the only tool to use middleware to map data to a custom representation.
<details>
<summary>Click to expand</summary>
[An action call a front method and dispatch a new action with the response](https://searchfox.org/mozilla-central/rev/b29663c6c9c61b0bf29e8add490cbd6bad293a67/devtools/client/aboutdebugging-new/src/actions/debug-targets.js#199-220)
```js
const {
otherWorkers,
serviceWorkers,
sharedWorkers,
} = await clientWrapper.listWorkers();
...
dispatch({
type: REQUEST_WORKERS_SUCCESS,
otherWorkers,
serviceWorkers,
sharedWorkers,
});The reducer just pass the data as-is
case REQUEST_WORKERS_SUCCESS: {
const { otherWorkers, serviceWorkers, sharedWorkers } = action;
return Object.assign({}, state, { otherWorkers, serviceWorkers, sharedWorkers });
}A middleware is processing the front response
case REQUEST_WORKERS_SUCCESS: {
action.otherWorkers = toComponentData(action.otherWorkers);
action.serviceWorkers = toComponentData(action.serviceWorkers, true);
action.sharedWorkers = toComponentData(action.sharedWorkers);And maps it to a custom representation
function toComponentData(workers, isServiceWorker) {
return workers.map(worker => {
// Here `worker` is the worker object created by RootFront.listAllWorkers
const type = DEBUG_TARGETS.WORKER;
const icon = "chrome://devtools/skin/images/debugging-workers.svg";
let { fetch } = worker;
const {
name,
registrationFront,
scope,
subscription,
workerTargetFront,
} = worker;
// For registering service workers, workerTargetFront will not be available.
// The only valid identifier we can use at that point is the actorID for the
// service worker registration.
const id = workerTargetFront ? workerTargetFront.actorID : registrationFront.actorID;
let isActive = false;
let isRunning = false;
let pushServiceEndpoint = null;
let status = null;
if (isServiceWorker) {
fetch = fetch ? SERVICE_WORKER_FETCH_STATES.LISTENING
: SERVICE_WORKER_FETCH_STATES.NOT_LISTENING;
isActive = worker.active;
isRunning = !!worker.workerTargetFront;
status = getServiceWorkerStatus(isActive, isRunning);
pushServiceEndpoint = subscription ? subscription.endpoint : null;
}
return {
details: {
fetch,
isActive,
isRunning,
pushServiceEndpoint,
registrationFront, <==== Front
scope,
status,
},
icon,
id,
name,
type,
};
}); dispatch(Actions.startServiceWorker(target.details.registrationFront));The action calls a front method
function startServiceWorker(registrationFront) {
return async (_, getState) => {
try {
await registrationFront.start();Otherwise for every other resources we retrieve the front out of the ID, like here for addons:
const front = devtoolsClient.getActor(id); const workerActor = await clientWrapper.getServiceWorkerFront({ id });
await workerActor.push(); async getServiceWorkerFront({ id }) {
const { serviceWorkers } = await this.listWorkers();
const workerFronts = serviceWorkers.map(sw => sw.workerTargetFront);
return workerFronts.find(front => front && front.actorID === id);
} const addonTargetFront = await clientWrapper.getAddon({ id });
await addonTargetFront.reload();Accessibility
- React components are having access to target scoped front (walker/accessibility fronts) via props.
- Actions receive the target scoped front, call a method and reducers translate front response into a custom object.
Click to expand
exports.updateDetails = (domWalker, accessible, supports) =>
dispatch => Promise.all([
domWalker.getNodeFromActor(accessible.actorID, ["rawAccessible", "DOMNode"]),
supports.relations ? accessible.getRelations() : [],
]).then(response => dispatch({ accessible, type: UPDATE_DETAILS, response }))
.catch(error => dispatch({ accessible, type: UPDATE_DETAILS, error }));Reducer process the response and maps it to a custom representation
case UPDATE_DETAILS:
return onUpdateDetails(state, action);
function onUpdateDetails(state, action) {
const { accessible, response, error } = action;
if (error) {
console.warn("Error fetching DOMNode for accessible", accessible, error);
return state;
}
const [ DOMNode, relationObjects ] = response;
const relations = {};
relationObjects.forEach(({ type, targets }) => {
relations[type] = targets.length === 1 ? targets[0] : targets;
});
return { accessible, DOMNode, relations };
}performance-new
- React components only use front from here so we can almost say that only actions are using front.
- Target scoped front (Perf front) is stored in state object and actions are retrieving it from it.
- There is no particular mapping of data as we don't use much data out coming out from this front.
Click to expand
An action retrieve the target scoped front from the state and call a front method
const perfFront = selectors.getPerfFront(getState());
perfFront.startProfiler(recordingSettings);Details on how we retrieve the front from the state
const getPerfFront = state => getInitializedValues(state).perfFront;Application
- React components have fronts set on props and use it directly.
- "initializer", a main startup module/class, populate the store by calling front itself and dispatching action with front responses.
- The actions and reducers pass front data as-is.
Click to expand
React component calling a front method
const { registrationFront } = this.props.worker;
registrationFront.start();initializer module call front method and then an action with the response without any mapping
const { service } = await this.client.mainRoot.listAllWorkers();
this.actions.updateWorkers(service);Memory
- React components store the target scope front on props, call actions with it as argument
- I couldn't find any particular mapping of memory front generated data.
Click to expand
React component calling an action with front coming from props
dispatch(toggleRecordingAllocationStacks(front)), await front.startRecordingAllocations(ALLOCATION_RECORDING_OPTIONS);Netmonitor
- React components as well as actions go through a "Connector" class that itself communicated with the Client/Front
- This Connector listen for client events and do the necessary request to populate state via actions. It does map data to a custom representation and automatically does some request to expand long string for example.
Click to expand
Connector is calling an action with processed/aggregated data comping front Client responses
await this.actions.addRequest(id, {
// Convert the received date/time string to a unix timestamp.
startedMillis: Date.parse(startedDateTime),
method,
url,
isXHR,
cause,
// Compatibility code to support Firefox 58 and earlier that always
// send stack-trace immediately on networkEvent message.
// FF59+ supports fetching the traces lazily via requestData.
stacktrace: cause.stacktrace,
fromCache,
fromServiceWorker,
isThirdPartyTrackingResource,
referrerPolicy,
}, true);It does map data coming from RDP like here:
const payload = {};
if (responseContent && responseContent.content) {
const { text } = responseContent.content;
const response = await this.getLongString(text);
responseContent.content.text = response;
payload.responseContent = responseContent;
}
return payload;In is interesting to note a usage of target front usage
const toolbox = gDevTools.getToolbox(this.props.connector.getTabTarget());
toolbox.viewSourceInDebugger(url, 0); connector.sendHTTPRequest(data, (response) => {
return dispatch({
type: SEND_CUSTOM_REQUEST,
id: response.eventActor.actor,
});
});The connector is calling the client method
sendHTTPRequest(data, callback) {
this.webConsoleClient.sendHTTPRequest(data, callback);console
- Only one action calls a client method for autocompletion. It fetches the client reference from the "services".
- This "services" is exposed to all actions via a middleware
- And to many React components via "serviceContainer" props.
- JSTerm, WebConsoleFrame and WebConsoleOutputWrapper are all doing request to the client
- One React components craft clients and hand it over to an action to do more requests on the given client.
- Otherwise, the typical flow is to have WebConsoleProxy listen for client events and do the requests and then dispatch actions which are mapping data to a custom representation.
Click to expand
JSTerm is calling a client method
return this.webConsoleClient.evaluateJSAsync(str, null, {
frameActor: this.props.serviceContainer.getFrameActor(options.frame),
...options,
});WebConsoleFrame is calling a client method
this.webConsoleClient.setPreferences(toSet, response => {
if (!response.error) {
this._saveRequestAndResponseBodies = newValue;
deferred.resolve(response);WebConsoleOutputWrapper calls a client method
this.hud.webConsoleClient.clearNetworkRequests();A React component is instantiating a client class
const client = new ObjectClient(serviceContainer.hudProxy.client, parameters[0]);
const dataType = getParametersDataType(parameters);
// Get all the object properties.
dispatch(actions.messageTableDataGet(id, client, dataType));WebConsoleProxy listen to client events and call WebConsoleOutputWrapper
_onLogMessage: function(type, packet) {
if (!this.webConsoleFrame || packet.from != this.webConsoleClient.actor) {
return;
}
this.dispatchMessageAdd(packet);WebConsoleOutputWrapper dispatch actions (with some batching optimizations in the way)
store.dispatch(actions.messagesAdd(this.queuedMessageAdds));The action is using a transformation to map the data to a custom representation
There is one action using the console client directly
client.autocomplete(
input,
undefined,
frameActorId,
selectedNodeActor,
authorizedEvaluations
).then(data => {
dispatch(
autocompleteDataReceive({
id,
input,
force,
frameActorId,
data,
authorizedEvaluations,
}));