2.6 KiB
Worktree waiter dedup + pruning
Prevent Worktree.wait() from accumulating resolver closures
Summary
packages/app/src/utils/worktree.ts stores waiters as Map<string, Array<(state) => void>>. If multiple callers call wait() while a directory is pending (or when callers stop awaiting due to their own timeouts), resolver closures can accumulate until a ready()/failed() event arrives.
In this app, Worktree.wait() is used inside a Promise.race with a timeout, so it is possible to create many resolvers that remain stored for a long time.
This spec changes wait() to share a single promise per directory key (dedup), eliminating unbounded waiter arrays. Optionally, it prunes resolved state entries to keep the map small.
Scoped files (parallel-safe)
packages/app/src/utils/worktree.ts
Goals
- Ensure there is at most one pending promise per directory key
- Avoid accumulating arrays of resolver closures
- Keep current API surface for callers (
Worktree.wait(directory))
Non-goals
- Adding abort/cancel APIs that require callsite changes
- Changing UI behavior around worktree readiness
Proposed approach
- Replace
waiterswith a single in-flight promise per key
- Change:
- from:
Map<string, Array<(state: State) => void>> - to:
Map<string, { promise: Promise<State>; resolve: (state: State) => void }>
- from:
- Implement
wait()dedup
- If state is present and not pending: return
Promise.resolve(state). - Else if there is an in-flight waiter entry: return its
promise. - Else create and store a new
{ promise, resolve }.
- Resolve and clear on
ready()/failed()
- When setting state to ready/failed:
- look up waiter entry
- delete it
- call
resolve(state)
- (Optional) prune resolved state entries
- Keep pending states.
- Drop old ready/failed entries if
state.sizeexceeds a small cap.
Implementation steps
-
Refactor
waitersrepresentation -
Update
Worktree.wait,Worktree.ready,Worktree.failed -
Add small inline comments describing dedup semantics
Acceptance criteria
- Calling
Worktree.wait(directory)repeatedly while pending does not growwaitersunbounded ready()andfailed()still resolve any in-flight waiter promise- Existing callsites continue to work without modification
Validation plan
- Manual (or small ad-hoc dev snippet):
- call
Worktree.pending(dir) - call
Worktree.wait(dir)many times - confirm only one waiter entry exists
- call
Worktree.ready(dir)and confirm all awaiting callers resolve
- call