event SDK
event/sdk.js is about forty lines. It gives an app two verbs and hides everything else:
event.call('fileClick', { path }); // emit
event.on('loadSession', (p) => { … }); // receive
An app never imports another app, never learns which apps are running, and never sees the graph.
The graph
event/graph.json is the routing table:
[
{ "from": { "app": "explorer", "event": "fileClick" }, "to": { "app": "editor", "call": "openFile" } },
{ "from": { "app": "explorer", "event": "sessionClick" }, "to": { "app": "agent", "call": "loadSession" } }
]
Read it as: when explorer emits fileClick, call openFile on every editor window.
Edges are data. Rewiring two apps is a JSON edit; neither app changes, and no build step is involved.
The wire protocol
app iframe container other app iframes
───────── ───────── ──────────────────
event.call('fileClick', payload)
│
└─ postMessage ────────▶ { kind: 'event', name, payload }
│
look up edges
│
postMessage ────────────────▶ { kind: 'call', name, payload }
│
event.on(name) runs
Both directions are plain postMessage on the same origin. No request/response pairing, no promises, no acknowledgement.
Rules
Emission is conditional. An app declares its events implicitly through the graph: until the graph finishes loading, call fires conservatively; afterwards it only posts if some edge consumes that event. Emitting an unbound event is a silent no-op — it will not queue, retry, or warn.
Receivers are zero or many. The container sends the call to every window of the target app. If none are open, the call simply goes nowhere.
Delivery is one-way and unordered. Two calls from the same app arrive in order, but there is no reply channel. If an app needs a result, it owns the state and the other app reads it — the explorer opens a file; it does not ask the editor what it opened.
Payloads must be structured-clone-able. postMessage serialises them; functions, DOM nodes and class instances do not survive.
Standalone dev has no events. Run an app outside the container and window.parent === window: call becomes a no-op and no graph is fetched. This is why each app must still work when opened directly — that is the copy-it-out property in practice.
Writing a new wire
Add the handler in the receiving app:
useEffect(() => { event?.on('reveal', (payload) => scrollTo(payload.path)); }, []);Emit from the sender:
event.call('rowActivated', { path });Add the edge to
event/graph.json:{ "from": { "app": "explorer", "event": "rowActivated" }, "to": { "app": "editor", "call": "reveal" } }Reload the container. The graph is fetched once at startup, so a stale tab will not see new edges.
Pitfalls
| Symptom | Cause |
|---|---|
call seems to do nothing |
No edge consumes it, or the target app is not open. Both are silent by design. |
| First event after load is missing | The emitting app fired before graph.json finished loading — rare, but possible on a cold start. |
| Name collision | Names are global. Prefix them by intent (sessionClick, not click) rather than assuming isolation. |
Payload arrives undefined |
The value was not structured-clone-able, or the emitter passed the wrong argument order. |
| Works in dev, dead in the container | Standalone mode has no graph by design; test the wire inside dox. |