Writing Code for the Message Path
The Process Boundary describes what crosses between the two sides of the Urlbar actor pair. This page gives the rules for writing code that works on both paths. Code that breaks them usually still works on the direct path, so run its tests over the message path too.
Some failures only show in a content process, because in the parent process every realm is privileged. The tests in tests/browser-newtab run the New Tab search bar, which lives in a content process.
A payload is plain data
Whatever crosses the boundary arrives as a structured clone: no getters, no
private fields, no class identity, no DOM nodes, and no properties the sender
didn’t serialize. Object identity is lost too, so results are matched by their
id rather than by reference. The Process Boundary describes how results,
payloads and loads follow this rule.
A value has to be cloned into the realm that reads it
The UrlbarChild actor runs with system privileges. In a content process, an
object it leaves in its own realm reaches content code through an Xray wrapper.
An array then throws as soon as content code iterates it
(Permission denied to access property Symbol.iterator). A class instance
fails without an error, with every property reading undefined.
Cu.cloneInto(value, win) copies a value into the realm of the content window.
That fixes an array, but drops the prototype of a class instance, so a class
crosses in its wire form and is rebuilt on the content side, as
UrlbarQueryContext.fromWire() does. An object with methods, such as a
listener, needs cloneFunctions: true. The caller has to keep the clone,
because removeListener() only matches the object that was added;
NewtabSearchbarContentTestUtils.addControllerListener() returns it for that
reason.
Waiving Xrays on the object you call says nothing about the arguments you pass
it. UrlbarChild waives Xrays on the content-side input and view to call their
methods, and still clones the arguments into the content window. Waiving Xrays
on an element also changes which members you see: its JS properties appear, and
its [ChromeOnly] WebIDL members such as documentGlobal disappear.
One strong reference keeps the whole chain alive
On the message path the parent holds its controller until the input is
garbage collected. UrlbarChild registers each input in a
FinalizationRegistry and sends Destroy when the input is collected, and the
parent then drops the controller. A strong reference to the input from anything
that outlives it keeps the input alive, so the registry never fires. The parent
still drops the controller when the actor is torn down with its window global,
so the controller lives as long as the page, or the browser window for an input
in chrome, rather than forever.
A strong reference to anything that holds the input has the same effect. The
child controller holds the input, so UrlbarChild holds each child controller
only through a WeakRef.
Calls to the parent are asynchronous
On the direct path, a synchronous call to the parent controller may have done all of its work, apart from any asynchronous work it starts, by the time it returns. On the message path it returns a promise or nothing, and the parent’s answer arrives at least one round trip later. That has two consequences:
Anything the view needs synchronously has to arrive with the results. This is why a provider’s view data is computed in the parent and stored in the result (see View data).
Anything that resolved by returning has to resolve when the work is done, not when the message is sent, or the caller acts on state that hasn’t arrived yet.
The search engine store is an example. On the direct path, an input fills its
store synchronously through maybeInitEngineStore() when the search service is
already initialized. The message path has no synchronous call, so every input
fills its store after a round trip. Each consumer of the store decides what to
do until then: code that picks results waits for engineStore.init(), and the
placeholder and search icon update once the store is ready.
A difference between transports belongs to the transport
Where the two paths have to behave differently, put the difference inside the
transport and keep one implementation above it. For example, UrlbarChild
clones a value into the content window only when the input runs in a content
process, and passes it through unchanged in the parent. The code that sends the
value is the same on both paths.
Rebuilding a class instance from its wire form is the exception. The transport
runs in the system global, so an object it built there would reach content code
as an Xray. The child controller rebuilds the query context in its own realm
instead, in notifyFromWire().
Failures are silent in a content process
Code that breaks these rules in a content process rarely throws where you can
see it. UrlbarInputBase.handleEvent() catches any exception from an _on_*
event handler and reports it with console.error(), and a content process’s
console.error() output doesn’t reach a mochitest’s log. A chrome-only access
in content code therefore looks like a feature doing nothing. dump() output
does reach the log.
Content code that needs a chrome-only API such as windowUtils, or something
only the browser window has, such as gBrowser, has two options. It can ask
the parent through the actor, as the accessors in
UrlbarContentUtils do. Or it can check
typeof ChromeUtils and fall back to a content-safe equivalent, as
UrlbarShared.getBoundsWithoutFlushing() and UrlbarShared.isInstance() do.