Skip to content
This repository was archived by the owner on Nov 15, 2019. It is now read-only.
This repository was archived by the owner on Nov 15, 2019. It is now read-only.

How React/Redux codebases integrate with Fronts/Clients #58

@ochameau

Description

@ochameau

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,
  ...

An action calling a command

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,
    };
  });

SW registration front is saved in the redux store and used from a React component to hand it over to an action

  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);

Or for SW

  const workerActor = await clientWrapper.getServiceWorkerFront({ id });
  await workerActor.push();

https://searchfox.org/mozilla-central/rev/b29663c6c9c61b0bf29e8add490cbd6bad293a67/devtools/client/aboutdebugging-new/src/modules/client-wrapper.js#119-123

  async getServiceWorkerFront({ id }) {
    const { serviceWorkers } = await this.listWorkers();
    const workerFronts = serviceWorkers.map(sw => sw.workerTargetFront);
    return workerFronts.find(front => front && front.actorID === id);
  }

Or addons

  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

An action receive the fronts from React props and call a front method. The response is passed to reducers.

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)),

Action calling a front method

            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);

An action call the connector

    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,
        }));

https://searchfox.org/mozilla-central/rev/b29663c6c9c61b0bf29e8add490cbd6bad293a67/devtools/client/webconsole/reducers/autocomplete.js#45-84

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions