feat(shard): ingest guild rosters and departures (protocol 4)
Protocol 2 gave the guild board a member *count* and nothing else, so the Guilds page could say a guild had 155 members but never who they were, and findGuildForActor deliberately answered only for leaders because membership for rank-and-file was not in the feed at all. Protocol 4 puts it there. `shard_guild_members` holds one row per member per guild, keyed on (guild_id, serial). `guild.roster` replaces a guild's rows; `guild.leave` removes one. A guild.remove now clears the membership too, so a disbanded guild does not leave orphaned rows behind. The chunking needs explaining. A roster over the shard's per-frame cap arrives as several frames carrying seq/more/total. The sidecar reassembles them for its own GET /guilds board, but the live WebSocket feed and the /history backfill both carry the individual frames — so this ingest sees them unreassembled. It copes without buffering, because a table expresses what the sidecar's single JSON column could not: the frame carrying seq 0 clears the guild first, and every frame then upserts its own rows. Upsert rather than insert because the /history backfill replays stored frames on every reconnect, and a redelivery has to be a no-op rather than a duplicate-key error. The cost is a sub-second window during a multi-frame update where the table holds part of a roster; buffering to close it would duplicate the sidecar's reassembly for a projection that is already only as fresh as a 60s sweep. On visibility: both kinds are mapped to the existing `guilds` feature. Without that mapping rule 2 fails an unmapped kind closed to admin-only, which would have quietly kept rosters off the public page forever. Mapping them is safe because a roster is the first frame carrying locked fields inside an ARRAY of actors rather than one nested actor, and the projection walker already recurses into arrays and matches acct/webId by suffix — so a member's account name is stripped below admin by exactly the rule that already strips guild.leader.acct. There is a test for that specifically, because the difference is a public page listing character names versus one publishing 150 account names. `acct`/`web_id` are still stored, since that is what lets a linked member be matched to a site user; they are just never projected below admin. guild.leave is appended to the event log, as the departure counterpart to guild.join and for the same reason — it is what a "so-and-so left" feed reads. guild.roster stays out: it is board state like guild.update, and it is the one fat frame on the wire, so logging it would put a full membership snapshot into shard_events on every membership change. The PUBLIC_KINDS guard test caught the addition, which is what it is for; its expected set now carries a v4 group alongside the v3 one. Refs: docs/website/TEAMS.md Part 12 Phase 1 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -45,6 +45,7 @@ DROP TABLE IF EXISTS `shard_ruleset`;
|
||||
DROP TABLE IF EXISTS `shard_presence`;
|
||||
DROP TABLE IF EXISTS `shard_governor_terms`;
|
||||
DROP TABLE IF EXISTS `shard_governors`;
|
||||
DROP TABLE IF EXISTS `shard_guild_members`;
|
||||
DROP TABLE IF EXISTS `shard_guilds`;
|
||||
DROP TABLE IF EXISTS `shard_pages`;
|
||||
DROP TABLE IF EXISTS `shard_champs`;
|
||||
|
||||
@@ -220,6 +220,34 @@ CREATE TABLE IF NOT EXISTS shard_guilds (
|
||||
INDEX idx_shard_guilds_name (name)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Guild membership (Protocol 4). One row per member per guild, replaced on
|
||||
-- guild.roster and thinned by guild.leave. Protocol 2 could only say HOW MANY
|
||||
-- members a guild had, so this table has no pre-4 equivalent and the Guilds page
|
||||
-- could show a count but never a roster.
|
||||
--
|
||||
-- `acct` / `web_id` are the site-identity fields and are stored because the
|
||||
-- sidecar forwards them; they are NOT public. shardVisibility locks any key that
|
||||
-- is or ends in acct/webId to `admin` and recurses into arrays, so a projected
|
||||
-- roster loses them below that rung — storing them here is what lets a linked
|
||||
-- member be matched to a site user at all.
|
||||
--
|
||||
-- A roster over the shard's per-frame cap arrives in several frames, so rows are
|
||||
-- keyed on (guild_id, serial) and the frame carrying seq 0 clears the guild first;
|
||||
-- see upsertGuildRoster.
|
||||
CREATE TABLE IF NOT EXISTS shard_guild_members (
|
||||
guild_id INT NOT NULL,
|
||||
serial VARCHAR(20) NOT NULL, -- in-game mobile serial, "0x1F5"
|
||||
name VARCHAR(120) NULL,
|
||||
acct VARCHAR(120) NULL, -- absent for a mobile with no account
|
||||
web_id INT NULL, -- set only when the account is linked
|
||||
is_player TINYINT(1) NOT NULL DEFAULT 1,
|
||||
t BIGINT NULL, -- roster event time, epoch ms
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (guild_id, serial),
|
||||
INDEX idx_shard_guild_members_acct (acct),
|
||||
INDEX idx_shard_guild_members_web (web_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Town-governor board (Protocol 2.0, City Loyalty). One row per city, upserted on
|
||||
-- city.update (full-state, emitted only on change; there is no remove event since
|
||||
-- the set of cities is fixed). governor / governorElect are actor objects
|
||||
|
||||
Reference in New Issue
Block a user