FrontendBridge Messaging
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:
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:
npm install zeromq
Create one SUB socket (receive) and one PUB socket (send):
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:
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:
interface Sender {
name: string;
version: string;
}
interface FoopMessage<TBody> {
sender: Sender;
envelopeVersion: number;
messageType: string;
messageTypeVersion: number;
body: TBody;
}
const SENDER: Sender = { name: 'shell', version: '1.0.0.0' };
function buildEnvelope<TBody>(messageType: string, body: TBody): FoopMessage<TBody> {
return { sender: SENDER, envelopeVersion: 0, messageType, messageTypeVersion: 0, body };
}
async function publish<TBody>(messageType: string, body: TBody): Promise<void> {
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
{
"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.
nullfields are serialized as JSONnull.- 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.
// ---- Envelope ----
interface Sender {
name: string;
version: string;
}
interface FoopMessage<TBody> {
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.
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<TBody>(messageType: string, body: TBody): FoopMessage<TBody> {
return { sender: SENDER, envelopeVersion: 0, messageType, messageTypeVersion: 0, body };
}
async function publish<TBody>(messageType: string, body: TBody): Promise<void> {
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<void> {
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<string> {
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<void> {
const body: JavascriptResultBody = { functionName, argumentsJson, requestId };
await publish('JavascriptResultMessage', body);
}
// ---- Receive loop ----
async function startReceiving(): Promise<void> {
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<unknown>;
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<void> {
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 |
{
"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 |
{
"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 |
{
"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
NavigatedToMessageevents if not guarded. The bridge tolerates duplicates; any shell implementation should suppress them. See ELECTRON.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) |
{
"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:
{
"body": {
"url": "https://example.com/store/22/checkout",
"requestId": null
}
}
Navigation failure example:
{
"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 |
{
"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.requestIdechoes the request. - Unsolicited event: JavaScript inside the web content fires autonomously (e.g. menuUpdated).
requestIdis 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:
{
"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):
{
"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 for a conforming Electron implementation.
window.__foopBridge contract
Interface
interface FoopBridgeMessage {
type: string;
data?: unknown;
}
interface FoopBridgeDisposable {
dispose(): void;
}
interface FoopBridge {
sendMessage(msg: FoopBridgeMessage): void;
sendMessageWithResponse(msg: FoopBridgeMessage): Promise<unknown>;
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)
msgmust be{ type: string, data?: unknown }.datais optional and may be any JSON-serializable value, including primitives.- Publishes
msgas aJavascriptResultMessageover ZeroMQ withfunctionNameset frommsg.typeandargumentsJsonset toJSON.stringify(msg.data). - Fire-and-forget — no return value, no acknowledgement.
sendMessageWithResponse(msg)
msgmust be{ type: string, data?: unknown }.datais 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
JavascriptResultMessageover ZeroMQ with thatrequestId. - Returns a
Promisethat resolves with the response payload when a matchingInvokeJavascriptMessagearrives from the bridge with the samerequestId. - Routing: when an
InvokeJavascriptMessagearrives with arequestIdthat matches a pendingsendMessageWithResponsecall, it resolves that promise and is not delivered toaddMessageListenerhandlers. 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
handlerto be called whenever the bridge pushes a message to the plugin (i.e. when anInvokeJavascriptMessagearrives over ZeroMQ that does not match a pendingsendMessageWithResponsecall). - The
msgpassed tohandleris the plain payload object — norequestId, no envelope fields. - Returns a
FoopBridgeDisposable. Callingdispose()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 requestIds, 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.).
// 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
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 commandsNavigatedToMessage— shell publishes URL changesJavascriptResultMessage(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.
- Bridge starts, reads
FrontendUrlfrom settings. - Bridge publishes
NavigateToMessagewith that URL and a new UUIDrequestId. - Shell receives
NavigateToMessage, loads the URL in the WebView. - Shell publishes
NavigatedToMessagewith the URL it landed on and therequestId. - Bridge receives
NavigatedToMessageand 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.
- Shell starts or reconnects, has no URL state.
- Shell generates a UUID
requestIdand publishesGetDesiredUrlMessage. - Bridge reads the current
FrontendUrlfrom settings. - Bridge publishes
DesiredUrlResponseMessageechoing therequestIdand the URL. - Shell matches the response by
requestId, loads the URL. - 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.
JavaScript Invocation
The bridge can instruct the shell to call a named JavaScript function in the loaded web content.
- Bridge publishes
InvokeJavascriptMessagewith afunctionName,argumentsJson, and an optionalrequestId. - Shell invokes the JavaScript function in the web content with the parsed
argumentsJsonpayload. - When
requestIdis present, shell always replies withJavascriptResultMessage. On JS error, include error details inargumentsJson. - When
requestIdis 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.
- JavaScript inside the web content fires an event.
- Shell captures the event.
- Shell publishes
JavascriptResultMessagewith norequestIdto indicate it is unsolicited. - 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 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:
// 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:
http://localhost:3000/plugins/my-plugin/1.0.0/my-plugin.js