@pinia/colada
Version:
The smart data fetching layer for Vue.js
1 lines • 153 kB
Source Map (JSON)
{"version":3,"sources":["../src/index.ts","../src/define-query-options.ts","../src/define-query.ts","../src/query-store.ts","../src/query-options.ts","../src/entry-keys.ts","../src/utils.ts","../src/use-query.ts","../src/use-query-state.ts","../src/infinite-query.ts","../src/mutation-store.ts","../src/mutation-options.ts","../src/use-mutation.ts","../src/define-mutation.ts","../src/devtools/plugin.ts","../src/pinia-colada.ts","../src/plugins/query-hooks.ts"],"sourcesContent":["/**\n * Pinia Colada\n * @module @pinia/colada\n */\n\nexport type {\n AsyncStatus,\n DataState,\n DataState_Error,\n DataState_Pending,\n DataState_Success,\n DataStateStatus,\n} from './data-state'\n\nexport type { EntryKey, EntryKeyTagged, toCacheKey } from './entry-keys'\n\nexport { defineQueryOptions, type DefineQueryOptionsTagged } from './define-query-options'\n\nexport type {\n RefetchOnControl,\n UseQueryOptions,\n UseQueryOptionsGlobal,\n UseQueryOptionsWithDefaults,\n} from './query-options'\n\nexport { defineQuery, type DefineQueryOptions } from './define-query'\nexport { useQuery, type UseQueryReturn } from './use-query'\n\nexport { useQueryState, type UseQueryStateReturn } from './use-query-state'\n\nexport {\n useInfiniteQuery,\n type UseInfiniteQueryOptions,\n type UseInfiniteQueryReturn,\n} from './infinite-query'\n\nexport {\n hydrateQueryCache,\n isQueryCache,\n type QueryCache,\n serializeQueryCache,\n useQueryCache,\n type UseQueryEntry,\n type UseQueryEntryExtensions,\n type UseQueryEntryFilter,\n} from './query-store'\n\nexport type {\n UseMutationOptions,\n UseMutationOptionsGlobal,\n UseMutationOptionsGlobalDefaults,\n} from './mutation-options'\n\nexport { defineMutation } from './define-mutation'\nexport type { UseMutationReturn } from './use-mutation'\nexport { useMutation } from './use-mutation'\n\nexport { PiniaColada } from './pinia-colada'\nexport type { PiniaColadaOptions } from './pinia-colada'\n\nexport {\n PiniaColadaQueryHooksPlugin,\n type PiniaColadaQueryHooksPluginOptions,\n} from './plugins/query-hooks'\n\nexport type { TypesConfig } from './types-extension'\n\nexport type { PiniaColadaPlugin, PiniaColadaPluginContext } from './plugins'\n\n// internals\nexport type {\n ENTRY_DATA_INITIAL_TAG as _ENTRY_DATA_INITIAL_TAG,\n ENTRY_DATA_TAG as _ENTRY_DATA_TAG,\n ENTRY_ERROR_TAG as _ENTRY_ERROR_TAG,\n JSONArray as _JSONArray,\n JSONObject as _JSONObject,\n JSONPrimitive as _JSONPrimitive,\n JSONValue as _JSONValue,\n} from './entry-keys'\n\nexport type {\n EntryFilter as _EntryFilter,\n EntryFilter_Base as _EntryFilter_Base,\n EntryFilter_Key as _EntryFilter_Key,\n EntryFilter_NoKey as _EntryFilter_NoKey,\n} from './entry-filter'\nexport type { _UseQueryEntryNodeValueSerialized } from './query-store'\n\nexport type {\n _Awaitable,\n _EmptyObject,\n IsAny as _IsAny,\n IsUnknown as _IsUnknown,\n _MaybeArray,\n} from './utils'\n\nexport type { _ReduceContext } from './use-mutation'\n\nexport type { _DataState_Base } from './data-state'\n","import type { DefineQueryOptions } from './define-query'\nimport type { EntryKeyTagged } from './entry-keys'\nimport type { ErrorDefault } from './types-extension'\nimport { useQuery } from './use-query'\n\n/**\n * Tagged version of {@link DefineQueryOptions} that includes a key with\n * data type information.\n */\nexport interface DefineQueryOptionsTagged<\n TData = unknown,\n TError = ErrorDefault,\n TDataInitial extends TData | undefined = undefined,\n> extends DefineQueryOptions<TData, TError, TDataInitial> {\n key: EntryKeyTagged<TData, TError, TDataInitial>\n}\n\n/**\n * Define dynamic query options by passing a function that accepts an arbitrary\n * parameter and returns the query options. Enables type-safe query keys.\n * Must be passed to {@link useQuery} alongside a getter for the params.\n *\n * @param setupOptions - A function that returns the query options.\n */\nexport function defineQueryOptions<\n Params,\n TData,\n TError = ErrorDefault,\n TDataInitial extends TData | undefined = undefined,\n>(\n setupOptions: (params: Params) => DefineQueryOptions<TData, TError, TDataInitial>,\n): (params: Params) => DefineQueryOptionsTagged<TData, TError, TDataInitial>\n\n/**\n * Define static query options that are type safe with\n * `queryCache.getQueryData()`. Can be passed directly to {@link useQuery}.\n *\n * @param options - The query options.\n */\nexport function defineQueryOptions<\n TData,\n TError = ErrorDefault,\n TDataInitial extends TData | undefined = undefined,\n>(\n options: DefineQueryOptions<TData, TError, TDataInitial>,\n): DefineQueryOptionsTagged<TData, TError, TDataInitial>\n\n/**\n * Define type-safe query options. Can be static or dynamic. Define the arguments based\n * on what's needed on the query and the key. Use an object if you need\n * multiple properties.\n *\n * @param setupOrOptions - The query options or a function that returns the query options.\n *\n * @example\n * ```ts\n * import { defineQueryOptions } from '@pinia/colada'\n *\n * const documentDetailsQuery = defineQueryOptions((id: number ) => ({\n * key: ['documents', id],\n * query: () => fetchDocument(id),\n * }))\n *\n * queryCache.getQueryData(documentDetailsQuery(4).key) // typed\n * ```\n *\n * @__NO_SIDE_EFFECTS__\n */\nexport function defineQueryOptions<const Options extends DefineQueryOptions, Params>(\n setupOrOptions: Options | ((params: Params) => Options),\n): Options | ((params: Params) => Options) {\n return setupOrOptions\n}\n","import { getCurrentInstance, getCurrentScope, onScopeDispose, toValue } from 'vue'\nimport type { EffectScope } from 'vue'\nimport type { UseQueryOptions } from './query-options'\nimport { useQueryCache } from './query-store'\nimport type { ErrorDefault } from './types-extension'\nimport type { UseQueryReturn } from './use-query'\nimport { useQuery } from './use-query'\nimport { noop } from './utils'\nimport type { _RemoveMaybeRef } from './utils'\n\n/**\n * The current effect scope where the function returned by `defineQuery` is\n * being called. This allows `useQuery()` to know if it should be attached to\n * an effect scope or not\n *\n * @internal\n */\n// eslint-disable-next-line import/no-mutable-exports\nexport let currentDefineQueryEffect: undefined | EffectScope\n\n/**\n * Options to define a query with `defineQuery()`. Similar to\n * {@link UseQueryOptions} but disallows reactive values as `defineQuery()` is\n * used outside of an effect scope.\n */\nexport type DefineQueryOptions<\n TData = unknown,\n TError = ErrorDefault,\n TDataInitial extends TData | undefined = undefined,\n> = _RemoveMaybeRef<UseQueryOptions<TData, TError, TDataInitial>> & {\n // NOTE: we need to duplicate the types for initialData and placeholderData to make everything work\n // we omit the descriptions because they are inherited from the original type\n initialData?: () => TDataInitial\n\n placeholderData?:\n | NoInfer<TDataInitial>\n | NoInfer<TData>\n | (<T extends TData>(\n previousData: T | undefined,\n ) => NoInfer<TDataInitial> | NoInfer<TData> | undefined)\n}\n\n/**\n * Define a query with the given options. Similar to `useQuery(options)` but\n * allows you to reuse **all** of the query state in multiple places. It only\n * allow static values in options. If you need dynamic values, use the function\n * version.\n *\n * @param options - the options to define the query\n *\n * @example\n * ```ts\n * const useTodoList = defineQuery({\n * key: ['todos'],\n * query: () => fetch('/api/todos', { method: 'GET' }),\n * })\n * ```\n */\nexport function defineQuery<TData, TError = ErrorDefault>(\n options: DefineQueryOptions<TData, TError>,\n): () => UseQueryReturn<TData, TError>\n\n/**\n * Define a query with a setup function. Allows to return arbitrary values from\n * the query function, create contextual refs, rename the returned values, etc.\n * The setup function will be called only once, like stores, and **must be\n * synchronous**.\n *\n * @param setup - a function to setup the query\n *\n * @example\n * ```ts\n * const useFilteredTodos = defineQuery(() => {\n * const todoFilter = ref<'all' | 'finished' | 'unfinished'>('all')\n * const { data, ...rest } = useQuery({\n * key: ['todos', { filter: todoFilter.value }],\n * query: () =>\n * fetch(`/api/todos?filter=${todoFilter.value}`, { method: 'GET' }),\n * })\n * // expose the todoFilter ref and rename data for convenience\n * return { ...rest, todoList: data, todoFilter }\n * })\n * ```\n */\nexport function defineQuery<T>(setup: () => T): () => T\nexport function defineQuery(optionsOrSetup: DefineQueryOptions | (() => unknown)): () => unknown {\n const setupFn\n = typeof optionsOrSetup === 'function' ? optionsOrSetup : () => useQuery(optionsOrSetup)\n\n let hasBeenEnsured: boolean | undefined\n // allows pausing the scope when the defined query is no used anymore\n let refCount = 0\n return () => {\n const queryCache = useQueryCache()\n // preserve any current effect to account for nested usage of these functions\n const previousEffect = currentDefineQueryEffect\n const currentScope = getCurrentInstance() || (currentDefineQueryEffect = getCurrentScope())\n\n const [ensuredEntries, ret, scope, isPaused] = queryCache.ensureDefinedQuery(setupFn)\n\n // subsequent calls to the composable returned by useQuery will not trigger the `useQuery()`,\n // this ensures the refetchOnMount option is respected\n if (hasBeenEnsured) {\n ensuredEntries.forEach((entry) => {\n // since defined query can be activated multiple times without executing useQuery,\n // we need to execute it here too\n if (entry.options?.refetchOnMount && toValue(entry.options.enabled)) {\n if (toValue(entry.options.refetchOnMount) === 'always') {\n // we catch the error to avoid unhandled rejections\n queryCache.fetch(entry).catch(noop)\n } else {\n queryCache.refresh(entry).catch(noop)\n }\n }\n })\n }\n hasBeenEnsured = true\n\n // NOTE: most of the time this should be set, so maybe we should show a dev warning\n // if it's not set instead\n //\n // Because `useQuery()` might already be called before and we might be reusing an existing query\n // we need to manually track and untrack. When untracking, we cannot use the ensuredEntries because\n // there might be another component using the defineQuery, so we simply count how many are using it\n if (currentScope) {\n refCount++\n ensuredEntries.forEach((entry) => {\n queryCache.track(entry, currentScope)\n })\n onScopeDispose(() => {\n ensuredEntries.forEach((entry) => {\n queryCache.untrack(entry, currentScope)\n })\n // if all entries become inactive, we pause the scope\n // to avoid triggering the effects within useQuery. This immitates the behavior\n // of a component that unmounts\n if (--refCount < 1) {\n scope.pause()\n isPaused.value = true\n }\n })\n }\n\n // reset the previous effect\n currentDefineQueryEffect = previousEffect\n\n return ret\n }\n}\n","import { defineStore, getActivePinia, skipHydrate } from 'pinia'\nimport {\n customRef,\n effectScope,\n getCurrentInstance,\n getCurrentScope,\n hasInjectionContext,\n markRaw,\n shallowRef,\n toValue,\n} from 'vue'\nimport type { App, ComponentInternalInstance, EffectScope, ShallowRef } from 'vue'\nimport type { AsyncStatus, DataState } from './data-state'\nimport type { EntryKeyTagged, EntryKey } from './entry-keys'\nimport { useQueryOptions } from './query-options'\nimport type { UseQueryOptions, UseQueryOptionsWithDefaults } from './query-options'\nimport type { EntryFilter } from './entry-filter'\nimport { find, toCacheKey } from './entry-keys'\nimport type { ErrorDefault } from './types-extension'\nimport { noop, toValueWithArgs, warnOnce } from './utils'\n\n/**\n * Allows defining extensions to the query entry that are returned by `useQuery()`.\n */\n\nexport interface UseQueryEntryExtensions<\n TData,\n /* eslint-disable-next-line unused-imports/no-unused-vars */\n TError,\n /* eslint-disable-next-line unused-imports/no-unused-vars */\n TDataInitial extends TData | undefined = undefined,\n> {}\n\n/**\n * NOTE: Entries could be classes but the point of having all functions within the store is to allow plugins to hook\n * into actions.\n */\n\n/**\n * A query entry in the cache.\n */\nexport interface UseQueryEntry<\n TData = unknown,\n TError = unknown,\n // allows for UseQueryEntry to have unknown everywhere (generic version)\n TDataInitial extends TData | undefined = unknown extends TData ? unknown : undefined,\n> {\n /**\n * The state of the query. Contains the data, error and status.\n */\n state: ShallowRef<DataState<TData, TError, TDataInitial>>\n\n /**\n * A placeholder `data` that is initially shown while the query is loading for the first time. This will also show the\n * `status` as `success` until the query finishes loading (no matter the outcome).\n */\n placeholderData: TDataInitial | TData | null | undefined\n\n /**\n * The status of the query.\n */\n asyncStatus: ShallowRef<AsyncStatus>\n\n /**\n * When was this data set in the entry for the last time in ms. It can also\n * be 0 if the entry has been invalidated.\n */\n when: number\n\n /**\n * The serialized key associated with this query entry.\n */\n key: EntryKey\n\n /**\n * Seriaized version of the key. Used to retrieve the entry from the cache.\n */\n keyHash: string\n\n /**\n * Components and effects scopes that use this query entry.\n */\n deps: Set<EffectScope | ComponentInternalInstance>\n\n /**\n * Timeout id that scheduled a garbage collection. It is set here to clear it when the entry is used by a different component\n */\n gcTimeout: ReturnType<typeof setTimeout> | undefined\n\n /**\n * The current pending request.\n */\n pending: null | {\n /**\n * The abort controller used to cancel the request and which `signal` is passed to the query function.\n */\n abortController: AbortController\n /**\n * The promise created by `queryCache.fetch` that is currently pending.\n */\n refreshCall: Promise<DataState<TData, TError, TDataInitial>>\n /**\n * When was this `pending` object created.\n */\n when: number\n }\n\n /**\n * Options used to create the query. They can be `null` during hydration but are needed for fetching. This is why\n * `store.ensure()` sets this property. Note these options might be shared by multiple query entries when the key is\n * dynamic and that's why some methods like {@link fetch} receive the options as an argument.\n */\n options: UseQueryOptionsWithDefaults<TData, TError, TDataInitial> | null\n\n /**\n * Whether the data is stale or not, requires `options.staleTime` to be set.\n */\n readonly stale: boolean\n\n /**\n * Whether the query is currently being used by a Component or EffectScope (e.g. a store).\n */\n readonly active: boolean\n\n /**\n * Extensions to the query entry added by plugins.\n */\n ext: UseQueryEntryExtensions<TData, TError, TDataInitial>\n\n /**\n * Internal property to store the HMR ids of the components that are using\n * this query and force refetching.\n *\n * @internal\n */\n __hmr?: {\n /**\n * Reference count of the components using this query.\n */\n ids: Map<string, number>\n }\n}\n\n/**\n * Keep track of the entry being defined so we can add the queries in ensure\n * this allows us to refresh the entry when a defined query is used again\n * and refetchOnMount is true\n *\n * @internal\n */\n// eslint-disable-next-line import/no-mutable-exports\nexport let currentDefineQueryEntry: DefineQueryEntry | undefined | null\n\n/**\n * Returns whether the entry is using a placeholder data.\n *\n * @template TDataInitial - Initial data type\n * @param entry - entry to check\n */\nexport function isEntryUsingPlaceholderData<TDataInitial>(\n entry: UseQueryEntry<unknown, unknown, TDataInitial> | undefined | null,\n): entry is UseQueryEntry<unknown, unknown, TDataInitial> & { placeholderData: TDataInitial } {\n return entry?.placeholderData != null && entry.state.value.status === 'pending'\n}\n\n/**\n * Filter object to get entries from the query cache.\n *\n * @see {@link QueryCache.getEntries}\n * @see {@link QueryCache.cancelQueries}\n * @see {@link QueryCache#invalidateQueries}\n */\nexport type UseQueryEntryFilter = EntryFilter<UseQueryEntry>\n\n/**\n * Empty starting object for extensions that allows to detect when to update.\n *\n * @internal\n */\nexport const START_EXT = {}\n\n/**\n * UseQueryEntry method to serialize the entry to JSON.\n *\n * @param entry - entry to serialize\n * @param entry.when - when the data was fetched the last time\n * @param entry.state - data state of the entry\n * @param entry.state.value - value of the data state\n * @returns Serialized version of the entry\n */\nexport const queryEntry_toJSON: <TData, TError>(\n entry: UseQueryEntry<TData, TError>,\n) => _UseQueryEntryNodeValueSerialized<TData, TError> = ({ state: { value }, when }) => [\n value.data,\n value.error,\n // because of time zones, we create a relative time\n when ? Date.now() - when : -1,\n]\n// TODO: errors are not serializable by default. We should provide a way to serialize custom errors and, by default provide one that serializes the name and message\n\n/**\n * UseQueryEntry method to serialize the entry to a string.\n *\n * @internal\n * @param entry - entry to serialize\n * @returns Stringified version of the entry\n */\nexport const queryEntry_toString: <TData, TError>(entry: UseQueryEntry<TData, TError>) => string = (\n entry,\n) => String(queryEntry_toJSON(entry))\n\n/**\n * The id of the store used for queries.\n * @internal\n */\nexport const QUERY_STORE_ID = '_pc_query'\n\n/**\n * A query entry that is defined with {@link defineQuery}.\n * @internal\n */\ntype DefineQueryEntry = [\n lastEnsuredEntries: UseQueryEntry[],\n returnValue: unknown,\n effect: EffectScope,\n paused: ShallowRef<boolean>,\n]\n\n/**\n * Composable to get the cache of the queries. As any other composable, it can be used inside the `setup` function of a\n * component, within another composable, or in injectable contexts like stores and navigation guards.\n */\nexport const useQueryCache = /* @__PURE__ */ defineStore(QUERY_STORE_ID, ({ action }) => {\n // We have two versions of the cache, one that track changes and another that doesn't so the actions can be used\n // inside computed properties\n const cachesRaw = new Map<string, UseQueryEntry<unknown, unknown, unknown>>()\n let triggerCache!: () => void\n const caches = skipHydrate(\n customRef(\n (track, trigger) =>\n (triggerCache = trigger) && {\n // eslint-disable-next-line no-sequences\n get: () => (track(), cachesRaw),\n set:\n process.env.NODE_ENV !== 'production'\n ? () => {\n console.error(\n `[@pinia/colada]: The query cache instance cannot be set directly, it must be modified. This will fail in production.`,\n )\n }\n : noop,\n },\n ),\n )\n\n // this version of the cache cannot be hydrated because it would miss all of the actions\n // and plugins won't be able to hook into entry creation and fetching\n // this allows use to attach reactive effects to the scope later on\n const scope = getCurrentScope()!\n const app: App<unknown>\n // @ts-expect-error: internal\n = getActivePinia()!._a\n\n if (process.env.NODE_ENV !== 'production') {\n if (!hasInjectionContext()) {\n warnOnce(\n `useQueryCache() was called outside of an injection context (component setup, store, navigation guard) You will get a warning about \"inject\" being used incorrectly from Vue. Make sure to use it only in allowed places.\\n`\n + `See https://vuejs.org/guide/reusability/composables.html#usage-restrictions`,\n )\n }\n }\n\n const optionDefaults = useQueryOptions()\n\n /**\n * Creates a new query entry in the cache. Shouldn't be called directly.\n *\n * @param key - Serialized key of the query\n * @param [options] - options attached to the query\n * @param [initialData] - initial data of the query if any\n * @param [error] - initial error of the query if any\n * @param [when] - relative when was the data or error fetched (will be added to Date.now())\n */\n const create = action(\n <TData, TError, TDataInitial extends TData | undefined>(\n key: EntryKey,\n options: UseQueryOptionsWithDefaults<TData, TError, TDataInitial> | null = null,\n initialData?: TDataInitial,\n error: TError | null = null,\n when: number = 0,\n // when: number = initialData === undefined ? 0 : Date.now(),\n ): UseQueryEntry<TData, TError, TDataInitial> =>\n scope.run(() => {\n const state = shallowRef<DataState<TData, TError, TDataInitial>>(\n // @ts-expect-error: to make the code shorter we are using one declaration instead of multiple ternaries\n {\n // NOTE: we could move the `initialData` parameter before `options` and make it required\n // but that would force `create` call in `setQueryData` to pass an extra `undefined` argument\n data: initialData as TDataInitial,\n error,\n status: error ? 'error' : initialData !== undefined ? 'success' : 'pending',\n },\n )\n const asyncStatus = shallowRef<AsyncStatus>('idle')\n // we markRaw to avoid unnecessary vue traversal\n return markRaw<UseQueryEntry<TData, TError, TDataInitial>>({\n key,\n keyHash: toCacheKey(key),\n state,\n placeholderData: null,\n when: initialData === undefined ? 0 : Date.now() - when,\n asyncStatus,\n pending: null,\n // this set can contain components and effects and worsen the performance\n // and create weird warnings\n deps: markRaw(new Set()),\n gcTimeout: undefined,\n // eslint-disable-next-line ts/ban-ts-comment\n // @ts-ignore: some plugins are adding properties to the entry type\n ext: START_EXT,\n options,\n get stale() {\n return !this.options || !this.when || Date.now() >= this.when + this.options.staleTime\n },\n get active() {\n return this.deps.size > 0\n },\n } satisfies UseQueryEntry<TData, TError, TDataInitial>)\n })!,\n )\n\n const defineQueryMap = new WeakMap<() => unknown, DefineQueryEntry>()\n\n /**\n * Ensures a query created with {@link defineQuery} is present in the cache. If it's not, it creates a new one.\n * @param fn - function that defines the query\n */\n const ensureDefinedQuery = action(<T>(fn: () => T) => {\n let defineQueryEntry = defineQueryMap.get(fn)\n if (!defineQueryEntry) {\n // create the entry first\n currentDefineQueryEntry = defineQueryEntry = scope.run(() => [\n [],\n null,\n effectScope(),\n shallowRef(false),\n ])!\n\n // then run it so it can add the queries to the entry\n // we use the app context for injections and the scope for effects\n defineQueryEntry[1] = app.runWithContext(() => defineQueryEntry![2].run(fn)!)\n currentDefineQueryEntry = null\n defineQueryMap.set(fn, defineQueryEntry)\n } else {\n // ensure the scope is active so effects computing inside `useQuery()` run (e.g. the entry computed)\n defineQueryEntry[2].resume()\n defineQueryEntry[3].value = false\n // if the entry already exists, we know the queries inside\n // we should consider as if they are activated again\n defineQueryEntry[0] = defineQueryEntry[0].map((oldEntry) =>\n // the entries' key might have changed (e.g. Nuxt navigation)\n // so we need to ensure them again\n oldEntry.options ? ensure(oldEntry.options, oldEntry) : oldEntry,\n )\n }\n\n return defineQueryEntry\n })\n\n /**\n * Tracks an effect or component that uses a query.\n *\n * @param entry - the entry of the query\n * @param effect - the effect or component to untrack\n *\n * @see {@link untrack}\n */\n function track(\n entry: UseQueryEntry,\n effect: EffectScope | ComponentInternalInstance | null | undefined,\n ) {\n if (!effect) return\n entry.deps.add(effect)\n // clearTimeout ignores anything that isn't a timerId\n clearTimeout(entry.gcTimeout)\n entry.gcTimeout = undefined\n triggerCache()\n }\n\n /**\n * Untracks an effect or component that uses a query.\n *\n * @param entry - the entry of the query\n * @param effect - the effect or component to untrack\n *\n * @see {@link track}\n */\n function untrack(\n entry: UseQueryEntry,\n effect: EffectScope | ComponentInternalInstance | undefined | null,\n ) {\n // avoid clearing an existing timeout\n if (!effect || !entry.deps.has(effect)) return\n\n // clear any HMR to avoid letting the set grow\n if (process.env.NODE_ENV !== 'production') {\n if ('type' in effect && '__hmrId' in effect.type && entry.__hmr) {\n const count = (entry.__hmr.ids.get(effect.type.__hmrId) ?? 1) - 1\n if (count > 0) {\n entry.__hmr.ids.set(effect.type.__hmrId, count)\n } else {\n entry.__hmr.ids.delete(effect.type.__hmrId)\n }\n }\n }\n\n entry.deps.delete(effect)\n triggerCache()\n\n scheduleGarbageCollection(entry)\n }\n\n function scheduleGarbageCollection(entry: UseQueryEntry) {\n // schedule a garbage collection if the entry is not active\n // and we know its gcTime value\n if (entry.deps.size > 0 || !entry.options) return\n clearTimeout(entry.gcTimeout)\n // avoid setting a timeout with false, Infinity or NaN\n if ((Number.isFinite as (val: unknown) => val is number)(entry.options.gcTime)) {\n entry.gcTimeout = setTimeout(() => {\n remove(entry)\n }, entry.options.gcTime)\n }\n }\n\n /**\n * Invalidates and cancel matched queries, and then refetches (in parallel)\n * all active ones. If you need to further control which queries are\n * invalidated, canceled, and/or refetched, you can use the filters, you\n * can direcly call {@link invalidate} on {@link getEntries}:\n *\n * ```ts\n * // instead of doing\n * await queryCache.invalidateQueries(filters)\n * await Promise.all(queryCache.getEntries(filters).map(entry => {\n * queryCache.invalidate(entry)\n * // this is the default behavior of invalidateQueries\n * // return entry.active && queryCache.fetch(entry)\n * // here to refetch everything, even non active queries\n * return queryCache.fetch(entry)\n * })\n * ```\n *\n * @param filters - filters to apply to the entries\n * @param refetchActive - whether to refetch active queries or not. Set\n * to `'all'` to refetch all queries\n *\n * @see {@link invalidate}\n * @see {@link cancel}\n */\n const invalidateQueries = action(\n (filters?: UseQueryEntryFilter, refetchActive: boolean | 'all' = true): Promise<unknown> => {\n return Promise.all(\n getEntries(filters).map((entry) => {\n invalidate(entry)\n return (\n (refetchActive === 'all' || (entry.active && refetchActive))\n && toValue(entry.options?.enabled)\n && fetch(entry)\n )\n }),\n )\n },\n )\n\n /**\n * Returns all the entries in the cache that match the filters.\n *\n * @param filters - filters to apply to the entries\n */\n const getEntries = action((filters: UseQueryEntryFilter = {}): UseQueryEntry[] => {\n // TODO: Iterator.from(node) in 2028 once widely available? or maybe not worth it\n return (\n filters.exact\n ? filters.key\n ? [caches.value.get(toCacheKey(filters.key))].filter((v) => !!v)\n : [] // exact with no key can't match anything\n : [...find(caches.value, filters.key)]\n ).filter(\n (entry) =>\n (filters.stale == null || entry.stale === filters.stale)\n && (filters.active == null || entry.active === filters.active)\n && (!filters.status || entry.state.value.status === filters.status)\n && (!filters.predicate || filters.predicate(entry)),\n )\n })\n\n /**\n * Ensures a query entry is present in the cache. If it's not, it creates a new one. The resulting entry is required\n * to call other methods like {@link fetch}, {@link refresh}, or {@link invalidate}.\n *\n * @param opts - options to create the query\n * @param previousEntry - the previous entry that was associated with the same options\n */\n const ensure = action(\n <TData = unknown, TError = ErrorDefault, TDataInitial extends TData | undefined = undefined>(\n opts: UseQueryOptions<TData, TError, TDataInitial>,\n previousEntry?: UseQueryEntry<TData, TError, TDataInitial>,\n ): UseQueryEntry<TData, TError, TDataInitial> => {\n const options: UseQueryOptionsWithDefaults<TData, TError, TDataInitial> = {\n ...optionDefaults,\n ...opts,\n }\n const key = toValue(options.key)\n const keyHash = toCacheKey(key)\n\n if (process.env.NODE_ENV !== 'production' && keyHash === '[]') {\n throw new Error(\n `useQuery() was called with an empty array as the key. It must have at least one element.`,\n )\n }\n\n // do not reinitialize the entry\n // because of the immediate watcher in useQuery, the `ensure()` action is called twice on mount\n // we return early to avoid pushing to currentDefineQueryEntry\n if (previousEntry && keyHash === previousEntry.keyHash) {\n return previousEntry\n }\n\n // Since ensure() is called within a computed, we cannot let Vue track cache, so we use the raw version instead\n let entry = cachesRaw.get(keyHash) as UseQueryEntry<TData, TError, TDataInitial> | undefined\n // ensure the state\n if (!entry) {\n cachesRaw.set(keyHash, (entry = create(key, options, options.initialData?.())))\n // the placeholderData is only used if the entry is initially loading\n if (options.placeholderData && entry.state.value.status === 'pending') {\n entry.placeholderData = toValueWithArgs(\n options.placeholderData,\n // pass the previous entry placeholder data if it was in placeholder state\n isEntryUsingPlaceholderData(previousEntry)\n ? previousEntry.placeholderData\n : previousEntry?.state.value.data,\n )\n }\n triggerCache()\n }\n\n // during HMR, staleTime could be long and if we change the query function, the query won't trigger a refetch\n // so we need to detect and trigger just in case\n if (process.env.NODE_ENV !== 'production') {\n const currentInstance = getCurrentInstance()\n if (currentInstance) {\n entry.__hmr ??= { ids: new Map() }\n\n const id\n // @ts-expect-error: internal property\n = currentInstance.type?.__hmrId\n\n if (id) {\n if (entry.__hmr.ids.has(id)) {\n invalidate(entry)\n }\n const count = (entry.__hmr.ids.get(id) ?? 0) + 1\n entry.__hmr.ids.set(id, count)\n }\n }\n }\n\n // we set it every time to ensure we are using up to date key getters and others options\n entry.options = options\n\n // extend the entry with plugins the first time only\n if (entry.ext === START_EXT) {\n entry.ext = {} as UseQueryEntryExtensions<TData, TError>\n extend(entry)\n }\n\n // if this query was defined within a defineQuery call, add it to the list\n currentDefineQueryEntry?.[0].push(entry)\n\n return entry\n },\n )\n\n /**\n * Action called when an entry is ensured for the first time to allow plugins to extend it.\n *\n * @param _entry - the entry of the query to extend\n */\n const extend = action(\n <TData = unknown, TError = ErrorDefault, TDataInitial extends TData | undefined = undefined>(\n _entry: UseQueryEntry<TData, TError, TDataInitial>,\n ) => {},\n )\n\n /**\n * Invalidates and cancels a query entry. It effectively sets the `when`\n * property to `0` and {@link cancel | cancels} the pending request.\n *\n * @param entry - the entry of the query to invalidate\n *\n * @see {@link cancel}\n */\n const invalidate = action((entry: UseQueryEntry) => {\n // will force a fetch next time\n entry.when = 0\n // ignores the pending query\n cancel(entry)\n })\n\n /**\n * Ensures the current data is fresh. If the data is stale or if the status\n * is 'error', calls {@link fetch}, if not return the current data. Can only\n * be called if the entry has been initialized with `useQuery()` and has\n * options.\n *\n * @param entry - the entry of the query to refresh\n * @param options - the options to use for the fetch\n *\n * @see {@link fetch}\n */\n const refresh = action(\n async <TData, TError, TDataInitial extends TData | undefined>(\n entry: UseQueryEntry<TData, TError, TDataInitial>,\n options = entry.options,\n ): Promise<DataState<TData, TError, TDataInitial>> => {\n if (process.env.NODE_ENV !== 'production' && !options) {\n throw new Error(\n `\"entry.refresh()\" was called but the entry has no options. This is probably a bug, report it to pinia-colada with a boiled down example to reproduce it. Thank you!`,\n )\n }\n\n if (entry.state.value.error || entry.stale) {\n return entry.pending?.refreshCall ?? fetch(entry, options)\n }\n\n return entry.state.value\n },\n )\n\n /**\n * Fetch an entry. Ignores fresh data and triggers a new fetch. Can only be called if the entry has options.\n *\n * @param entry - the entry of the query to fetch\n * @param options - the options to use for the fetch\n */\n const fetch = action(\n async <TData, TError, TDataInitial extends TData | undefined>(\n entry: UseQueryEntry<TData, TError, TDataInitial>,\n options = entry.options,\n ): Promise<DataState<TData, TError, TDataInitial>> => {\n if (process.env.NODE_ENV !== 'production' && !options) {\n throw new Error(\n `\"entry.fetch()\" was called but the entry has no options. This is probably a bug, report it to pinia-colada with a boiled down example to reproduce it. Thank you!`,\n )\n }\n\n entry.asyncStatus.value = 'loading'\n\n const abortController = new AbortController()\n const { signal } = abortController\n // Abort any ongoing request without a reason to keep `AbortError` even with\n // signal.throwIfAborted() in the query function\n entry.pending?.abortController.abort()\n\n const pendingCall = (entry.pending = {\n abortController,\n // wrapping with async allows us to catch synchronous errors too\n refreshCall: (async () => options!.query({ signal }))()\n .then((data) => {\n if (pendingCall === entry.pending) {\n setEntryState(entry, {\n data,\n error: null,\n status: 'success',\n })\n }\n return entry.state.value\n })\n .catch((error) => {\n if (\n pendingCall === entry.pending\n && error\n // when the error is an abort error, it means the request was cancelled\n // we should just ignore the result of the query but not error\n && error.name !== 'AbortError'\n ) {\n setEntryState(entry, {\n status: 'error',\n data: entry.state.value.data,\n error,\n })\n }\n\n // always propagate up the error\n throw error\n // NOTE: other options included returning an ongoing request if the error was a cancellation but it seems not worth it\n })\n .finally(() => {\n entry.asyncStatus.value = 'idle'\n if (pendingCall === entry.pending) {\n // update the time amounts based on the current request\n // NOTE: Normally these should be the same everywhere but the\n // same query could be instantiated with different options\n // entry.gcTime = options!.gcTime\n // entry.staleTime = options!.staleTime\n entry.pending = null\n // there are cases when the result is ignored, in that case, we still\n // do not have a real result so we keep the placeholder data\n if (entry.state.value.status !== 'pending') {\n // reset the placeholder data to free up memory\n entry.placeholderData = null\n }\n entry.when = Date.now()\n }\n }),\n when: Date.now(),\n })\n\n return pendingCall.refreshCall\n },\n )\n\n /**\n * Cancels an entry's query if it's currently pending. This will effectively abort the `AbortSignal` of the query and any\n * pending request will be ignored.\n *\n * @param entry - the entry of the query to cancel\n * @param reason - the reason passed to the abort controller\n */\n const cancel = action((entry: UseQueryEntry, reason?: unknown) => {\n entry.pending?.abortController.abort(reason)\n // eagerly set the status to idle because the abort signal might not\n // be consumed by the user's query\n entry.asyncStatus.value = 'idle'\n entry.pending = null\n })\n\n /**\n * Cancels queries if they are currently pending. This will effectively abort the `AbortSignal` of the query and any\n * pending request will be ignored.\n *\n * @param filters - filters to apply to the entries\n * @param reason - the reason passed to the abort controller\n *\n * @see {@link cancel}\n */\n const cancelQueries = action((filters?: UseQueryEntryFilter, reason?: unknown) => {\n getEntries(filters).forEach((entry) => cancel(entry, reason))\n })\n\n /**\n * Sets the state of a query entry in the cache and updates the\n * {@link UseQueryEntry['pending']['when'] | `when` property}. This action is\n * called every time the cache state changes and can be used by plugins to\n * detect changes.\n *\n * @param entry - the entry of the query to set the state\n * @param state - the new state of the entry\n */\n const setEntryState = action(\n <TData, TError, TDataInitial extends TData | undefined = TData | undefined>(\n entry: UseQueryEntry<TData, TError, TDataInitial>,\n // NOTE: NoInfer ensures correct inference of TData and TError\n state: DataState<NoInfer<TData>, NoInfer<TError>, NoInfer<TDataInitial>>,\n ) => {\n entry.state.value = state\n entry.when = Date.now()\n // if we need to, we could schedule a garbage collection here but I don't\n // see why would one create entries that are not used (not tracked immediately after)\n },\n )\n\n /**\n * Gets a single query entry from the cache based on the key of the query.\n *\n * @param key - the key of the query\n */\n function get<\n TData = unknown,\n TError = ErrorDefault,\n TDataInitial extends TData | undefined = undefined,\n >(\n key: EntryKeyTagged<TData, TError, TDataInitial> | EntryKey,\n ): UseQueryEntry<TData, TError, TDataInitial> | undefined {\n return caches.value.get(toCacheKey(key)) as\n | UseQueryEntry<TData, TError, TDataInitial>\n | undefined\n }\n\n /**\n * Set the data of a query entry in the cache. It also sets the `status` to `success`.\n *\n * @param key - the key of the query\n * @param data - the new data to set\n *\n * @see {@link setEntryState}\n */\n const setQueryData = action(\n <TData = unknown, TError = ErrorDefault, TDataInitial extends TData | undefined = undefined>(\n key: EntryKeyTagged<TData, TError, TDataInitial> | EntryKey,\n data:\n | NoInfer<TData>\n // a success query cannot have undefined data\n | Exclude<NoInfer<TDataInitial>, undefined>\n // but could not be there and therefore have undefined here\n | ((oldData: TData | TDataInitial | undefined) => TData | Exclude<TDataInitial, undefined>),\n ) => {\n const keyHash = toCacheKey(key)\n let entry = cachesRaw.get(keyHash) as UseQueryEntry<TData, unknown, TDataInitial> | undefined\n\n // if the entry doesn't exist, we create it to set the data\n // it cannot be refreshed or fetched since the options\n // will be missing\n if (!entry) {\n cachesRaw.set(keyHash, (entry = create<TData, unknown, TDataInitial>(key)))\n }\n\n setEntryState(entry, {\n // we assume the data accounts for a successful state\n error: null,\n status: 'success',\n data: toValueWithArgs(data, entry.state.value.data),\n })\n scheduleGarbageCollection(entry)\n triggerCache()\n },\n )\n\n /**\n * Sets the data of all queries in the cache that are children of a key. It\n * also sets the `status` to `success`. Differently from {@link\n * setQueryData}, this method recursively sets the data for all queries. This\n * is why it requires a function to set the data.\n *\n * @param filters - filters used to get the entries\n * @param updater - the function to set the data\n *\n * @example\n * ```ts\n * // let's suppose we want to optimistically update all contacts in the cache\n * setQueriesData(['contacts', 'list'], (contactList: Contact[]) => {\n * const contactToReplaceIndex = contactList.findIndex(c => c.id === updatedContact.id)\n * return contactList.toSpliced(contactToReplaceIndex, 1, updatedContact)\n * })\n * ```\n *\n * @see {@link setQueryData}\n */\n function setQueriesData<TData = unknown>(\n filters: UseQueryEntryFilter,\n updater: (previous: TData | undefined) => TData,\n ): void {\n for (const entry of getEntries(filters)) {\n setEntryState(entry, {\n error: null,\n status: 'success',\n data: updater(entry.state.value.data as TData | undefined),\n })\n scheduleGarbageCollection(entry)\n }\n triggerCache()\n }\n\n /**\n * Gets the data of a query entry in the cache based on the key of the query.\n *\n * @param key - the key of the query\n */\n function getQueryData<\n TData = unknown,\n TError = ErrorDefault,\n TDataInitial extends TData | undefined = undefined,\n >(key: EntryKeyTagged<TData, TError, TDataInitial> | EntryKey): TData | TDataInitial | undefined {\n return caches.value.get(toCacheKey(key))?.state.value.data as TData | TDataInitial | undefined\n }\n\n /**\n * Removes a query entry from the cache.\n *\n * @param entry - the entry of the query to remove\n */\n const remove = action((entry: UseQueryEntry) => {\n // setting without a value is like setting it to undefined\n cachesRaw.delete(entry.keyHash)\n triggerCache()\n })\n\n return {\n caches,\n\n ensureDefinedQuery,\n /**\n * Scope to track effects and components that use the query cache.\n * @internal\n */\n _s: markRaw(scope),\n setQueryData,\n setQueriesData,\n getQueryData,\n\n invalidateQueries,\n cancelQueries,\n\n // Actions for entries\n invalidate,\n fetch,\n refresh,\n ensure,\n extend,\n track,\n untrack,\n cancel,\n create,\n remove,\n get,\n setEntryState,\n getEntries,\n }\n})\n\n/**\n * The cache of the queries. It's the store returned by {@link useQueryCache}.\n */\nexport type QueryCache = ReturnType<typeof useQueryCache>\n\n/**\n * Checks if the given object is a query cache. Used in SSR to apply custom serialization.\n *\n * @param cache - the object to check\n *\n * @see {@link QueryCache}\n * @see {@link serializeQueryCache}\n */\nexport function isQueryCache(cache: unknown): cache is QueryCache {\n return (\n typeof cache === 'object'\n && !!cache\n && (cache as Record<string, unknown>).$id === QUERY_STORE_ID\n )\n}\n\n/**\n * Raw data of a query entry. Can be serialized from the server and used to\n * hydrate the store.\n *\n * @internal\n */\nexport type _UseQueryEntryNodeValueSerialized<TData = unknown, TError = unknown> = [\n /**\n * The data returned by the query.\n */\n data: TData | undefined,\n\n /**\n * The error thrown by the query.\n */\n error: TError | null,\n\n /**\n * When was this data fetched the last time in ms\n */\n when?: number,\n]\n\n/**\n * Hydrates the query cache with the serialized cache. Used during SSR.\n * @param queryCache - query cache\n * @param serializedCache - serialized cache\n */\nexport function hydrateQueryCache(\n queryCache: QueryCache,\n serializedCache: Record<string, _UseQueryEntryNodeValueSerialized>,\n) {\n for (const keyHash in serializedCache) {\n queryCache.caches.set(\n keyHash,\n queryCache.create(JSON.parse(keyHash), undefined, ...(serializedCache[keyHash] ?? [])),\n )\n }\n}\n\n/**\n * Serializes the query cache to a compressed version. Used during SSR.\n *\n * @param queryCache - query cache\n */\nexport function serializeQueryCache(\n queryCache: QueryCache,\n): Record<string, _UseQueryEntryNodeValueSerialized> {\n return Object.fromEntries(\n // TODO: 2028: directly use .map on the iterator\n [...queryCache.caches.entries()].map(([keyHash, entry]) => [keyHash, queryEntry_toJSON(entry)]),\n )\n}\n","import { inject } from 'vue'\nimport type { InjectionKey, MaybeRefOrGetter } from 'vue'\nimport type { EntryKey } from './entry-keys'\nimport type { ErrorDefault } from './types-extension'\n\n/**\n * Possible values for `refetchOnMount`, `refetchOnWindowFocus`, and `refetchOnReconnect`.\n * `true` refetches if data is stale (calles `refresh()`), `false` never refetches, `'always'` always refetches.\n */\nexport type RefetchOnControl = boolean | 'always'\n\n/**\n * Options for queries that can be globally overridden.\n */\nexport interface UseQueryOptionsGlobal {\n /**\n * Whether the query should be enabled or not. If `false`, the query will not\n * be executed until `refetch()` or `refresh()` is called. If it becomes\n * `true`, the query will be refreshed.\n */\n enabled?: MaybeRefOrGetter<boolean>\n\n /**\n * Time in ms after which the data is considered stale and will be refreshed\n * on next read.\n *\n * @default 5000 (5 seconds)\n */\n staleTime?: number\n\n /**\n * Time in ms after which, once the data is no longer being used, it will be\n * garbage collected to free resources. Set to `false` to disable garbage\n * collection.\n *\n * @default 300_000 (5 minutes)\n */\n gcTime?: number | false\n\n /**\n * Whether to refetch the query when the component is mounted.\n * @default true\n */\n refetchOnMount?: MaybeRefOrGetter<RefetchOnControl>\n\n /**\n * Whether to refetch the query when the window regains focus.\n * @default true\n */\n refetchOnWindowFocus?: MaybeRefOrGetter<RefetchOnControl>\n\n /**\n * Whether to refetch the query when the network reconnects.\n * @default true\n */\n refetchOnReconnect?: MaybeRefOrGetter<RefetchOnControl>\n\n /**\n * A placeholder data that is initially shown while the query is loading for\n * the first time. This will also show the `status` as `success` until the\n * query finishes loading (no matter the outcome of the query). Note: unlike\n * with `initialData`, the placeholder does not change the cache state.\n */\n placeholderData?: (previousData: unknown) => any // any allows us to not worry about the types when merging options\n}\n\n/**\n * Context object passed to the `query` function of `useQuery()`.\n * @see {@link UseQueryOptions}\n */\nexport interface UseQueryFnContext {\n /**\n * `AbortSignal` instance attached to the query call. If the call becomes\n * outdated (e.g. due to a new call with the same key), the signal will be\n * aborted.\n */\n signal: AbortSignal\n}\n\n/**\n * Options for `useQuery()`. Can be extended by plugins.\n *\n * @example\n * ```ts\n * // use-query-plugin.d.ts\n * export {} // needed\n * declare module '@pinia/colada' {\n * interface UseQueryOptions {\n * // Whether to refresh the data when the component is mounted.\n * refreshOnMount?: boolean\n * }\n * }\n * ```\n */\nexport interface UseQueryOptions<\n TData = unknown,\n // eslint-disable-next-line unused-imports/no-unused-vars\n TError = ErrorDefault,\n TDataInitial extends TData | undefined = undefined,\n> extends Pick<\n UseQueryOptionsGlobal,\n | 'gcTime'\n | 'enabled'\n | 'refetchOnMount'\n | 'refetchOnReconnect'\n | 'refetchOnWindowFocus'\n | 'staleTime'\n > {\n /**\n * The key used to identify the query. Array of primitives **without**\n * reactive values or a reactive array or getter. It should be treaded as an\n * array of dependencies of your queries, e.g. if you use the\n * `route.params.id` property, it should also be part of the key:\n *\n * ```ts\n * import { useRoute } from 'vue-router'\n * import { useQuery } from '@pinia/colada'\n *\n * const route = useRoute()\n * const { data } = useQuery({\n * // pass a getter function (or compu