( config: WorktreePoolConfig, initial?: PoolEntry, initialPending?: Promise<PoolEntry>, )
| 56 | } |
| 57 | |
| 58 | export function createWorktreePool( |
| 59 | config: WorktreePoolConfig, |
| 60 | initial?: PoolEntry, |
| 61 | initialPending?: Promise<PoolEntry>, |
| 62 | ): WorktreePool { |
| 63 | const pool = new Map<string, PoolEntry>(); |
| 64 | const pending = new Map<string, Promise<PoolEntry>>(); |
| 65 | // FETCH_HEAD is shared per-repo state: a creation's PR-head fetch must not |
| 66 | // run while another creation (or the seeded background warmup) is between |
| 67 | // its own fetch and `git worktree add`. Serialize all creations through |
| 68 | // this chain. |
| 69 | let creationChain: Promise<unknown> = Promise.resolve(); |
| 70 | if (initial) pool.set(initial.prUrl, initial); |
| 71 | |
| 72 | // Seeded background warmup: the initial entry starts ready:false while the |
| 73 | // caller builds its checkout (fetch/clone) off the request path. ensure() |
| 74 | // awaits the in-flight warmup instead of starting a duplicate creation. |
| 75 | // On failure the entry is KEPT as ready:false — resolve() stays undefined so |
| 76 | // consumers never receive a path that was never created; same-repo ensure() |
| 77 | // can retry creation once the failed warmup is cleared from pending. |
| 78 | if (initial && initialPending) { |
| 79 | const tracked = initialPending.then( |
| 80 | (entry) => { |
| 81 | pool.set(initial.prUrl, entry); |
| 82 | return entry; |
| 83 | }, |
| 84 | (err) => { |
| 85 | pending.delete(initial.prUrl); |
| 86 | throw err; |
| 87 | }, |
| 88 | ); |
| 89 | pending.set(initial.prUrl, tracked); |
| 90 | creationChain = tracked.catch(() => {}); |
| 91 | tracked |
| 92 | .then(() => pending.delete(initial.prUrl)) |
| 93 | .catch(() => {}); // warmup may complete with nobody awaiting it |
| 94 | } |
| 95 | |
| 96 | return { |
| 97 | get(prUrl) { return pool.get(prUrl); }, |
| 98 | has(prUrl) { return pool.has(prUrl); }, |
| 99 | resolve(prUrl) { |
| 100 | const entry = pool.get(prUrl); |
| 101 | return entry?.ready ? entry.path : undefined; |
| 102 | }, |
| 103 | |
| 104 | async ensure(runtime, metadata) { |
| 105 | const existing = pool.get(metadata.url); |
| 106 | if (existing?.ready) return existing; |
| 107 | |
| 108 | const inflight = pending.get(metadata.url); |
| 109 | if (inflight) return inflight; |
| 110 | |
| 111 | if (!config.isSameRepo) { |
| 112 | throw new Error("Cross-repo pool cannot create worktrees for other PRs"); |
| 113 | } |
| 114 | |
| 115 | const create = async (): Promise<PoolEntry> => { |
no test coverage detected