From c38de79c7817f0f904613ba93587f75841780643 Mon Sep 17 00:00:00 2001 From: Albert Franzi <3647015+afranzi@users.noreply.github.com> Date: Fri, 9 Oct 2026 12:20:13 +0200 Subject: [PATCH] fix(core): stop waitAndRetry polling once retries are exhausted After the last retry, performCheck resolved with a final predicate call but did not return, so it kept polling every sleepTime forever. When the predicate is a network call (deleteQueue confirmation), the leftover loop outlives the caller and its rejections surface as unhandled once the endpoint goes away. Co-Authored-By: Claude Opus 5.5 (1M context) --- packages/core/lib/utils/waitUtils.ts | 1 + packages/core/test/utils/waitUtils.spec.ts | 57 ++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 packages/core/test/utils/waitUtils.spec.ts diff --git a/packages/core/lib/utils/waitUtils.ts b/packages/core/lib/utils/waitUtils.ts index 9678f4602..4e3d9cf00 100644 --- a/packages/core/lib/utils/waitUtils.ts +++ b/packages/core/lib/utils/waitUtils.ts @@ -8,6 +8,7 @@ export const waitAndRetry = ( function performCheck() { if (maxRetryCount !== 0 && retryCount > maxRetryCount) { resolve(predicateFn()) + return } Promise.resolve() .then(() => { diff --git a/packages/core/test/utils/waitUtils.spec.ts b/packages/core/test/utils/waitUtils.spec.ts new file mode 100644 index 000000000..821d838a8 --- /dev/null +++ b/packages/core/test/utils/waitUtils.spec.ts @@ -0,0 +1,57 @@ +import { setTimeout } from 'node:timers/promises' +import { describe, expect, it } from 'vitest' + +import { waitAndRetry } from '../../lib/utils/waitUtils.ts' + +describe('waitUtils', () => { + describe('waitAndRetry', () => { + it('resolves with the predicate result once it is truthy', async () => { + let calls = 0 + + const result = await waitAndRetry(() => { + calls++ + return calls === 3 ? 'done' : undefined + }, 1) + + expect(result).toBe('done') + expect(calls).toBe(3) + }) + + it('stops polling after the retries are exhausted', async () => { + let calls = 0 + + const result = await waitAndRetry( + () => { + calls++ + return false + }, + 1, + 3, + ) + + expect(result).toBe(false) + // 4 checks that count as retries, plus the final check whose result is returned + expect(calls).toBe(5) + + await setTimeout(50) + expect(calls).toBe(5) + }) + + it('rejects when the final check after the retries are exhausted rejects', async () => { + let calls = 0 + + const promise = waitAndRetry( + () => { + calls++ + return calls > 4 ? Promise.reject(new Error('unreachable')) : Promise.resolve(false) + }, + 1, + 3, + ) + + await expect(promise).rejects.toThrow('unreachable') + await setTimeout(50) + expect(calls).toBe(5) + }) + }) +})