diff --git a/packages/next/src/client/components/router-reducer/router-reducer-types.ts b/packages/next/src/client/components/router-reducer/router-reducer-types.ts index 142e68e46c594..15a98c9a01cf6 100644 --- a/packages/next/src/client/components/router-reducer/router-reducer-types.ts +++ b/packages/next/src/client/components/router-reducer/router-reducer-types.ts @@ -272,15 +272,3 @@ export type ReducerActions = Readonly< | HmrRefreshAction | ServerActionAction > - -export function isThenable(value: any): value is Promise { - // TODO: We don't gain anything from this abstraction. It's unsound, and only - // makes sense in the specific places where we use it. So it's better to keep - // the type coercion inline, instead of leaking this to other places in - // the codebase. - return ( - value && - (typeof value === 'object' || typeof value === 'function') && - typeof value.then === 'function' - ) -} diff --git a/packages/next/src/client/components/use-reducer-with-devtools.ts b/packages/next/src/client/components/use-reducer-with-devtools.ts index 45f7ba0eb820d..9e5636d711cac 100644 --- a/packages/next/src/client/components/use-reducer-with-devtools.ts +++ b/packages/next/src/client/components/use-reducer-with-devtools.ts @@ -1,13 +1,13 @@ import type { Dispatch } from 'react' import React, { use } from 'react' import { useRef, useEffect, useCallback } from 'react' -import { - isThenable, - type AppRouterState, - type ReducerActions, - type ReducerState, +import type { + AppRouterState, + ReducerActions, + ReducerState, } from './router-reducer/router-reducer-types' import type { AppRouterActionQueue } from '../../shared/lib/router/action-queue' +import { isThenable } from '../../shared/lib/is-thenable' export type ReduxDevtoolsSyncFn = (state: AppRouterState) => void diff --git a/packages/next/src/server/after/after-context.ts b/packages/next/src/server/after/after-context.ts index 55f23af6e26a7..ee877fb698b4b 100644 --- a/packages/next/src/server/after/after-context.ts +++ b/packages/next/src/server/after/after-context.ts @@ -7,6 +7,7 @@ import { ResponseCookies } from '../web/spec-extension/cookies' import type { RequestLifecycleOpts } from '../base-server' import type { AfterCallback, AfterTask } from './after' import { InvariantError } from '../../shared/lib/invariant-error' +import { isThenable } from '../../shared/lib/is-thenable' export type AfterContextOpts = { waitUntil: RequestLifecycleOpts['waitUntil'] | undefined @@ -36,7 +37,7 @@ export class AfterContext { } public after(task: AfterTask): void { - if (isPromise(task)) { + if (isThenable(task)) { task.catch(() => {}) // avoid unhandled rejection crashes if (!this.waitUntil) { errorWaitUntilNotAvailable() @@ -141,12 +142,3 @@ function wrapRequestStoreForAfterCallbacks( serverComponentsHmrCache: requestStore.serverComponentsHmrCache, } } - -function isPromise(p: unknown): p is Promise { - return ( - p !== null && - typeof p === 'object' && - 'then' in p && - typeof p.then === 'function' - ) -} diff --git a/packages/next/src/server/lib/trace/tracer.ts b/packages/next/src/server/lib/trace/tracer.ts index 1636c43758885..78c544209395c 100644 --- a/packages/next/src/server/lib/trace/tracer.ts +++ b/packages/next/src/server/lib/trace/tracer.ts @@ -11,6 +11,7 @@ import type { AttributeValue, TextMapGetter, } from 'next/dist/compiled/@opentelemetry/api' +import { isThenable } from '../../../shared/lib/is-thenable' let api: typeof import('next/dist/compiled/@opentelemetry/api') @@ -34,10 +35,6 @@ if (process.env.NEXT_RUNTIME === 'edge') { const { context, propagation, trace, SpanStatusCode, SpanKind, ROOT_CONTEXT } = api -const isPromise = (p: any): p is Promise => { - return p !== null && typeof p === 'object' && typeof p.then === 'function' -} - export class BubbledError extends Error { constructor( public readonly bubble?: boolean, @@ -352,7 +349,7 @@ class NextTracerImpl implements NextTracer { } const result = fn(span) - if (isPromise(result)) { + if (isThenable(result)) { // If there's error make sure it throws return result .then((res) => { diff --git a/packages/next/src/shared/lib/is-thenable.ts b/packages/next/src/shared/lib/is-thenable.ts new file mode 100644 index 0000000000000..8ed39fa335646 --- /dev/null +++ b/packages/next/src/shared/lib/is-thenable.ts @@ -0,0 +1,16 @@ +/** + * Check to see if a value is Thenable. + * + * @param promise the maybe-thenable value + * @returns true if the value is thenable + */ +export function isThenable( + promise: Promise | T +): promise is Promise { + return ( + promise !== null && + typeof promise === 'object' && + 'then' in promise && + typeof promise.then === 'function' + ) +} diff --git a/packages/next/src/shared/lib/router/action-queue.ts b/packages/next/src/shared/lib/router/action-queue.ts index c970d1634cafb..acc5b3afd8bc2 100644 --- a/packages/next/src/shared/lib/router/action-queue.ts +++ b/packages/next/src/shared/lib/router/action-queue.ts @@ -1,5 +1,4 @@ import { - isThenable, type AppRouterState, type ReducerActions, type ReducerState, @@ -11,6 +10,7 @@ import { import type { ReduxDevToolsInstance } from '../../../client/components/use-reducer-with-devtools' import { reducer } from '../../../client/components/router-reducer/router-reducer' import { startTransition } from 'react' +import { isThenable } from '../is-thenable' export type DispatchStatePromise = React.Dispatch