Skip to content

Commit

Permalink
use promiseWithResolvers for RTKQ lifecycle management
Browse files Browse the repository at this point in the history
  • Loading branch information
ben.durrant committed Jun 6, 2023
1 parent b65f6b5 commit 8c38f92
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 75 deletions.
29 changes: 16 additions & 13 deletions packages/toolkit/src/query/core/buildMiddleware/cacheLifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ import type { PatchCollection, Recipe } from '../buildThunks'
import type {
ApiMiddlewareInternalHandler,
InternalHandlerBuilder,
PromiseWithKnownReason,
SubMiddlewareApi,
} from './types'
import type { PromiseWithKnownReason } from '../../utils/promiseWithResolvers'
import { promiseWithResolvers } from '../../utils/promiseWithResolvers'

export type ReferenceCacheLifecycle = never

Expand Down Expand Up @@ -274,20 +275,22 @@ export const buildCacheLifecycleHandler: InternalHandlerBuilder = ({

let lifecycle = {} as CacheLifecycle

const cacheEntryRemoved = new Promise<void>((resolve) => {
lifecycle.cacheEntryRemoved = resolve
})
const cacheDataLoaded: PromiseWithKnownReason<
let cacheEntryRemoved: Promise<void>
;({ promise: cacheEntryRemoved, resolve: lifecycle.cacheEntryRemoved } =
promiseWithResolvers<void>())

const {
promise: cacheDataLoaded,
resolve,
reject,
} = promiseWithResolvers<
{ data: unknown; meta: unknown },
typeof neverResolvedError
> = Promise.race([
new Promise<{ data: unknown; meta: unknown }>((resolve) => {
lifecycle.valueResolved = resolve
}),
cacheEntryRemoved.then(() => {
throw neverResolvedError
}),
])
>()
lifecycle.valueResolved = resolve

cacheEntryRemoved.then(() => reject(neverResolvedError), reject)

// prevent uncaught promise rejections from happening.
// if the original promise is used in any way, that will create a new promise that will throw again
cacheDataLoaded.catch(() => {})
Expand Down
32 changes: 17 additions & 15 deletions packages/toolkit/src/query/core/buildMiddleware/queryLifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@ import { DefinitionType } from '../../endpointDefinitions'
import type { QueryFulfilledRejectionReason } from '../../endpointDefinitions'
import type { Recipe } from '../buildThunks'
import type {
PromiseWithKnownReason,
PromiseConstructorWithKnownReason,
InternalHandlerBuilder,
ApiMiddlewareInternalHandler,
} from './types'
import type {
PromiseWithKnownReason,
PromiseWithResolvers,
} from '../../utils/promiseWithResolvers'
import { promiseWithResolvers } from '../../utils/promiseWithResolvers'

export type ReferenceQueryLifecycle = never

Expand Down Expand Up @@ -211,10 +214,14 @@ export const buildQueryLifecycleHandler: InternalHandlerBuilder = ({
const isRejectedThunk = isRejected(queryThunk, mutationThunk)
const isFullfilledThunk = isFulfilled(queryThunk, mutationThunk)

type CacheLifecycle = {
resolve(value: { data: unknown; meta: unknown }): unknown
reject(value: QueryFulfilledRejectionReason<any>): unknown
}
type CacheLifecycle = Omit<
PromiseWithResolvers<
{ data: unknown; meta: unknown },
QueryFulfilledRejectionReason<any>
>,
'promise'
>

const lifecycleMap: Record<string, CacheLifecycle> = {}

const handler: ApiMiddlewareInternalHandler = (action, mwApi) => {
Expand All @@ -226,15 +233,10 @@ export const buildQueryLifecycleHandler: InternalHandlerBuilder = ({
const endpointDefinition = context.endpointDefinitions[endpointName]
const onQueryStarted = endpointDefinition?.onQueryStarted
if (onQueryStarted) {
const lifecycle = {} as CacheLifecycle
const queryFulfilled =
new (Promise as PromiseConstructorWithKnownReason)<
{ data: unknown; meta: unknown },
QueryFulfilledRejectionReason<any>
>((resolve, reject) => {
lifecycle.resolve = resolve
lifecycle.reject = reject
})
const { promise: queryFulfilled, ...lifecycle } = promiseWithResolvers<
{ data: unknown; meta: unknown },
QueryFulfilledRejectionReason<any>
>()
// prevent uncaught promise rejections from happening.
// if the original promise is used in any way, that will create a new promise that will throw again
queryFulfilled.catch(() => {})
Expand Down
47 changes: 0 additions & 47 deletions packages/toolkit/src/query/core/buildMiddleware/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,50 +83,3 @@ export type ApiMiddlewareInternalHandler<Return = void> = (
export type InternalHandlerBuilder<ReturnType = void> = (
input: BuildSubMiddlewareInput
) => ApiMiddlewareInternalHandler<ReturnType>

export interface PromiseConstructorWithKnownReason {
/**
* Creates a new Promise with a known rejection reason.
* @param executor A callback used to initialize the promise. This callback is passed two arguments:
* a resolve callback used to resolve the promise with a value or the result of another promise,
* and a reject callback used to reject the promise with a provided reason or error.
*/
new <T, R>(
executor: (
resolve: (value: T | PromiseLike<T>) => void,
reject: (reason?: R) => void
) => void
): PromiseWithKnownReason<T, R>
}

export interface PromiseWithKnownReason<T, R>
extends Omit<Promise<T>, 'then' | 'catch'> {
/**
* Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved.
* @param onrejected The callback to execute when the Promise is rejected.
* @returns A Promise for the completion of which ever callback is executed.
*/
then<TResult1 = T, TResult2 = never>(
onfulfilled?:
| ((value: T) => TResult1 | PromiseLike<TResult1>)
| undefined
| null,
onrejected?:
| ((reason: R) => TResult2 | PromiseLike<TResult2>)
| undefined
| null
): Promise<TResult1 | TResult2>

/**
* Attaches a callback for only the rejection of the Promise.
* @param onrejected The callback to execute when the Promise is rejected.
* @returns A Promise for the completion of the callback.
*/
catch<TResult = never>(
onrejected?:
| ((reason: R) => TResult | PromiseLike<TResult>)
| undefined
| null
): Promise<T | TResult>
}
67 changes: 67 additions & 0 deletions packages/toolkit/src/query/utils/promiseWithResolvers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { safeAssign } from "../tsHelpers";

export interface PromiseConstructorWithKnownReason {
/**
* Creates a new Promise with a known rejection reason.
* @param executor A callback used to initialize the promise. This callback is passed two arguments:
* a resolve callback used to resolve the promise with a value or the result of another promise,
* and a reject callback used to reject the promise with a provided reason or error.
*/
new <T, R>(
executor: (
resolve: (value: T | PromiseLike<T>) => void,
reject: (reason?: R) => void
) => void
): PromiseWithKnownReason<T, R>
}

export interface PromiseWithKnownReason<T, R>
extends Omit<Promise<T>, 'then' | 'catch'> {
/**
* Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved.
* @param onrejected The callback to execute when the Promise is rejected.
* @returns A Promise for the completion of which ever callback is executed.
*/
then<TResult1 = T, TResult2 = never>(
onfulfilled?:
| ((value: T) => TResult1 | PromiseLike<TResult1>)
| undefined
| null,
onrejected?:
| ((reason: R) => TResult2 | PromiseLike<TResult2>)
| undefined
| null
): Promise<TResult1 | TResult2>

/**
* Attaches a callback for only the rejection of the Promise.
* @param onrejected The callback to execute when the Promise is rejected.
* @returns A Promise for the completion of the callback.
*/
catch<TResult = never>(
onrejected?:
| ((reason: R) => TResult | PromiseLike<TResult>)
| undefined
| null
): Promise<T | TResult>
}

export const PromiseWithKnownReason = Promise as PromiseConstructorWithKnownReason


type PromiseExecutor<T, R> = ConstructorParameters<typeof PromiseWithKnownReason<T, R>>[0];

export type PromiseWithResolvers<T, R> = {
promise: PromiseWithKnownReason<T, R>;
resolve: Parameters<PromiseExecutor<T, R>>[0];
reject: Parameters<PromiseExecutor<T, R>>[1];
};

export const promiseWithResolvers = <T, R = unknown>(): PromiseWithResolvers<T, R> => {
const result = {} as PromiseWithResolvers<T, R>;
result.promise = new PromiseWithKnownReason<T, R>((resolve, reject) => {
safeAssign(result, { reject, resolve });
});
return result;
};

1 comment on commit 8c38f92

@EskiMojo14
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@phryneas tagging for future review

Please sign in to comment.