Commit Graph

3 Commits

Author SHA1 Message Date
946ba7027b Sidecar: SQLite persistence
store.rs (sqlx/sqlite) makes the data durable and queryable over time. Three
tables: events (the full live stream, append-only), links (account <-> website
user, mirrored from link.ok), profiles (last-known character sheet, cached from
char.profile). The event loop persists every live event before broadcasting it;
pong is dropped as ephemeral chatter.

New read endpoints served from the DB rather than the shard: GET /history
(optionally ?kind=), GET /economy (the money-supply series), GET /link/{account}.
GET /char/serial/{serial} now falls back to the cached profile when the shard is
unreachable, so an already-viewed character still renders during an outage;
link.confirm mirrors a successful link into the store.

Verified end to end: 10 economy.supply snapshots and the rest of the live stream
persisted and served via /history and /economy; the data survived a sidecar
restart (14 events still present, and the shard reconnected to the new sidecar);
and with the shard killed, a cached profile returned at HTTP 200 while an uncached
query failed cleanly at 503.

The sidecar is feature-complete: shard link, WebSocket feed, REST queries, and
persistence all work end-to-end against the live shard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 16:20:33 -05:00
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
b46005b333 Sidecar: shard link (Rust)
First cut of the Rust sidecar, in sidecar/. It is the TCP listener the shard
dials out to; that asymmetry is what keeps the game unreachable from the website.

shard.rs: serve() binds 127.0.0.1:7788 and accepts shard connections in a loop,
re-accepting on disconnect. Each connection splits read/write: the reader parses
newline-JSON into ShardEvent { kind, value } and forwards over an mpsc; the writer
drains a command mpsc. ShardHandle::send posts to whichever shard is connected and
drops with a warning if none is -- a website query during an outage should fail
fast, not queue; live events that must survive an outage are buffered by the shard.

main.rs wires it up, logs events by kind, and runs a 15s heartbeat ping to
exercise the command path. Later phases fan events out to a WebSocket broadcaster
and SQLite, and turn REST calls into shard commands.

Verified against the live shard: the sidecar received the shard's server.hello
(parsed, fields intact), round-tripped its heartbeat ping -> pong, and after a
sidecar restart the shard reconnected on its own and re-sent hello. Tokio + serde;
axum/sqlx/tungstenite come with the WS and REST phases.

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