---
sidebar_label: 'FrontendBridge Messaging'
sidebar_position: 2
---
# FrontendBridge Messaging
:::note
Information in this article is in preview status. Do not use information in this article without being in contact with Future Ordering.
:::
This document describes the messages exchanged between **FrontendBridge** and the **Shell** over the ZeroMQ pub/sub bus. It is the primary reference for external integrators.
Code examples use **TypeScript** for shell/transport code and **JavaScript** for plugin and page loader code (plugins are plain ES modules with no build step).
---
## Vocabulary
| Term | Definition |
|---|---|
| **shell** | The Electron app (or equivalent) that hosts and displays the web page. May not literally be Electron, but is treated as such throughout this document. |
| **FrontendBridge** | The .NET module running inside the FO.OP host process that is the other party in all communication described here. It manages navigation state, settings, and plugin invocation on the backend side. |
| **WebView** | The embedded browser component inside the shell that loads and renders the web page. The shell navigates the WebView in response to bridge commands and reports URL changes back to the bridge. |
| **plugin** | A JavaScript module loaded by the web page at runtime via dynamic `import()`. Plugins communicate with the backend exclusively through `window.__foopBridge` and have no knowledge of the underlying ZeroMQ transport. |
| **requestId** | A UUID generated by the sender of a request message. The receiver echoes it back in the response, allowing the original sender to match the response to its outstanding request. |
---
## Transport
The bus uses **ZeroMQ pub/sub** brokered through an XSUB/XPUB proxy running inside the FO.OP host process. The ports are fixed and cannot be changed.
### Ports
| Port | Address | Purpose |
|---|---|---|
| 42421 | `tcp://127.0.0.1:42421` | **Publish** — connect your PUB socket here to send messages |
| 42422 | `tcp://127.0.0.1:42422` | **Subscribe** — connect your SUB socket here to receive messages |
Both ports are bound to `127.0.0.1`. No authentication or encryption is used or expected.
### Concurrency
Exactly **one shell** should be connected at a time. Multiple shells are not prevented by the transport but are unsupported and will produce undefined behaviour.
### Wire frame layout
Every message on the bus uses three frames when received and two frames when sent:
```text
Sending (shell → bus): [frame1: topic (string)] [frame2: JSON (string)]
Receiving (bus → shell): [frame1: topic (string)] [frame2: seqId (4-byte LE int32)] [frame3: JSON (string)]
```
The proxy inserts the sequence ID frame automatically. The sequence ID increments per topic and starts at 1. **Implementors must read this frame but may discard it** — its purpose is internal to the proxy.
### Connecting from the Shell
Install the ZeroMQ bindings:
```bash
npm install zeromq
```
Create one SUB socket (receive) and one PUB socket (send):
```typescript
import * as zmq from 'zeromq';
const sub = new zmq.Subscriber();
const pub = new zmq.Publisher();
sub.connect('tcp://127.0.0.1:42422'); // receive from XPUB port
pub.connect('tcp://127.0.0.1:42421'); // send to XSUB port
```
### Reconnection
If the ZeroMQ connection is lost and re-established, the shell should send a `GetDesiredUrlMessage` to resync URL state. The bridge may have changed the desired URL while the shell was disconnected. The bridge is fully stateless with respect to connections — it will simply receive the message as a normal request.
### Topic reference
| Topic | Direction | When |
|---|---|---|
| `NavigateToMessage` | Bridge → Shell | Always subscribe |
| `DesiredUrlResponseMessage` | Bridge → Shell | After sending `GetDesiredUrlMessage` (or always) |
| `InvokeJavascriptMessage` | Bridge → Shell | Always subscribe |
| `NavigatedToMessage` | Shell → Bridge | Every time the WebView URL changes |
| `GetDesiredUrlMessage` | Shell → Bridge | On startup or reconnect |
| `JavascriptResultMessage` | Shell → Bridge | After JS invocation, or on unsolicited JS events |
### Subscribing to a topic
Subscribe by the exact message type name (the string value in the `messageType` field). Then read three frames per message:
```typescript
sub.subscribe('NavigateToMessage');
sub.subscribe('DesiredUrlResponseMessage');
sub.subscribe('InvokeJavascriptMessage');
for await (const [topicFrame, seqIdFrame, jsonFrame] of sub) {
const topic = topicFrame.toString();
const _sequenceId = seqIdFrame.readInt32LE(0); // proxy artifact — may be discarded
const envelope = JSON.parse(jsonFrame.toString());
const body = envelope.body;
// dispatch on topic...
}
```
### Publishing a message
Send two frames — topic and JSON. Build the envelope, then publish:
```typescript
interface Sender {
name: string;
version: string;
}
interface FoopMessage
{
sender: Sender;
envelopeVersion: number;
messageType: string;
messageTypeVersion: number;
body: TBody;
}
const SENDER: Sender = { name: 'shell', version: '1.0.0.0' };
function buildEnvelope(messageType: string, body: TBody): FoopMessage {
return { sender: SENDER, envelopeVersion: 0, messageType, messageTypeVersion: 0, body };
}
async function publish(messageType: string, body: TBody): Promise {
const json = JSON.stringify(buildEnvelope(messageType, body));
await pub.send([messageType, json]);
}
// Example:
await publish('NavigatedToMessage', { url: 'https://example.com/store/22/start', requestId: null });
```
---
## Envelope Format
All messages on the bus share the same JSON envelope, regardless of direction or layer.
### Schema
```json
{
"sender": {
"name": "string",
"version": "string (dotted quad, e.g. 1.0.0.0)",
},
"envelopeVersion": 0,
"messageType": "string (message type identifier, e.g. \"NavigateToMessage\")",
"messageTypeVersion": 0,
"body": {}
}
```
### Field descriptions
| Field | Type | Required | Description |
|---|---|---|---|
| `sender.name` | string | Yes | Logical name of the sending module (e.g. `"frontendbridge"`, `"shell"`, `"FO.Kiosk.Shell"`) |
| `sender.version` | string | Yes | Version of the sending module as a dotted quad |
| `envelopeVersion` | integer | Yes | Envelope schema version. Currently always `0`. Reserved for future versioning — no behaviour is defined for non-zero values |
| `messageType` | string | Yes | The message type identifier (e.g. `"NavigateToMessage"`). Must match the ZeroMQ topic frame |
| `messageTypeVersion` | integer | Yes | Body schema version. Currently always `0`. Reserved for future versioning — no behaviour is defined for non-zero values |
| `body` | object | Yes | Message-specific payload. See each message type below |
### Serialization rules
- Property names are **camelCase** in both directions.
- Deserialization is **case-insensitive**.
- `null` fields are serialized as JSON `null`.
- Version strings use dotted quad format (e.g. `"1.0.0.0"`).
## TypeScript Implementation
### Types
The following TypeScript interfaces cover all messages in the FrontendBridge ↔ Shell protocol.
```typescript
// ---- Envelope ----
interface Sender {
name: string;
version: string;
}
interface FoopMessage {
sender: Sender;
envelopeVersion: number;
messageType: string;
messageTypeVersion: number;
body: TBody;
}
// ---- Bridge → Shell message bodies ----
interface NavigateToBody {
url: string;
requestId: string;
}
interface DesiredUrlResponseBody {
url: string | null;
requestId: string;
}
interface InvokeJavascriptBody {
functionName: string;
argumentsJson: string;
requestId?: string;
}
// ---- Shell → Bridge message bodies ----
type NavigationErrorCategory = 'navigation_failed' | 'ssl_error' | 'access_denied' | 'unknown';
interface NavigationError {
category: NavigationErrorCategory;
/** Implementation-specific error detail, e.g. "net::ERR_NAME_NOT_RESOLVED" */
message: string;
}
interface NavigatedToBody {
/** The URL navigated to, or the URL that was attempted on failure */
url: string;
/** Echoes requestId from NavigateToMessage when navigating in response to a bridge request; null for spontaneous navigations */
requestId: string | null;
/** Present when the WebView failed to load the URL */
error?: NavigationError;
}
interface GetDesiredUrlBody {
requestId: string;
}
interface JavascriptResultBody {
functionName: string;
argumentsJson: string;
/** Echoes requestId from InvokeJavascriptMessage when responding to a bridge request; omitted for unsolicited events */
requestId?: string;
}
```
### Complete Implementation Example
The following shows a minimal but complete setup: connecting the sockets, subscribing to all bridge-originated messages, dispatching on topic, and publishing messages back to the bridge.
```typescript
import * as zmq from 'zeromq';
import { randomUUID } from 'crypto';
// ---- Setup ----
const sub = new zmq.Subscriber();
const pub = new zmq.Publisher();
sub.connect('tcp://127.0.0.1:42422'); // receive (XPUB port)
pub.connect('tcp://127.0.0.1:42421'); // send (XSUB port)
// Subscribe to all messages the bridge sends to the shell
sub.subscribe('NavigateToMessage');
sub.subscribe('DesiredUrlResponseMessage');
sub.subscribe('InvokeJavascriptMessage');
// ---- Publishing helpers ----
const SENDER: Sender = { name: 'shell', version: '1.0.0.0' };
function buildEnvelope(messageType: string, body: TBody): FoopMessage {
return { sender: SENDER, envelopeVersion: 0, messageType, messageTypeVersion: 0, body };
}
async function publish(messageType: string, body: TBody): Promise {
const json = JSON.stringify(buildEnvelope(messageType, body));
await pub.send([messageType, json]);
}
// Notify the bridge that the WebView has navigated to a new URL.
// Call this on every navigation, including in-page navigations.
// Pass requestId when responding to a NavigateToMessage; pass null for spontaneous navigations.
// Pass error when the WebView failed to load the URL.
async function sendNavigatedTo(
url: string,
requestId: string | null = null,
error?: NavigationError,
): Promise {
const body: NavigatedToBody = { url, requestId, ...(error && { error }) };
await publish('NavigatedToMessage', body);
}
// Ask the bridge for the current desired URL.
// Useful on startup or after a reconnect when local URL state is lost.
// The bridge will respond with DesiredUrlResponseMessage.
async function requestDesiredUrl(): Promise {
const requestId = randomUUID();
const body: GetDesiredUrlBody = { requestId };
await publish('GetDesiredUrlMessage', body);
return requestId; // match against DesiredUrlResponseMessage.requestId
}
// Send a JavaScript result to the bridge.
async function sendJavascriptResult(
functionName: string,
argumentsJson: string,
requestId?: string,
): Promise {
const body: JavascriptResultBody = { functionName, argumentsJson, requestId };
await publish('JavascriptResultMessage', body);
}
// ---- Receive loop ----
async function startReceiving(): Promise {
for await (const [topicFrame, seqIdFrame, jsonFrame] of sub) {
const topic = topicFrame.toString();
const _sequenceId = seqIdFrame.readInt32LE(0); // proxy artifact — may be discarded
const envelope = JSON.parse(jsonFrame.toString()) as FoopMessage;
const body = envelope.body;
switch (topic) {
case 'NavigateToMessage': {
const { url, requestId } = body as NavigateToBody;
console.log(`Navigate to: ${url} (requestId: ${requestId})`);
// Navigate your WebView to `url`.
// On success: sendNavigatedTo(url, requestId)
// On failure: sendNavigatedTo(url, requestId, { category: 'navigation_failed', message: '...' })
break;
}
case 'DesiredUrlResponseMessage': {
const { url, requestId } = body as DesiredUrlResponseBody;
if (url === null) {
// Bridge is still starting up — retry after a delay (1000ms is a reasonable default)
setTimeout(() => requestDesiredUrl(), 1000);
} else {
console.log(`Desired URL: ${url} (requestId: ${requestId})`);
// Navigate your WebView to `url`
}
break;
}
case 'InvokeJavascriptMessage': {
const { functionName, argumentsJson, requestId } = body as InvokeJavascriptBody;
console.log(`Invoke JS: ${functionName}(${argumentsJson ?? ''})`);
// Call the function in the WebView and collect the return value.
// Always reply when requestId is present. On JS error, include error details in argumentsJson.
const resultJson = JSON.stringify({ success: true }); // replace with actual JS invocation result
await sendJavascriptResult(functionName, resultJson, requestId);
break;
}
default:
console.warn(`Unhandled topic: ${topic}`);
}
}
}
// ---- Entry point ----
async function main(): Promise {
startReceiving(); // runs indefinitely
// On startup, ask the bridge for the current URL
await requestDesiredUrl();
}
main().catch(console.error);
```
---
## FrontendBridge ↔ Shell Messages
These messages are exchanged between FrontendBridge and the Shell. They are the primary protocol of this module.
### Bridge → Shell
#### `NavigateToMessage`
Instructs the shell to navigate its WebView to a given URL. Sent by the bridge on startup (from settings) and whenever the configured URL needs to change. The bridge is solely responsible for deciding when to send this message — the shell has no involvement in that decision.
| Field | Type | Description |
|---|---|---|
| `url` | string | The URL the shell should navigate to |
| `requestId` | string (UUID) | Identifies this navigation request; echoed back in `NavigatedToMessage` |
```json
{
"sender": { "name": "frontendbridge", "version": "1.0.0.0" },
"envelopeVersion": 0,
"messageType": "NavigateToMessage",
"messageTypeVersion": 0,
"body": {
"url": "https://example.com/store/22/start",
"requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
}
```
#### `DesiredUrlResponseMessage`
Sent in response to `GetDesiredUrlMessage`. Returns the URL that is currently configured in bridge settings.
| Field | Type | Description |
|---|---|---|
| `url` | string or null | The configured `FrontendUrl`. May be `null` during early startup — shell should retry after a short delay. No maximum retry count is defined; a reasonable implementation uses incremental backoff starting at ~1000ms |
| `requestId` | string (UUID) | Echoes the `requestId` from the originating `GetDesiredUrlMessage` |
```json
{
"sender": { "name": "frontendbridge", "version": "1.0.0.0" },
"envelopeVersion": 0,
"messageType": "DesiredUrlResponseMessage",
"messageTypeVersion": 0,
"body": {
"url": "https://example.com/store/22/start",
"requestId": "x1y2z3a4-b5c6-d7e8-f9a0-b1c2d3e4f5a6"
}
}
```
#### `InvokeJavascriptMessage`
Instructs the shell to invoke a named JavaScript function inside the loaded web content and report back the result.
| Field | Type | Description |
|---|---|---|
| `functionName` | string | Identifies the JS function or handler to invoke |
| `argumentsJson` | string | The stringified JSON payload to pass to the handler |
| `requestId` | string (UUID) or omitted | When present, the shell must reply with a `JavascriptResultMessage` echoing this value. When absent, the JS is still invoked but any resulting event is treated as unsolicited |
```json
{
"sender": { "name": "frontendbridge", "version": "1.0.0.0" },
"envelopeVersion": 0,
"messageType": "InvokeJavascriptMessage",
"messageTypeVersion": 0,
"body": {
"functionName": "setLanguage",
"argumentsJson": "{\"locale\":\"sv-SE\"}",
"requestId": "j1k2l3m4-n5o6-p7q8-r9s0-t1u2v3w4x5y6"
}
}
```
### Shell → Bridge
#### `NavigatedToMessage`
Notifies the bridge that the shell's WebView has navigated to a URL. Should be sent on every URL change, including in-page navigations, and on navigation failure.
> **Shell implementation note:** Bridge-triggered navigations can produce duplicate `NavigatedToMessage` events if not guarded. The bridge tolerates duplicates; any shell implementation should suppress them. See [ELECTRON.md](./example-electron-shell.md) for an Electron-specific example.
| Field | Type | Description |
|---|---|---|
| `url` | string | The URL navigated to, or the URL that was attempted on failure |
| `requestId` | string (UUID) or null | Echoes the `requestId` from the originating `NavigateToMessage` when navigating in response to a bridge request. `null` for spontaneous navigations (e.g. in-page navigations, redirects) |
| `error` | object or omitted | Present when the WebView failed to load the URL. Contains `category` (one of `navigation_failed`, `ssl_error`, `access_denied`, `unknown`) and `message` (implementation-specific detail string) |
```json
{
"sender": { "name": "shell", "version": "1.0.0.0" },
"envelopeVersion": 0,
"messageType": "NavigatedToMessage",
"messageTypeVersion": 0,
"body": {
"url": "https://example.com/store/22/checkout",
"requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
}
```
Spontaneous navigation example:
```json
{
"body": {
"url": "https://example.com/store/22/checkout",
"requestId": null
}
}
```
Navigation failure example:
```json
{
"body": {
"url": "https://example.com/store/22/start",
"requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"error": {
"category": "navigation_failed",
"message": "net::ERR_NAME_NOT_RESOLVED"
}
}
}
```
#### `GetDesiredUrlMessage`
Asks the bridge for the current desired URL. Used on startup or after a reconnect when the shell does not have URL state. The bridge responds with `DesiredUrlResponseMessage`.
| Field | Type | Description |
|---|---|---|
| `requestId` | string (UUID) | Generated by the shell; echoed back in the response for matching |
```json
{
"sender": { "name": "shell", "version": "1.0.0.0" },
"envelopeVersion": 0,
"messageType": "GetDesiredUrlMessage",
"messageTypeVersion": 0,
"body": {
"requestId": "x1y2z3a4-b5c6-d7e8-f9a0-b1c2d3e4f5a6"
}
}
```
#### `JavascriptResultMessage`
Reports the return value of a JavaScript function. Used in two cases:
- **Response to `InvokeJavascriptMessage`**: the bridge asked a JS function to run; this is the reply. `requestId` echoes the request.
- **Unsolicited event**: JavaScript inside the web content fires autonomously (e.g. menuUpdated). `requestId` is omitted.
| Field | Type | Description |
|---|---|---|
| `functionName` | string | Identifies the function or event name. For responses, echoes `functionName` from the originating `InvokeJavascriptMessage`; for unsolicited events, uses the plugin-defined event name |
| `argumentsJson` | string | The stringified JSON result or event payload. On JS error, include error details here |
| `requestId` | string (UUID) or omitted | Echoes `requestId` from `InvokeJavascriptMessage` when this is a response. Omitted for unsolicited events |
Response to `InvokeJavascriptMessage` example:
```json
{
"sender": { "name": "shell", "version": "1.0.0.0" },
"envelopeVersion": 0,
"messageType": "JavascriptResultMessage",
"messageTypeVersion": 0,
"body": {
"functionName": "setLanguage",
"argumentsJson": "{\"success\":true}",
"requestId": "j1k2l3m4-n5o6-p7q8-r9s0-t1u2v3w4x5y6"
}
}
```
Unsolicited event example (JS fires autonomously inside web content):
```json
{
"sender": { "name": "shell", "version": "1.0.0.0" },
"envelopeVersion": 0,
"messageType": "JavascriptResultMessage",
"messageTypeVersion": 0,
"body": {
"functionName": "onPaymentComplete",
"argumentsJson": "{\"orderId\":\"12345\"}"
}
}
```
---
## Plugin Messaging
Plugins communicate with the backend exclusively through `window.__foopBridge`, a messaging object injected into the page by the shell before any page code runs. The page itself has no involvement in this communication — it only loads and calls the plugin. Plugins have no knowledge of the underlying transport (ZeroMQ, WebView2 postMessage, etc.) — that is entirely the shell's concern.
Plugin selection is entirely the web content's concern — plugins are configured via the backoffice and loaded by the page at runtime. The shell's only responsibilities are to serve plugin files and inject `window.__foopBridge`.
Any shell implementation (Electron, UWP WebView2, etc.) must satisfy the `window.__foopBridge` contract below. See [ELECTRON.md](./example-electron-shell.md) for a conforming Electron implementation.
### `window.__foopBridge` contract
#### Interface
```typescript
interface FoopBridgeMessage {
type: string;
data?: unknown;
}
interface FoopBridgeDisposable {
dispose(): void;
}
interface FoopBridge {
sendMessage(msg: FoopBridgeMessage): void;
sendMessageWithResponse(msg: FoopBridgeMessage): Promise;
addMessageListener(handler: (msg: object) => void): FoopBridgeDisposable;
}
declare const window: Window & { __foopBridge: FoopBridge };
```
#### Required behaviors
**Injection timing**
`window.__foopBridge` must be available before any page or plugin code runs. The shell must inject it synchronously as part of its preload / world-injection step.
**`sendMessage(msg)`**
- `msg` must be `{ type: string, data?: unknown }`. `data` is optional and may be any JSON-serializable value, including primitives.
- Publishes `msg` as a `JavascriptResultMessage` over ZeroMQ with `functionName` set from `msg.type` and `argumentsJson` set to `JSON.stringify(msg.data)`.
- Fire-and-forget — no return value, no acknowledgement.
**`sendMessageWithResponse(msg)`**
- `msg` must be `{ type: string, data?: unknown }`. `data` is optional and may be any JSON-serializable value, including primitives.
- The shell generates a `requestId` (UUID v4) internally — the plugin never sees it.
- Publishes a `JavascriptResultMessage` over ZeroMQ with that `requestId`.
- Returns a `Promise` that resolves with the response payload when a matching `InvokeJavascriptMessage` arrives from the bridge with the same `requestId`.
- **Routing**: when an `InvokeJavascriptMessage` arrives with a `requestId` that matches a pending `sendMessageWithResponse` call, it resolves that promise and is **not** delivered to `addMessageListener` handlers. Messages without a matching pending call are delivered to listeners as normal.
- **Timeout**: implementations should add a timeout. The duration is implementation-defined. On timeout the promise must reject with an error that identifies it as a timeout, so callers can handle it.
- The shell is responsible for matching responses to outstanding promises and for cleaning up after resolution.
**`addMessageListener(handler)`**
- Registers `handler` to be called whenever the bridge pushes a message to the plugin (i.e. when an `InvokeJavascriptMessage` arrives over ZeroMQ that does not match a pending `sendMessageWithResponse` call).
- The `msg` passed to `handler` is the plain payload object — no `requestId`, no envelope fields.
- Returns a `FoopBridgeDisposable`. Calling `dispose()` must immediately stop delivery to that handler and release all references to it.
- Multiple listeners may be registered simultaneously; all must be called for each incoming message.
**Isolation guarantee**
Plugins must never observe `requestId`s, ZeroMQ topic names, or message envelope fields. The shell strips all transport-level concerns before delivering to handlers or resolving promises.
### Plugin format
A plugin is a JS module that exports a single async function as its default export. It receives no arguments — all backend communication goes through `window.__foopBridge`. The plugin may also interact with the page through other means (registering services, DOM manipulation, etc.).
```js
// my-plugin/1.0.0/my-plugin.js
export default async function() {
// Listen for messages from the backend
window.__foopBridge.addMessageListener((msg) => {
if (msg.type === 'someBackendEvent') {
// handle it
}
});
// Send a fire-and-forget message
window.__foopBridge.sendMessage({ type: 'pluginReady' });
// Send a message and await the backend's reply
try {
const result = await window.__foopBridge.sendMessageWithResponse({
type: 'startPayment',
data: { orderId: 'ord-42', amount: 1500, currencyIsoCode: 'SEK' },
});
} catch (err) {
// Handle timeout or other errors
}
}
```
### Page loader
```js
const mod = await import('/plugins/my-plugin/1.0.0/my-plugin.js');
await mod.default();
```
## Communication Patterns
### Publish / Subscribe (Fire-and-Forget)
Most messages follow a one-way notification pattern. The sender publishes a message and does not wait for acknowledgement.
- `NavigateToMessage` — bridge publishes navigation commands
- `NavigatedToMessage` — shell publishes URL changes
- `JavascriptResultMessage` (unsolicited) — plugin fires an event without a preceding request
### Request / Response (Correlated)
| Request | Response | Correlation Field |
|---|---|---|
| `GetDesiredUrlMessage` | `DesiredUrlResponseMessage` | `requestId` |
| `InvokeJavascriptMessage` | `JavascriptResultMessage` | `requestId` |
---
## Interaction Flows
### Normal Startup
The bridge starts first. On startup it immediately sends a `NavigateToMessage` with the URL from its settings.
```mermaid
sequenceDiagram
participant B as FrontendBridge
participant S as Shell
B->>S: NavigateToMessage(url, requestId)
note over S: navigates WebView
S->>B: NavigatedToMessage(url, requestId)
```
1. Bridge starts, reads `FrontendUrl` from settings.
2. Bridge publishes `NavigateToMessage` with that URL and a new UUID `requestId`.
3. Shell receives `NavigateToMessage`, loads the URL in the WebView.
4. Shell publishes `NavigatedToMessage` with the URL it landed on and the `requestId`.
5. Bridge receives `NavigatedToMessage` and records the current URL.
### Frontend Restart (Reconnect)
When the shell restarts or reconnects (crash, update, etc.) it does not know which URL to display. It asks the bridge before doing anything.
```mermaid
sequenceDiagram
participant B as FrontendBridge
participant S as Shell
note over S: shell starts up or reconnects
S->>B: GetDesiredUrlMessage(requestId)
B->>S: DesiredUrlResponseMessage(url, requestId)
note over S: navigates WebView
S->>B: NavigatedToMessage(url, requestId: null)
```
1. Shell starts or reconnects, has no URL state.
2. Shell generates a UUID `requestId` and publishes `GetDesiredUrlMessage`.
3. Bridge reads the current `FrontendUrl` from settings.
4. Bridge publishes `DesiredUrlResponseMessage` echoing the `requestId` and the URL.
5. Shell matches the response by `requestId`, loads the URL.
6. Shell publishes `NavigatedToMessage`.
If `url` is `null` in the response, the bridge is still starting up. The shell should retry with incremental backoff (1000ms is a reasonable starting interval). The failure policy after repeated nulls is implementation-defined.
### Deferred Navigation
The bridge is solely responsible for deciding when to send a `NavigateToMessage`. In some cases the bridge may defer a navigation until it is safe to proceed — for example, when a purchase is in progress. From the shell's perspective this is transparent: it simply receives a `NavigateToMessage` when the bridge is ready.
```mermaid
sequenceDiagram
participant B as FrontendBridge
participant S as Shell
note over S: user is mid-purchase
note over B: settings change arrives — navigation deferred internally
note over S: purchase completes
B->>S: NavigateToMessage(newUrl, requestId)
note over S: navigates to new URL
S->>B: NavigatedToMessage(newUrl, requestId)
```
### JavaScript Invocation
The bridge can instruct the shell to call a named JavaScript function in the loaded web content.
```mermaid
sequenceDiagram
participant B as FrontendBridge
participant S as Shell
B->>S: InvokeJavascriptMessage(functionName, argumentsJson, requestId)
note over S: calls JS function, collects return value
S->>B: JavascriptResultMessage(functionName, argumentsJson, requestId)
```
1. Bridge publishes `InvokeJavascriptMessage` with a `functionName`, `argumentsJson`, and an optional `requestId`.
2. Shell invokes the JavaScript function in the web content with the parsed `argumentsJson` payload.
3. When `requestId` is present, shell always replies with `JavascriptResultMessage`. On JS error, include error details in `argumentsJson`.
4. When `requestId` is absent, the JS is still invoked but any resulting event is treated as unsolicited.
### Unsolicited JavaScript Event
JavaScript running inside the web content may fire events autonomously (e.g. payment completed). The shell captures these and forwards them to the bus.
```mermaid
sequenceDiagram
participant B as FrontendBridge
participant S as Shell
note over S: JS event fires in web content
S->>B: JavascriptResultMessage(functionName, argumentsJson)
note over B: dispatches based on functionName
```
1. JavaScript inside the web content fires an event.
2. Shell captures the event.
3. Shell publishes `JavascriptResultMessage` with no `requestId` to indicate it is unsolicited.
4. Bridge receives the message and dispatches based on `functionName`.
---
## Shell ↔ Web Content Bridging
The shell connects the ZeroMQ bus to the web content running inside a WebView. It bridges in both directions: delivering backend-pushed messages to `window.__foopBridge` listeners, and forwarding plugin-initiated messages back to the bridge as `JavascriptResultMessage`.
Plugins are loaded by the page at runtime via dynamic `import()`. Each plugin exports a single async function as its default export; the page calls it with no arguments. The plugin uses `window.__foopBridge` — injected by the shell — for all backend communication.
See [ELECTRON.md](./example-electron-shell.md) for a complete Electron implementation.
### Plugin file serving
The shell serves plugin files from a local HTTP server. The page loads them via standard ES module `import()` and has no knowledge that the shell supplies the files:
```js
// main.js — serve plugins from a local directory
const express = require('express');
const app = express();
app.use('/plugins', express.static(path.join(__dirname, 'plugins')));
app.listen(3000, '127.0.0.1');
```
The page loads plugins as:
```text
http://localhost:3000/plugins/my-plugin/1.0.0/my-plugin.js
```