Files
link/sidecar/README.md
colby aff10e846c Sidecar: WebSocket live feed
web.rs adds the website-facing HTTP surface (axum): GET /health and GET /ws.
Every shard event is broadcast to all connected WebSocket clients as a JSON text
frame. main.rs's event loop now broadcast::sends each event after logging it.

A lagging client is warned and kept live (misses events) rather than stalling
the others; a dead-but-not-closed socket is caught by a 30s server ping. The feed
is live-only -- no replay -- since history belongs to REST + SQLite. This side may
be exposed beyond loopback (it is the gatekeeper); it defaults to 127.0.0.1:8080
and wants auth before going public.

Verified end to end: a WebSocket client connected to /ws, received ws.hello, then
live pong events relayed from the shard through the shard-link -> broadcast -> ws
path -- the same path a login, sale, or IDOC alert will take to a browser.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 16:04:58 -05:00

3.3 KiB

uo-link sidecar

The Rust half of the bridge. It terminates the loopback link to the ServUO shard and (as it grows) exposes WebSocket + REST to the website.

website ──WS (live feed) / REST (queries)──►  sidecar  ──loopback TCP 127.0.0.1:7788──►  shard
                                              (this)      newline-JSON, bidirectional

The sidecar is the TCP listener; the shard dials out to it. That is what keeps the game unreachable from the website — the game exposes no port of its own. See ../docs/PLAN.md §2.

Run

cargo run                 # info logging
RUST_LOG=debug cargo run  # see every event, incl. pong heartbeats

Binds 127.0.0.1:7788 and waits for the shard to connect. Boot the shard (or it will reconnect on its own) and watch server.hello arrive.

Status

Piece State
Shard link (shard.rs) done — accepts the shard, reads events, sends commands, re-accepts on disconnect. Verified against the live shard: received server.hello, round-tripped a pingpong, and reconnected after a sidecar restart.
WebSocket feed (web.rs) done/ws fans every shard event out to connected clients via a broadcast. Verified: a WS client received ws.hello then live pong events relayed from the shard. Live-only, no replay.
REST queries (char profile, roster, vendor snapshot, link submit) not started
SQLite persistence (event history, economy, cached profiles, link map) not started

The web server binds 127.0.0.1:8080 by default (WEB_ADDR in main.rs). Routes: GET /healthok, GET /ws → the live feed. Widen the bind and add auth before exposing it off-host.

Design

  • shard.rsserve() binds the listener and accepts shard connections in a loop. Each connection splits into read/write halves: the read half parses newline-JSON into ShardEvent { kind, value } and forwards them; the write half drains an mpsc of command lines. ShardHandle::send posts a command to whichever shard is currently connected, and drops with a warning if none is — a website query during a shard outage should fail fast and retry, not queue behind a reconnect. Live events that must survive an outage are buffered by the shard, not here.
  • web.rs — the website-facing HTTP surface (axum). AppState holds the broadcast::Sender<String>; each /ws client subscribes and forwards every event as a text frame. A client that lags past the broadcast buffer is warned and kept live (it just misses events) rather than stalling the others. This side may be exposed beyond loopback — it is the gatekeeper, so add auth when you do.
  • main.rs — wires it together: the shard event loop logs each event and broadcast::sends it to the WS feed. Later phases also persist to SQLite here and turn REST calls into shard commands via ShardHandle.

Wire protocol

Every line is one JSON object with t (epoch ms) and kind. The shard→sidecar events and sidecar→shard commands are catalogued in ../docs/PLAN.md (§5 data catalog, §7 protocol) and were all validated end-to-end while building the plugin. Notable inbound commands the sidecar will issue: char.request, account.roster, vendor.snapshot, link.confirm, towncrier.add/remove, ping.