-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
feat(nuxt): add abortController option to useAsyncData
#32531
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
|
WalkthroughReplaces manual in-flight promise flags with AbortController/AbortSignal-based cancellation across asyncData and useFetch. AsyncData handler signature becomes (nuxtApp, { signal }) => Promise, AsyncDataOptions and execute options gain timeout and signal, and CreatedAsyncData entries (and Nuxt app _asyncData) now include an optional _abortController. useFetch types are adjusted to omit timeout from ComputedFetchOptions while exposing a public timeout option; fetch calls now accept and propagate an external signal (removing internal per-request controller/timeout scaffolding). Documentation and tests (including /api/sleep) updated to cover timeout and abort behaviours. Estimated code review effort๐ฏ 4 (Complex) | โฑ๏ธ ~45 minutes Pre-merge checks and finishing touchesโ Failed checks (2 warnings)
โ Passed checks (3 passed)
โจ Finishing touches
๐งช Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
๐ Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
๐ Files selected for processing (2)
packages/nuxt/src/app/composables/fetch.ts(1 hunks)test/nuxt/composables.test.ts(3 hunks)
๐งฐ Additional context used
๐ Path-based instructions (3)
`**/*.{ts,tsx}`: Follow standard TypeScript conventions and best practices
**/*.{ts,tsx}: Follow standard TypeScript conventions and best practices
๐ Source: CodeRabbit Inference Engine (.github/copilot-instructions.md)
List of files the instruction was applied to:
packages/nuxt/src/app/composables/fetch.tstest/nuxt/composables.test.ts
`**/*.{ts,tsx,vue}`: Use clear, descriptive variable and function names Add comm...
**/*.{ts,tsx,vue}: Use clear, descriptive variable and function names
Add comments only to explain complex logic or non-obvious implementations
Keep functions focused and manageable (generally under 50 lines), and extract complex logic into separate domain-specific files
Remove code that is not used or needed
Use error handling patterns consistently
๐ Source: CodeRabbit Inference Engine (.github/copilot-instructions.md)
List of files the instruction was applied to:
packages/nuxt/src/app/composables/fetch.tstest/nuxt/composables.test.ts
`**/*.{test,spec}.{ts,tsx}`: Write unit tests for core functionality using vitest
**/*.{test,spec}.{ts,tsx}: Write unit tests for core functionality using vitest
๐ Source: CodeRabbit Inference Engine (.github/copilot-instructions.md)
List of files the instruction was applied to:
test/nuxt/composables.test.ts
๐ง Learnings (1)
test/nuxt/composables.test.ts (6)
Learnt from: CR
PR: nuxt/nuxt#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-06-30T13:48:28.134Z
Learning: Applies to **/*.{test,spec}.{ts,tsx} : Write unit tests for core functionality using vitest
Learnt from: CR
PR: nuxt/nuxt#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-06-30T13:48:28.134Z
Learning: Applies to **/*.e2e.{ts,js} : Write end-to-end tests using Playwright and @nuxt/test-utils
Learnt from: huang-julien
PR: nuxt/nuxt#29366
File: packages/nuxt/src/app/components/nuxt-root.vue:16-19
Timestamp: 2024-12-12T12:36:34.871Z
Learning: In `packages/nuxt/src/app/components/nuxt-root.vue`, when optimizing bundle size by conditionally importing components based on route metadata, prefer using inline conditional imports like:
```js
const IsolatedPage = route?.meta?.isolate ? defineAsyncComponent(() => import('#build/isolated-page.mjs')) : null
```
instead of wrapping the import in a computed property or importing the component unconditionally.
Learnt from: CR
PR: nuxt/nuxt#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-06-30T13:48:28.134Z
Learning: Applies to **/*.vue : Use `<script setup lang="ts">` and the composition API when creating Vue components
Learnt from: GalacticHypernova
PR: nuxt/nuxt#26468
File: packages/nuxt/src/components/plugins/loader.ts:24-24
Timestamp: 2024-11-05T15:22:54.759Z
Learning: In `packages/nuxt/src/components/plugins/loader.ts`, the references to `resolve` and `distDir` are legacy code from before Nuxt used the new unplugin VFS and will be removed.
Learnt from: CR
PR: nuxt/nuxt#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-06-30T13:48:28.134Z
Learning: Applies to **/*.{ts,tsx,vue} : Use error handling patterns consistently
๐ช GitHub Check: code
test/nuxt/composables.test.ts
[failure] 966-966:
Async arrow function has no 'await' expression
๐ช GitHub Actions: autofix.ci
test/nuxt/composables.test.ts
[error] 966-966: ESLint: Async arrow function has no 'await' expression (require-await)
โฐ Context from checks skipped due to timeout of 90000ms (20)
- GitHub Check: test-fixtures (windows-latest, built, vite, async, manifest-off, json, 20)
- GitHub Check: test-fixtures (windows-latest, dev, vite, default, manifest-on, json, 20)
- GitHub Check: test-fixtures (windows-latest, built, vite, async, manifest-on, json, 20)
- GitHub Check: test-fixtures (windows-latest, dev, vite, async, manifest-on, json, 20)
- GitHub Check: test-fixtures (ubuntu-latest, built, vite, async, manifest-on, js, 20)
- GitHub Check: test-fixtures (ubuntu-latest, built, webpack, async, manifest-on, json, 20)
- GitHub Check: test-fixtures (ubuntu-latest, built, rspack, default, manifest-on, json, 20)
- GitHub Check: test-fixtures (windows-latest, dev, vite, async, manifest-off, json, 20)
- GitHub Check: test-fixtures (ubuntu-latest, built, vite, default, manifest-off, json, 20)
- GitHub Check: test-fixtures (ubuntu-latest, built, vite, default, manifest-on, json, 20)
- GitHub Check: test-fixtures (ubuntu-latest, built, rspack, async, manifest-on, json, 20)
- GitHub Check: test-fixtures (ubuntu-latest, dev, vite, async, manifest-on, json, 20)
- GitHub Check: test-fixtures (ubuntu-latest, dev, vite, default, manifest-off, json, 20)
- GitHub Check: test-fixtures (ubuntu-latest, dev, vite, async, manifest-off, json, 20)
- GitHub Check: test-fixtures (ubuntu-latest, dev, vite, default, manifest-on, json, 20)
- GitHub Check: test-fixtures (ubuntu-latest, dev, vite, async, manifest-on, js, 20)
- GitHub Check: release-pr
- GitHub Check: test-benchmark
- GitHub Check: typecheck (windows-latest, bundler)
- GitHub Check: typecheck (ubuntu-latest, bundler)
๐ Additional comments (2)
packages/nuxt/src/app/composables/fetch.ts (1)
179-184: LGTM! Clean implementation of abort-on-clear functionality.The implementation correctly preserves the original
clearbehaviour whilst adding the requested abort functionality. The use of optional chaining and a descriptiveDOMExceptionmessage enhances robustness and debugging experience.test/nuxt/composables.test.ts (1)
35-45: Good test infrastructure setup.The new
/api/sleependpoint and global stub reset inbeforeEachprovide proper test isolation and simulation capabilities for the abort functionality testing.
@nuxt/kit
nuxt
@nuxt/rspack-builder
@nuxt/schema
@nuxt/vite-builder
@nuxt/webpack-builder
commit: |
|
maybe we can add controller as an option to useAsyncData instead, or implement in useAsyncData rather than useFetch? |
CodSpeed Performance ReportMerging #32531 will not alter performanceComparing Summary
Footnotes |
useAsyncData
useAsyncDatauseAsyncData
| execute: (...args) => nuxtApp._asyncData[key.value]!.execute(...args), | ||
| clear: () => clearNuxtDataByKey(nuxtApp, key.value), | ||
| clear: () => { | ||
| options.abortController?.abort?.(new DOMException('Request aborted as the async data was cleared.', 'AbortError')) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think if an abortController is triggered, then we should also track this and avoid doing anything with the finished result of an asyncData. so it shouldn't update the data/status, just as if it were cancelled.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
โป๏ธ Duplicate comments (2)
packages/nuxt/src/app/composables/asyncData.ts (2)
381-387: clear(): always abort and reset controller (donโt depend on_asyncDataPromises)Current logic skips abort when the promise reference is missing and keeps an aborted controller around.
Apply:
- clear: () => { - if (nuxtApp._asyncDataPromises[key.value]) { - if (nuxtApp._asyncData[key.value]?._abortController) { - nuxtApp._asyncData[key.value]?._abortController?.abort(new DOMException('AsyncData aborted by user.', 'AbortError')) - } - } - clearNuxtDataByKey(nuxtApp, key.value) - }, + clear: () => { + const entry = nuxtApp._asyncData[key.value] + if (entry?._abortController) { + try { + entry._abortController.abort(toAbortError('AsyncData aborted by user.')) + } finally { + entry._abortController = undefined + } + } + clearNuxtDataByKey(nuxtApp, key.value) + },
711-719: Treat any AbortError as cancellation: donโt mutateerror/statuson cancel/timeout/external signalCurrently only internal aborts are ignored; external signal/timeout still set
error/status.Apply:
// If the promise was replaced by another one, we do not update the asyncData if (nuxtApp._asyncDataPromises[key] && nuxtApp._asyncDataPromises[key] !== promise) { return } - // If the asyncData was explicitly aborted internally (dedupe or clear), we do not update the asyncData - if (asyncData._abortController?.signal.aborted) { - return - } + // If cancelled for any reason (internal, external or timeout), do not update + if (error?.name === 'AbortError' || (typeof DOMException !== 'undefined' && error instanceof DOMException && error.name === 'AbortError')) { + return + }
๐งน Nitpick comments (4)
packages/nuxt/src/app/composables/asyncData.ts (2)
103-107: Timeout API: clarify/normalise inputsDocument or coerce negative/NaN timeouts to
undefinedso behaviour is predictable across code paths.You can normalise at use-site (see patch below on Lines 675โ683).
Also applies to: 119-125
667-671: Use a portable AbortError factory instead ofnew DOMException(...)
DOMExceptionis not guaranteed everywhere. Use a helper that falls back toError.Apply:
- if (asyncData._abortController) { - asyncData._abortController.abort(new DOMException('AsyncData request cancelled by deduplication', 'AbortError')) - } + if (asyncData._abortController) { + asyncData._abortController.abort(toAbortError('AsyncData request cancelled by deduplication')) + }docs/3.api/2.composables/use-async-data.md (2)
21-26: Consider adding a small example that uses{ signal }Show how to forward the signal to
$fetchfor cancellation.Example:
<script setup lang="ts"> const { data } = await useAsyncData('mountains', (_nuxt, { signal }) => $fetch('https://api.nuxtjs.dev/mountains', { signal }) ) </script>
150-164: Clarify cancellation semantics in Return ValuesExplicitly state that aborted requests donโt set
status: 'error'and donโt mutateerror/data.Apply:
- `clear`: a function that can be used to set `data` to `undefined` (or the value of `options.default()` if provided), set `error` to `undefined`, set `status` to `idle`, and mark any currently pending requests as cancelled. + `clear`: a function that can be used to set `data` to `undefined` (or the value of `options.default()` if provided), set `error` to `undefined`, set `status` to `idle`, and mark any currently pending requests as cancelled. + + Cancellation semantics: if a request is aborted (via `clear`, deduplication, an external `AbortSignal`, or a timeout), `data` and `error` are left unchanged and `status` is not set to `'error'`.
๐ Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
๐ก Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
๐ Files selected for processing (2)
docs/3.api/2.composables/use-async-data.md(4 hunks)packages/nuxt/src/app/composables/asyncData.ts(18 hunks)
๐งฐ Additional context used
๐ Path-based instructions (1)
**/*.{ts,tsx}
๐ CodeRabbit inference engine (.github/copilot-instructions.md)
Follow standard TypeScript conventions and best practices
Files:
packages/nuxt/src/app/composables/asyncData.ts
๐ช LanguageTool
docs/3.api/2.composables/use-async-data.md
[uncategorized] ~105-~105: The preposition โofโ seems more likely in this position.
Context: ...ending request - timeout - a number in milliseconds to wait before timing out ...
(AI_HYDRA_LEO_REPLACE_IN_OF)
โฐ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: build
- GitHub Check: code
๐ Additional comments (6)
packages/nuxt/src/app/composables/asyncData.ts (4)
603-603: Per-entry_abortControllershape looks goodThis enables precise cancellation per key and aligns with the new API.
616-624: SSR prerender cache: options propagation preservedWrapping
_handlerwithrunWithContextwhile forwarding{ signal }is correct.
772-779: Hash inputs remain correctIncluding
getCachedDatain the hash is appropriate to enforce option consistency.
74-75: Remove review comment โ no customgetCachedDataproviders to update
The library itself only defines the defaultgetCachedDataand its internal uses; there are no existing overrides or custom providers within this repo that need signature changes.Likely an incorrect or invalid review comment.
docs/3.api/2.composables/use-async-data.md (2)
105-105: Timeout option doc looks goodClear and aligned with the code changes.
172-215: Types section updates are consistent with runtime changesHandler overloads,
timeout, andsignalare correctly reflected.
| export type AsyncDataHandler<ResT> = (nuxtApp: NuxtApp, options: { signal: AbortSignal }) => Promise<ResT> | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
๐ก Verification agent
๐งฉ Analysis chain
Breaking change: handler now receives { signal } โ verify all call sites and wrappers
Ensure every internal wrapper and userland example that passes a handler compiles with the new (nuxtApp, { signal }) signature. Consider a brief migration note.
Run to find potential offenders (handlers with 0โ1 params):
Also applies to: 153-166, 181-195, 425-437, 453-466
๐ Script executed:
#!/bin/bash
rg -nP --type=ts -C2 '\buse(Lazy)?AsyncData\s*\(\s*(?:[^,]+,)?\s*\(([^,)]+)\)\s*=>'Length of output: 85
๐ Script executed:
#!/bin/bash
set -euo pipefail
# 1. List all useAsyncData and useLazyAsyncData call sites with surrounding context
rg -n --type=ts 'use(Lazy)?AsyncData' -C2
# 2. Highlight handlers defined with a single parameter (potentially missing { signal })
rg -nP --type=ts -C2 'use(Lazy)?AsyncData\(\s*[^,]+,\s*\(\s*[^,)]+\s*\)\s*=>'Length of output: 65342
๐ Script executed:
#!/bin/bash
set -euo pipefail
# Zero-arg handler call sites for useAsyncData and useLazyAsyncData
rg -nP --type=ts -C2 'use(Lazy)?AsyncData\(\s*(?:[^,]+,\s*)?\(\)\s*=>' || true
# Single-arg handler call sites (potentially missing { signal })
rg -nP --type=ts -C2 'use(Lazy)?AsyncData\(\s*(?:[^,]+,\s*)?\(\s*[^,)]+\s*\)\s*=>' || trueLength of output: 25717
Update AsyncData handlers to accept signal
All calls to useAsyncData/useLazyAsyncData must now pass a handler with two parametersโ(nuxtApp, { signal })โinstead of a zero- or single-arg function.
- packages/nuxt/src/app/composables/component.ts:30 โ change
() => โฆto(_, { signal }) => โฆ. - test/nuxt/use-async-data.test.ts and fixtures (e.g. test/fixtures/basic/app/composables/asyncDataTests.ts) include numerous
() => โฆor(arg) => โฆhandlers; update them to(_, { signal }) => โฆ.
Add a brief migration note in the docs/release notes.
๐ค Prompt for AI Agents
packages/nuxt/src/app/composables/asyncData.ts around lines 21-22, the AsyncData
handler signature must require the second options parameter with a signal;
ensure the exported type is exactly (nuxtApp: NuxtApp, options: { signal:
AbortSignal }) => Promise<ResT> (or make the options parameter non-optional if
currently optional), then update all call sites to match: change component
handler at packages/nuxt/src/app/composables/component.ts line ~30 from () =>
... to (_, { signal }) => ... and update tests/fixtures
(test/nuxt/use-async-data.test.ts and test/fixtures/**/asyncDataTests.ts) to use
(_, { signal }) => ... for every useAsyncData/useLazyAsyncData handler; finally
add a short migration note to docs/release notes describing the new handler
signature and example usage.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
โป๏ธ Duplicate comments (2)
packages/nuxt/src/app/composables/asyncData.ts (2)
673-681: Use a ponyfill to merge signals, short-circuit alreadyโaborted, and listen once- const timeout = opts.timeout ?? options.timeout - const mergedSignal = AbortSignal.any([asyncData._abortController?.signal, opts?.signal, typeof timeout === 'number' ? AbortSignal.timeout(timeout) : undefined].filter((s): s is NonNullable<typeof s> => Boolean(s))) - mergedSignal.addEventListener('abort', (event) => { - const reason = (event.target as any)?.reason ?? mergedSignal.reason - reject(reason instanceof Error ? reason : new DOMException(reason, 'AbortError')) - }) - - return Promise.resolve(handler(nuxtApp, { signal: mergedSignal })).then(resolve, reject) + const rawTimeout = opts.timeout ?? options.timeout + const timeout = (typeof rawTimeout === 'number' && isFinite(rawTimeout) && rawTimeout >= 0) ? rawTimeout : undefined + const mergedSignal = mergeAbortSignals( + [asyncData._abortController?.signal, opts?.signal], + timeout, + ) + if (mergedSignal.aborted) { + reject(toAbortError(mergedSignal.reason)) + return + } + mergedSignal.addEventListener('abort', () => { + reject(toAbortError(mergedSignal.reason)) + }, { once: true }) + + Promise.resolve(handler(nuxtApp, { signal: mergedSignal })).then(resolve, reject)Add these helpers in this module (outside the selected range):
function toAbortError (reason?: unknown): Error { try { return reason instanceof Error ? reason : new DOMException(String(reason ?? 'Aborted'), 'AbortError') } catch { return reason instanceof Error ? reason : new Error(String(reason ?? 'Aborted')) } } function mergeAbortSignals (signals: Array<AbortSignal | null | undefined>, timeout?: number): AbortSignal { const list: AbortSignal[] = signals.filter(Boolean) as AbortSignal[] if (typeof timeout === 'number') { const t = (AbortSignal as any).timeout?.(timeout) if (t) list.push(t) } const AS: any = AbortSignal as any if (AS.any) return AS.any(list) const controller = new AbortController() const onAbort = (sig: AbortSignal) => () => { controller.abort(sig.reason ?? toAbortError()) list.forEach(s => s.removeEventListener?.('abort', onAbort(s))) } for (const s of list) { if (s.aborted) return s s.addEventListener?.('abort', onAbort(s), { once: true }) } return controller.signal }
709-723: Treat any AbortError (internal/external/timeout) as cancellation; donโt mutate data/error- // If the asyncData was explicitly aborted internally (dedupe or clear), we do not update the asyncData - if (asyncData._abortController?.signal.aborted) { - return - } - - // if the asyncData was explicitly aborted by user, we set it back to idle state - if (typeof DOMException !== 'undefined' && error instanceof DOMException && error.name === 'AbortError') { - asyncData.status.value = 'idle' - return - } + // Treat any AbortError (internal, external, timeout) as cancellation: do nothing except reset status. + if ( + (error && typeof error === 'object' && (error as any).name === 'AbortError') || + (typeof DOMException !== 'undefined' && error instanceof DOMException && error.name === 'AbortError') + ) { + asyncData.status.value = 'idle' + return + }
๐งน Nitpick comments (1)
packages/nuxt/src/app/composables/asyncData.ts (1)
103-107: Timeout/signal options added โ validate inputs and define precedenceGuard against negative/NaN timeouts and clarify in JSDoc that
opts.{signal,timeout}overrideoptions.{timeout}. The input validation is addressed in my abort-merge suggestion below.Also applies to: 121-125
๐ Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
๐ก Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
๐ Files selected for processing (2)
packages/nuxt/src/app/composables/asyncData.ts(18 hunks)test/nuxt/use-async-data.test.ts(2 hunks)
๐ง Files skipped from review as they are similar to previous changes (1)
- test/nuxt/use-async-data.test.ts
๐งฐ Additional context used
๐ Path-based instructions (1)
**/*.{ts,tsx}
๐ CodeRabbit inference engine (.github/copilot-instructions.md)
Follow standard TypeScript conventions and best practices
Files:
packages/nuxt/src/app/composables/asyncData.ts
โฐ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: build
- GitHub Check: link-checker
- GitHub Check: code
๐ Additional comments (3)
packages/nuxt/src/app/composables/asyncData.ts (3)
21-21: Breaking: AsyncDataHandler now requires{ signal }โ verify all call sites/tests/docsEnsure every
useAsyncData/useLazyAsyncDatahandler uses(nuxtApp, { signal }) => โฆand docs/migration notes reflect this.Also applies to: 153-165, 181-194, 206-206, 425-437, 453-466, 477-477
615-626: Prerender cache + per-call cancellation โ confirm intended semanticsShared prerender promise ignores later callersโ distinct
signal/timeout. If a later call times out/aborts, it cannot cancel a cached in-flight promise. Confirm this trade-off is acceptable for prerender.
776-784: Hashing update to new handler type โ LGTM
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
๐งน Nitpick comments (1)
packages/nuxt/src/app/composables/asyncData.ts (1)
668-671: Consider safe DOMException construction.
DOMExceptionmay not be available in all environments (older Node versions, some workers). Consider wrapping in a try-catch or checking availability.if (asyncData._abortController) { - asyncData._abortController.abort(new DOMException('AsyncData request cancelled by deduplication', 'AbortError')) + try { + asyncData._abortController.abort(new DOMException('AsyncData request cancelled by deduplication', 'AbortError')) + } catch { + const err = new Error('AsyncData request cancelled by deduplication') + err.name = 'AbortError' + asyncData._abortController.abort(err) + } }
๐ Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
๐ Files selected for processing (5)
docs/3.api/2.composables/use-async-data.md(4 hunks)docs/3.api/2.composables/use-fetch.md(3 hunks)packages/nuxt/src/app/composables/asyncData.ts(18 hunks)packages/nuxt/src/app/composables/fetch.ts(5 hunks)packages/nuxt/src/app/nuxt.ts(1 hunks)
๐ง Files skipped from review as they are similar to previous changes (3)
- packages/nuxt/src/app/nuxt.ts
- docs/3.api/2.composables/use-fetch.md
- packages/nuxt/src/app/composables/fetch.ts
๐งฐ Additional context used
๐ Path-based instructions (1)
**/*.{ts,tsx}
๐ CodeRabbit inference engine (.github/copilot-instructions.md)
Follow standard TypeScript conventions and best practices
Files:
packages/nuxt/src/app/composables/asyncData.ts
๐งฌ Code graph analysis (1)
packages/nuxt/src/app/composables/asyncData.ts (1)
packages/nuxt/src/app/composables/error.ts (1)
NuxtError(15-17)
โฐ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: build
- GitHub Check: code
๐ Additional comments (7)
packages/nuxt/src/app/composables/asyncData.ts (3)
20-20: Handler signature breaking change is correctly enforced.The new
AsyncDataHandler<ResT>type properly requires the{ signal: AbortSignal }parameter. This aligns with the PR objectives for abort controller support.
102-105: Timeout option correctly added to AsyncDataOptions.The
timeoutoption is well-documented and integrates cleanly with the abort signal mechanism.
711-725: Abort handling logic is correct.The error handler properly distinguishes between superseded requests, internal aborts, and user aborts. Setting status to 'idle' for AbortError is the right semantic choice.
docs/3.api/2.composables/use-async-data.md (4)
105-105: Timeout option is well documented.Clear explanation of the timeout behaviour with appropriate defaults.
173-181: Handler signature correctly documents the new { signal } parameter.The type signatures accurately reflect the breaking change requiring the options parameter with signal.
194-195: AsyncDataOptions type signature is accurate.The timeout option is correctly shown in the type definition.
213-215: AsyncDataExecuteOptions correctly documents signal and timeout.Both new options are properly typed and documented for the execute method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
โป๏ธ Duplicate comments (2)
packages/nuxt/src/app/composables/asyncData.ts (2)
413-420: Reset_abortControllerafter aborting to prevent immediate cancellation of subsequent requests.Currently, the aborted controller remains attached to the asyncData entry. If a new request is initiated after calling
clear(), it would be immediately aborted by the stale signal.Based on learnings
Apply this diff:
clear: () => { - if (nuxtApp._asyncDataPromises[key.value]) { - if (nuxtApp._asyncData[key.value]?._abortController) { - nuxtApp._asyncData[key.value]?._abortController?.abort(new DOMException('AsyncData aborted by user.', 'AbortError')) - } - } + const entry = nuxtApp._asyncData[key.value] + if (entry?._abortController) { + try { + entry._abortController.abort(new DOMException('AsyncData aborted by user.', 'AbortError')) + } finally { + entry._abortController = undefined + } + } clearNuxtDataByKey(nuxtApp, key.value) },
703-713: Add polyfill forAbortSignal.any/timeout, fix type mismatch, and usemergedSignal.reason.Multiple issues require attention:
- Type mismatch (line 703): Variable typed as
Promise<ResT | void>but constructor usesPromise<ResT>- Missing polyfills (line 707):
AbortSignal.anyandAbortSignal.timeoutare not available in Node < 19.7 and older browsers- Unreliable reason access (line 709):
event.target?.reasonis not type-safe; usemergedSignal.reasoninstead- No early abort check: Should reject immediately if the merged signal is already aborted
Based on learnings
Apply this diff:
- const promise: Promise<ResT | void> = new Promise<ResT>( + const promise: Promise<ResT | void> = new Promise<ResT | void>( (resolve, reject) => { try { const timeout = opts.timeout ?? options.timeout - const mergedSignal = AbortSignal.any([asyncData._abortController?.signal, opts?.signal, typeof timeout === 'number' ? AbortSignal.timeout(timeout) : undefined].filter((s): s is NonNullable<typeof s> => Boolean(s))) - mergedSignal.addEventListener('abort', (event) => { - const reason = (event.target as any)?.reason ?? mergedSignal.reason + const mergedSignal = mergeAbortSignals( + [asyncData._abortController?.signal, opts?.signal], + typeof timeout === 'number' && timeout >= 0 ? timeout : undefined + ) + + if (mergedSignal.aborted) { + const reason = mergedSignal.reason + reject(reason instanceof Error ? reason : toAbortError(reason)) + return + } + + mergedSignal.addEventListener('abort', () => { + const reason = mergedSignal.reason reject(reason instanceof Error ? reason : new DOMException(reason, 'AbortError')) - }) + }, { once: true }) - return Promise.resolve(handler(nuxtApp, { signal: mergedSignal })).then(resolve, reject) + Promise.resolve(handler(nuxtApp, { signal: mergedSignal })).then(resolve, reject) } catch (err) { reject(err) }Add these helper functions at module scope (after imports):
function toAbortError(reason?: unknown): Error { try { return reason instanceof Error ? reason : new DOMException(String(reason ?? 'Aborted'), 'AbortError') } catch { return reason instanceof Error ? reason : new Error(String(reason ?? 'Aborted')) } } function mergeAbortSignals(signals: Array<AbortSignal | null | undefined>, timeout?: number): AbortSignal { const list: AbortSignal[] = signals.filter(Boolean) as AbortSignal[] if (typeof timeout === 'number' && timeout >= 0) { const timeoutSignal = (AbortSignal as any).timeout?.(timeout) if (timeoutSignal) list.push(timeoutSignal) } // Use native if available if ((AbortSignal as any).any) { return (AbortSignal as any).any(list) } // Polyfill const controller = new AbortController() const abort = (sig: AbortSignal) => () => { controller.abort(sig.reason ?? toAbortError()) list.forEach(s => s.removeEventListener?.('abort', abort(s))) } for (const sig of list) { if (sig.aborted) return sig sig.addEventListener?.('abort', abort(sig), { once: true }) } return controller.signal }
๐งน Nitpick comments (1)
packages/nuxt/src/app/composables/asyncData.ts (1)
741-755: Consider checkingerror.name === 'AbortError'instead of internal controller state.The current implementation checks
asyncData._abortController?.signal.abortedto detect internal aborts. However, a cleaner approach would be to check if the error is anAbortErrorregardless of source (internal, external, or timeout), as suggested in previous reviews.Apply this diff:
// If the promise was replaced by another one, we do not update the asyncData if (nuxtApp._asyncDataPromises[key] && nuxtApp._asyncDataPromises[key] !== promise) { return } - - // If the asyncData was explicitly aborted internally (dedupe or clear), we do not update the asyncData - if (asyncData._abortController?.signal.aborted) { - return - } - - // if the asyncData was explicitly aborted by user, we set it back to idle state - if (typeof DOMException !== 'undefined' && error instanceof DOMException && error.name === 'AbortError') { + + // If cancelled for any reason (internal, external, or timeout), reset to idle + if (error?.name === 'AbortError' || (typeof DOMException !== 'undefined' && error instanceof DOMException && error.name === 'AbortError')) { asyncData.status.value = 'idle' return }
๐ Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
๐ Files selected for processing (2)
packages/nuxt/src/app/composables/asyncData.ts(18 hunks)test/nuxt/use-async-data.test.ts(2 hunks)
๐งฐ Additional context used
๐ Path-based instructions (2)
**/*.{ts,tsx}
๐ CodeRabbit inference engine (.github/copilot-instructions.md)
Follow standard TypeScript conventions and best practices
Files:
packages/nuxt/src/app/composables/asyncData.tstest/nuxt/use-async-data.test.ts
**/*.{test,spec}.{ts,tsx,js,jsx}
๐ CodeRabbit inference engine (.github/copilot-instructions.md)
Write unit tests for core functionality using
vitest
Files:
test/nuxt/use-async-data.test.ts
๐งฌ Code graph analysis (1)
test/nuxt/use-async-data.test.ts (1)
packages/nuxt/src/app/composables/asyncData.ts (1)
useAsyncData(194-428)
๐ Additional comments (6)
test/nuxt/use-async-data.test.ts (3)
19-25: LGTM!The
/api/sleependpoint is well-suited for testing cancellation and timeout scenarios with a predictable 100ms delay.
27-29: LGTM!Unstubbing all globals before each test ensures proper test isolation and prevents mock pollution across tests.
838-894: LGTM! Comprehensive cancellation test coverage.The tests properly validate:
- External signal cancellation via
execute({ signal })- Internal cancellation via
clear()- Handler signal propagation
- Timeout behaviour
The
vi.waitForworkaround in the timeout test (line 890) is acceptable given the note aboutadvanceTimersToNextTimernot working as expected.packages/nuxt/src/app/composables/asyncData.ts (3)
20-20: LGTM! Type definitions correctly reflect the new cancellation API.The
AsyncDataHandlersignature now requires a second parameter withsignal, andtimeoutoptions are properly typed in bothAsyncDataOptionsandAsyncDataExecuteOptions.Also applies to: 102-105, 121-123
648-658: LGTM! Prerender wrapper correctly propagates signal.The handler wrapper now passes the
optionsparameter containing the signal to the underlying_handler, ensuring cancellation works in prerender scenarios.
698-701: LGTM! AbortController lifecycle management is correct.Creating a new
AbortControllerfor each execution and aborting any existing controller ensures proper cancellation semantics. UsingDOMExceptionwith name'AbortError'is the standard way to create abort errors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
๐งน Nitpick comments (3)
test/nuxt/use-async-data.test.ts (1)
19-25: Great coverage for abort/timeout semantics; minor stability nit
- New endpoint, global unstub, and abort/timeout scenarios are well covered. LGTM.
- Optional: in the timeout test (Lines 969-978), prefer advancing fake timers deterministically (e.g. vi.advanceTimersByTime(2)) before waitFor to reduce flakiness.
Also applies to: 27-29, 901-920, 922-937, 939-951, 953-967, 969-978, 980-999, 1000-1025, 1027-1058, 1060-1080, 1082-1102, 1104-1119, 1121-1132, 1134-1153
packages/nuxt/src/app/composables/asyncData.ts (2)
706-722: Normalise abort reason and align Promise generic
- Use the same generic in Promise construction to avoid confusion.
- Prefer a helper to normalise abort reasons rather than constructing DOMException inline.
Apply:
- const promise: Promise<ResT | void> = new Promise<ResT>( + const promise: Promise<ResT | void> = new Promise<ResT | void>( (resolve, reject) => { try { const timeout = opts.timeout ?? options.timeout - const mergedSignal = mergeAbortSignals([asyncData._abortController?.signal, opts?.signal], timeout) + const mergedSignal = mergeAbortSignals([asyncData._abortController?.signal, opts?.signal], timeout) if (mergedSignal.aborted) { - const reason = mergedSignal.reason - reject(reason instanceof Error ? reason : new DOMException(String(reason ?? 'Aborted'), 'AbortError')) + reject(mergedSignal.reason instanceof Error ? mergedSignal.reason : toAbortError(mergedSignal.reason)) return } - mergedSignal.addEventListener('abort', () => { - const reason = mergedSignal.reason - reject(reason instanceof Error ? reason : new DOMException(String(reason ?? 'Aborted'), 'AbortError')) - }, { once: true }) + mergedSignal.addEventListener('abort', () => { + const r = mergedSignal.reason + reject(r instanceof Error ? r : toAbortError(r)) + }, { once: true }) return Promise.resolve(handler(nuxtApp, { signal: mergedSignal })).then(resolve, reject)
701-705: Avoid direct DOMException construction in abortsConstructing DOMException directly is brittle in some runtimes. Use a helper to produce an AbortError.
Apply:
- if (asyncData._abortController) { - asyncData._abortController.abort(new DOMException('AsyncData request cancelled by deduplication', 'AbortError')) - } + if (asyncData._abortController) { + asyncData._abortController.abort(toAbortError('AsyncData request cancelled by deduplication')) + }- if (entry?._abortController) { + if (entry?._abortController) { try { - entry._abortController.abort(new DOMException('AsyncData aborted by user.', 'AbortError')) + entry._abortController.abort(toAbortError('AsyncData aborted by user.')) } finally { entry._abortController = undefined } }Add the toAbortError helper once per module (see previous comment).
Also applies to: 413-423
๐ Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
๐ Files selected for processing (2)
packages/nuxt/src/app/composables/asyncData.ts(18 hunks)test/nuxt/use-async-data.test.ts(2 hunks)
๐งฐ Additional context used
๐ Path-based instructions (2)
**/*.{ts,tsx}
๐ CodeRabbit inference engine (.github/copilot-instructions.md)
Follow standard TypeScript conventions and best practices
Files:
test/nuxt/use-async-data.test.tspackages/nuxt/src/app/composables/asyncData.ts
**/*.{test,spec}.{ts,tsx,js,jsx}
๐ CodeRabbit inference engine (.github/copilot-instructions.md)
Write unit tests for core functionality using
vitest
Files:
test/nuxt/use-async-data.test.ts
๐งฌ Code graph analysis (2)
test/nuxt/use-async-data.test.ts (1)
packages/nuxt/src/app/composables/asyncData.ts (1)
useAsyncData(194-431)
packages/nuxt/src/app/composables/asyncData.ts (2)
packages/nuxt/src/app/nuxt.ts (1)
NuxtApp(199-199)packages/nuxt/src/app/composables/error.ts (1)
NuxtError(15-17)
โฐ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: code
| function mergeAbortSignals (signals: Array<AbortSignal | null | undefined>, timeout?: number): AbortSignal { | ||
| const list = signals.filter(s => !!s) | ||
| if (typeof timeout === 'number' && timeout >= 0) { | ||
| const timeoutSignal = AbortSignal.timeout?.(timeout) | ||
| if (timeoutSignal) { list.push(timeoutSignal) } | ||
| } | ||
|
|
||
| // Use native if available | ||
| if (AbortSignal.any) { | ||
| return AbortSignal.any(list) | ||
| } | ||
|
|
||
| // Polyfill | ||
| const controller = new AbortController() | ||
|
|
||
| for (const sig of list) { | ||
| if (sig.aborted) { | ||
| const reason = sig.reason ?? new DOMException('Aborted', 'AbortError') | ||
| try { | ||
| controller.abort(reason) | ||
| } catch { | ||
| controller.abort() | ||
| } | ||
| return controller.signal | ||
| } | ||
| } | ||
|
|
||
| const onAbort = () => { | ||
| const abortedSignal = list.find(s => s.aborted) | ||
| const reason = abortedSignal?.reason ?? new DOMException('Aborted', 'AbortError') | ||
| try { | ||
| controller.abort(reason) | ||
| } catch { | ||
| controller.abort() | ||
| } | ||
| } | ||
|
|
||
| for (const sig of list) { | ||
| sig.addEventListener?.('abort', onAbort, { once: true }) | ||
| } | ||
|
|
||
| return controller.signal | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Type-safety and portability: avoid direct AbortSignal.any/timeout; add type guards
Current implementation directly references AbortSignal.any/timeout (not in all TS lib targets) and filters signals without a type guard, risking TS errors (โProperty 'any'/'timeout' does not existโฆโ, โObject is possibly 'undefined'โ). Use a small ponyfill and a type-guarded list.
Apply:
-function mergeAbortSignals (signals: Array<AbortSignal | null | undefined>, timeout?: number): AbortSignal {
- const list = signals.filter(s => !!s)
- if (typeof timeout === 'number' && timeout >= 0) {
- const timeoutSignal = AbortSignal.timeout?.(timeout)
- if (timeoutSignal) { list.push(timeoutSignal) }
- }
-
- // Use native if available
- if (AbortSignal.any) {
- return AbortSignal.any(list)
- }
-
- // Polyfill
- const controller = new AbortController()
-
- for (const sig of list) {
- if (sig.aborted) {
- const reason = sig.reason ?? new DOMException('Aborted', 'AbortError')
- try {
- controller.abort(reason)
- } catch {
- controller.abort()
- }
- return controller.signal
- }
- }
-
- const onAbort = () => {
- const abortedSignal = list.find(s => s.aborted)
- const reason = abortedSignal?.reason ?? new DOMException('Aborted', 'AbortError')
- try {
- controller.abort(reason)
- } catch {
- controller.abort()
- }
- }
-
- for (const sig of list) {
- sig.addEventListener?.('abort', onAbort, { once: true })
- }
-
- return controller.signal
-}
+function mergeAbortSignals (signals: Array<AbortSignal | null | undefined>, timeout?: number): AbortSignal {
+ const list: AbortSignal[] = signals.filter((s): s is AbortSignal => Boolean(s))
+ const AS: any = AbortSignal as any
+ if (typeof timeout === 'number' && timeout >= 0) {
+ const t: AbortSignal | undefined = AS.timeout?.(timeout)
+ if (t) list.push(t)
+ }
+ if (AS.any) {
+ return AS.any(list)
+ }
+ const controller = new AbortController()
+ for (const sig of list) {
+ if (sig.aborted) {
+ const reason = sig.reason instanceof Error ? sig.reason : toAbortError(sig.reason)
+ try { controller.abort(reason) } catch { controller.abort() }
+ return controller.signal
+ }
+ }
+ const onAbort = () => {
+ const aborted = list.find(s => s.aborted)
+ const reason = aborted?.reason instanceof Error ? aborted.reason : toAbortError(aborted?.reason)
+ try { controller.abort(reason) } catch { controller.abort() }
+ }
+ for (const sig of list) {
+ sig.addEventListener?.('abort', onAbort, { once: true })
+ }
+ return controller.signal
+}Add helper (module scope):
function toAbortError (reason?: unknown): Error {
try {
return reason instanceof Error ? reason : new DOMException(String(reason ?? 'Aborted'), 'AbortError')
} catch {
return reason instanceof Error ? reason : Object.assign(new Error(String(reason ?? 'Aborted')), { name: 'AbortError' })
}
}๐ค Prompt for AI Agents
In packages/nuxt/src/app/composables/asyncData.ts around lines 824 to 866, the
code directly references AbortSignal.any and AbortSignal.timeout and filters
signals without type guards which can cause TS/lib-target errors and unsafe
undefined access; instead add the suggested toAbortError helper at module scope,
use explicit type guards when building the list (e.g., filter for non-null
AbortSignal instances), check existence of AbortSignal.any and
AbortSignal.timeout via typeof before calling them and provide a
ponyfill/fallback implementation for `any` behavior, convert signal.reason to a
proper Error using toAbortError when aborting, and guard addEventListener calls
so they only run on defined signals; ensure all abort calls wrap with try/catch
as currently done and return controller.signal for the fallback.
Co-authored-by: Robin <robin.kehl@singular-it.de>
|
ุชุญูุงุชู ููุฌู ูุน ุจุงูุชูููู ุงู ุดุงุก ุงููู |
๐ Linked issue
fix #32530
๐ Description
I don't really find this PR satisfying as we need to overwrite
clearfromasyncDatainuseFetch๐ค should we instead add aonClearinuseAsyncData?