6b162deeb0
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.
120 lines
5.1 KiB
Markdown
120 lines
5.1 KiB
Markdown
# 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:
|
||
|
||
```bash
|
||
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
|
||
|
||
1. Open **two browser tabs** on http://localhost:7700.
|
||
2. Tab A: type `alice`, click **Connect**.
|
||
3. Tab B: type `bob`, click **Connect**.
|
||
4. Tab A (alice): type a subject like `room.general`, tick **🔒 encrypted
|
||
(E2E)**, click **Create room**. Copy the resulting `room_id`.
|
||
5. Tab A (alice): in the Action panel, pick `bob` as the target peer (use the
|
||
↻ button to refresh the peer list if needed) and click **Invite to this
|
||
room**.
|
||
6. Tab B (bob): paste the `room_id` into the join field and click **Join**.
|
||
7. Type messages in **both** tabs and hit Send — each message appears live in
|
||
both tabs, tagged with subject, sender, time, and 🔒 (encrypted) or `clear`.
|
||
8. Tab A (alice): click **Kick from this room** with `bob` selected. 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:
|
||
|
||
```bash
|
||
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.
|