import type { Dispatch, UnknownAction } from 'redux' import type { PayloadAction, ActionCreatorWithPreparedPayload, } from './createAction' import { createAction } from './createAction' import type { ThunkDispatch } from 'redux-thunk' import type { ActionFromMatcher, FallbackIfUnknown, Id, IsAny, IsUnknown, SafePromise, TypeGuard, } from './tsHelpers' import { nanoid } from './nanoid' import { isAnyOf } from './matchers' // @ts-ignore we need the import of these types due to a bundling issue. type _Keep = PayloadAction | ActionCreatorWithPreparedPayload export type BaseThunkAPI< S, E, D extends Dispatch = Dispatch, RejectedValue = unknown, RejectedMeta = unknown, FulfilledMeta = unknown, > = { dispatch: D getState: () => S extra: E requestId: string signal: AbortSignal abort: (reason?: string) => void rejectWithValue: IsUnknown< RejectedMeta, (value: RejectedValue) => RejectWithValue, ( value: RejectedValue, meta: RejectedMeta, ) => RejectWithValue > fulfillWithValue: IsUnknown< FulfilledMeta, (value: FulfilledValue) => FulfilledValue, ( value: FulfilledValue, meta: FulfilledMeta, ) => FulfillWithMeta > } /** * @public */ export interface SerializedError { name?: string message?: string stack?: string code?: string } const commonProperties: Array = [ 'name', 'message', 'stack', 'code', ] class RejectWithValue { /* type-only property to distinguish between RejectWithValue and FulfillWithMeta does not exist at runtime */ private readonly _type!: 'RejectWithValue' constructor( public readonly payload: Payload, public readonly meta: RejectedMeta, ) {} } class FulfillWithMeta { /* type-only property to distinguish between RejectWithValue and FulfillWithMeta does not exist at runtime */ private readonly _type!: 'FulfillWithMeta' constructor( public readonly payload: Payload, public readonly meta: FulfilledMeta, ) {} } /** * Serializes an error into a plain object. * Reworked from https://github.com/sindresorhus/serialize-error * * @public */ export const miniSerializeError = (value: any): SerializedError => { if (typeof value === 'object' && value !== null) { const simpleError: SerializedError = {} for (const property of commonProperties) { if (typeof value[property] === 'string') { simpleError[property] = value[property] } } return simpleError } return { message: String(value) } } export type AsyncThunkConfig = { state?: unknown dispatch?: ThunkDispatch extra?: unknown rejectValue?: unknown serializedErrorType?: unknown pendingMeta?: unknown fulfilledMeta?: unknown rejectedMeta?: unknown } type GetState = ThunkApiConfig extends { state: infer State } ? State : unknown type GetExtra = ThunkApiConfig extends { extra: infer Extra } ? Extra : unknown type GetDispatch = ThunkApiConfig extends { dispatch: infer Dispatch } ? FallbackIfUnknown< Dispatch, ThunkDispatch< GetState, GetExtra, UnknownAction > > : ThunkDispatch< GetState, GetExtra, UnknownAction > export type GetThunkAPI = BaseThunkAPI< GetState, GetExtra, GetDispatch, GetRejectValue, GetRejectedMeta, GetFulfilledMeta > type GetRejectValue = ThunkApiConfig extends { rejectValue: infer RejectValue } ? RejectValue : unknown type GetPendingMeta = ThunkApiConfig extends { pendingMeta: infer PendingMeta } ? PendingMeta : unknown type GetFulfilledMeta = ThunkApiConfig extends { fulfilledMeta: infer FulfilledMeta } ? FulfilledMeta : unknown type GetRejectedMeta = ThunkApiConfig extends { rejectedMeta: infer RejectedMeta } ? RejectedMeta : unknown type GetSerializedErrorType = ThunkApiConfig extends { serializedErrorType: infer GetSerializedErrorType } ? GetSerializedErrorType : SerializedError type MaybePromise = T | Promise | (T extends any ? Promise : never) /** * A type describing the return value of the `payloadCreator` argument to `createAsyncThunk`. * Might be useful for wrapping `createAsyncThunk` in custom abstractions. * * @public */ export type AsyncThunkPayloadCreatorReturnValue< Returned, ThunkApiConfig extends AsyncThunkConfig, > = MaybePromise< | IsUnknown< GetFulfilledMeta, Returned, FulfillWithMeta> > | RejectWithValue< GetRejectValue, GetRejectedMeta > > /** * A type describing the `payloadCreator` argument to `createAsyncThunk`. * Might be useful for wrapping `createAsyncThunk` in custom abstractions. * * @public */ export type AsyncThunkPayloadCreator< Returned, ThunkArg = void, ThunkApiConfig extends AsyncThunkConfig = {}, > = ( arg: ThunkArg, thunkAPI: GetThunkAPI, ) => AsyncThunkPayloadCreatorReturnValue /** * A ThunkAction created by `createAsyncThunk`. * Dispatching it returns a Promise for either a * fulfilled or rejected action. * Also, the returned value contains an `abort()` method * that allows the asyncAction to be cancelled from the outside. * * @public */ export type AsyncThunkAction< Returned, ThunkArg, ThunkApiConfig extends AsyncThunkConfig, > = ( dispatch: NonNullable>, getState: () => GetState, extra: GetExtra, ) => SafePromise< | ReturnType> | ReturnType> > & { abort: (reason?: string) => void requestId: string arg: ThunkArg unwrap: () => Promise } type AsyncThunkActionCreator< Returned, ThunkArg, ThunkApiConfig extends AsyncThunkConfig, > = IsAny< ThunkArg, // any handling (arg: ThunkArg) => AsyncThunkAction, // unknown handling unknown extends ThunkArg ? (arg: ThunkArg) => AsyncThunkAction // argument not specified or specified as void or undefined : [ThunkArg] extends [void] | [undefined] ? () => AsyncThunkAction // argument contains void : [void] extends [ThunkArg] // make optional ? ( arg?: ThunkArg, ) => AsyncThunkAction // argument contains undefined : [undefined] extends [ThunkArg] ? WithStrictNullChecks< // with strict nullChecks: make optional ( arg?: ThunkArg, ) => AsyncThunkAction, // without strict null checks this will match everything, so don't make it optional ( arg: ThunkArg, ) => AsyncThunkAction > // default case: normal argument : ( arg: ThunkArg, ) => AsyncThunkAction > /** * Options object for `createAsyncThunk`. * * @public */ export type AsyncThunkOptions< ThunkArg = void, ThunkApiConfig extends AsyncThunkConfig = {}, > = { /** * A method to control whether the asyncThunk should be executed. Has access to the * `arg`, `api.getState()` and `api.extra` arguments. * * @returns `false` if it should be skipped */ condition?( arg: ThunkArg, api: Pick, 'getState' | 'extra'>, ): MaybePromise /** * If `condition` returns `false`, the asyncThunk will be skipped. * This option allows you to control whether a `rejected` action with `meta.condition == false` * will be dispatched or not. * * @default `false` */ dispatchConditionRejection?: boolean serializeError?: (x: unknown) => GetSerializedErrorType /** * A function to use when generating the `requestId` for the request sequence. * * @default `nanoid` */ idGenerator?: (arg: ThunkArg) => string } & IsUnknown< GetPendingMeta, { /** * A method to generate additional properties to be added to `meta` of the pending action. * * Using this optional overload will not modify the types correctly, this overload is only in place to support JavaScript users. * Please use the `ThunkApiConfig` parameter `pendingMeta` to get access to a correctly typed overload */ getPendingMeta?( base: { arg: ThunkArg requestId: string }, api: Pick, 'getState' | 'extra'>, ): GetPendingMeta }, { /** * A method to generate additional properties to be added to `meta` of the pending action. */ getPendingMeta( base: { arg: ThunkArg requestId: string }, api: Pick, 'getState' | 'extra'>, ): GetPendingMeta } > export type AsyncThunkPendingActionCreator< ThunkArg, ThunkApiConfig = {}, > = ActionCreatorWithPreparedPayload< [string, ThunkArg, GetPendingMeta?], undefined, string, never, { arg: ThunkArg requestId: string requestStatus: 'pending' } & GetPendingMeta > export type AsyncThunkRejectedActionCreator< ThunkArg, ThunkApiConfig = {}, > = ActionCreatorWithPreparedPayload< [ Error | null, string, ThunkArg, GetRejectValue?, GetRejectedMeta?, ], GetRejectValue | undefined, string, GetSerializedErrorType, { arg: ThunkArg requestId: string requestStatus: 'rejected' aborted: boolean condition: boolean } & ( | ({ rejectedWithValue: false } & { [K in keyof GetRejectedMeta]?: undefined }) | ({ rejectedWithValue: true } & GetRejectedMeta) ) > export type AsyncThunkFulfilledActionCreator< Returned, ThunkArg, ThunkApiConfig = {}, > = ActionCreatorWithPreparedPayload< [Returned, string, ThunkArg, GetFulfilledMeta?], Returned, string, never, { arg: ThunkArg requestId: string requestStatus: 'fulfilled' } & GetFulfilledMeta > /** * A type describing the return value of `createAsyncThunk`. * Might be useful for wrapping `createAsyncThunk` in custom abstractions. * * @public */ export type AsyncThunk< Returned, ThunkArg, ThunkApiConfig extends AsyncThunkConfig, > = AsyncThunkActionCreator & { pending: AsyncThunkPendingActionCreator rejected: AsyncThunkRejectedActionCreator fulfilled: AsyncThunkFulfilledActionCreator< Returned, ThunkArg, ThunkApiConfig > // matchSettled? settled: ( action: any, ) => action is ReturnType< | AsyncThunkRejectedActionCreator | AsyncThunkFulfilledActionCreator > typePrefix: string } export type OverrideThunkApiConfigs = Id< NewConfig & Omit > type CreateAsyncThunk = { /** * * @param typePrefix * @param payloadCreator * @param options * * @public */ // separate signature without `AsyncThunkConfig` for better inference ( typePrefix: string, payloadCreator: AsyncThunkPayloadCreator< Returned, ThunkArg, CurriedThunkApiConfig >, options?: AsyncThunkOptions, ): AsyncThunk /** * * @param typePrefix * @param payloadCreator * @param options * * @public */ ( typePrefix: string, payloadCreator: AsyncThunkPayloadCreator< Returned, ThunkArg, OverrideThunkApiConfigs >, options?: AsyncThunkOptions< ThunkArg, OverrideThunkApiConfigs >, ): AsyncThunk< Returned, ThunkArg, OverrideThunkApiConfigs > withTypes(): CreateAsyncThunk< OverrideThunkApiConfigs > } export const createAsyncThunk = /* @__PURE__ */ (() => { function createAsyncThunk< Returned, ThunkArg, ThunkApiConfig extends AsyncThunkConfig, >( typePrefix: string, payloadCreator: AsyncThunkPayloadCreator< Returned, ThunkArg, ThunkApiConfig >, options?: AsyncThunkOptions, ): AsyncThunk { type RejectedValue = GetRejectValue type PendingMeta = GetPendingMeta type FulfilledMeta = GetFulfilledMeta type RejectedMeta = GetRejectedMeta const fulfilled: AsyncThunkFulfilledActionCreator< Returned, ThunkArg, ThunkApiConfig > = createAction( typePrefix + '/fulfilled', ( payload: Returned, requestId: string, arg: ThunkArg, meta?: FulfilledMeta, ) => ({ payload, meta: { ...((meta as any) || {}), arg, requestId, requestStatus: 'fulfilled' as const, }, }), ) const pending: AsyncThunkPendingActionCreator = createAction( typePrefix + '/pending', (requestId: string, arg: ThunkArg, meta?: PendingMeta) => ({ payload: undefined, meta: { ...((meta as any) || {}), arg, requestId, requestStatus: 'pending' as const, }, }), ) const rejected: AsyncThunkRejectedActionCreator = createAction( typePrefix + '/rejected', ( error: Error | null, requestId: string, arg: ThunkArg, payload?: RejectedValue, meta?: RejectedMeta, ) => ({ payload, error: ((options && options.serializeError) || miniSerializeError)( error || 'Rejected', ) as GetSerializedErrorType, meta: { ...((meta as any) || {}), arg, requestId, rejectedWithValue: !!payload, requestStatus: 'rejected' as const, aborted: error?.name === 'AbortError', condition: error?.name === 'ConditionError', }, }), ) function actionCreator( arg: ThunkArg, ): AsyncThunkAction> { return (dispatch, getState, extra) => { const requestId = options?.idGenerator ? options.idGenerator(arg) : nanoid() const abortController = new AbortController() let abortHandler: (() => void) | undefined let abortReason: string | undefined function abort(reason?: string) { abortReason = reason abortController.abort() } const promise = (async function () { let finalAction: ReturnType try { let conditionResult = options?.condition?.(arg, { getState, extra }) if (isThenable(conditionResult)) { conditionResult = await conditionResult } if (conditionResult === false || abortController.signal.aborted) { // eslint-disable-next-line no-throw-literal throw { name: 'ConditionError', message: 'Aborted due to condition callback returning false.', } } const abortedPromise = new Promise((_, reject) => { abortHandler = () => { reject({ name: 'AbortError', message: abortReason || 'Aborted', }) } abortController.signal.addEventListener('abort', abortHandler) }) dispatch( pending( requestId, arg, options?.getPendingMeta?.( { requestId, arg }, { getState, extra }, ), ) as any, ) finalAction = await Promise.race([ abortedPromise, Promise.resolve( payloadCreator(arg, { dispatch, getState, extra, requestId, signal: abortController.signal, abort, rejectWithValue: (( value: RejectedValue, meta?: RejectedMeta, ) => { return new RejectWithValue(value, meta) }) as any, fulfillWithValue: ((value: unknown, meta?: FulfilledMeta) => { return new FulfillWithMeta(value, meta) }) as any, }), ).then((result) => { if (result instanceof RejectWithValue) { throw result } if (result instanceof FulfillWithMeta) { return fulfilled(result.payload, requestId, arg, result.meta) } return fulfilled(result as any, requestId, arg) }), ]) } catch (err) { finalAction = err instanceof RejectWithValue ? rejected(null, requestId, arg, err.payload, err.meta) : rejected(err as any, requestId, arg) } finally { if (abortHandler) { abortController.signal.removeEventListener('abort', abortHandler) } } // We dispatch the result action _after_ the catch, to avoid having any errors // here get swallowed by the try/catch block, // per https://twitter.com/dan_abramov/status/770914221638942720 // and https://github.com/reduxjs/redux-toolkit/blob/e85eb17b39a2118d859f7b7746e0f3fee523e089/docs/tutorials/advanced-tutorial.md#async-error-handling-logic-in-thunks const skipDispatch = options && !options.dispatchConditionRejection && rejected.match(finalAction) && (finalAction as any).meta.condition if (!skipDispatch) { dispatch(finalAction as any) } return finalAction })() return Object.assign(promise as SafePromise, { abort, requestId, arg, unwrap() { return promise.then(unwrapResult) }, }) } } return Object.assign( actionCreator as AsyncThunkActionCreator< Returned, ThunkArg, ThunkApiConfig >, { pending, rejected, fulfilled, settled: isAnyOf(rejected, fulfilled), typePrefix, }, ) } createAsyncThunk.withTypes = () => createAsyncThunk return createAsyncThunk as CreateAsyncThunk })() interface UnwrappableAction { payload: any meta?: any error?: any } type UnwrappedActionPayload = Exclude< T, { error: any } >['payload'] /** * @public */ export function unwrapResult( action: R, ): UnwrappedActionPayload { if (action.meta && action.meta.rejectedWithValue) { throw action.payload } if (action.error) { throw action.error } return action.payload } type WithStrictNullChecks = undefined extends boolean ? False : True function isThenable(value: any): value is PromiseLike { return ( value !== null && typeof value === 'object' && typeof value.then === 'function' ) }