| 162 | * Store for GitHub PR status. Fetches status via gh CLI and caches results. |
| 163 | */ |
| 164 | export class PRStatusStore { |
| 165 | private client: RouterClient<AppRouter> | null = null; |
| 166 | private readonly refreshController: RefreshController; |
| 167 | private isActive = true; |
| 168 | |
| 169 | // Workspace-based PR detection (keyed by workspaceId) |
| 170 | private workspacePRSubscriptions = new MapStore<string, WorkspacePRCacheEntry>(); |
| 171 | private workspacePRCache = new Map<string, WorkspacePRCacheEntry>(); |
| 172 | private runtimeRetryUnsubscribers = new Map<string, () => void>(); |
| 173 | |
| 174 | // Track active subscriptions per workspace so we only refresh workspaces that are actually visible. |
| 175 | private workspaceSubscriptionCounts = new Map<string, number>(); |
| 176 | |
| 177 | // Track latest merge queue enrichment request while an in-flight fetch is running. |
| 178 | private mergeQueueRefreshPending = new Map<string, MergeQueueRefreshRequest>(); |
| 179 | |
| 180 | // Track per-workspace async merge queue enrichment to avoid overlapping gh api calls. |
| 181 | private mergeQueueRefreshInFlight = new Map<string, Promise<void>>(); |
| 182 | |
| 183 | // Like GitStatusStore: batch immediate refreshes triggered by subscriptions. |
| 184 | private immediateUpdateQueued = false; |
| 185 | private workspaceMetadata = new Map<string, FrontendWorkspaceMetadata>(); |
| 186 | private readonly runtimeStatusStore: PassiveRuntimeDeps; |
| 187 | |
| 188 | constructor(runtimeStatusStore?: PassiveRuntimeDeps); |
| 189 | constructor(runtimeStatusStore?: Pick<RuntimeStatusStore, "getStatus">); |
| 190 | constructor( |
| 191 | runtimeStatusStore: |
| 192 | | PassiveRuntimeDeps |
| 193 | | Pick<RuntimeStatusStore, "getStatus"> = getRuntimeStatusStore() |
| 194 | ) { |
| 195 | this.runtimeStatusStore = { |
| 196 | getStatus: (workspaceId) => runtimeStatusStore.getStatus(workspaceId), |
| 197 | subscribeKey: |
| 198 | "subscribeKey" in runtimeStatusStore |
| 199 | ? (workspaceId, listener) => runtimeStatusStore.subscribeKey(workspaceId, listener) |
| 200 | : () => () => undefined, |
| 201 | }; |
| 202 | this.refreshController = new RefreshController({ |
| 203 | onRefresh: () => this.refreshAll(), |
| 204 | onRefreshError: (failure) => { |
| 205 | console.error("[PRStatusStore] refresh failed:", failure.errorMessage); |
| 206 | }, |
| 207 | debounceMs: 5000, |
| 208 | refreshOnFocus: true, |
| 209 | focusDebounceMs: 1000, |
| 210 | }); |
| 211 | } |
| 212 | |
| 213 | setClient(client: RouterClient<AppRouter> | null): void { |
| 214 | this.client = client; |
| 215 | |
| 216 | if (!client) { |
| 217 | return; |
| 218 | } |
| 219 | |
| 220 | // If hooks subscribed before the client was ready, ensure we refresh once it is. |
| 221 | if (this.workspaceSubscriptionCounts.size > 0) { |
nothing calls this directly
no test coverage detected