Files
opencode/specs/16-worktree-waiter-dedup-and-prune.md
2026-01-27 15:25:07 -06:00

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

  1. Replace waiters with 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 }>
  1. 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 }.
  1. Resolve and clear on ready() / failed()
  • When setting state to ready/failed:
    • look up waiter entry
    • delete it
    • call resolve(state)
  1. (Optional) prune resolved state entries
  • Keep pending states.
  • Drop old ready/failed entries if state.size exceeds a small cap.

Implementation steps

  1. Refactor waiters representation

  2. Update Worktree.wait, Worktree.ready, Worktree.failed

  3. Add small inline comments describing dedup semantics


Acceptance criteria

  • Calling Worktree.wait(directory) repeatedly while pending does not grow waiters unbounded
  • ready() and failed() 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