Repository navigation
fix(plugin-gateway): forward the guildDelete of a guild READY drops - #247
Conversation
dropUnlistedGuilds now takes the made-up GUILD_DELETE through the steps handleDispatch takes for a real one: the action's before, the cache write, dispatch, then the action's handle. forwardGatewayDispatches only listens to dispatch, so broker workers never saw these deletions. Refs #243
A shard's new READY replaced nothing in the global expected guilds set, so a guild the bot left between two READYs kept clientReady waiting until waitGuildTimeout, and READYs after clientReady filled a set never emptied. Mirrors discordjs/discord.js#11547. Refs #243
|
Architecture Inside the changed components — 2 viewsComponent view — Gateway reconciliation Internal dispatch and action lifecycle when GatewayClient reconciles dropped guilds on READY. Component view — Broker dispatch forwarding Forwarding dropped guild dispatches across broker producers and consumers. Data flow ReviewNothing flagged · reviewed Tip After a push, tick Redraw to draw the latest commit 🪧 More tips
Thanks for using PR Lens! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. |
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes: the full diff of both commits, checked against handleDispatch, the GuildDelete dispatch handler and its state codec, and #checkClientReady.
- Forward the dropped guild's delete:
dropUnlistedGuildsnow takes the made-upGUILD_DELETEthroughbefore, the cache write,emit("dispatch")andhandle, in the same order ashandleDispatch. It skipsraw, soforwardGatewayDispatchespublishes it with the serialized guild state. TheGuildDeleteentry inDispatchStateCodecsalready covers that state. - Per-shard
expectedGuilds:#expectedGuildsbecomesMap<number, Set<string>>. A shard'sREADYreplaces its own entry only beforeclientReady, and empty entries are removed. Thesizeandclear()checks in#checkClientReadytherefore keep their meaning. - Docs, changesets and tests:
events.tsand both READMEs are updated, and there are two patch changesets. Tests cover the dispatch payload, state, order and absence ofraw, the end-to-end broker replay, and two per-shardclientReadycases.
claude-sonnet-5-5 | 𝕏

🔗 Linked issue
Resolves #243
🧭 Context
When a
READYno longer lists a cached guild,GatewayClientdrops it from the cache and emitsguildDelete, but it did so with a directemit. It never emitteddispatch, which is the only eventforwardGatewayDispatcheslistens to. A bot that runs its dispatch listeners only on@wolfstar/plugin-brokerworkers never saw these deletions, so it did not clean up the guilds it left while offline.While looking at discordjs/discord.js#11547 (the same reconnect scenario), I checked whether plugin-gateway has the same problems. The main one, left guilds staying cached after a new
READY, was already handled bydropUnlistedGuilds. The second part of that PR, trackingexpectedGuildsper shard, does apply here and is fixed in its own commit.📚 Description
Forwarding the dropped guild's delete (the issue).
dropUnlistedGuildsnow runs the made-upGUILD_DELETEthrough the same stepshandleDispatchtakes for a real one: the action'sbefore(the cached guild, as the state), the cache write,dispatch, then the action'shandle, which emitsguildDelete. The broker therefore publishes it with the serialized guild state, andreplayDispatchon the workers emitsguildDeletewith[Guild, { id }], the same arguments the gateway process's listeners get.rawis not emitted for it, since Discord never sent it, and it carriess: 0, whichreplayDispatchalready reads as "unknown".plugin-brokerneeds no code change.Tracking
clientReady's expected guilds by shard (from discord.js#11547).#expectedGuildswas oneSetthat every shard'sREADYadded to. As a result:clientReady(its session was invalidated during startup) and the bot had left a guild in between, the stale ID keptclientReadywaiting untilwaitGuildTimeout;clientReady, every reconnect'sREADYadded IDs that nothing ever removed.It is now a
Map<shardId, Set>. A shard'sREADYreplaces its own entry, and only beforeclientReady. Empty entries are removed, so the existingsize/clear()checks in#checkClientReadykeep their meaning.How this compares to the Pullfrog plan: I followed it as written, including the
action.before/action.handlerefactor it suggested over the minimalemitversion. The per-shardexpectedGuildscommit is not in the plan. It comes from comparing against discord.js#11547, as requested.Verification: after rebasing on
main,pnpm lint,pnpm build,pnpm typecheckandpnpm test(121 files, 1712 tests) all pass. Each new test fails without its fix and passes with it.Key changes
READYguildDelete, emitted directlydispatch(after the cache write, with the cached guild as state), thenguildDeletethrough theGUILD_DELETEactionguildDeletegets[Guild, { id }]clientReadyexpected guildsREADY; no longer collected afterclientReadypackages/plugin-gateway/src/GatewayClient.ts:dropUnlistedGuilds,#expectedGuilds, new#forgetExpectedGuild.packages/plugin-gateway/src/util/events.tsand both READMEs: document thatdispatchcovers the made-upGUILD_DELETE.hardening.test.ts(dispatch payload, state, order, noraw),plugin-broker/tests/gateway-replay.test.ts(end to end through the fake Redis),client-ready.test.ts(two per-shard cases).plugin-gateway+plugin-broker(README) for the forwarding, andplugin-gatewayfor the per-shard tracking.Type of Change
Pre-flight Checklist
pnpm changeset(orpnpm changeset add --emptyif no release is needed). See .changeset/README.md.