The Process Boundary
On the message path, the input and the view run on the child side of the Urlbar JSWindowActor pair, and the parent controller, the providers and the muxers run on the parent side. Everything that passes between them is a message, so what arrives is a structured clone of what was sent. This page describes what crosses that boundary and what provider and view code has to account for.
On the direct path both sides share the same objects, so code that breaks these
rules usually still works there. To catch it, run the tests with
browser.urlbar.ipc.chromeMessagePassing set to true, which puts the
toolbar’s address and search bars on the message path too.
Results
A result crosses as the plain object that
UrlbarResult.toWire()
returns, and UrlbarResult.fromWire() turns it back into a result. Any property
set on a result other than id, rowIndex, commands and isSERP is lost.
Every result a provider adds gets an id from the providers manager. Results
that the view builds itself have none. When a result comes back to the parent,
for example on a selection or an engagement, fromWire() looks the id up among
the results of the parent’s last query and returns the original object, so a
provider sees the result it created. If the result isn’t there, because the
query has moved on or the result came from a one-off heuristic query such as
paste and go, fromWire() builds a new result from the wire form. That copy is
a different object from the one the provider created, its payload is a
structured clone of the original, as described below, and it skips payload
validation.
Payloads
The UrlbarResult constructor makes a shallow copy of the payload it is given.
This copy reads each getter once and keeps only own enumerable properties that
aren’t null or undefined, on both paths. On the message path the payload is
then structured-cloned, so a function in it makes the message fail, and a class
instance arrives as a plain object without its prototype or methods. For
example, a Firefox Suggest result from the Rust backend keeps the Rust
component’s Suggestion object in its payload as suggestionObject. The view
gets it as a plain object, so code that needs the real object, such as a
dismissal, has to run in the parent against the original result.
Payload validation needs system modules, so it is skipped in a content realm.
Provider results are validated in the parent before they cross, but a result
that a view in a content process builds itself is not validated. An invalid
payload in such a result throws when the view runs in the parent, regardless of
browser.urlbar.ipc.chromeMessagePassing, but goes unnoticed in a content
process.
View data
Everything the view needs from the provider is computed in the parent and put
into the UrlbarResult, so the view can read it synchronously without calling
back across the boundary.
A provider that needs to change a row afterwards has to add a new result in a
later query. For the result menu only, view.updateResultMenuCommands()
replaces the commands of a row that is already shown.
DOM nodes and events
DOM nodes and events never cross. The engagement data sent to the parent drops
details.element and details.event, so a provider’s onEngagement() sees
null for both on the message path. onBeforeSelection() receives no element
there either.
Loads
When the user picks a result, the content side asks the parent to load it. The
load parameters it sends can include principals such as triggeringPrincipal.
Post data travels as a string, and the parent turns it back into a stream.
Neither a <browser> nor the chrome document can be sent, so the parent adds
them itself: the target <browser> for a load into the current tab, and the
chrome document for a load elsewhere.
The parent decides which <browser> a load targets. An input in a content
process always targets its own tab, whatever browserId it sends. An input in
the chrome window identifies the browser by its browserId, which the parent
resolves with BrowsingContext.getCurrentTopByBrowserId(), and gets the
selected browser when it sends none.
An nsIURI doesn’t survive a structured clone, and neither does an
nsIURIFixupInfo. URI fixup results reach the content side as plain values
instead: URIFixupPrimitives holds the keywordAsSent and the
preferredURIDisplaySpec of a fixup, and the fallback navigation on Enter
returns the URL to load as a string, with its post data and keywordAsSent.
Provider hooks in the parent
Provider hooks such as onEngagement(), onImpression(), onAbandonment()
and onSelection() always run in the parent. On the message path they receive
the results that fromWire() resolved, and the view sends
onBeforeSelection() and onSelection() as messages without waiting for the
provider.
The controller passed to a provider is the parent controller. On the message
path, its input and view are stand-ins built by
UrlbarParent.
They offer only the methods listed in
UrlbarShared.INVOKABLE_CONTENT_ACTIONS, such as:
input:search(),setValue()andstartQuery()view:acknowledgeFeedback(),clearL10nCache(),clearTopSitesCache(),close()andupdateResultMenuCommands()
Every other property reads undefined. A call sends a message and returns
nothing, so its effect on the input or the view happens after the hook has
returned. If the page with the input has already gone away, the call is dropped
silently. Calling another method from the parent requires adding it to
INVOKABLE_CONTENT_ACTIONS, and its arguments have to be structured-clonable.
A result passed as an argument arrives without its private fields, so pass the
result’s id instead, as acknowledgeFeedback() and
updateResultMenuCommands() do.
A provider that needs the chrome window reads controller.browserWindow, which
the parent resolves from the actor. controller.input.window doesn’t exist on
the message path. The results that were visible at engagement time come with the
engagement data rather than from the view, since the parent’s view has none.