From c7626cff45068ab1a6589446960ff3492cc3e149 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 08:57:01 +0000 Subject: [PATCH 1/4] Initial plan From 54cbfee6ca840cd2b7d9dda312ed07c97bde54e4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 09:15:02 +0000 Subject: [PATCH 2/4] Add AbortSignal support to createTimeout utility Agent-Logs-Url: https://github.com/mediamonks/react-kit/sessions/757121d0-6651-4aa3-b4d3-e6cbdf8a6300 --- src/utils/createTimeout/createTimeout.mdx | 28 +++++++++++++++-- src/utils/createTimeout/createTimeout.test.ts | 31 +++++++++++++++++++ src/utils/createTimeout/createTimeout.ts | 30 ++++++++++++++++-- 3 files changed, 84 insertions(+), 5 deletions(-) diff --git a/src/utils/createTimeout/createTimeout.mdx b/src/utils/createTimeout/createTimeout.mdx index 4a039623..261c7568 100644 --- a/src/utils/createTimeout/createTimeout.mdx +++ b/src/utils/createTimeout/createTimeout.mdx @@ -9,12 +9,18 @@ Creates a promise that resolves after given timeout. ## Reference ```ts -createTimeout(ms = 0): Promise +createTimeout(ms = 0, options?: { signal?: AbortSignal }): Promise ``` +### Parameters + +- `ms` - The number of milliseconds to wait before resolving. Defaults to `0`. +- `options.signal` - An optional `AbortSignal` to cancel the timeout. When aborted, the promise + rejects with the signal's reason. + ### Returns -Promise that resolves after given timeout. +Promise that resolves after given timeout, or rejects if the provided `signal` is aborted. ## Usage @@ -26,6 +32,24 @@ await createTimeout(1000); console.log("I'm called after 1 second"); ``` +Using with an `AbortSignal` to cancel the timeout + +```ts +const controller = new AbortController(); + +// Abort the timeout after 500ms +setTimeout(() => { + controller.abort(); +}, 500); + +try { + await createTimeout(2000, { signal: controller.signal }); + console.log('Timeout completed'); +} catch { + console.log('Timeout was cancelled'); +} +``` + Using `Promise.then` ```ts diff --git a/src/utils/createTimeout/createTimeout.test.ts b/src/utils/createTimeout/createTimeout.test.ts index c4731a80..76dbf57d 100644 --- a/src/utils/createTimeout/createTimeout.test.ts +++ b/src/utils/createTimeout/createTimeout.test.ts @@ -27,4 +27,35 @@ describe('createTimeout', () => { expect(currentTime - startTime >= timeout).toBe(true); }); + + it('should reject immediately when signal is already aborted', async () => { + const controller = new AbortController(); + controller.abort(); + + await expect(createTimeout(100, { signal: controller.signal })).rejects.toThrow(); + }); + + it('should reject when signal is aborted before timeout', async () => { + const controller = new AbortController(); + + const promise = createTimeout(1000, { signal: controller.signal }); + + controller.abort(); + + await expect(promise).rejects.toThrow(); + }); + + it('should resolve normally when no signal is provided', async () => { + await expect(createTimeout(0)).resolves.toBeUndefined(); + }); + + it('should reject with the abort reason', async () => { + const controller = new AbortController(); + const reason = new Error('custom abort reason'); + controller.abort(reason); + + await expect(createTimeout(100, { signal: controller.signal })).rejects.toThrow( + 'custom abort reason', + ); + }); }); diff --git a/src/utils/createTimeout/createTimeout.ts b/src/utils/createTimeout/createTimeout.ts index 16cf73e6..8f10f00c 100644 --- a/src/utils/createTimeout/createTimeout.ts +++ b/src/utils/createTimeout/createTimeout.ts @@ -1,9 +1,33 @@ /** * Returns a promise that resolves after given amount of time. Use the default * value of `0` to wait for the next tick. + * + * @param ms - The number of milliseconds to wait before resolving. Defaults to `0`. + * @param options - Optional options object. + * @param options.signal - An optional AbortSignal to cancel the timeout. When aborted, the promise rejects with the signal's reason. */ -export function createTimeout(ms = 0): Promise { - return new Promise((resolve) => { - setTimeout(resolve, ms); +export function createTimeout(ms = 0, { signal }: { signal?: AbortSignal } = {}): Promise { + return new Promise((resolve, reject) => { + if (signal?.aborted) { + reject(signal.reason); + return; + } + + if (signal === undefined) { + setTimeout(resolve, ms); + return; + } + + const onAbort = (): void => { + clearTimeout(timeoutId); + reject(signal.reason); + }; + + const timeoutId = setTimeout(() => { + signal.removeEventListener('abort', onAbort); + resolve(); + }, ms); + + signal.addEventListener('abort', onAbort, { once: true }); }); } From fee35387d2d97b6ad827d499ff796780405f9765 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 10:13:25 +0000 Subject: [PATCH 3/4] Pass abort signal to createTimeout calls in TransitionPresence Agent-Logs-Url: https://github.com/mediamonks/react-kit/sessions/268ea3b9-4071-4c02-8e91-43b6a88c8bbf Co-authored-by: ThaNarie <1449580+ThaNarie@users.noreply.github.com> --- .../TransitionPresence/TransitionPresence.tsx | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/lifecycle/components/TransitionPresence/TransitionPresence.tsx b/src/lifecycle/components/TransitionPresence/TransitionPresence.tsx index 6fbd2b28..8d733f33 100644 --- a/src/lifecycle/components/TransitionPresence/TransitionPresence.tsx +++ b/src/lifecycle/components/TransitionPresence/TransitionPresence.tsx @@ -71,24 +71,30 @@ export function TransitionPresence({ const abortController = new AbortController(); (async (): Promise => { - onPreviousChildrenUnmountingRef.current?.(previousChildren, children); + try { + onPreviousChildrenUnmountingRef.current?.(previousChildren, children); - // Defer children update for before unmount lifecycle - await beforeUnmountPreviousChildren(abortController.signal); + // Defer children update for before unmount lifecycle + await beforeUnmountPreviousChildren(abortController.signal); - setPreviousChildren(null); + setPreviousChildren(null); - // Wait a tick after removing previous children to make sure new children - // are re-initialized - await createTimeout(); + // Wait a tick after removing previous children to make sure new children + // are re-initialized + await createTimeout(0, { signal: abortController.signal }); - onPreviousChildrenUnmountedRef.current?.(previousChildren, children); + onPreviousChildrenUnmountedRef.current?.(previousChildren, children); - // Set new children - setPreviousChildren(children); - await createTimeout(); + // Set new children + setPreviousChildren(children); + await createTimeout(0, { signal: abortController.signal }); - onChildrenMountedRef.current?.(previousChildren, children); + onChildrenMountedRef.current?.(previousChildren, children); + } catch (error) { + if (!abortController.signal.aborted) { + throw new Error(`Unexpected error in TransitionPresence transition: ${error}`); + } + } })(); return () => { From e2fe1212e5cc1fc33b4d93aa7ec74709023a5690 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 11:25:11 +0000 Subject: [PATCH 4/4] Tighten catch to only swallow the exact abort reason in TransitionPresence Agent-Logs-Url: https://github.com/mediamonks/react-kit/sessions/253df10c-79a1-4d0c-943a-7d752ef07d7b Co-authored-by: ThaNarie <1449580+ThaNarie@users.noreply.github.com> --- .../components/TransitionPresence/TransitionPresence.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lifecycle/components/TransitionPresence/TransitionPresence.tsx b/src/lifecycle/components/TransitionPresence/TransitionPresence.tsx index 8d733f33..83d787d0 100644 --- a/src/lifecycle/components/TransitionPresence/TransitionPresence.tsx +++ b/src/lifecycle/components/TransitionPresence/TransitionPresence.tsx @@ -91,8 +91,8 @@ export function TransitionPresence({ onChildrenMountedRef.current?.(previousChildren, children); } catch (error) { - if (!abortController.signal.aborted) { - throw new Error(`Unexpected error in TransitionPresence transition: ${error}`); + if (error !== abortController.signal.reason) { + throw new Error(`Unexpected error in TransitionPresence transition`, { cause: error }); } } })();