UNPKG

ngrx-rtk-query

Version:
712 lines (697 loc) 32.5 kB
import { computed, signal, effect, untracked, isDevMode, inject, DestroyRef, runInInjectionContext } from '@angular/core'; import { QueryStatus, skipToken, defaultSerializeQueryArgs, fetchBaseQuery as fetchBaseQuery$1, buildCreateApi, coreModule } from '@reduxjs/toolkit/query'; export * from '@reduxjs/toolkit/query'; const UNINITIALIZED_VALUE = Symbol(); var DefinitionType; (function (DefinitionType) { DefinitionType["query"] = "query"; DefinitionType["mutation"] = "mutation"; DefinitionType["infinitequery"] = "infinitequery"; })(DefinitionType || (DefinitionType = {})); function isQueryDefinition(e) { return e.type === DefinitionType.query; } function isMutationDefinition(e) { return e.type === DefinitionType.mutation; } function isInfiniteQueryDefinition(e) { return e.type === DefinitionType.infinitequery; } function useStableQueryArgs(queryArgs, serialize, endpointDefinition, endpointName) { const incoming = computed(() => { const incomingArgs = queryArgs(); return { queryArgs: incomingArgs, serialized: typeof incomingArgs == 'object' ? serialize({ queryArgs: incomingArgs, endpointDefinition, endpointName }) : incomingArgs, }; }, { equal: (a, b) => a.serialized === b.serialized }); return computed(() => incoming().queryArgs); } function capitalize(str) { return str.replace(str[0], str[0].toUpperCase()); } function toLazySignal(inputSignal, { initialValue }) { const s = signal(initialValue); effect(() => { const input = inputSignal(); untracked(() => s.set(input)); }); return s.asReadonly(); } function is(x, y) { if (x === y) { return x !== 0 || y !== 0 || 1 / x === 1 / y; } else { return x !== x && y !== y; } } function shallowEqual(objA, objB) { if (is(objA, objB)) { return true; } if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) { return false; } const keysA = Object.keys(objA); const keysB = Object.keys(objB); if (keysA.length !== keysB.length) { return false; } // eslint-disable-next-line @typescript-eslint/prefer-for-of for (let i = 0; i < keysA.length; i++) { if (!Object.prototype.hasOwnProperty.call(objB, keysA[i]) || !is(objA[keysA[i]], objB[keysA[i]])) { return false; } } return true; } /** * The code in this file is adapted from TanStack/query * * TanStack/query is an open-source project licensed under the MIT license. * * For more information about the original code, see * https://github.com/TanStack/query */ /** * Exposes fields of an object passed via an Angular `Signal` as `Computed` signals. * * Functions on the object are passed through as-is. * * @param signal - `Signal` that must return an object. * */ function signalProxy(signal) { const internalState = {}; return new Proxy(internalState, { get(target, prop) { // first check if we have it in our internal state and return it const computedField = target[prop]; if (computedField) return computedField; // then, check if it's a function on the resultState and return it const targetField = untracked(signal)[prop]; if (typeof targetField === 'function') return targetField; // finally, create a computed field, store it and return it // @ts-expect-error bypass return (target[prop] = computed(() => signal()[prop])); }, has(_, prop) { return !!untracked(signal)[prop]; }, ownKeys() { return Reflect.ownKeys(untracked(signal)); }, getOwnPropertyDescriptor() { return { enumerable: true, configurable: true, }; }, }); } function toDeepSignal(signal) { const deepSignal = signalProxy(signal); return Object.assign(signal, deepSignal); } function safeAssign(target, ...args) { return Object.assign(target, ...args); } /** * Wrapper around `defaultQueryStateSelector` to be used in `useQuery`. * We want the initial render to already come back with * `{ isUninitialized: false, isFetching: true, isLoading: true }` * to prevent that the library user has to do an additional check for `isUninitialized`/ */ const noPendingQueryStateSelector = (selected) => { if (selected.isUninitialized) { return { ...selected, isUninitialized: false, isFetching: true, isLoading: selected.data !== undefined ? false : true, status: QueryStatus.pending, }; } return selected; }; /** * * @param opts.api - An API with defined endpoints to create hooks for * @param opts.moduleOptions.dispatch - The version of the `dispatch` to be used * @param opts.moduleOptions.useSelector - The version of the `useSelector` hook to be used * @param opts.moduleOptions.getState - The version of the `getState` to be used * @returns An object containing functions to generate hooks based on an endpoint */ function buildHooks({ api, moduleOptions: { hooks: { dispatch, useSelector, getState }, createSelector, }, serializeQueryArgs, context, }) { return { buildQueryHooks, buildInfiniteQueryHooks, buildMutationHook, usePrefetch }; function queryStatePreSelector(currentState, lastResult, queryArgs) { // if we had a last result and the current result is uninitialized, // we might have called `api.util.resetApiState` // in this case, reset the hook if (lastResult?.endpointName && currentState.isUninitialized) { const { endpointName } = lastResult; const endpointDefinition = context.endpointDefinitions[endpointName]; if (queryArgs !== skipToken && serializeQueryArgs({ queryArgs: lastResult.originalArgs, endpointDefinition, endpointName, }) === serializeQueryArgs({ queryArgs, endpointDefinition, endpointName, })) lastResult = undefined; } // data is the last known good request result we have tracked // or if none has been tracked yet the last good result for the current args let data = currentState.isSuccess ? currentState.data : lastResult?.data; if (data === undefined) data = currentState.data; const hasData = data !== undefined; // isFetching = true any time a request is in flight const isFetching = currentState.isLoading; // isLoading = true only when loading while no data is present yet (initial load with no data in the cache) const isLoading = (!lastResult || lastResult.isLoading || lastResult.isUninitialized) && !hasData && isFetching; // 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 { ...currentState, data, currentData: currentState.data, isFetching, isLoading, isSuccess, // Deep signals required init properties undefined atleast endpointName: currentState.endpointName, error: currentState.error, fulfilledTimeStamp: currentState.fulfilledTimeStamp, originalArgs: currentState.originalArgs, requestId: currentState.requestId, startedTimeStamp: currentState.startedTimeStamp, }; } function infiniteQueryStatePreSelector(currentState, lastResult, queryArgs) { // if we had a last result and the current result is uninitialized, // we might have called `api.util.resetApiState` // in this case, reset the hook if (lastResult?.endpointName && currentState.isUninitialized) { const { endpointName } = lastResult; const endpointDefinition = context.endpointDefinitions[endpointName]; if (serializeQueryArgs({ queryArgs: lastResult.originalArgs, endpointDefinition, endpointName, }) === serializeQueryArgs({ queryArgs, endpointDefinition, endpointName, })) lastResult = undefined; } // data is the last known good request result we have tracked - or if none has been tracked yet the last good result for the current args let data = currentState.isSuccess ? currentState.data : lastResult?.data; if (data === undefined) data = currentState.data; const hasData = data !== undefined; // isFetching = true any time a request is in flight const isFetching = currentState.isLoading; // isLoading = true only when loading while no data is present yet (initial load with no data in the cache) const isLoading = (!lastResult || lastResult.isLoading || lastResult.isUninitialized) && !hasData && isFetching; // isSuccess = true when data is present const isSuccess = currentState.isSuccess || (isFetching && hasData); return { ...currentState, data, currentData: currentState.data, isFetching, isLoading, isSuccess, // Deep signals required init properties undefined atleast endpointName: currentState.endpointName, error: currentState.error, fulfilledTimeStamp: currentState.fulfilledTimeStamp, originalArgs: currentState.originalArgs, requestId: currentState.requestId, startedTimeStamp: currentState.startedTimeStamp, }; } function usePrefetch(endpointName, defaultOptions) { return (arg, options) => dispatch(api.util.prefetch(endpointName, arg, { ...defaultOptions, ...options })); } function useQuerySubscriptionCommonImpl(endpointName, arg, options = {}) { const { initiate } = api.endpoints[endpointName]; const subscriptionOptions = computed(() => { const { refetchOnReconnect, refetchOnFocus, refetchOnMountOrArgChange, skip = false, pollingInterval = 0, skipPollingIfUnfocused = false, ...rest } = typeof options === 'function' ? options() : options; return { refetchOnReconnect, refetchOnFocus, refetchOnMountOrArgChange, skip, pollingInterval, skipPollingIfUnfocused, ...rest, }; }); const subscriptionArg = computed(() => { const subscriptionArg = typeof arg === 'function' ? arg() : arg; return subscriptionOptions().skip ? skipToken : subscriptionArg; }); let subscriptionSelectorsRef; if (!subscriptionSelectorsRef) { const returnedValue = dispatch(api.internalActions.internal_getRTKQSubscriptions()); if (isDevMode()) { if (typeof returnedValue !== 'object' || typeof returnedValue?.type === 'string') { throw new Error(`Warning: Middleware for RTK-Query API at reducerPath "${api.reducerPath}" has not been added to the store. You must add the middleware for RTK-Query to function correctly!`); } } subscriptionSelectorsRef = returnedValue; } const stableArg = useStableQueryArgs(subscriptionArg, // Even if the user provided a per-endpoint `serializeQueryArgs` with // a consistent return value, _here_ we want to use the default behavior // so we can tell if _anything_ actually changed. Otherwise, we can end up // with a case where the query args did change but the serialization doesn't, // and then we never try to initiate a refetch. defaultSerializeQueryArgs, context.endpointDefinitions[endpointName], endpointName); const stableSubscriptionOptions = computed(() => { const { refetchOnReconnect, refetchOnFocus, pollingInterval, skipPollingIfUnfocused } = subscriptionOptions(); return { refetchOnReconnect, refetchOnFocus, pollingInterval, skipPollingIfUnfocused }; }, { equal: shallowEqual }); let lastRenderHadSubscription = false; const promiseRef = { current: undefined }; const forceRefetch = computed(() => subscriptionOptions().refetchOnMountOrArgChange); const initialPageParam = computed(() => subscriptionOptions().initialPageParam); effect(() => { const { queryCacheKey, requestId } = promiseRef.current || {}; const stableArgValue = stableArg(); const stableSubscriptionOptionsValue = stableSubscriptionOptions(); const forceRefetchValue = forceRefetch(); const initialPageParamValue = initialPageParam(); // HACK We've saved the middleware subscription lookup callbacks into a ref, // so we can directly check here if the subscription exists for this query. let currentRenderHasSubscription = false; if (queryCacheKey && requestId) { currentRenderHasSubscription = !!subscriptionSelectorsRef?.isRequestSubscribed(queryCacheKey, requestId); } const subscriptionRemoved = !currentRenderHasSubscription && lastRenderHadSubscription; lastRenderHadSubscription = currentRenderHasSubscription; if (subscriptionRemoved) { promiseRef.current = undefined; } const lastPromise = promiseRef; if (stableArgValue === skipToken) { lastPromise.current?.unsubscribe(); promiseRef.current = undefined; return; } const lastSubscriptionOptions = promiseRef.current?.subscriptionOptions; if (!lastPromise.current || lastPromise.current.arg !== stableArgValue) { lastPromise.current?.unsubscribe(); const promise = dispatch(initiate(stableArgValue, { subscriptionOptions: stableSubscriptionOptionsValue, forceRefetch: forceRefetchValue, ...(isInfiniteQueryDefinition(context.endpointDefinitions[endpointName]) ? { initialPageParam: initialPageParamValue, } : {}), })); promiseRef.current = promise; } else if (stableSubscriptionOptionsValue !== lastSubscriptionOptions) { lastPromise.current.updateSubscriptionOptions(stableSubscriptionOptionsValue); } }, { allowSignalWrites: true }); return [promiseRef, dispatch, initiate, stableSubscriptionOptions]; } function buildUseQueryState(endpointName, preSelector) { const useQueryState = (arg, options = {}) => { const { select } = api.endpoints[endpointName]; // We need to use `toLazySignal` here to prevent 'signal required inputs' errors const lazyArg = typeof arg === 'function' ? toLazySignal(arg, { initialValue: skipToken }) : () => arg; const lazyOptions = typeof options === 'function' ? toLazySignal(options, { initialValue: { selectFromResult: noPendingQueryStateSelector } }) : () => options; const stateOptions = computed(() => { const { skip = false, selectFromResult } = lazyOptions(); return { skip, selectFromResult }; }); const subscriptionArg = computed(() => { const subscriptionArg = lazyArg(); return stateOptions().skip ? skipToken : subscriptionArg; }); const stableArg = useStableQueryArgs(subscriptionArg, serializeQueryArgs, context.endpointDefinitions[endpointName], endpointName); let lastValue; const currentState = computed(() => { const selectDefaultResult = createSelector(select(stableArg()), (subState) => preSelector(subState, lastValue, stableArg())); const { selectFromResult } = stateOptions(); const querySelector = selectFromResult ? createSelector(selectDefaultResult, selectFromResult) : selectDefaultResult; const currentState = useSelector((state) => querySelector(state), { equal: shallowEqual, }); lastValue = selectDefaultResult(getState()); return currentState(); }); const deepSignal = toDeepSignal(currentState); return deepSignal; }; return useQueryState; } function usePromiseRefUnsubscribeOnUnmount(promiseRef) { inject(DestroyRef).onDestroy(() => { promiseRef.current?.unsubscribe?.(); promiseRef.current = undefined; }); } function refetchOrErrorIfUnmounted(promiseRef) { if (!promiseRef.current) throw new Error('Cannot refetch a query that has not been started yet.'); return promiseRef.current.refetch(); } function buildQueryHooks(endpointName) { const useQuerySubscription = (arg, options = {}) => { const [promiseRef] = useQuerySubscriptionCommonImpl(endpointName, arg, options); usePromiseRefUnsubscribeOnUnmount(promiseRef); return { /** * A method to manually refetch data for the query */ refetch: () => refetchOrErrorIfUnmounted(promiseRef), }; }; const useLazyQuerySubscription = (options = {}) => { const { initiate } = api.endpoints[endpointName]; const subscriptionArg = signal(UNINITIALIZED_VALUE); let promiseRef; const stableSubscriptionOptions = computed(() => { const { refetchOnReconnect, refetchOnFocus, pollingInterval = 0, skipPollingIfUnfocused = false, } = typeof options === 'function' ? options() : options; return { refetchOnReconnect, refetchOnFocus, pollingInterval, skipPollingIfUnfocused }; }, { equal: shallowEqual }); effect(() => { const lastSubscriptionOptions = promiseRef?.subscriptionOptions; if (stableSubscriptionOptions() !== lastSubscriptionOptions) { promiseRef?.updateSubscriptionOptions(stableSubscriptionOptions()); } }, { allowSignalWrites: true }); let subscriptionOptionsRef = stableSubscriptionOptions(); effect(() => { subscriptionOptionsRef = stableSubscriptionOptions(); }); const trigger = (arg, { preferCacheValue = false } = {}) => { let promise; promiseRef?.unsubscribe(); promiseRef = promise = dispatch(initiate(arg, { subscriptionOptions: subscriptionOptionsRef, forceRefetch: !preferCacheValue })); subscriptionArg.set(arg); return promise; }; const reset = () => { if (promiseRef?.queryCacheKey) { dispatch(api.internalActions.removeQueryResult({ queryCacheKey: promiseRef?.queryCacheKey, })); } }; /* cleanup on unmount */ inject(DestroyRef).onDestroy(() => { promiseRef?.unsubscribe(); }); /* if "cleanup on unmount" was triggered from a fast refresh, we want to reinstate the query */ effect(() => { if (subscriptionArg() !== UNINITIALIZED_VALUE && !promiseRef) { trigger(subscriptionArg(), { preferCacheValue: true }); } }); const lastArg = computed(() => (subscriptionArg() !== UNINITIALIZED_VALUE ? subscriptionArg() : skipToken), { equal: shallowEqual, }); return [trigger, lastArg, { reset }]; }; const useQueryState = buildUseQueryState(endpointName, queryStatePreSelector); return { useQueryState, useQuerySubscription, useLazyQuerySubscription, useLazyQuery(options) { const [trigger, arg, { reset }] = useLazyQuerySubscription(options); const subscriptionOptions = computed(() => ({ ...options, skip: arg() === UNINITIALIZED_VALUE, })); const queryStateResults = useQueryState(arg, subscriptionOptions); const signalsMap = signalProxy(queryStateResults); Object.assign(trigger, { lastArg: arg, reset }); Object.assign(trigger, signalsMap); return trigger; }, useQuery(arg, options) { const querySubscriptionResults = useQuerySubscription(arg, options); const subscriptionOptions = computed(() => { const subscriptionArg = typeof arg === 'function' ? arg() : arg; const subscriptionOptions = typeof options === 'function' ? options() : options; return { selectFromResult: subscriptionArg === skipToken || subscriptionOptions?.skip ? undefined : noPendingQueryStateSelector, ...subscriptionOptions, }; }); const queryStateResults = useQueryState(arg, subscriptionOptions); Object.assign(queryStateResults, querySubscriptionResults); return queryStateResults; }, }; } function buildInfiniteQueryHooks(endpointName) { const useInfiniteQuerySubscription = (arg, options = {}) => { const [promiseRef, dispatch, initiate, stableSubscriptionOptions] = useQuerySubscriptionCommonImpl(endpointName, arg, options); let subscriptionOptionsRef = stableSubscriptionOptions(); effect(() => { subscriptionOptionsRef = stableSubscriptionOptions(); }); const trigger = (arg, direction) => { let promise; promiseRef.current?.unsubscribe(); promiseRef.current = promise = dispatch(initiate(arg, { subscriptionOptions: subscriptionOptionsRef, direction, })); return promise; }; usePromiseRefUnsubscribeOnUnmount(promiseRef); const fetchNextPage = () => { return trigger(arg, 'forward'); }; const fetchPreviousPage = () => { return trigger(arg, 'backward'); }; return { trigger, /** * A method to manually refetch data for the query */ refetch: () => refetchOrErrorIfUnmounted(promiseRef), fetchNextPage, fetchPreviousPage, }; }; const useInfiniteQueryState = buildUseQueryState(endpointName, infiniteQueryStatePreSelector); return { useInfiniteQueryState, useInfiniteQuerySubscription, useInfiniteQuery(arg, options) { const { refetch, fetchNextPage, fetchPreviousPage } = useInfiniteQuerySubscription(arg, options); const subscriptionOptions = computed(() => { const subscriptionArg = typeof arg === 'function' ? arg() : arg; const subscriptionOptions = typeof options === 'function' ? options() : options; return { selectFromResult: subscriptionArg === skipToken || subscriptionOptions?.skip ? undefined : noPendingQueryStateSelector, ...subscriptionOptions, }; }); const queryStateResults = useInfiniteQueryState(arg, subscriptionOptions); Object.assign(queryStateResults, { fetchNextPage, fetchPreviousPage, refetch }); return queryStateResults; }, }; } function buildMutationHook(name) { const { initiate, select } = api.endpoints[name]; const useMutation = ({ selectFromResult, fixedCacheKey } = {}) => { const promiseRef = signal(undefined); effect((onCleanup) => { const currentPromise = promiseRef(); onCleanup(() => { if (!currentPromise?.arg.fixedCacheKey) { untracked(() => currentPromise?.reset()); } }); }); const triggerMutation = (arg) => { const promise = dispatch(initiate(arg, { fixedCacheKey })); promiseRef.set(promise); return promise; }; const fixedSelect = (args) => (state) => { const currentState = select(args)(state); return { ...currentState, // Deep signals required init properties undefined atleast data: currentState.data, endpointName: currentState.endpointName, error: currentState.error, fulfilledTimeStamp: currentState.fulfilledTimeStamp, requestId: currentState.requestId, startedTimeStamp: currentState.startedTimeStamp, }; }; const requestId = computed(() => promiseRef()?.requestId); const selectDefaultResult = (requestId) => fixedSelect({ fixedCacheKey, requestId }); const mutationSelector = (requestId) => selectFromResult ? createSelector(selectDefaultResult(requestId), selectFromResult) : selectDefaultResult(requestId); const currentState = computed(() => useSelector(mutationSelector(requestId()), { equal: shallowEqual })); const originalArgs = computed(() => (fixedCacheKey == null ? promiseRef()?.arg.originalArgs : undefined)); const reset = () => { if (promiseRef()) { promiseRef.set(undefined); } if (fixedCacheKey) { dispatch(api.internalActions.removeMutationResult({ requestId: requestId(), fixedCacheKey, })); } }; const finalState = computed(() => currentState()()); const signalsMap = signalProxy(finalState); Object.assign(triggerMutation, { originalArgs }); Object.assign(triggerMutation, { reset }); Object.assign(triggerMutation, signalsMap); return triggerMutation; }; return { useMutation }; } } const angularHooksModuleName = /* @__PURE__ */ Symbol(); /** * Creates a module that generates angular hooks from endpoints, for use with `buildCreateApi`. * * @example * ```ts * const customCreateApi = buildCreateApi( * coreModule(), * angularHooksModule(() => myCreateAngularHooksModule()) * ); * ``` * * @returns A module for use with `buildCreateApi` */ const angularHooksModule = ({ hooks, createSelector, getInjector, }) => { return { name: angularHooksModuleName, init(api, { serializeQueryArgs }, context) { const anyApi = api; const { buildQueryHooks, buildInfiniteQueryHooks, buildMutationHook, usePrefetch } = buildHooks({ api, moduleOptions: { hooks, createSelector, getInjector }, serializeQueryArgs, context, }); safeAssign(anyApi, { usePrefetch }); safeAssign(anyApi, { dispatch: hooks.dispatch }); safeAssign(anyApi, { selectSignal: hooks.useSelector }); safeAssign(anyApi, { getInjector }); return { injectEndpoint(endpointName, definition) { if (isQueryDefinition(definition)) { const { useQuery, useLazyQuery, useLazyQuerySubscription, useQueryState, useQuerySubscription } = buildQueryHooks(endpointName); safeAssign(anyApi.endpoints[endpointName], { useQuery, useLazyQuery, useLazyQuerySubscription, useQueryState, useQuerySubscription, }); api[`use${capitalize(endpointName)}Query`] = useQuery; api[`useLazy${capitalize(endpointName)}Query`] = useLazyQuery; } if (isMutationDefinition(definition)) { const { useMutation } = buildMutationHook(endpointName); safeAssign(anyApi.endpoints[endpointName], { useMutation }); api[`use${capitalize(endpointName)}Mutation`] = useMutation; } else if (isInfiniteQueryDefinition(definition)) { const { useInfiniteQuery, useInfiniteQuerySubscription, useInfiniteQueryState } = buildInfiniteQueryHooks(endpointName); safeAssign(anyApi.endpoints[endpointName], { useInfiniteQuery, useInfiniteQuerySubscription, useInfiniteQueryState, }); api[`use${capitalize(endpointName)}InfiniteQuery`] = useInfiniteQuery; } }, }; }, }; }; function fetchBaseQuery(paramsOrFactory = {}) { if (typeof paramsOrFactory === 'object') return fetchBaseQuery$1(paramsOrFactory); return async (args, api, extraOptions) => { const injector = api.extra.injector; const baseQuery = runInInjectionContext(injector, paramsOrFactory); return await baseQuery(args, api, extraOptions); }; } const createApi = (options) => { const next = (action) => { if (typeof action === 'function') { return action(dispatch, getState, { injector: getInjector() }); } return store.hooks.dispatch(action); }; const dispatch = (action) => { if (!store) { const reducerPath = options.reducerPath; throw new Error(`Provide the API (${reducerPath}) is necessary to use the queries. Did you forget to provide the queries api?`); } return middleware(next)(action); }; const getState = () => store.hooks.getState(); const useSelector = (mapFn, options) => store?.hooks.useSelector(mapFn, options); const createSelector = (...input) => store.createSelector(...input); const getInjector = () => store.getInjector(); const createApi = /* @__PURE__ */ buildCreateApi(coreModule(), angularHooksModule({ hooks: { dispatch: dispatch, getState, useSelector, }, createSelector, getInjector, })); const api = createApi(options); let store; const initApiStore = (setupFn) => { store = setupFn(); }; Object.assign(api, { initApiStore }); const middleware = api.middleware({ dispatch, getState }); return api; }; /** * Generated bundle index. Do not edit. */ export { UNINITIALIZED_VALUE, angularHooksModule, createApi, fetchBaseQuery, shallowEqual }; //# sourceMappingURL=ngrx-rtk-query-core.mjs.map