Sidecar: REST query layer
rpc.rs bridges synchronous REST to the async shard stream. A call registers a
pending entry under a correlation id, sends the command, and awaits the reply
(10s timeout). The event loop routes any incoming line whose id is pending back
to the waiting caller; everything else stays a live event and is broadcast. Three
correlation fields are recognized, matching what the plugin echoes: reqId
(queries), code (link.confirm), id (towncrier).
web.rs adds the routes: GET /char/{account}/{slot}, /char/serial/{serial},
/roster/{account}, /vendors/{account}; POST /link/confirm, POST /towncrier,
DELETE /towncrier/{id}. A shard *.error reply maps to 404 or 400; no shard -> 503;
no reply in time -> 504.
Verified end to end against the live shard: roster and full char profile returned
as JSON (reqId correlation visible as r-1, r-2, ...), an unknown account returned
bridge.error as HTTP 404, vendor snapshot returned seed_000's two shops, towncrier
publish and remove returned towncrier.ok, and a bad link code returned link.error
as 404. The website can now query the game and push commands, all correlated over
the single loopback socket, all through the sidecar the game never directly
exposes.
Only SQLite persistence remains on the sidecar.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -24,16 +24,33 @@ Binds `127.0.0.1:7788` and waits for the shard to connect. Boot the shard (or it
|
||||
|-------|-------|
|
||||
| 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 `ping`→`pong`, 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 |
|
||||
| REST queries (`rpc.rs` + `web.rs`) | **done** — synchronous queries and commands, correlated to shard replies by id. Verified end-to-end against the live shard, success and error paths. |
|
||||
| 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 /health` → `ok`, `GET /ws` → the live feed. Widen the bind and add auth before exposing it off-host.
|
||||
The web server binds `127.0.0.1:8080` by default (`WEB_ADDR` in `main.rs`). Widen the bind and add auth before exposing it off-host.
|
||||
|
||||
### Routes
|
||||
|
||||
| Method | Path | Shard command | Reply |
|
||||
|--------|------|---------------|-------|
|
||||
| GET | `/health` | — | `ok` |
|
||||
| GET | `/ws` | — | live event feed (WebSocket) |
|
||||
| GET | `/char/{account}/{slot}` | `char.request` | `char.profile` |
|
||||
| GET | `/char/serial/{serial}` | `char.request` | `char.profile` |
|
||||
| GET | `/roster/{account}` | `account.roster` | `account.roster` |
|
||||
| GET | `/vendors/{account}` | `vendor.snapshot` | `vendor.snapshot` |
|
||||
| POST | `/link/confirm` `{code, websiteUserId}` | `link.confirm` | `link.ok` / `link.error` |
|
||||
| POST | `/towncrier` `{id, lines, durationSec}` | `towncrier.add` | `towncrier.ok` / `towncrier.error` |
|
||||
| DELETE | `/towncrier/{id}` | `towncrier.remove` | `towncrier.ok` / `towncrier.error` |
|
||||
|
||||
A shard `*.error` reply maps to HTTP 404 (unknown/not-found) or 400 (bad request). No shard connected → 503; no reply within 10 s → 504.
|
||||
|
||||
## Design
|
||||
|
||||
- **`shard.rs`** — `serve()` 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::send`s it to the WS feed. Later phases also persist to SQLite here and turn REST calls into shard commands via `ShardHandle`.
|
||||
- **`rpc.rs`** — request/reply correlation over the one shard socket. A REST call registers a pending entry under a correlation id, sends the command, and awaits the reply (10 s timeout). The event loop routes any incoming line whose id is pending back to the waiter; everything else flows on as a live event. Recognizes three correlation fields, matching what the plugin echoes: `reqId` (queries), `code` (link), `id` (town-crier).
|
||||
- **`main.rs`** — wires it together: the shard event loop first tries to route each line as an RPC reply; if it isn't one, the line is a live event, logged and broadcast to WS. Later phases persist to SQLite here.
|
||||
|
||||
## Wire protocol
|
||||
|
||||
|
||||
Reference in New Issue
Block a user