fix(background): cap concurrently open requests - #1521
Merged
Conversation
ost-ptk
force-pushed
the
WALLET-1419-cw-open-request-cap
branch
from
August 25, 2026 11:26
349f4fb to
ca546c5
Compare
ost-ptk
marked this pull request as ready for review
August 25, 2026 11:37
Comp0te
reviewed
Aug 25, 2026
Comp0te
left a comment
Collaborator
There was a problem hiding this comment.
Read the cap end to end — the reducer's refusal path, the six handler read-backs, the session mirror, and the tests — against the merge base. The cap itself is the right shape and the ordinal/no-evict invariants hold. The comments below run along one seam: the refusal is a silent state transition, and the code around it hasn't caught up. One path strands an already-accepted payload; two texts assert the strand can't happen; two tests restate definitions rather than exercise the boundary; and the count that drives the refusal is blind to both origin and provenance.
ost-ptk
force-pushed
the
WALLET-1419-cw-open-request-cap
branch
from
August 26, 2026 06:03
ca546c5 to
ea8f4cf
Compare
ost-ptk
force-pushed
the
WALLET-1419-cw-open-request-cap
branch
from
August 26, 2026 06:32
ea8f4cf to
a52509e
Compare
ost-ptk
force-pushed
the
WALLET-1419-cw-open-request-cap
branch
from
August 26, 2026 09:56
a52509e to
c9b65ad
Compare
Comp0te
force-pushed
the
WALLET-1419-cw-open-request-cap
branch
from
August 26, 2026 10:15
c9b65ad to
13769b9
Compare
Comp0te
force-pushed
the
WALLET-1419-cw-open-request-cap
branch
from
August 26, 2026 10:44
13769b9 to
6b60ac9
Compare
Comp0te
force-pushed
the
WALLET-1419-cw-open-request-cap
branch
from
August 26, 2026 10:55
6b60ac9 to
efabb5a
Compare
ost-ptk
force-pushed
the
WALLET-1419-cw-open-request-cap
branch
from
August 26, 2026 11:21
efabb5a to
0572598
Compare
Comp0te
force-pushed
the
WALLET-1419-cw-open-request-cap
branch
from
August 26, 2026 14:52
0572598 to
155942f
Compare
Base automatically changed from
WALLET-1419-cw-reset-cancel-then-clear
to
develop
August 26, 2026 15:01
Comp0te
force-pushed
the
WALLET-1419-cw-open-request-cap
branch
from
August 26, 2026 15:01
155942f to
7c0efa9
Compare
Comp0te
approved these changes
Aug 26, 2026
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.
Description
Hardening on top of the sweep.
signandsignTypedDataalready refuse atMAX_STORED_PAYLOADS = 10before registering a request;connect,switchAccount,signMessageanddecryptMessagehad no cap at all — supersede was the only brake, andCANCEL_GRACE_MS = 250lets a burst register everything first. With the mirror persisting open rows, an unbounded burst would also be a session-storage write-amplification vector.The sweep (previous PR) is the prerequisite that makes a cap safe: without it, a pinned-open row would consume a cap slot for the whole browser session, turning frozen states into a slow denial-of-approvals.
What changed
MAX_OPEN_REQUESTSbesideMAX_RESPONDED_TOMBSTONESinwindowManagement/reducer.ts. Steady-state concurrency is ~1–2 (one shared approval window plus the Ledger second window) — the cap only bounds the 250 ms burst.windowRequestOpenedrefuses the write at the cap and returnsstateunchanged (reference-identical). Open requests are never evicted, and a refused write consumes no ordinal.A silent reducer refusal produces no response by itself, so each of the four capless handlers gets an explicit post-dispatch re-read — the analogue of the existing
getPayload(...) == nullcheck. On refusal the dapp receives that method's existing negative shape andopenWindowis never called.Cap only — no
errorCode. Four of the six response shapes cannot carry one (connectResponse/switchAccountResponseare bare booleans;signMessageResponse/decryptMessageResponseare{cancelled} | {…}), and their*Errorcreators lackerror: true, so the SDK would resolve the dapp promise instead of rejecting (the WALLET-1346 masking defect). Widening the wire shapes is a separate ticket.The mirror's write cap is tightened from an undocumented headroom to the enforced relation
MAX_SESSION_ROWS = MAX_RESPONDED_TOMBSTONES + MAX_OPEN_REQUESTS, with a test asserting it.sign/signTypedData(second commit): the same post-dispatch re-read applies to them too — at the cap they refuse with their ownMAX_STORED_PAYLOADS-style response (errorCode: tooManyPendingRequests, a real error channel) and never open a window. Their payload actions are dispatched beforewindowRequestOpened, so on refusal the branch also dispatcheswindowRequestResponded({requestId})— the WALLET-1418 orphan mechanism the vault reducer already keys off — reclaiming the stored payload immediately (thewindowManagementcase no-ops for an id with no open row). Without this, ~20 capless opens plus 10 refused signs would pin every payload slot and kill signing for all origins until the next unlock (review finding). TheMAX_STORED_PAYLOADSrefusal itself fires earlier and is byte-unchanged.The four capless methods still answer a cap refusal with their negative shapes only — an explicit
errorCodefor them is WALLET-1436 (wire-shape change + the WALLET-1346 masking fix).On the number itself:
MAX_OPEN_REQUESTS = 20is not arbitrary — the vault already admits2 × MAX_STORED_PAYLOADS(deploys + EIP-712 maps, 10 each) concurrently open sign-family requests, so any smaller cap would fire before the payload caps in the all-sign-family case. That floor is necessary, not sufficient — the four capless methods consume open slots outside its quantifier (review finding; the comment and test are scoped accordingly).Verification
npx jest src/background/— 936 tests pass;npx tsc --noEmitclean; eslint/prettier clean.windowManagement/reducer.tsstays at 100/100/100/100; handlers/sagas floors hold.openWindowcall; a completed request frees a slot;sign's existing refusal still fires first; theMAX_SESSION_ROWSrelation.Linked tickets
WALLET-1419
Checklist
Make sure this PR title follows semantic release conventions: https://semantic-release.gitbook.io/semantic-release/#commit-message-format
If the PR adds any new text to the UI, make sure they are localized — no UI text added
Include a screenshot or recording if implementing significant UI or user flow change — background-only, no UI change
When this PR affects architecture changes wait for review from Dmytro before merging