feat(shard): ingest guild rosters and departures (protocol 4) #8
Reference in New Issue
Block a user
No description provided.
Delete Branch "feat/teams-phase1-guild-roster"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Teams Phase 1, website-module half. Targets
edge;edge→mainis the v4 cutover.Spec:
docs/link/v4.md· pairs with servuo-plugins #12, link #31What
Protocol 2 gave the guild board a member count and nothing else — the Guilds page could say a guild had 155 members but never who they were, and
findGuildForActoranswered only for leaders because rank-and-file membership was not in the feed at all.shard_guild_membersholds one row per member per guild, keyed(guild_id, serial).guild.rosterreplaces a guild's rows,guild.leaveremoves one, andguild.removenow clears membership too so a disbanded guild leaves no orphans.Chunking, and why there's no buffer here
A roster over the shard's per-frame cap arrives as several frames. The sidecar reassembles them for its own
GET /guildsboard — but the live WebSocket feed and the/historybackfill 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: 0clears the guild first, and every frame then upserts its own rows. Upsert rather than insert because the/historybackfill replays stored frames on every reconnect, so a redelivery must 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. Acceptable for a projection already only as fresh as a 60s sweep; buffering to close it would duplicate the sidecar's reassembly.
Visibility — the part to review closely
Both kinds are mapped to the existing
guildsfeature. That mapping is required: 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/webIdby suffix, on meaning rather than spelling. So a member's account name is stripped belowadminby exactly the rule that already stripsguild.leader.acct.There is a test for this specifically, because the difference is a public Guilds page listing character names versus one publishing 150 account names.
acct/web_idare still stored — that is what lets a linked member be matched to a site user — just never projected below admin.Logging
guild.leaveis appended to the event log, as the counterpart toguild.joinand for the same reason: it is what a "so-and-so left" feed reads.guild.rosterstays out — it is board state likeguild.updateand the one fat frame on the wire, so logging it would put a full membership snapshot intoshard_eventson every membership change.Notes
PUBLIC_KINDSguard test caught the addition, which is what it's for; its expected set now carries a v4 group alongside the v3 one. Worth a look that the two new public kinds are ones you want public.npm test: 393 passed, 0 failed.edge, not justmain— ten phases land there, and a nine-PR Android workstream previously landed on an ungatededgewith no CI at all.AI-assisted: written with Claude Code. Commits carry
Co-Authored-By: Claude <noreply@anthropic.com>.