Añade GET /api/bench (SSE) y una seccion de simulador en index.html: un publisher inunda una room con miles de mensajes a N subscribers y una grafica en vivo anima el throughput. Las dos politicas de room se exponen como flags independientes (persist=JetStream, encrypt=E2E AEAD+Ed25519) mas tamano de payload, midiendo el coste de cada capa con la libreria cliente real. El benchmark usa peers efimeros propios, sin tocar los peers nombrados del sandbox manual. Verificado: las 4 combinaciones enc x persist con fan-out exacto. Bump app v0.2.0.
5.1 KiB
unibus playground
An all-in-one, web-based sandbox for the unibus message bus. One command brings up the entire stack embedded — no NATS to install, no services to wire — and a browser UI lets you exercise the bus visually: create peers, create and join rooms (cleartext or end-to-end encrypted), invite, publish, watch messages arrive live, and kick members (forward secrecy).
This is a playground (see .claude/rules/playgrounds.md): it lives inside
the unibus app, reuses the parent Go module (no separate go.mod), is not
indexed, and keeps all runtime state under playground/local_files/ (ephemeral,
safe to delete).
Run
From the unibus app directory:
cd /home/enmanuel/fn_registry/projects/message_bus/apps/unibus
go run ./playground
Then open http://localhost:7700 in your browser.
Stop with Ctrl-C — the server tears down the web UI, every bus client, the
control plane, and the embedded NATS cleanly (no orphaned processes).
Architecture
The browser never speaks NATS. The Go server is the actual bus peer:
browser ──fetch/SSE──▶ playground server (:7700)
│ holds one unibus client per named peer
├──HTTP──▶ membership control plane (127.0.0.1:8480)
└──NATS──▶ embedded NATS + JetStream (:4260)
- :7700 — web UI (the only browser-facing port).
- 127.0.0.1:8480 — membership control plane (rooms, members, sealed keys, rekey, blobs). Internal only.
- :4260 — embedded NATS + JetStream (the data plane). Internal only.
Each named peer gets its own long-term identity, persisted to
playground/local_files/<name>.id, so a peer keeps the same endpoint across
restarts. When a peer creates or joins a room, the server subscribes on its
behalf and streams every received frame to that peer's open browser tabs over
Server-Sent Events.
The playground only orchestrates the public unibus client API
(CreateRoom, Join, Subscribe, Publish, Invite, Kick); it never
reimplements bus or crypto logic.
Try it: 2 peers + encryption + kick
- Open two browser tabs on http://localhost:7700.
- Tab A: type
alice, click Connect. - Tab B: type
bob, click Connect. - Tab A (alice): type a subject like
room.general, tick 🔒 encrypted (E2E), click Create room. Copy the resultingroom_id. - Tab A (alice): in the Action panel, pick
bobas the target peer (use the ↻ button to refresh the peer list if needed) and click Invite to this room. - Tab B (bob): paste the
room_idinto the join field and click Join. - Type messages in both tabs and hit Send — each message appears live in
both tabs, tagged with subject, sender, time, and 🔒 (encrypted) or
clear. - Tab A (alice): click Kick from this room with
bobselected. The room key rotates to a new epoch. New messages alice sends are no longer visible to bob — forward secrecy: bob no longer holds the current key.
Cleartext rooms (leave the checkbox unticked) behave like plain NATS fan-out: fast, ephemeral, unsigned. Encrypted rooms are the Matrix-like mode: E2E encrypted, persisted, and per-message signed.
Benchmark: throughput simulator
The bottom panel of the UI is a performance simulator. Press ▶ Ejecutar benchmark and one publisher floods a fresh room with thousands of messages that N subscribers receive (fan-out); a live canvas chart animates the sent vs received totals while it runs.
The two policy axes are exposed as independent flags, so the benchmark measures the cost of each layer in isolation:
| JetStream | Encryption | Room policy | What it costs |
|---|---|---|---|
| off | off | {Encrypt:false, Persist:false} |
plain core NATS fan-out |
| on | off | {Encrypt:false, Persist:true} |
durable JetStream (publish ack per message) |
| off | on | {Encrypt:true, Persist:false} |
AEAD + Ed25519 signature per message, core transport |
| on | on | {Encrypt:true, Persist:true} |
full E2E + durable history |
A payload size slider (16 B – 8 KiB) sets the message size. Encrypted or persistent runs are capped to 30 000 messages (each message pays per-message crypto and/or a JetStream ack, so they run much slower than plain NATS).
The benchmark uses its own ephemeral peers (fresh identities, never persisted), so it never touches the named peers of the manual sandbox.
It is driven by an SSE endpoint that streams progress samples:
curl -N "http://localhost:7700/api/bench?n_msgs=20000&n_subs=3&payload=128&encrypt=0&persist=0"
# emits: data: {"type":"start",...} data: {"type":"sample",...} data: {"type":"done",...}
Query params: n_msgs, n_subs (1–16), payload (bytes), encrypt (0/1),
persist (0/1).
State / cleanup
All writable state lives under playground/local_files/:
<name>.id— per-peer identity (private keys; treat like an SSH key).play.db— membership store (rooms, members, sealed keys).blobs/— media blob store.js/— embedded JetStream store.
Delete the whole playground/local_files/ directory to reset to a clean slate.
It is gitignored and never distributed.