Skip to content

Commit

Permalink
Merge pull request #4102 from JoshuaKGoldberg/safe-promises
Browse files Browse the repository at this point in the history
Added 'SafePromise' branded Promises for createAsyncThunk
  • Loading branch information
EskiMojo14 authored Jan 23, 2024
2 parents c3cf5a7 + ea73204 commit 6e66f4f
Show file tree
Hide file tree
Showing 4 changed files with 34 additions and 11 deletions.
5 changes: 3 additions & 2 deletions packages/toolkit/src/createAsyncThunk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
Id,
IsAny,
IsUnknown,
SafePromise,
TypeGuard,
} from './tsHelpers'
import { nanoid } from './nanoid'
Expand Down Expand Up @@ -242,7 +243,7 @@ export type AsyncThunkAction<
dispatch: GetDispatch<ThunkApiConfig>,
getState: () => GetState<ThunkApiConfig>,
extra: GetExtra<ThunkApiConfig>
) => Promise<
) => SafePromise<
| ReturnType<AsyncThunkFulfilledActionCreator<Returned, ThunkArg>>
| ReturnType<AsyncThunkRejectedActionCreator<ThunkArg, ThunkApiConfig>>
> & {
Expand Down Expand Up @@ -676,7 +677,7 @@ export const createAsyncThunk = /* @__PURE__ */ (() => {
}
return finalAction
})()
return Object.assign(promise as Promise<any>, {
return Object.assign(promise as SafePromise<any>, {
abort,
requestId,
arg,
Expand Down
2 changes: 1 addition & 1 deletion packages/toolkit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,6 @@ export { combineSlices } from './combineSlices'

export type { WithSlice } from './combineSlices'

export type { ExtractDispatchExtensions as TSHelpersExtractDispatchExtensions } from './tsHelpers'
export type { ExtractDispatchExtensions as TSHelpersExtractDispatchExtensions, SafePromise } from './tsHelpers'

export { formatProdErrorMessage } from './formatProdErrorMessage'
20 changes: 12 additions & 8 deletions packages/toolkit/src/query/core/buildInitiate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import type { QueryResultSelectorResult } from './buildSelectors'
import type { Dispatch } from 'redux'
import { isNotNullish } from '../utils/isNotNullish'
import { countObjectKeys } from '../utils/countObjectKeys'
import type { SafePromise } from '../../tsHelpers'
import { asSafePromise } from '../../tsHelpers'

declare module './module' {
export interface ApiEndpointQuery<
Expand Down Expand Up @@ -60,7 +62,7 @@ type StartQueryActionCreator<

export type QueryActionCreatorResult<
D extends QueryDefinition<any, any, any, any>
> = Promise<QueryResultSelectorResult<D>> & {
> = SafePromise<QueryResultSelectorResult<D>> & {
arg: QueryArgFrom<D>
requestId: string
subscriptionOptions: SubscriptionOptions | undefined
Expand Down Expand Up @@ -90,7 +92,7 @@ type StartMutationActionCreator<

export type MutationActionCreatorResult<
D extends MutationDefinition<any, any, any, any>
> = Promise<
> = SafePromise<
| { data: ResultTypeFrom<D> }
| {
error:
Expand Down Expand Up @@ -335,7 +337,7 @@ You must add the middleware for RTK-Query to function correctly!`
const selectFromState = () => selector(getState())

const statePromise: QueryActionCreatorResult<any> = Object.assign(
forceQueryFn
(forceQueryFn
? // a query has been forced (upsertQueryData)
// -> we want to resolve it once data has been written with the data that will be written
thunkResult.then(selectFromState)
Expand All @@ -345,7 +347,9 @@ You must add the middleware for RTK-Query to function correctly!`
Promise.resolve(stateAfter)
: // query just started or one is already in flight
// -> wait for the running query, then resolve with data from after that
Promise.all([runningQuery, thunkResult]).then(selectFromState),
Promise.all([runningQuery, thunkResult]).then(
selectFromState
)) as SafePromise<any>,
{
arg,
requestId,
Expand Down Expand Up @@ -421,10 +425,10 @@ You must add the middleware for RTK-Query to function correctly!`
const thunkResult = dispatch(thunk)
middlewareWarning(dispatch)
const { requestId, abort, unwrap } = thunkResult
const returnValuePromise = thunkResult
.unwrap()
.then((data) => ({ data }))
.catch((error) => ({ error }))
const returnValuePromise = asSafePromise(
thunkResult.unwrap().then((data) => ({ data })),
(error) => ({ error })
)

const reset = () => {
dispatch(removeMutationResult({ requestId, fixedCacheKey }))
Expand Down
18 changes: 18 additions & 0 deletions packages/toolkit/src/tsHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,3 +207,21 @@ export type Tail<T extends any[]> = T extends [any, ...infer Tail]
: never

export type UnknownIfNonSpecific<T> = {} extends T ? unknown : T

/**
* A Promise that will never reject.
* @see https://github.com/reduxjs/redux-toolkit/issues/4101
*/
export type SafePromise<T> = Promise<T> & {
__linterBrands: 'SafePromise'
}

/**
* Properly wraps a Promise as a {@link SafePromise} with .catch(fallback).
*/
export function asSafePromise<Resolved, Rejected>(
promise: Promise<Resolved>,
fallback: (error: unknown) => Rejected
) {
return promise.catch(fallback) as SafePromise<Resolved | Rejected>
}

0 comments on commit 6e66f4f

Please sign in to comment.