( projectKey: string, abortSignal: AbortSignal | undefined, fn: () => Promise<void> )
| 160 | const sharedProjectSyncTails = new Map<string, Promise<void>>(); |
| 161 | |
| 162 | async function enqueueProjectSync( |
| 163 | projectKey: string, |
| 164 | abortSignal: AbortSignal | undefined, |
| 165 | fn: () => Promise<void> |
| 166 | ): Promise<void> { |
| 167 | const previous = sharedProjectSyncTails.get(projectKey) ?? Promise.resolve(); |
| 168 | let releaseCurrent: (() => void) | undefined; |
| 169 | const current = new Promise<void>((resolve) => { |
| 170 | releaseCurrent = resolve; |
| 171 | }); |
| 172 | const tail = previous.then( |
| 173 | () => current, |
| 174 | () => current |
| 175 | ); |
| 176 | sharedProjectSyncTails.set(projectKey, tail); |
| 177 | void tail.finally(() => { |
| 178 | if (sharedProjectSyncTails.get(projectKey) === tail) { |
| 179 | sharedProjectSyncTails.delete(projectKey); |
| 180 | } |
| 181 | }); |
| 182 | |
| 183 | let onAbort: (() => void) | undefined; |
| 184 | const waitForPrevious = previous.catch(() => undefined); |
| 185 | const waitForTurn = abortSignal |
| 186 | ? Promise.race([ |
| 187 | waitForPrevious, |
| 188 | new Promise<never>((_, reject) => { |
| 189 | onAbort = () => reject(new Error(OPERATION_ABORTED_ERROR)); |
| 190 | if (abortSignal.aborted) { |
| 191 | onAbort(); |
| 192 | return; |
| 193 | } |
| 194 | abortSignal.addEventListener("abort", onAbort, { once: true }); |
| 195 | }), |
| 196 | ]) |
| 197 | : waitForPrevious; |
| 198 | |
| 199 | try { |
| 200 | await waitForTurn; |
| 201 | if (abortSignal?.aborted) { |
| 202 | throw new Error(OPERATION_ABORTED_ERROR); |
| 203 | } |
| 204 | await fn(); |
| 205 | } finally { |
| 206 | if (onAbort) { |
| 207 | abortSignal?.removeEventListener("abort", onAbort); |
| 208 | } |
| 209 | releaseCurrent?.(); |
| 210 | } |
| 211 | } |
| 212 | |
| 213 | function isGitConfigLockConflict(message: string): boolean { |
| 214 | return /could not lock config file/i.test(message); |
no test coverage detected