feat(web): server-busy toast for rate_limited errors (TASK-2080)#888
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Wires the 429
rate_limitedhandling from TASK-2026 into a dedicated, user-facing "Server busy" toast. Previously a surfaced 429 fell through to generic error handling; now it shows a distinct, non-alarming notification.Chokepoint
client.tsalready funnels every surfaced 429 through a singlethrow errin the 429 branch ofrequest(). I added asetRateLimitHandler(handler)seam (mirroring the existingsetAccessRevokedHandlerpattern) and callnotifyRateLimited(err.retryAfterMs)right before that throw. This keepsclient.tsfree of any UI/Svelte import — the toast wiring lives in the layout + a small helper, exactly like the access-revoked split.+layout.svelteregisters the handler once at bootstrap:setRateLimitHandler((ms) => notifyServerBusy(ms)).serverBusyToast.tsowns the message + dedupe, calling the existingtoastStore.show(..., 'info', ...).Dedupe
A busy server 429s a burst of independent in-flight requests at once; the client faithfully fires the seam per request.
notifyServerBusythrottles to one toast per 6s window (module-levellastShownAt, seeded to-Infinityso the first call always fires).retryAfterMsnames the wait ("try again in 3s") when the server sent a usableRetry-After, else stays vague ("in a moment"). Severity is'info'(least alarming; transient condition).Tests
serverBusyToast.test.ts(node, mocks toast store): single 429 -> one toast; burst of 10 within window -> one toast; window elapsed -> toast fires again; message formatting.client.test.ts(new describe): a surfaced 429 fires the handler exactly once with the parsedretryAfterMs; a non-429 (500) does not fire it.All 160 node tests pass;
npm run checkclean for touched files;npm run buildsucceeds. (The 2 jsdom suites from TASK-2081 fail to load in this worktree due to a pre-existing shared-node_modules resolution artifact — verified identical with my changes stashed.)Codex review: CLEAN.
Refs TASK-2080, PLAN-1984.