feat: ingest protocol 2, and keep the record a wipe cannot erase
The module half of the read path. Seven tables, an ingest cursor, four public routes, and one file whose only job is deciding who may see what. **The record and the window are different things.** `rust_player_wipe_stats` and `rust_gather_totals` are permanent and per-wipe, so all-time is those rows SUMmed rather than a second set of counters that can disagree with them — that is R12's "per-wipe detail plus all-time rollups" in one table instead of two. `rust_events` is a bounded 30-day window of raw frames for the killfeed, and `rust_presence` is a board: replaced wholesale, never appended. **The feed is a cursor, not a socket, and the header says why.** Core runs Node 20, where a global WebSocket is still behind a flag, so a socket means taking `ws` — against a release that asserts it has no runtime dependencies (D5). The deciding argument is the other one though: a socket needs a cursor anyway, for whatever it missed while the module was restarting, and the catch-up path is the one that has to be right. A cursor alone is one mechanism exercised every five seconds rather than two where the second only runs after an outage. **The cursor advances after the batch, never before.** A crash between the two re-reads events already counted, which inflates a total; the other order loses them silently and for ever. One is visible and bounded, the other is invisible and permanent, so the code fails in the visible direction. A server with no cursor starts at the sidecar's current END rather than at zero — replaying a fortnight of deaths into stats for wipes the site never saw is not a catch-up. **`catalogue.js` is a security boundary, default-deny.** Protocol 2 carries IP addresses (login attempts, approvals, bans), one player's report about another, and the grid reference of somebody's base. They are stored, because an operator chasing ban evasion needs them; they are not served below the admin tier. The allowlist lives here rather than as a field on the wire, because a boundary declared by the sender is one a compromised or merely out-of-date game host can widen — the same reason core's own shard fan-out filters on the serving side. A kind this build has never heard of is not public, and a test holds the list against PROTOCOL.md §8.4 so that adding a kind to the protocol without classifying it fails a build. `PROTOCOL_VERSION` goes to 2 here in the same change as the emitters, though this module consumes none of the new frames yet: the sidecar refuses a mismatched client with a 409, so a module left on 1 would stop being able to read the board it has been reading all along. A constant that lags the deployment is an outage with a version number on it. 95 server tests, 20 client tests, every guard green, and `routes.manifest.json` regenerated against a real core at the pinned ref: 10 routes, all documented, none of core's moved. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
This commit is contained in:
@@ -79,7 +79,8 @@ async function listState() {
|
||||
return core.query(
|
||||
`SELECT server_id AS serverId, reachable, online, players, max_players AS maxPlayers,
|
||||
hostname, level, seed, world_size AS worldSize, boot_id AS bootId,
|
||||
save_created_at AS saveCreatedAt, protocol, updated_at AS updatedAt
|
||||
save_created_at AS saveCreatedAt, wipe_id AS wipeId, protocol,
|
||||
updated_at AS updatedAt
|
||||
FROM ${STATE}`,
|
||||
)
|
||||
}
|
||||
@@ -99,13 +100,14 @@ async function putState(state) {
|
||||
await core.query(
|
||||
`INSERT INTO ${STATE}
|
||||
(server_id, reachable, online, players, max_players, hostname, level, seed,
|
||||
world_size, boot_id, save_created_at, protocol, raw, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||
world_size, boot_id, save_created_at, wipe_id, protocol, raw, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
reachable = VALUES(reachable), online = VALUES(online), players = VALUES(players),
|
||||
max_players = VALUES(max_players), hostname = VALUES(hostname), level = VALUES(level),
|
||||
seed = VALUES(seed), world_size = VALUES(world_size), boot_id = VALUES(boot_id),
|
||||
save_created_at = VALUES(save_created_at), protocol = VALUES(protocol),
|
||||
save_created_at = VALUES(save_created_at), wipe_id = VALUES(wipe_id),
|
||||
protocol = VALUES(protocol),
|
||||
raw = VALUES(raw), updated_at = CURRENT_TIMESTAMP`,
|
||||
[
|
||||
state.serverId,
|
||||
@@ -119,6 +121,7 @@ async function putState(state) {
|
||||
state.worldSize === undefined ? null : state.worldSize,
|
||||
state.bootId || null,
|
||||
state.saveCreatedAt || null,
|
||||
state.wipeId || null,
|
||||
state.protocol === undefined ? null : state.protocol,
|
||||
state.raw ? JSON.stringify(state.raw) : null,
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user