@reduxjs/toolkit
Advanced tools
Comparing version 2.3.0 to 2.4.0
@@ -133,6 +133,18 @@ import * as _reduxjs_toolkit_query from '@reduxjs/toolkit/query'; | ||
LazyQueryTrigger<D>, | ||
UseQueryStateResult<D, R>, | ||
UseLazyQueryStateResult<D, R>, | ||
UseLazyQueryLastPromiseInfo<D> | ||
]; | ||
type TypedUseLazyQuery<ResultType, QueryArg, BaseQuery extends BaseQueryFn> = UseLazyQuery<QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>>; | ||
type UseLazyQueryStateResult<D extends QueryDefinition<any, any, any, any>, R = UseQueryStateDefaultResult<D>> = UseQueryStateResult<D, R> & { | ||
/** | ||
* Resets the hook state to its initial `uninitialized` state. | ||
* This will also remove the last result from the cache. | ||
*/ | ||
reset: () => void; | ||
}; | ||
/** | ||
* Helper type to manually type the result | ||
* of the `useLazyQuery` hook in userland code. | ||
*/ | ||
type TypedUseLazyQueryStateResult<ResultType, QueryArg, BaseQuery extends BaseQueryFn, R = UseQueryStateDefaultResult<QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>>> = UseLazyQueryStateResult<QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>, R>; | ||
type LazyQueryTrigger<D extends QueryDefinition<any, any, any, any>> = { | ||
@@ -173,3 +185,9 @@ /** | ||
*/ | ||
type UseLazyQuerySubscription<D extends QueryDefinition<any, any, any, any>> = (options?: SubscriptionOptions) => readonly [LazyQueryTrigger<D>, QueryArgFrom<D> | UninitializedValue]; | ||
type UseLazyQuerySubscription<D extends QueryDefinition<any, any, any, any>> = (options?: SubscriptionOptions) => readonly [ | ||
LazyQueryTrigger<D>, | ||
QueryArgFrom<D> | UninitializedValue, | ||
{ | ||
reset: () => void; | ||
} | ||
]; | ||
type TypedUseLazyQuerySubscription<ResultType, QueryArg, BaseQuery extends BaseQueryFn> = UseLazyQuerySubscription<QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>>; | ||
@@ -266,3 +284,3 @@ /** | ||
* | ||
* @since 2.7.9 | ||
* @since 2.3.0 | ||
* @public | ||
@@ -410,3 +428,3 @@ */ | ||
* | ||
* @since 2.7.8 | ||
* @since 2.2.8 | ||
* @public | ||
@@ -663,2 +681,2 @@ */ | ||
export { ApiProvider, type TypedLazyQueryTrigger, type TypedMutationTrigger, type TypedQueryStateSelector, type TypedUseLazyQuery, type TypedUseLazyQuerySubscription, type TypedUseMutation, type TypedUseMutationResult, type TypedUseQuery, type TypedUseQueryHookResult, type TypedUseQueryState, type TypedUseQueryStateOptions, type TypedUseQueryStateResult, type TypedUseQuerySubscription, type TypedUseQuerySubscriptionResult, createApi, reactHooksModule }; | ||
export { ApiProvider, type TypedLazyQueryTrigger, type TypedMutationTrigger, type TypedQueryStateSelector, type TypedUseLazyQuery, type TypedUseLazyQueryStateResult, type TypedUseLazyQuerySubscription, type TypedUseMutation, type TypedUseMutationResult, type TypedUseQuery, type TypedUseQueryHookResult, type TypedUseQueryState, type TypedUseQueryStateOptions, type TypedUseQueryStateResult, type TypedUseQuerySubscription, type TypedUseQuerySubscriptionResult, createApi, reactHooksModule }; |
@@ -41,15 +41,2 @@ var __defProp = Object.defineProperty; | ||
// src/query/endpointDefinitions.ts | ||
function isQueryDefinition(e) { | ||
return e.type === "query" /* query */; | ||
} | ||
function isMutationDefinition(e) { | ||
return e.type === "mutation" /* mutation */; | ||
} | ||
// src/query/tsHelpers.ts | ||
function safeAssign(target, ...args) { | ||
return Object.assign(target, ...args); | ||
} | ||
// src/query/utils/capitalize.ts | ||
@@ -72,2 +59,15 @@ function capitalize(str) { | ||
// src/query/endpointDefinitions.ts | ||
function isQueryDefinition(e) { | ||
return e.type === "query" /* query */; | ||
} | ||
function isMutationDefinition(e) { | ||
return e.type === "mutation" /* mutation */; | ||
} | ||
// src/query/tsHelpers.ts | ||
function safeAssign(target, ...args) { | ||
return Object.assign(target, ...args); | ||
} | ||
// src/query/react/buildHooks.ts | ||
@@ -204,3 +204,3 @@ import { formatProdErrorMessage as _formatProdErrorMessage, formatProdErrorMessage as _formatProdErrorMessage2 } from "@reduxjs/toolkit"; | ||
const isLoading = (!lastResult || lastResult.isLoading || lastResult.isUninitialized) && !hasData && isFetching; | ||
const isSuccess = currentState.isSuccess || isFetching && hasData; | ||
const isSuccess = currentState.isSuccess || hasData && (isFetching && !(lastResult == null ? void 0 : lastResult.isError) || currentState.isUninitialized); | ||
return __spreadProps(__spreadValues({}, currentState), { | ||
@@ -362,2 +362,10 @@ data, | ||
}, [dispatch, initiate]); | ||
const reset = useCallback(() => { | ||
var _a, _b; | ||
if ((_a = promiseRef.current) == null ? void 0 : _a.queryCacheKey) { | ||
dispatch(api.internalActions.removeQueryResult({ | ||
queryCacheKey: (_b = promiseRef.current) == null ? void 0 : _b.queryCacheKey | ||
})); | ||
} | ||
}, [dispatch]); | ||
useEffect3(() => { | ||
@@ -374,3 +382,5 @@ return () => { | ||
}, [arg, trigger]); | ||
return useMemo2(() => [trigger, arg], [trigger, arg]); | ||
return useMemo2(() => [trigger, arg, { | ||
reset | ||
}], [trigger, arg, reset]); | ||
}; | ||
@@ -409,3 +419,5 @@ const useQueryState = (arg, { | ||
useLazyQuery(options) { | ||
const [trigger, arg] = useLazyQuerySubscription(options); | ||
const [trigger, arg, { | ||
reset | ||
}] = useLazyQuerySubscription(options); | ||
const queryStateResults = useQueryState(arg, __spreadProps(__spreadValues({}, options), { | ||
@@ -417,3 +429,5 @@ skip: arg === UNINITIALIZED_VALUE | ||
}), [arg]); | ||
return useMemo2(() => [trigger, queryStateResults, info], [trigger, queryStateResults, info]); | ||
return useMemo2(() => [trigger, __spreadProps(__spreadValues({}, queryStateResults), { | ||
reset | ||
}), info], [trigger, queryStateResults, reset, info]); | ||
}, | ||
@@ -420,0 +434,0 @@ useQuery(arg, options) { |
{ | ||
"name": "@reduxjs/toolkit", | ||
"version": "2.3.0", | ||
"version": "2.4.0", | ||
"description": "The official, opinionated, batteries-included toolset for efficient Redux development", | ||
@@ -5,0 +5,0 @@ "author": "Mark Erikson <mark@isquaredsoftware.com>", |
@@ -18,9 +18,2 @@ import type { StoreEnhancer } from 'redux' | ||
// requestAnimationFrame won't exist in SSR environments. | ||
// Fall back to a vague approximation just to keep from erroring. | ||
const rAF = | ||
typeof window !== 'undefined' && window.requestAnimationFrame | ||
? window.requestAnimationFrame | ||
: createQueueWithTimer(10) | ||
export type AutoBatchOptions = | ||
@@ -70,3 +63,6 @@ | { type: 'tick' } | ||
: options.type === 'raf' | ||
? rAF | ||
? // requestAnimationFrame won't exist in SSR environments. Fall back to a vague approximation just to keep from erroring. | ||
typeof window !== 'undefined' && window.requestAnimationFrame | ||
? window.requestAnimationFrame | ||
: createQueueWithTimer(10) | ||
: options.type === 'callback' | ||
@@ -73,0 +69,0 @@ ? options.queueNotification |
@@ -11,3 +11,3 @@ import type { Reducer, StateFromReducersMapObject, UnknownAction } from 'redux' | ||
} from './tsHelpers' | ||
import { emplace } from './utils' | ||
import { getOrInsertComputed } from './utils' | ||
@@ -328,4 +328,6 @@ type SliceLike<ReducerPath extends string, State> = { | ||
) => | ||
emplace(stateProxyMap, state, { | ||
insert: () => | ||
getOrInsertComputed( | ||
stateProxyMap, | ||
state, | ||
() => | ||
new Proxy(state, { | ||
@@ -355,3 +357,3 @@ get: (target, prop, receiver) => { | ||
}), | ||
}) as State | ||
) as State | ||
@@ -358,0 +360,0 @@ const original = (state: any) => { |
@@ -440,3 +440,5 @@ import type { Dispatch, UnknownAction } from 'redux' | ||
type CreateAsyncThunk<CurriedThunkApiConfig extends AsyncThunkConfig> = { | ||
export type CreateAsyncThunkFunction< | ||
CurriedThunkApiConfig extends AsyncThunkConfig, | ||
> = { | ||
/** | ||
@@ -485,8 +487,11 @@ * | ||
> | ||
withTypes<ThunkApiConfig extends AsyncThunkConfig>(): CreateAsyncThunk< | ||
OverrideThunkApiConfigs<CurriedThunkApiConfig, ThunkApiConfig> | ||
> | ||
} | ||
type CreateAsyncThunk<CurriedThunkApiConfig extends AsyncThunkConfig> = | ||
CreateAsyncThunkFunction<CurriedThunkApiConfig> & { | ||
withTypes<ThunkApiConfig extends AsyncThunkConfig>(): CreateAsyncThunk< | ||
OverrideThunkApiConfigs<CurriedThunkApiConfig, ThunkApiConfig> | ||
> | ||
} | ||
export const createAsyncThunk = /* @__PURE__ */ (() => { | ||
@@ -493,0 +498,0 @@ function createAsyncThunk< |
@@ -29,3 +29,3 @@ import type { Action, Reducer, UnknownAction } from 'redux' | ||
import type { Id, TypeGuard } from './tsHelpers' | ||
import { emplace } from './utils' | ||
import { getOrInsertComputed } from './utils' | ||
@@ -773,21 +773,21 @@ const asyncThunkSymbol = /* @__PURE__ */ Symbol.for( | ||
) { | ||
const selectorCache = emplace(injectedSelectorCache, injected, { | ||
insert: () => new WeakMap(), | ||
}) | ||
const selectorCache = getOrInsertComputed( | ||
injectedSelectorCache, | ||
injected, | ||
() => new WeakMap(), | ||
) | ||
return emplace(selectorCache, selectState, { | ||
insert: () => { | ||
const map: Record<string, Selector<any, any>> = {} | ||
for (const [name, selector] of Object.entries( | ||
options.selectors ?? {}, | ||
)) { | ||
map[name] = wrapSelector( | ||
selector, | ||
selectState, | ||
getInitialState, | ||
injected, | ||
) | ||
} | ||
return map | ||
}, | ||
return getOrInsertComputed(selectorCache, selectState, () => { | ||
const map: Record<string, Selector<any, any>> = {} | ||
for (const [name, selector] of Object.entries( | ||
options.selectors ?? {}, | ||
)) { | ||
map[name] = wrapSelector( | ||
selector, | ||
selectState, | ||
getInitialState, | ||
injected, | ||
) | ||
} | ||
return map | ||
}) as any | ||
@@ -794,0 +794,0 @@ } |
@@ -6,3 +6,3 @@ import type { Dispatch, Middleware, UnknownAction } from 'redux' | ||
import { nanoid } from '../nanoid' | ||
import { emplace, find } from '../utils' | ||
import { getOrInsertComputed } from '../utils' | ||
import type { | ||
@@ -27,3 +27,2 @@ AddMiddleware, | ||
): MiddlewareEntry<State, DispatchType> => ({ | ||
id: nanoid(), | ||
middleware, | ||
@@ -43,3 +42,6 @@ applied: new Map(), | ||
const instanceId = nanoid() | ||
const middlewareMap = new Map<string, MiddlewareEntry<State, DispatchType>>() | ||
const middlewareMap = new Map< | ||
Middleware<any, State, DispatchType>, | ||
MiddlewareEntry<State, DispatchType> | ||
>() | ||
@@ -64,10 +66,3 @@ const withMiddleware = Object.assign( | ||
middlewares.forEach((middleware) => { | ||
let entry = find( | ||
Array.from(middlewareMap.values()), | ||
(entry) => entry.middleware === middleware, | ||
) | ||
if (!entry) { | ||
entry = createMiddlewareEntry(middleware) | ||
} | ||
middlewareMap.set(entry.id, entry) | ||
getOrInsertComputed(middlewareMap, middleware, createMiddlewareEntry) | ||
}) | ||
@@ -80,3 +75,3 @@ }, | ||
const appliedMiddleware = Array.from(middlewareMap.values()).map((entry) => | ||
emplace(entry.applied, api, { insert: () => entry.middleware(api) }), | ||
getOrInsertComputed(entry.applied, api, entry.middleware), | ||
) | ||
@@ -83,0 +78,0 @@ return compose(...appliedMiddleware) |
@@ -62,3 +62,2 @@ import type { Dispatch, Middleware, MiddlewareAPI, UnknownAction } from 'redux' | ||
> = { | ||
id: string | ||
middleware: Middleware<any, State, DispatchType> | ||
@@ -65,0 +64,0 @@ applied: Map< |
@@ -136,2 +136,3 @@ // This must remain here so that the `mangleErrors.cjs` build script | ||
SerializedError, | ||
CreateAsyncThunkFunction, | ||
} from './createAsyncThunk' | ||
@@ -138,0 +139,0 @@ |
@@ -7,3 +7,2 @@ import type { Action, Dispatch, MiddlewareAPI, UnknownAction } from 'redux' | ||
import { find } from '../utils' | ||
import { | ||
@@ -225,5 +224,4 @@ TaskAbortError, | ||
const id = nanoid() | ||
const entry: ListenerEntry<unknown> = { | ||
id, | ||
id: nanoid(), | ||
effect, | ||
@@ -243,2 +241,18 @@ type, | ||
const findListenerEntry = ( | ||
listenerMap: Map<string, ListenerEntry>, | ||
options: FallbackAddListenerOptions, | ||
) => { | ||
const { type, effect, predicate } = getListenerEntryPropsFrom(options) | ||
return Array.from(listenerMap.values()).find((entry) => { | ||
const matchPredicateOrType = | ||
typeof type === 'string' | ||
? entry.type === type | ||
: entry.predicate === predicate | ||
return matchPredicateOrType && entry.effect === effect | ||
}) | ||
} | ||
const cancelActiveListeners = ( | ||
@@ -336,3 +350,3 @@ entry: ListenerEntry<unknown, Dispatch<UnknownAction>>, | ||
const insertEntry = (entry: ListenerEntry) => { | ||
entry.unsubscribe = () => listenerMap.delete(entry!.id) | ||
entry.unsubscribe = () => listenerMap.delete(entry.id) | ||
@@ -349,11 +363,6 @@ listenerMap.set(entry.id, entry) | ||
const startListening = ((options: FallbackAddListenerOptions) => { | ||
let entry = find( | ||
Array.from(listenerMap.values()), | ||
(existingEntry) => existingEntry.effect === options.effect, | ||
) | ||
const entry = | ||
findListenerEntry(listenerMap, options) ?? | ||
createListenerEntry(options as any) | ||
if (!entry) { | ||
entry = createListenerEntry(options as any) | ||
} | ||
return insertEntry(entry) | ||
@@ -369,13 +378,4 @@ }) as AddListenerOverloads<any> | ||
): boolean => { | ||
const { type, effect, predicate } = getListenerEntryPropsFrom(options) | ||
const entry = findListenerEntry(listenerMap, options) | ||
const entry = find(Array.from(listenerMap.values()), (entry) => { | ||
const matchPredicateOrType = | ||
typeof type === 'string' | ||
? entry.type === type | ||
: entry.predicate === predicate | ||
return matchPredicateOrType && entry.effect === effect | ||
}) | ||
if (entry) { | ||
@@ -382,0 +382,0 @@ entry.unsubscribe() |
@@ -120,2 +120,3 @@ import { | ||
const testAction2 = createAction<string>('testAction2') | ||
type TestAction2 = ReturnType<typeof testAction2> | ||
const testAction3 = createAction<string>('testAction3') | ||
@@ -343,2 +344,23 @@ | ||
test('subscribing with the same effect but different predicate is allowed', () => { | ||
const effect = vi.fn((_: TestAction1 | TestAction2) => {}) | ||
startListening({ | ||
actionCreator: testAction1, | ||
effect, | ||
}) | ||
startListening({ | ||
actionCreator: testAction2, | ||
effect, | ||
}) | ||
store.dispatch(testAction1('a')) | ||
store.dispatch(testAction2('b')) | ||
expect(effect.mock.calls).toEqual([ | ||
[testAction1('a'), middlewareApi], | ||
[testAction2('b'), middlewareApi], | ||
]) | ||
}) | ||
test('unsubscribing via callback', () => { | ||
@@ -345,0 +367,0 @@ const effect = vi.fn((_: TestAction1) => {}) |
@@ -581,5 +581,9 @@ import type { | ||
UnknownAction | ||
>, | ||
OverrideExtraArgument = unknown, | ||
>() => TypedAddListener<OverrideStateType, OverrideDispatchType, OverrideExtraArgument> | ||
>, | ||
OverrideExtraArgument = unknown, | ||
>() => TypedAddListener< | ||
OverrideStateType, | ||
OverrideDispatchType, | ||
OverrideExtraArgument | ||
> | ||
} | ||
@@ -645,3 +649,7 @@ | ||
OverrideExtraArgument = unknown, | ||
>() => TypedRemoveListener<OverrideStateType, OverrideDispatchType, OverrideExtraArgument> | ||
>() => TypedRemoveListener< | ||
OverrideStateType, | ||
OverrideDispatchType, | ||
OverrideExtraArgument | ||
> | ||
} | ||
@@ -706,3 +714,7 @@ | ||
OverrideExtraArgument = unknown, | ||
>() => TypedStartListening<OverrideStateType, OverrideDispatchType, OverrideExtraArgument> | ||
>() => TypedStartListening< | ||
OverrideStateType, | ||
OverrideDispatchType, | ||
OverrideExtraArgument | ||
> | ||
} | ||
@@ -762,3 +774,7 @@ | ||
OverrideExtraArgument = unknown, | ||
>() => TypedStopListening<OverrideStateType, OverrideDispatchType, OverrideExtraArgument> | ||
>() => TypedStopListening< | ||
OverrideStateType, | ||
OverrideDispatchType, | ||
OverrideExtraArgument | ||
> | ||
} | ||
@@ -820,3 +836,7 @@ | ||
OverrideExtraArgument = unknown, | ||
>() => TypedStopListening<OverrideStateType, OverrideDispatchType, OverrideExtraArgument> | ||
>() => TypedStopListening< | ||
OverrideStateType, | ||
OverrideDispatchType, | ||
OverrideExtraArgument | ||
> | ||
} | ||
@@ -823,0 +843,0 @@ |
@@ -19,3 +19,3 @@ import type { | ||
} from '../endpointDefinitions' | ||
import { countObjectKeys, isNotNullish } from '../utils' | ||
import { countObjectKeys, getOrInsert, isNotNullish } from '../utils' | ||
import type { SubscriptionOptions } from './apiState' | ||
@@ -395,5 +395,4 @@ import type { QueryResultSelectorResult } from './buildSelectors' | ||
if (!runningQuery && !skippedSynchronously && !forceQueryFn) { | ||
const running = runningQueries.get(dispatch) || {} | ||
const running = getOrInsert(runningQueries, dispatch, {}) | ||
running[queryCacheKey] = statePromise | ||
runningQueries.set(dispatch, running) | ||
@@ -400,0 +399,0 @@ statePromise.then(() => { |
@@ -38,2 +38,4 @@ import type { | ||
ReferenceQueryLifecycle, | ||
TypedMutationOnQueryStarted, | ||
TypedQueryOnQueryStarted, | ||
} from './queryLifecycle' | ||
@@ -52,3 +54,3 @@ export type { SubscriptionSelectors } from './types' | ||
invalidateTags: createAction< | ||
Array<TagTypes | FullTagDescription<TagTypes>> | ||
Array<TagTypes | FullTagDescription<TagTypes> | null | undefined> | ||
>(`${reducerPath}/invalidateTags`), | ||
@@ -153,3 +155,8 @@ } | ||
) { | ||
return (input.api.endpoints[querySubState.endpointName] as ApiEndpointQuery<any, any>).initiate(querySubState.originalArgs as any, { | ||
return ( | ||
input.api.endpoints[querySubState.endpointName] as ApiEndpointQuery< | ||
any, | ||
any | ||
> | ||
).initiate(querySubState.originalArgs as any, { | ||
subscribe: false, | ||
@@ -156,0 +163,0 @@ forceRefetch: true, |
@@ -114,4 +114,9 @@ import type { | ||
onQueryStarted?( | ||
arg: QueryArg, | ||
api: QueryLifecycleApi<QueryArg, BaseQuery, ResultType, ReducerPath>, | ||
queryArgument: QueryArg, | ||
queryLifeCycleApi: QueryLifecycleApi< | ||
QueryArg, | ||
BaseQuery, | ||
ResultType, | ||
ReducerPath | ||
>, | ||
): Promise<void> | void | ||
@@ -175,4 +180,9 @@ } | ||
onQueryStarted?( | ||
arg: QueryArg, | ||
api: MutationLifecycleApi<QueryArg, BaseQuery, ResultType, ReducerPath>, | ||
queryArgument: QueryArg, | ||
mutationLifeCycleApi: MutationLifecycleApi< | ||
QueryArg, | ||
BaseQuery, | ||
ResultType, | ||
ReducerPath | ||
>, | ||
): Promise<void> | void | ||
@@ -197,2 +207,208 @@ } | ||
/** | ||
* Provides a way to define a strongly-typed version of | ||
* {@linkcode QueryLifecycleQueryExtraOptions.onQueryStarted | onQueryStarted} | ||
* for a specific query. | ||
* | ||
* @example | ||
* <caption>#### __Create and reuse a strongly-typed `onQueryStarted` function__</caption> | ||
* | ||
* ```ts | ||
* import type { TypedQueryOnQueryStarted } from '@reduxjs/toolkit/query' | ||
* import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query' | ||
* | ||
* type Post = { | ||
* id: number | ||
* title: string | ||
* userId: number | ||
* } | ||
* | ||
* type PostsApiResponse = { | ||
* posts: Post[] | ||
* total: number | ||
* skip: number | ||
* limit: number | ||
* } | ||
* | ||
* type QueryArgument = number | undefined | ||
* | ||
* type BaseQueryFunction = ReturnType<typeof fetchBaseQuery> | ||
* | ||
* const baseApiSlice = createApi({ | ||
* baseQuery: fetchBaseQuery({ baseUrl: 'https://dummyjson.com' }), | ||
* reducerPath: 'postsApi', | ||
* tagTypes: ['Posts'], | ||
* endpoints: (builder) => ({ | ||
* getPosts: builder.query<PostsApiResponse, void>({ | ||
* query: () => `/posts`, | ||
* }), | ||
* | ||
* getPostById: builder.query<Post, QueryArgument>({ | ||
* query: (postId) => `/posts/${postId}`, | ||
* }), | ||
* }), | ||
* }) | ||
* | ||
* const updatePostOnFulfilled: TypedQueryOnQueryStarted< | ||
* PostsApiResponse, | ||
* QueryArgument, | ||
* BaseQueryFunction, | ||
* 'postsApi' | ||
* > = async (queryArgument, { dispatch, queryFulfilled }) => { | ||
* const result = await queryFulfilled | ||
* | ||
* const { posts } = result.data | ||
* | ||
* // Pre-fill the individual post entries with the results | ||
* // from the list endpoint query | ||
* dispatch( | ||
* baseApiSlice.util.upsertQueryEntries( | ||
* posts.map((post) => ({ | ||
* endpointName: 'getPostById', | ||
* arg: post.id, | ||
* value: post, | ||
* })), | ||
* ), | ||
* ) | ||
* } | ||
* | ||
* export const extendedApiSlice = baseApiSlice.injectEndpoints({ | ||
* endpoints: (builder) => ({ | ||
* getPostsByUserId: builder.query<PostsApiResponse, QueryArgument>({ | ||
* query: (userId) => `/posts/user/${userId}`, | ||
* | ||
* onQueryStarted: updatePostOnFulfilled, | ||
* }), | ||
* }), | ||
* }) | ||
* ``` | ||
* | ||
* @template ResultType - The type of the result `data` returned by the query. | ||
* @template QueryArgumentType - The type of the argument passed into the query. | ||
* @template BaseQueryFunctionType - The type of the base query function being used. | ||
* @template ReducerPath - The type representing the `reducerPath` for the API slice. | ||
* | ||
* @since 2.4.0 | ||
* @public | ||
*/ | ||
export type TypedQueryOnQueryStarted< | ||
ResultType, | ||
QueryArgumentType, | ||
BaseQueryFunctionType extends BaseQueryFn, | ||
ReducerPath extends string = string, | ||
> = QueryLifecycleQueryExtraOptions< | ||
ResultType, | ||
QueryArgumentType, | ||
BaseQueryFunctionType, | ||
ReducerPath | ||
>['onQueryStarted'] | ||
/** | ||
* Provides a way to define a strongly-typed version of | ||
* {@linkcode QueryLifecycleMutationExtraOptions.onQueryStarted | onQueryStarted} | ||
* for a specific mutation. | ||
* | ||
* @example | ||
* <caption>#### __Create and reuse a strongly-typed `onQueryStarted` function__</caption> | ||
* | ||
* ```ts | ||
* import type { TypedMutationOnQueryStarted } from '@reduxjs/toolkit/query' | ||
* import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query' | ||
* | ||
* type Post = { | ||
* id: number | ||
* title: string | ||
* userId: number | ||
* } | ||
* | ||
* type PostsApiResponse = { | ||
* posts: Post[] | ||
* total: number | ||
* skip: number | ||
* limit: number | ||
* } | ||
* | ||
* type QueryArgument = Pick<Post, 'id'> & Partial<Post> | ||
* | ||
* type BaseQueryFunction = ReturnType<typeof fetchBaseQuery> | ||
* | ||
* const baseApiSlice = createApi({ | ||
* baseQuery: fetchBaseQuery({ baseUrl: 'https://dummyjson.com' }), | ||
* reducerPath: 'postsApi', | ||
* tagTypes: ['Posts'], | ||
* endpoints: (builder) => ({ | ||
* getPosts: builder.query<PostsApiResponse, void>({ | ||
* query: () => `/posts`, | ||
* }), | ||
* | ||
* getPostById: builder.query<Post, number>({ | ||
* query: (postId) => `/posts/${postId}`, | ||
* }), | ||
* }), | ||
* }) | ||
* | ||
* const updatePostOnFulfilled: TypedMutationOnQueryStarted< | ||
* Post, | ||
* QueryArgument, | ||
* BaseQueryFunction, | ||
* 'postsApi' | ||
* > = async ({ id, ...patch }, { dispatch, queryFulfilled }) => { | ||
* const patchCollection = dispatch( | ||
* baseApiSlice.util.updateQueryData('getPostById', id, (draftPost) => { | ||
* Object.assign(draftPost, patch) | ||
* }), | ||
* ) | ||
* | ||
* try { | ||
* await queryFulfilled | ||
* } catch { | ||
* patchCollection.undo() | ||
* } | ||
* } | ||
* | ||
* export const extendedApiSlice = baseApiSlice.injectEndpoints({ | ||
* endpoints: (builder) => ({ | ||
* addPost: builder.mutation<Post, Omit<QueryArgument, 'id'>>({ | ||
* query: (body) => ({ | ||
* url: `posts/add`, | ||
* method: 'POST', | ||
* body, | ||
* }), | ||
* | ||
* onQueryStarted: updatePostOnFulfilled, | ||
* }), | ||
* | ||
* updatePost: builder.mutation<Post, QueryArgument>({ | ||
* query: ({ id, ...patch }) => ({ | ||
* url: `post/${id}`, | ||
* method: 'PATCH', | ||
* body: patch, | ||
* }), | ||
* | ||
* onQueryStarted: updatePostOnFulfilled, | ||
* }), | ||
* }), | ||
* }) | ||
* ``` | ||
* | ||
* @template ResultType - The type of the result `data` returned by the query. | ||
* @template QueryArgumentType - The type of the argument passed into the query. | ||
* @template BaseQueryFunctionType - The type of the base query function being used. | ||
* @template ReducerPath - The type representing the `reducerPath` for the API slice. | ||
* | ||
* @since 2.4.0 | ||
* @public | ||
*/ | ||
export type TypedMutationOnQueryStarted< | ||
ResultType, | ||
QueryArgumentType, | ||
BaseQueryFunctionType extends BaseQueryFn, | ||
ReducerPath extends string = string, | ||
> = QueryLifecycleMutationExtraOptions< | ||
ResultType, | ||
QueryArgumentType, | ||
BaseQueryFunctionType, | ||
ReducerPath | ||
>['onQueryStarted'] | ||
export const buildQueryLifecycleHandler: InternalHandlerBuilder = ({ | ||
@@ -199,0 +415,0 @@ api, |
@@ -12,3 +12,3 @@ import type { InternalSerializeQueryArgs } from '../defaultSerializeQueryArgs' | ||
import { expandTagDescription } from '../endpointDefinitions' | ||
import { flatten } from '../utils' | ||
import { flatten, isNotNullish } from '../utils' | ||
import type { | ||
@@ -172,2 +172,5 @@ MutationSubState, | ||
return ((queryArgs: any) => { | ||
if (queryArgs === skipToken) { | ||
return createSelector(selectSkippedQuery, withRequestFlags) | ||
} | ||
const serializedArgs = serializeQueryArgs({ | ||
@@ -181,6 +184,4 @@ queryArgs, | ||
defaultQuerySubState | ||
const finalSelectQuerySubState = | ||
queryArgs === skipToken ? selectSkippedQuery : selectQuerySubstate | ||
return createSelector(finalSelectQuerySubState, withRequestFlags) | ||
return createSelector(selectQuerySubstate, withRequestFlags) | ||
}) as QueryResultSelectorFactory<any, RootState> | ||
@@ -211,3 +212,3 @@ } | ||
state: RootState, | ||
tags: ReadonlyArray<TagDescription<string>>, | ||
tags: ReadonlyArray<TagDescription<string> | null | undefined>, | ||
): Array<{ | ||
@@ -220,3 +221,3 @@ endpointName: string | ||
const toInvalidate = new Set<QueryCacheKey>() | ||
for (const tag of tags.map(expandTagDescription)) { | ||
for (const tag of tags.filter(isNotNullish).map(expandTagDescription)) { | ||
const provided = apiState.provided[tag.type] | ||
@@ -223,0 +224,0 @@ if (!provided) { |
@@ -27,3 +27,5 @@ import { buildCreateApi } from '../createApi' | ||
SubscriptionSelectors, | ||
} from './buildMiddleware' | ||
TypedMutationOnQueryStarted, | ||
TypedQueryOnQueryStarted, | ||
} from './buildMiddleware/index' | ||
export { skipToken } from './buildSelectors' | ||
@@ -30,0 +32,0 @@ export type { |
@@ -353,3 +353,3 @@ /** | ||
invalidateTags: ActionCreatorWithPayload< | ||
Array<TagDescription<TagTypes>>, | ||
Array<TagDescription<TagTypes> | null | undefined>, | ||
string | ||
@@ -365,3 +365,3 @@ > | ||
state: RootState<Definitions, string, ReducerPath>, | ||
tags: ReadonlyArray<TagDescription<TagTypes>>, | ||
tags: ReadonlyArray<TagDescription<TagTypes> | null | undefined>, | ||
) => Array<{ | ||
@@ -368,0 +368,0 @@ endpointName: string |
@@ -32,2 +32,3 @@ import type { Api } from '@reduxjs/toolkit/query' | ||
} from './tsHelpers' | ||
import { isNotNullish } from './utils' | ||
@@ -228,3 +229,3 @@ const resultType = /* @__PURE__ */ Symbol() | ||
meta: MetaType, | ||
) => ReadonlyArray<TagDescription<TagTypes>> | ||
) => ReadonlyArray<TagDescription<TagTypes> | undefined | null> | ||
@@ -247,3 +248,3 @@ export type FullTagDescription<TagType> = { | ||
> = | ||
| ReadonlyArray<TagDescription<TagTypes>> | ||
| ReadonlyArray<TagDescription<TagTypes> | undefined | null> | ||
| GetResultDescriptionFn<TagTypes, ResultType, QueryArg, ErrorType, MetaType> | ||
@@ -784,2 +785,3 @@ | ||
) | ||
.filter(isNotNullish) | ||
.map(expandTagDescription) | ||
@@ -786,0 +788,0 @@ .map(assertTagTypes) |
@@ -71,4 +71,8 @@ // This must remain here so that the `mangleErrors.cjs` build script | ||
export { copyWithStructuralSharing } from './utils/copyWithStructuralSharing' | ||
export { createApi, coreModule, coreModuleName } from './core' | ||
export { createApi, coreModule, coreModuleName } from './core/index' | ||
export type { | ||
TypedMutationOnQueryStarted, | ||
TypedQueryOnQueryStarted, | ||
} from './core/index' | ||
export type { | ||
ApiEndpointMutation, | ||
@@ -75,0 +79,0 @@ ApiEndpointQuery, |
@@ -21,2 +21,3 @@ import type { | ||
QueryArgFrom, | ||
QueryCacheKey, | ||
QueryDefinition, | ||
@@ -259,3 +260,3 @@ QueryKeys, | ||
LazyQueryTrigger<D>, | ||
UseQueryStateResult<D, R>, | ||
UseLazyQueryStateResult<D, R>, | ||
UseLazyQueryLastPromiseInfo<D>, | ||
@@ -272,2 +273,29 @@ ] | ||
export type UseLazyQueryStateResult< | ||
D extends QueryDefinition<any, any, any, any>, | ||
R = UseQueryStateDefaultResult<D>, | ||
> = UseQueryStateResult<D, R> & { | ||
/** | ||
* Resets the hook state to its initial `uninitialized` state. | ||
* This will also remove the last result from the cache. | ||
*/ | ||
reset: () => void | ||
} | ||
/** | ||
* Helper type to manually type the result | ||
* of the `useLazyQuery` hook in userland code. | ||
*/ | ||
export type TypedUseLazyQueryStateResult< | ||
ResultType, | ||
QueryArg, | ||
BaseQuery extends BaseQueryFn, | ||
R = UseQueryStateDefaultResult< | ||
QueryDefinition<QueryArg, BaseQuery, string, ResultType, string> | ||
>, | ||
> = UseLazyQueryStateResult< | ||
QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>, | ||
R | ||
> | ||
export type LazyQueryTrigger<D extends QueryDefinition<any, any, any, any>> = { | ||
@@ -323,3 +351,7 @@ /** | ||
options?: SubscriptionOptions, | ||
) => readonly [LazyQueryTrigger<D>, QueryArgFrom<D> | UninitializedValue] | ||
) => readonly [ | ||
LazyQueryTrigger<D>, | ||
QueryArgFrom<D> | UninitializedValue, | ||
{ reset: () => void }, | ||
] | ||
@@ -428,3 +460,3 @@ export type TypedUseLazyQuerySubscription< | ||
* | ||
* @since 2.7.9 | ||
* @since 2.3.0 | ||
* @public | ||
@@ -612,3 +644,3 @@ */ | ||
* | ||
* @since 2.7.8 | ||
* @since 2.2.8 | ||
* @public | ||
@@ -904,2 +936,3 @@ */ | ||
const isFetching = currentState.isLoading | ||
// isLoading = true only when loading while no data is present yet (initial load with no data in the cache) | ||
@@ -910,5 +943,11 @@ const isLoading = | ||
isFetching | ||
// isSuccess = true when data is present | ||
const isSuccess = currentState.isSuccess || (isFetching && hasData) | ||
// isSuccess = true when data is present and we're not refetching after an error. | ||
// That includes cases where the _current_ item is either actively | ||
// fetching or about to fetch due to an uninitialized entry. | ||
const isSuccess = | ||
currentState.isSuccess || | ||
(hasData && | ||
((isFetching && !lastResult?.isError) || currentState.isUninitialized)) | ||
return { | ||
@@ -1173,2 +1212,12 @@ ...currentState, | ||
const reset = useCallback(() => { | ||
if (promiseRef.current?.queryCacheKey) { | ||
dispatch( | ||
api.internalActions.removeQueryResult({ | ||
queryCacheKey: promiseRef.current?.queryCacheKey as QueryCacheKey, | ||
}), | ||
) | ||
} | ||
}, [dispatch]) | ||
/* cleanup on unmount */ | ||
@@ -1188,3 +1237,6 @@ useEffect(() => { | ||
return useMemo(() => [trigger, arg] as const, [trigger, arg]) | ||
return useMemo( | ||
() => [trigger, arg, { reset }] as const, | ||
[trigger, arg, reset], | ||
) | ||
} | ||
@@ -1262,3 +1314,3 @@ | ||
useLazyQuery(options) { | ||
const [trigger, arg] = useLazyQuerySubscription(options) | ||
const [trigger, arg, { reset }] = useLazyQuerySubscription(options) | ||
const queryStateResults = useQueryState(arg, { | ||
@@ -1271,4 +1323,4 @@ ...options, | ||
return useMemo( | ||
() => [trigger, queryStateResults, info], | ||
[trigger, queryStateResults, info], | ||
() => [trigger, { ...queryStateResults, reset }, info], | ||
[trigger, queryStateResults, reset, info], | ||
) | ||
@@ -1275,0 +1327,0 @@ }, |
@@ -31,4 +31,5 @@ // This must remain here so that the `mangleErrors.cjs` build script | ||
TypedUseQueryStateOptions, | ||
TypedUseLazyQueryStateResult, | ||
} from './buildHooks' | ||
export { UNINITIALIZED_VALUE } from './constants' | ||
export { createApi, reactHooksModule, reactHooksModuleName } |
@@ -0,0 +0,0 @@ import type { UseMutation, UseLazyQuery, UseQuery } from './buildHooks' |
@@ -8,2 +8,3 @@ import type { | ||
BaseQueryFn, | ||
BaseQueryMeta, | ||
} from './baseQueryTypes' | ||
@@ -68,4 +69,7 @@ import type { FetchBaseQueryError } from './fetchBaseQuery' | ||
function fail(e: any): never { | ||
throw Object.assign(new HandledError({ error: e }), { | ||
function fail<BaseQuery extends BaseQueryFn = BaseQueryFn>( | ||
error: BaseQueryError<BaseQuery>, | ||
meta?: BaseQueryMeta<BaseQuery>, | ||
): never { | ||
throw Object.assign(new HandledError({ error, meta }), { | ||
throwImmediately: true, | ||
@@ -72,0 +76,0 @@ }) |
@@ -42,3 +42,3 @@ import { setupApiStore } from '@internal/tests/utils/helpers' | ||
.parameter(0) | ||
.toEqualTypeOf<TagDescription<never>[]>() | ||
.toEqualTypeOf<(null | undefined | TagDescription<never>)[]>() | ||
}) | ||
@@ -45,0 +45,0 @@ |
@@ -1,2 +0,7 @@ | ||
import type { RetryOptions } from '@internal/query/retry' | ||
import { retry, type RetryOptions } from '@internal/query/retry' | ||
import { | ||
fetchBaseQuery, | ||
type FetchBaseQueryError, | ||
type FetchBaseQueryMeta, | ||
} from '@internal/query/fetchBaseQuery' | ||
@@ -17,4 +22,26 @@ describe('type tests', () => { | ||
}) | ||
test('fail can be pretyped to only accept correct error and meta', () => { | ||
expectTypeOf(retry.fail).parameter(0).toEqualTypeOf<unknown>() | ||
expectTypeOf(retry.fail).parameter(1).toEqualTypeOf<{} | undefined>() | ||
expectTypeOf(retry.fail).toBeCallableWith('Literally anything', {}) | ||
const myBaseQuery = fetchBaseQuery() | ||
const typedFail = retry.fail<typeof myBaseQuery> | ||
expectTypeOf(typedFail).parameter(0).toMatchTypeOf<FetchBaseQueryError>() | ||
expectTypeOf(typedFail) | ||
.parameter(1) | ||
.toMatchTypeOf<FetchBaseQueryMeta | undefined>() | ||
expectTypeOf(typedFail).toBeCallableWith( | ||
{ | ||
status: 401, | ||
data: 'Unauthorized', | ||
}, | ||
{ request: new Request('http://localhost') }, | ||
) | ||
expectTypeOf(typedFail).parameter(0).not.toMatchTypeOf<string>() | ||
expectTypeOf(typedFail).parameter(1).not.toMatchTypeOf<{}>() | ||
}) | ||
}) | ||
export {} |
@@ -12,2 +12,3 @@ import type { UseQueryStateOptions } from '@internal/query/react/buildHooks' | ||
TypedLazyQueryTrigger, | ||
TypedUseLazyQueryStateResult, | ||
TypedUseLazyQuery, | ||
@@ -824,3 +825,3 @@ TypedUseLazyQuerySubscription, | ||
expectTypeOf< | ||
TypedUseQueryHookResult<string, void, typeof baseQuery> | ||
TypedUseLazyQueryStateResult<string, void, typeof baseQuery> | ||
>().toMatchTypeOf(result) | ||
@@ -839,3 +840,8 @@ }) | ||
expectTypeOf< | ||
TypedUseQueryHookResult<string, void, typeof baseQuery, { x: boolean }> | ||
TypedUseLazyQueryStateResult< | ||
string, | ||
void, | ||
typeof baseQuery, | ||
{ x: boolean } | ||
> | ||
>().toMatchTypeOf(result) | ||
@@ -842,0 +848,0 @@ }) |
@@ -11,1 +11,2 @@ export * from './capitalize' | ||
export * from './joinUrls' | ||
export * from './getOrInsert' |
@@ -128,1 +128,82 @@ import { configureStore } from '../configureStore' | ||
}) | ||
describe.each(cases)( | ||
'autoBatchEnhancer with fake timers: %j', | ||
(autoBatchOptions) => { | ||
beforeAll(() => { | ||
vitest.useFakeTimers({ | ||
toFake: ['setTimeout', 'queueMicrotask', 'requestAnimationFrame'], | ||
}) | ||
}) | ||
afterAll(() => { | ||
vitest.useRealTimers() | ||
}) | ||
beforeEach(() => { | ||
subscriptionNotifications = 0 | ||
store = makeStore(autoBatchOptions) | ||
store.subscribe(() => { | ||
subscriptionNotifications++ | ||
}) | ||
}) | ||
test('Does not alter normal subscription notification behavior', () => { | ||
store.dispatch(decrementUnbatched()) | ||
expect(subscriptionNotifications).toBe(1) | ||
store.dispatch(decrementUnbatched()) | ||
expect(subscriptionNotifications).toBe(2) | ||
store.dispatch(decrementUnbatched()) | ||
expect(subscriptionNotifications).toBe(3) | ||
store.dispatch(decrementUnbatched()) | ||
vitest.runAllTimers() | ||
expect(subscriptionNotifications).toBe(4) | ||
}) | ||
test('Only notifies once if several batched actions are dispatched in a row', () => { | ||
store.dispatch(incrementBatched()) | ||
expect(subscriptionNotifications).toBe(0) | ||
store.dispatch(incrementBatched()) | ||
expect(subscriptionNotifications).toBe(0) | ||
store.dispatch(incrementBatched()) | ||
expect(subscriptionNotifications).toBe(0) | ||
store.dispatch(incrementBatched()) | ||
vitest.runAllTimers() | ||
expect(subscriptionNotifications).toBe(1) | ||
}) | ||
test('Notifies immediately if a non-batched action is dispatched', () => { | ||
store.dispatch(incrementBatched()) | ||
expect(subscriptionNotifications).toBe(0) | ||
store.dispatch(incrementBatched()) | ||
expect(subscriptionNotifications).toBe(0) | ||
store.dispatch(decrementUnbatched()) | ||
expect(subscriptionNotifications).toBe(1) | ||
store.dispatch(incrementBatched()) | ||
vitest.runAllTimers() | ||
expect(subscriptionNotifications).toBe(2) | ||
}) | ||
test('Does not notify at end of tick if last action was normal priority', () => { | ||
store.dispatch(incrementBatched()) | ||
expect(subscriptionNotifications).toBe(0) | ||
store.dispatch(incrementBatched()) | ||
expect(subscriptionNotifications).toBe(0) | ||
store.dispatch(decrementUnbatched()) | ||
expect(subscriptionNotifications).toBe(1) | ||
store.dispatch(incrementBatched()) | ||
store.dispatch(decrementUnbatched()) | ||
expect(subscriptionNotifications).toBe(2) | ||
store.dispatch(decrementUnbatched()) | ||
expect(subscriptionNotifications).toBe(3) | ||
vitest.runAllTimers() | ||
expect(subscriptionNotifications).toBe(3) | ||
}) | ||
}, | ||
) |
@@ -1,2 +0,2 @@ | ||
import type { UnknownAction } from '@reduxjs/toolkit' | ||
import type { CreateAsyncThunkFunction, UnknownAction } from '@reduxjs/toolkit' | ||
import { | ||
@@ -993,2 +993,23 @@ configureStore, | ||
}) | ||
test('createAsyncThunkWrapper using CreateAsyncThunkFunction', async () => { | ||
const customSerializeError = () => 'serialized!' | ||
const createAppAsyncThunk: CreateAsyncThunkFunction<{ | ||
serializedErrorType: ReturnType<typeof customSerializeError> | ||
}> = (prefix: string, payloadCreator: any, options: any) => | ||
createAsyncThunk(prefix, payloadCreator, { | ||
...options, | ||
serializeError: customSerializeError, | ||
}) as any | ||
const asyncThunk = createAppAsyncThunk('test', async () => { | ||
throw new Error('Panic!') | ||
}) | ||
const promise = store.dispatch(asyncThunk()) | ||
const result = await promise | ||
if (!asyncThunk.rejected.match(result)) { | ||
throw new Error('should have thrown') | ||
} | ||
expect(result.error).toEqual('serialized!') | ||
}) | ||
}) |
106
src/utils.ts
@@ -29,15 +29,2 @@ import { produce as createNextState, isDraftable } from 'immer' | ||
export function find<T>( | ||
iterable: Iterable<T>, | ||
comparator: (item: T) => boolean, | ||
): T | undefined { | ||
for (const entry of iterable) { | ||
if (comparator(entry)) { | ||
return entry | ||
} | ||
} | ||
return undefined | ||
} | ||
export class Tuple<Items extends ReadonlyArray<unknown> = []> extends Array< | ||
@@ -91,79 +78,36 @@ Items[number] | ||
interface WeakMapEmplaceHandler<K extends object, V> { | ||
/** | ||
* Will be called to get value, if no value is currently in map. | ||
*/ | ||
insert?(key: K, map: WeakMap<K, V>): V | ||
/** | ||
* Will be called to update a value, if one exists already. | ||
*/ | ||
update?(previous: V, key: K, map: WeakMap<K, V>): V | ||
} | ||
export function getOrInsert<K extends object, V>( | ||
map: WeakMap<K, V>, | ||
key: K, | ||
value: V, | ||
): V | ||
export function getOrInsert<K, V>(map: Map<K, V>, key: K, value: V): V | ||
export function getOrInsert<K extends object, V>( | ||
map: Map<K, V> | WeakMap<K, V>, | ||
key: K, | ||
value: V, | ||
): V { | ||
if (map.has(key)) return map.get(key) as V | ||
interface MapEmplaceHandler<K, V> { | ||
/** | ||
* Will be called to get value, if no value is currently in map. | ||
*/ | ||
insert?(key: K, map: Map<K, V>): V | ||
/** | ||
* Will be called to update a value, if one exists already. | ||
*/ | ||
update?(previous: V, key: K, map: Map<K, V>): V | ||
return map.set(key, value).get(key) as V | ||
} | ||
export function emplace<K, V>( | ||
map: Map<K, V>, | ||
export function getOrInsertComputed<K extends object, V>( | ||
map: WeakMap<K, V>, | ||
key: K, | ||
handler: MapEmplaceHandler<K, V>, | ||
compute: (key: K) => V, | ||
): V | ||
export function emplace<K extends object, V>( | ||
map: WeakMap<K, V>, | ||
export function getOrInsertComputed<K, V>( | ||
map: Map<K, V>, | ||
key: K, | ||
handler: WeakMapEmplaceHandler<K, V>, | ||
compute: (key: K) => V, | ||
): V | ||
/** | ||
* Allow inserting a new value, or updating an existing one | ||
* @throws if called for a key with no current value and no `insert` handler is provided | ||
* @returns current value in map (after insertion/updating) | ||
* ```ts | ||
* // return current value if already in map, otherwise initialise to 0 and return that | ||
* const num = emplace(map, key, { | ||
* insert: () => 0 | ||
* }) | ||
* | ||
* // increase current value by one if already in map, otherwise initialise to 0 | ||
* const num = emplace(map, key, { | ||
* update: (n) => n + 1, | ||
* insert: () => 0, | ||
* }) | ||
* | ||
* // only update if value's already in the map - and increase it by one | ||
* if (map.has(key)) { | ||
* const num = emplace(map, key, { | ||
* update: (n) => n + 1, | ||
* }) | ||
* } | ||
* ``` | ||
* | ||
* @remarks | ||
* Based on https://github.com/tc39/proposal-upsert currently in Stage 2 - maybe in a few years we'll be able to replace this with direct method calls | ||
*/ | ||
export function emplace<K extends object, V>( | ||
map: WeakMap<K, V>, | ||
export function getOrInsertComputed<K extends object, V>( | ||
map: Map<K, V> | WeakMap<K, V>, | ||
key: K, | ||
handler: WeakMapEmplaceHandler<K, V>, | ||
compute: (key: K) => V, | ||
): V { | ||
if (map.has(key)) { | ||
let value = map.get(key) as V | ||
if (handler.update) { | ||
value = handler.update(value, key, map) | ||
map.set(key, value) | ||
} | ||
return value | ||
} | ||
if (!handler.insert) | ||
throw new Error('No insert provided for key not already in map') | ||
const inserted = handler.insert(key, map) | ||
map.set(key, inserted) | ||
return inserted | ||
if (map.has(key)) return map.get(key) as V | ||
return map.set(key, compute(key)).get(key) as V | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
5604136
236
65748
144