UNPKG

@pinia/colada

Version:

The smart data fetching layer for Vue.js

1,283 lines (1,263 loc) 43.8 kB
var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { PiniaColada: () => PiniaColada, PiniaColadaQueryHooksPlugin: () => PiniaColadaQueryHooksPlugin, defineMutation: () => defineMutation, defineQuery: () => defineQuery, defineQueryOptions: () => defineQueryOptions, hydrateQueryCache: () => hydrateQueryCache, isQueryCache: () => isQueryCache, serializeQueryCache: () => serializeQueryCache, useInfiniteQuery: () => useInfiniteQuery, useMutation: () => useMutation, useQuery: () => useQuery, useQueryCache: () => useQueryCache, useQueryState: () => useQueryState }); module.exports = __toCommonJS(index_exports); // src/define-query-options.ts // @__NO_SIDE_EFFECTS__ function defineQueryOptions(setupOrOptions) { return setupOrOptions; } // src/define-query.ts var import_vue5 = require("vue"); // src/query-store.ts var import_pinia = require("pinia"); var import_vue3 = require("vue"); // src/query-options.ts var import_vue = require("vue"); var USE_QUERY_DEFAULTS = { staleTime: 1e3 * 5, // 5 seconds gcTime: 1e3 * 60 * 5, // 5 minutes // avoid type narrowing to `true` refetchOnWindowFocus: true, refetchOnReconnect: true, refetchOnMount: true, enabled: true }; var USE_QUERY_OPTIONS_KEY = process.env.NODE_ENV !== "production" ? Symbol("useQueryOptions") : Symbol(); var useQueryOptions = () => (0, import_vue.inject)(USE_QUERY_OPTIONS_KEY, USE_QUERY_DEFAULTS); // src/entry-keys.ts function toCacheKey(key) { return key && JSON.stringify(key, (_, val) => !val || typeof val !== "object" || Array.isArray(val) ? val : Object.keys(val).sort().reduce((result, key2) => { result[key2] = val[key2]; return result; }, {})); } function isSubsetOf(subsetKey, fullsetKey) { return subsetKey === fullsetKey ? true : typeof subsetKey !== typeof fullsetKey ? false : subsetKey && fullsetKey && typeof subsetKey === "object" && typeof fullsetKey === "object" ? Object.keys(subsetKey).every( (key) => isSubsetOf( // NOTE: this or making them `any` in the function signature subsetKey[key], fullsetKey[key] ) ) : false; } var ENTRY_DATA_TAG = Symbol("Pinia Colada data tag"); var ENTRY_ERROR_TAG = Symbol("Pinia Colada error tag"); var ENTRY_DATA_INITIAL_TAG = Symbol("Pinia Colada data initial tag"); function* find(map, partialKey) { for (const entry of map.values()) { if (!partialKey || entry.key && isSubsetOf(partialKey, entry.key)) { yield entry; } } } // src/utils.ts var import_vue2 = require("vue"); function useEventListener(target, event, listener, options) { target.addEventListener(event, listener, options); if ((0, import_vue2.getCurrentScope)()) { (0, import_vue2.onScopeDispose)(() => { target.removeEventListener(event, listener); }); } } var IS_CLIENT = typeof window !== "undefined"; function toValueWithArgs(valFn, ...args) { return typeof valFn === "function" ? valFn(...args) : valFn; } var noop = () => { }; var warnedMessages = /* @__PURE__ */ new Set(); function warnOnce(message, id = message) { if (warnedMessages.has(id)) return; warnedMessages.add(id); console.warn(`[@pinia/colada]: ${message}`); } // src/query-store.ts var currentDefineQueryEntry; function isEntryUsingPlaceholderData(entry) { return entry?.placeholderData != null && entry.state.value.status === "pending"; } var START_EXT = {}; var queryEntry_toJSON = ({ state: { value }, when }) => [ value.data, value.error, // because of time zones, we create a relative time when ? Date.now() - when : -1 ]; var QUERY_STORE_ID = "_pc_query"; var useQueryCache = /* @__PURE__ */ (0, import_pinia.defineStore)(QUERY_STORE_ID, ({ action }) => { const cachesRaw = /* @__PURE__ */ new Map(); let triggerCache; const caches = (0, import_pinia.skipHydrate)( (0, import_vue3.customRef)( (track2, trigger) => (triggerCache = trigger) && { // eslint-disable-next-line no-sequences get: () => (track2(), cachesRaw), set: process.env.NODE_ENV !== "production" ? () => { console.error( `[@pinia/colada]: The query cache instance cannot be set directly, it must be modified. This will fail in production.` ); } : noop } ) ); const scope = (0, import_vue3.getCurrentScope)(); const app = (0, import_pinia.getActivePinia)()._a; if (process.env.NODE_ENV !== "production") { if (!(0, import_vue3.hasInjectionContext)()) { warnOnce( `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. See https://vuejs.org/guide/reusability/composables.html#usage-restrictions` ); } } const optionDefaults = useQueryOptions(); const create = action( (key, options = null, initialData, error = null, when = 0) => scope.run(() => { const state = (0, import_vue3.shallowRef)( // @ts-expect-error: to make the code shorter we are using one declaration instead of multiple ternaries { // NOTE: we could move the `initialData` parameter before `options` and make it required // but that would force `create` call in `setQueryData` to pass an extra `undefined` argument data: initialData, error, status: error ? "error" : initialData !== void 0 ? "success" : "pending" } ); const asyncStatus = (0, import_vue3.shallowRef)("idle"); return (0, import_vue3.markRaw)({ key, keyHash: toCacheKey(key), state, placeholderData: null, when: initialData === void 0 ? 0 : Date.now() - when, asyncStatus, pending: null, // this set can contain components and effects and worsen the performance // and create weird warnings deps: (0, import_vue3.markRaw)(/* @__PURE__ */ new Set()), gcTimeout: void 0, // eslint-disable-next-line ts/ban-ts-comment // @ts-ignore: some plugins are adding properties to the entry type ext: START_EXT, options, get stale() { return !this.options || !this.when || Date.now() >= this.when + this.options.staleTime; }, get active() { return this.deps.size > 0; } }); }) ); const defineQueryMap = /* @__PURE__ */ new WeakMap(); const ensureDefinedQuery = action((fn) => { let defineQueryEntry = defineQueryMap.get(fn); if (!defineQueryEntry) { currentDefineQueryEntry = defineQueryEntry = scope.run(() => [ [], null, (0, import_vue3.effectScope)(), (0, import_vue3.shallowRef)(false) ]); defineQueryEntry[1] = app.runWithContext(() => defineQueryEntry[2].run(fn)); currentDefineQueryEntry = null; defineQueryMap.set(fn, defineQueryEntry); } else { defineQueryEntry[2].resume(); defineQueryEntry[3].value = false; defineQueryEntry[0] = defineQueryEntry[0].map( (oldEntry) => ( // the entries' key might have changed (e.g. Nuxt navigation) // so we need to ensure them again oldEntry.options ? ensure(oldEntry.options, oldEntry) : oldEntry ) ); } return defineQueryEntry; }); function track(entry, effect) { if (!effect) return; entry.deps.add(effect); clearTimeout(entry.gcTimeout); entry.gcTimeout = void 0; triggerCache(); } function untrack(entry, effect) { if (!effect || !entry.deps.has(effect)) return; if (process.env.NODE_ENV !== "production") { if ("type" in effect && "__hmrId" in effect.type && entry.__hmr) { const count = (entry.__hmr.ids.get(effect.type.__hmrId) ?? 1) - 1; if (count > 0) { entry.__hmr.ids.set(effect.type.__hmrId, count); } else { entry.__hmr.ids.delete(effect.type.__hmrId); } } } entry.deps.delete(effect); triggerCache(); scheduleGarbageCollection(entry); } function scheduleGarbageCollection(entry) { if (entry.deps.size > 0 || !entry.options) return; clearTimeout(entry.gcTimeout); if (Number.isFinite(entry.options.gcTime)) { entry.gcTimeout = setTimeout(() => { remove(entry); }, entry.options.gcTime); } } const invalidateQueries = action( (filters, refetchActive = true) => { return Promise.all( getEntries(filters).map((entry) => { invalidate(entry); return (refetchActive === "all" || entry.active && refetchActive) && (0, import_vue3.toValue)(entry.options?.enabled) && fetch(entry); }) ); } ); const getEntries = action((filters = {}) => { return (filters.exact ? filters.key ? [caches.value.get(toCacheKey(filters.key))].filter((v) => !!v) : [] : [...find(caches.value, filters.key)]).filter( (entry) => (filters.stale == null || entry.stale === filters.stale) && (filters.active == null || entry.active === filters.active) && (!filters.status || entry.state.value.status === filters.status) && (!filters.predicate || filters.predicate(entry)) ); }); const ensure = action( (opts, previousEntry) => { const options = { ...optionDefaults, ...opts }; const key = (0, import_vue3.toValue)(options.key); const keyHash = toCacheKey(key); if (process.env.NODE_ENV !== "production" && keyHash === "[]") { throw new Error( `useQuery() was called with an empty array as the key. It must have at least one element.` ); } if (previousEntry && keyHash === previousEntry.keyHash) { return previousEntry; } let entry = cachesRaw.get(keyHash); if (!entry) { cachesRaw.set(keyHash, entry = create(key, options, options.initialData?.())); if (options.placeholderData && entry.state.value.status === "pending") { entry.placeholderData = toValueWithArgs( options.placeholderData, // pass the previous entry placeholder data if it was in placeholder state isEntryUsingPlaceholderData(previousEntry) ? previousEntry.placeholderData : previousEntry?.state.value.data ); } triggerCache(); } if (process.env.NODE_ENV !== "production") { const currentInstance = (0, import_vue3.getCurrentInstance)(); if (currentInstance) { entry.__hmr ??= { ids: /* @__PURE__ */ new Map() }; const id = currentInstance.type?.__hmrId; if (id) { if (entry.__hmr.ids.has(id)) { invalidate(entry); } const count = (entry.__hmr.ids.get(id) ?? 0) + 1; entry.__hmr.ids.set(id, count); } } } entry.options = options; if (entry.ext === START_EXT) { entry.ext = {}; extend(entry); } currentDefineQueryEntry?.[0].push(entry); return entry; } ); const extend = action( (_entry) => { } ); const invalidate = action((entry) => { entry.when = 0; cancel(entry); }); const refresh = action( async (entry, options = entry.options) => { if (process.env.NODE_ENV !== "production" && !options) { throw new Error( `"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!` ); } if (entry.state.value.error || entry.stale) { return entry.pending?.refreshCall ?? fetch(entry, options); } return entry.state.value; } ); const fetch = action( async (entry, options = entry.options) => { if (process.env.NODE_ENV !== "production" && !options) { throw new Error( `"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!` ); } entry.asyncStatus.value = "loading"; const abortController = new AbortController(); const { signal } = abortController; entry.pending?.abortController.abort(); const pendingCall = entry.pending = { abortController, // wrapping with async allows us to catch synchronous errors too refreshCall: (async () => options.query({ signal }))().then((data) => { if (pendingCall === entry.pending) { setEntryState(entry, { data, error: null, status: "success" }); } return entry.state.value; }).catch((error) => { if (pendingCall === entry.pending && error && error.name !== "AbortError") { setEntryState(entry, { status: "error", data: entry.state.value.data, error }); } throw error; }).finally(() => { entry.asyncStatus.value = "idle"; if (pendingCall === entry.pending) { entry.pending = null; if (entry.state.value.status !== "pending") { entry.placeholderData = null; } entry.when = Date.now(); } }), when: Date.now() }; return pendingCall.refreshCall; } ); const cancel = action((entry, reason) => { entry.pending?.abortController.abort(reason); entry.asyncStatus.value = "idle"; entry.pending = null; }); const cancelQueries = action((filters, reason) => { getEntries(filters).forEach((entry) => cancel(entry, reason)); }); const setEntryState = action( (entry, state) => { entry.state.value = state; entry.when = Date.now(); } ); function get(key) { return caches.value.get(toCacheKey(key)); } const setQueryData = action( (key, data) => { const keyHash = toCacheKey(key); let entry = cachesRaw.get(keyHash); if (!entry) { cachesRaw.set(keyHash, entry = create(key)); } setEntryState(entry, { // we assume the data accounts for a successful state error: null, status: "success", data: toValueWithArgs(data, entry.state.value.data) }); scheduleGarbageCollection(entry); triggerCache(); } ); function setQueriesData(filters, updater) { for (const entry of getEntries(filters)) { setEntryState(entry, { error: null, status: "success", data: updater(entry.state.value.data) }); scheduleGarbageCollection(entry); } triggerCache(); } function getQueryData(key) { return caches.value.get(toCacheKey(key))?.state.value.data; } const remove = action((entry) => { cachesRaw.delete(entry.keyHash); triggerCache(); }); return { caches, ensureDefinedQuery, /** * Scope to track effects and components that use the query cache. * @internal */ _s: (0, import_vue3.markRaw)(scope), setQueryData, setQueriesData, getQueryData, invalidateQueries, cancelQueries, // Actions for entries invalidate, fetch, refresh, ensure, extend, track, untrack, cancel, create, remove, get, setEntryState, getEntries }; }); function isQueryCache(cache) { return typeof cache === "object" && !!cache && cache.$id === QUERY_STORE_ID; } function hydrateQueryCache(queryCache, serializedCache) { for (const keyHash in serializedCache) { queryCache.caches.set( keyHash, queryCache.create(JSON.parse(keyHash), void 0, ...serializedCache[keyHash] ?? []) ); } } function serializeQueryCache(queryCache) { return Object.fromEntries( // TODO: 2028: directly use .map on the iterator [...queryCache.caches.entries()].map(([keyHash, entry]) => [keyHash, queryEntry_toJSON(entry)]) ); } // src/use-query.ts var import_vue4 = require("vue"); function useQuery(...[_options, paramsGetter]) { if (paramsGetter != null) { return useQuery( () => ( // NOTE: we manually type cast here because TS cannot infer correctly in overloads _options( (0, import_vue4.toValue)(paramsGetter) ) ) ); } const queryCache = useQueryCache(); const optionDefaults = useQueryOptions(); const hasCurrentInstance = (0, import_vue4.getCurrentInstance)(); const defineQueryEffect = currentDefineQueryEntry?.[2]; const currentEffect = currentDefineQueryEffect || (0, import_vue4.getCurrentScope)(); const isPaused = currentDefineQueryEntry?.[3]; const options = (0, import_vue4.computed)( () => ({ ...optionDefaults, ...(0, import_vue4.toValue)( // NOTE: we manually type cast here because TS cannot infer correctly in overloads _options ) }) ); const enabled = () => (0, import_vue4.toValue)(options.value.enabled); let lastEntry; const entry = (0, import_vue4.computed)( () => ( // NOTE: there should be a `paused` property on the effect later on // if the effect is paused, we don't want to compute the entry because its key // might be referencing undefined values // https://github.com/posva/pinia-colada/issues/227 // NOTE: _isPaused isn't reactive which meant that reentering a component // would never recompute the entry, so _isPaused was replaced // this makes the computed depend on nothing initially, but the `watch` on the entry // with immediate: true will trigger it again // https://github.com/posva/pinia-colada/issues/290 isPaused?.value ? lastEntry : lastEntry = queryCache.ensure(options.value, lastEntry) ) ); lastEntry = entry.value; const errorCatcher = () => entry.value.state.value; const refresh = (throwOnError) => queryCache.refresh(entry.value, options.value).catch( // true is not allowed but it works per spec as only callable onRejected are used // https://tc39.es/ecma262/multipage/control-abstraction-objects.html#sec-performpromisethen // In other words `Promise.rejects('ok').catch(true)` still rejects // anything other than `true` falls back to the `errorCatcher` throwOnError || errorCatcher ); const refetch = (throwOnError) => queryCache.fetch(entry.value, options.value).catch( // same as above throwOnError || errorCatcher ); const isPlaceholderData = (0, import_vue4.computed)(() => isEntryUsingPlaceholderData(entry.value)); const state = (0, import_vue4.computed)( () => isPlaceholderData.value ? { status: "success", data: entry.value.placeholderData, error: null } : entry.value.state.value ); const extensions = {}; for (const key in lastEntry.ext) { extensions[key] = (0, import_vue4.computed)({ get: () => (0, import_vue4.toValue)(entry.value.ext[key]), set(value) { const target = entry.value.ext[key]; if ((0, import_vue4.isRef)(target)) { ; target.value = value; } else { ; entry.value.ext[key] = value; } } }); } const queryReturn = { ...extensions, state, status: (0, import_vue4.computed)(() => state.value.status), data: (0, import_vue4.computed)(() => state.value.data), error: (0, import_vue4.computed)(() => entry.value.state.value.error), asyncStatus: (0, import_vue4.computed)(() => entry.value.asyncStatus.value), isPlaceholderData, isPending: (0, import_vue4.computed)(() => state.value.status === "pending"), isLoading: (0, import_vue4.computed)(() => entry.value.asyncStatus.value === "loading"), refresh, refetch }; if (hasCurrentInstance) { (0, import_vue4.onServerPrefetch)(async () => { if ((0, import_vue4.toValue)(enabled)) await refresh(true); }); } let isActive = false; if (hasCurrentInstance) { (0, import_vue4.onMounted)(() => { isActive = true; queryCache.track(lastEntry, hasCurrentInstance); }); (0, import_vue4.onUnmounted)(() => { queryCache.untrack(lastEntry, hasCurrentInstance); }); } else { isActive = true; if (currentEffect !== defineQueryEffect) { queryCache.track(lastEntry, currentEffect); (0, import_vue4.onScopeDispose)(() => { queryCache.untrack(lastEntry, currentEffect); }); } } (0, import_vue4.watch)( entry, (entry2, previousEntry) => { if (!isActive) return; if (previousEntry) { queryCache.untrack(previousEntry, hasCurrentInstance); queryCache.untrack(previousEntry, currentEffect); } queryCache.track(entry2, hasCurrentInstance); if (currentEffect !== defineQueryEffect) { queryCache.track(entry2, currentEffect); } if ((0, import_vue4.toValue)(enabled)) refresh(); }, { immediate: true } ); if (typeof enabled !== "boolean") { (0, import_vue4.watch)(enabled, (newEnabled) => { if (newEnabled) refresh(); }); } if (hasCurrentInstance) { (0, import_vue4.onMounted)(() => { if (enabled()) { const refetchControl = (0, import_vue4.toValue)(options.value.refetchOnMount); if (refetchControl === "always") { refetch(); } else if (refetchControl || queryReturn.status.value === "pending") { refresh(); } } }); } if (IS_CLIENT) { useEventListener(document, "visibilitychange", () => { const refetchControl = (0, import_vue4.toValue)(options.value.refetchOnWindowFocus); if (document.visibilityState === "visible" && (0, import_vue4.toValue)(enabled)) { if (refetchControl === "always") { refetch(); } else if (refetchControl) { refresh(); } } }); useEventListener(window, "online", () => { if ((0, import_vue4.toValue)(enabled)) { const refetchControl = (0, import_vue4.toValue)(options.value.refetchOnReconnect); if (refetchControl === "always") { refetch(); } else if (refetchControl) { refresh(); } } }); } return queryReturn; } // src/define-query.ts var currentDefineQueryEffect; function defineQuery(optionsOrSetup) { const setupFn = typeof optionsOrSetup === "function" ? optionsOrSetup : () => useQuery(optionsOrSetup); let hasBeenEnsured; let refCount = 0; return () => { const queryCache = useQueryCache(); const previousEffect = currentDefineQueryEffect; const currentScope = (0, import_vue5.getCurrentInstance)() || (currentDefineQueryEffect = (0, import_vue5.getCurrentScope)()); const [ensuredEntries, ret, scope, isPaused] = queryCache.ensureDefinedQuery(setupFn); if (hasBeenEnsured) { ensuredEntries.forEach((entry) => { if (entry.options?.refetchOnMount && (0, import_vue5.toValue)(entry.options.enabled)) { if ((0, import_vue5.toValue)(entry.options.refetchOnMount) === "always") { queryCache.fetch(entry).catch(noop); } else { queryCache.refresh(entry).catch(noop); } } }); } hasBeenEnsured = true; if (currentScope) { refCount++; ensuredEntries.forEach((entry) => { queryCache.track(entry, currentScope); }); (0, import_vue5.onScopeDispose)(() => { ensuredEntries.forEach((entry) => { queryCache.untrack(entry, currentScope); }); if (--refCount < 1) { scope.pause(); isPaused.value = true; } }); } currentDefineQueryEffect = previousEffect; return ret; }; } // src/use-query-state.ts var import_vue6 = require("vue"); function useQueryState(...[_keyOrSetup, paramsGetter]) { const queryCache = useQueryCache(); const key = paramsGetter ? (0, import_vue6.computed)( () => ( // NOTE: we manually type cast here because TS cannot infer correctly in overloads _keyOrSetup( (0, import_vue6.toValue)(paramsGetter) ).key ) ) : _keyOrSetup; const entry = (0, import_vue6.computed)(() => queryCache.get((0, import_vue6.toValue)(key))); const state = (0, import_vue6.computed)(() => entry.value?.state.value); const data = (0, import_vue6.computed)(() => state.value?.data); const error = (0, import_vue6.computed)(() => state.value?.error); const status = (0, import_vue6.computed)(() => state.value?.status); const asyncStatus = (0, import_vue6.computed)(() => entry.value?.asyncStatus.value); const isPending = (0, import_vue6.computed)(() => !state.value || state.value.status === "pending"); return { state, data, error, status, asyncStatus, isPending }; } // src/infinite-query.ts var import_vue7 = require("vue"); function useInfiniteQuery(options) { let pages = (0, import_vue7.toValue)(options.initialPage); const { refetch, refresh, ...query } = useQuery({ ...options, initialData: () => pages, // since we hijack the query function and augment the data, we cannot refetch the data // like usual staleTime: Infinity, async query(context) { const data = await options.query(pages, context); return pages = options.merge(pages, data); } }); return { ...query, loadMore: () => refetch() }; } // src/mutation-store.ts var import_pinia2 = require("pinia"); var import_vue9 = require("vue"); // src/mutation-options.ts var import_vue8 = require("vue"); var USE_MUTATION_DEFAULTS = { gcTime: 1e3 * 60 // 1 minute }; var USE_MUTATION_OPTIONS_KEY = process.env.NODE_ENV !== "production" ? Symbol("useMutationOptions") : Symbol(); var useMutationOptions = () => (0, import_vue8.inject)(USE_MUTATION_OPTIONS_KEY, USE_MUTATION_DEFAULTS); // src/mutation-store.ts var MUTATION_STORE_ID = "_pc_mutation"; var useMutationCache = /* @__PURE__ */ (0, import_pinia2.defineStore)(MUTATION_STORE_ID, ({ action }) => { const cachesRaw = /* @__PURE__ */ new Map(); let triggerCache; const caches = (0, import_pinia2.skipHydrate)( (0, import_vue9.customRef)( (track, trigger) => (triggerCache = trigger) && { // eslint-disable-next-line no-sequences get: () => (track(), cachesRaw), set: process.env.NODE_ENV !== "production" ? () => { console.error( `[@pinia/colada]: The mutation cache instance cannot be set directly, it must be modified. This will fail in production.` ); } : noop } ) ); const scope = (0, import_vue9.getCurrentScope)(); const globalOptions = useMutationOptions(); const defineMutationMap = /* @__PURE__ */ new WeakMap(); let nextMutationId = 0; const generateMutationId = () => `$${nextMutationId++}`; const create = action( (options, key, vars) => scope.run( () => ({ id: "", // not a real id yet, indicates that the entry is not in the cache state: (0, import_vue9.shallowRef)({ status: "pending", data: void 0, error: null }), gcTimeout: void 0, asyncStatus: (0, import_vue9.shallowRef)("idle"), when: 0, vars, key, keyHash: toCacheKey(key), options }) ) ); function ensure(entry, vars) { const options = entry.options; const id = generateMutationId(); const key = [...toValueWithArgs(options.key || [], vars), id]; const keyHash = toCacheKey(key); if (process.env.NODE_ENV !== "production") { const badKey = key?.slice(0, -1).find( (k) => typeof k === "string" && k.startsWith("$") && String(Number(k.slice(1))) === k.slice(1) ); if (badKey) { console.warn( `[@pinia/colada] A mutation entry was created with a reserved key part "${badKey}". Do not name keys with "$<number>" as these are reserved in mutations.` ); } } entry = entry.id ? (untrack(entry), create(options, key, vars)) : entry; entry.id = id; entry.key = key; entry.keyHash = keyHash; entry.vars = vars; cachesRaw.set(keyHash, entry); triggerCache(); return entry; } const ensureDefinedMutation = action((fn) => { let defineMutationResult = defineMutationMap.get(fn); if (!defineMutationResult) { defineMutationMap.set(fn, defineMutationResult = scope.run(fn)); } return defineMutationResult; }); const setEntryState = action( (entry, state) => { entry.state.value = state; entry.when = Date.now(); } ); const remove = action( (entry) => { if (entry.keyHash) { cachesRaw.delete(entry.keyHash); triggerCache(); } } ); const getEntries = action((filters = {}) => { return filters.exact ? filters.key ? [caches.value.get(toCacheKey(filters.key))].filter((v) => !!v) : [] : [...find(caches.value, filters.key)].filter( (entry) => (filters.status == null || entry.state.value.status === filters.status) && (!filters.predicate || filters.predicate(entry)) ); }); const untrack = action( (entry) => { if (entry.gcTimeout) return; if (Number.isFinite(entry.options.gcTime)) { entry.gcTimeout = setTimeout(() => { remove(entry); }, entry.options.gcTime); } } ); async function mutate(entry) { const { vars, options } = entry; if (process.env.NODE_ENV !== "production") { const key = entry.key?.join("/"); const keyMessage = key ? `with key "${key}"` : "without a key"; if (!entry.id) { console.error( `[@pinia/colada] A mutation entry ${keyMessage} was mutated before being ensured. If you are manually calling the "mutationCache.mutate()", you should always ensure the entry first If not, this is probably a bug. Please, open an issue on GitHub with a boiled down reproduction.` ); } if ( // the entry has already an ongoing request entry.state.value.status !== "pending" || entry.asyncStatus.value === "loading" ) { console.error( `[@pinia/colada] A mutation entry ${keyMessage} was reused. If you are manually calling the "mutationCache.mutate()", you should always ensure the entry first: "mutationCache.mutate(mutationCache.ensure(entry, vars))". If not this is probably a bug. Please, open an issue on GitHub with a boiled down reproduction.` ); } } entry.asyncStatus.value = "loading"; let currentData; let currentError; let context = {}; try { const globalOnMutateContext = globalOptions.onMutate?.(vars); context = (globalOnMutateContext instanceof Promise ? await globalOnMutateContext : globalOnMutateContext) || {}; const onMutateContext = await options.onMutate?.( vars, context // NOTE: the cast makes it easier to write without extra code. It's safe because { ...null, ...undefined } works and TContext must be a Record<any, any> ); context = { ...context, ...onMutateContext // NOTE: needed for onSuccess cast }; const newData = currentData = await options.mutation(vars, context); await globalOptions.onSuccess?.(newData, vars, context); await options.onSuccess?.( newData, vars, // NOTE: cast is safe because of the satisfies above // using a spread also works context ); setEntryState(entry, { status: "success", data: newData, error: null }); } catch (newError) { currentError = newError; await globalOptions.onError?.(currentError, vars, context); await options.onError?.(currentError, vars, context); setEntryState(entry, { status: "error", data: entry.state.value.data, error: currentError }); throw newError; } finally { await globalOptions.onSettled?.(currentData, currentError, vars, context); await options.onSettled?.(currentData, currentError, vars, context); entry.asyncStatus.value = "idle"; } return currentData; } return { caches, create, ensure, ensureDefinedMutation, mutate, remove, setEntryState, getEntries, untrack }; }); // src/use-mutation.ts var import_vue10 = require("vue"); function useMutation(options) { const mutationCache = useMutationCache(); const hasCurrentInstance = (0, import_vue10.getCurrentInstance)(); const currentEffect = (0, import_vue10.getCurrentScope)(); const entry = (0, import_vue10.shallowRef)( mutationCache.create(options) ); if (hasCurrentInstance) { (0, import_vue10.onUnmounted)(() => { mutationCache.untrack(entry.value); }); } if (currentEffect) { (0, import_vue10.onScopeDispose)(() => { mutationCache.untrack(entry.value); }); } const state = (0, import_vue10.computed)(() => entry.value.state.value); const status = (0, import_vue10.computed)(() => state.value.status); const data = (0, import_vue10.computed)(() => state.value.data); const error = (0, import_vue10.computed)(() => state.value.error); const asyncStatus = (0, import_vue10.computed)(() => entry.value.asyncStatus.value); const variables = (0, import_vue10.computed)(() => entry.value.vars); async function mutateAsync(vars) { return mutationCache.mutate( // ensures we reuse the initial empty entry and adapt it or create a new one entry.value = mutationCache.ensure(entry.value, vars) ); } function mutate(vars) { mutateAsync(vars).catch(noop); } function reset() { entry.value = mutationCache.create(options); } return { state, data, isLoading: (0, import_vue10.computed)(() => asyncStatus.value === "loading"), status, variables, asyncStatus, error, // @ts-expect-error: because of the conditional type in UseMutationReturn // it would be nice to find a type-only refactor that works mutate, // @ts-expect-error: same as above mutateAsync, reset }; } // src/define-mutation.ts function defineMutation(optionsOrSetup) { const setupFn = typeof optionsOrSetup === "function" ? optionsOrSetup : () => useMutation(optionsOrSetup); return () => { const mutationCache = useMutationCache(); return mutationCache.ensureDefinedMutation(setupFn); }; } // src/devtools/plugin.ts var import_devtools_api = require("@vue/devtools-api"); var import_vue11 = require("vue"); var QUERY_INSPECTOR_ID = "pinia-colada-queries"; var ID_SEPARATOR = "\0"; function debounce(fn, delay) { let timeout; return () => { clearTimeout(timeout); timeout = setTimeout(fn, delay); }; } function addDevtools(app, pinia) { const queryCache = useQueryCache(pinia); (0, import_devtools_api.setupDevtoolsPlugin)( { id: "dev.esm.pinia-colada", app, label: "Pinia Colada", packageName: "pinia-colada", homepage: "https://pinia-colada.esm.dev/", logo: "https://pinia-colada.esm.dev/logo.svg", componentStateTypes: [] }, (api) => { const updateQueryInspectorTree = debounce(() => { api.sendInspectorTree(QUERY_INSPECTOR_ID); api.sendInspectorState(QUERY_INSPECTOR_ID); }, 100); api.addInspector({ id: QUERY_INSPECTOR_ID, label: "Pinia Queries", icon: "storage", noSelectionText: "Select a query entry to inspect it", treeFilterPlaceholder: "Filter query entries", stateFilterPlaceholder: "Find within the query entry", actions: [ { icon: "refresh", action: updateQueryInspectorTree, tooltip: "Sync" } ] }); let stopWatcher = () => { }; api.on.getInspectorState((payload) => { if (payload.app !== app) return; if (payload.inspectorId === QUERY_INSPECTOR_ID) { const entry = queryCache.getEntries({ key: payload.nodeId.split(ID_SEPARATOR), exact: true })[0]; if (!entry) { payload.state = { Error: [ { key: "error", value: new Error(`Query entry ${payload.nodeId} not found`), editable: false } ] }; return; } stopWatcher(); stopWatcher = (0, import_vue11.watch)( () => [entry.state.value, entry.asyncStatus.value], () => { api.sendInspectorState(QUERY_INSPECTOR_ID); } ); const state = entry.state.value; payload.state = { state: [ { key: "data", value: state.data, editable: true }, { key: "error", value: state.error, editable: true }, { key: "status", value: state.status, editable: true }, { key: "asyncStatus", value: entry.asyncStatus.value, editable: true } ], entry: [ { key: "key", value: entry.key, editable: false }, { key: "options", value: entry.options, editable: true } ] }; } }); api.on.editInspectorState((payload) => { if (payload.app !== app) return; if (payload.inspectorId === QUERY_INSPECTOR_ID) { const entry = queryCache.getEntries({ key: payload.nodeId.split(ID_SEPARATOR), exact: true })[0]; if (!entry) return; const path = payload.path.slice(); payload.set(entry, path, payload.state.value); api.sendInspectorState(QUERY_INSPECTOR_ID); } }); const QUERY_FILTER_RE = /\b(active|inactive|stale|fresh|exact|loading|idle)\b/gi; api.on.getInspectorTree((payload) => { if (payload.app !== app || payload.inspectorId !== QUERY_INSPECTOR_ID) return; const filters = payload.filter.match(QUERY_FILTER_RE); const filter = (filters ? payload.filter.replace(QUERY_FILTER_RE, "") : payload.filter).trim(); const active = filters?.includes("active") ? true : filters?.includes("inactive") ? false : void 0; const stale = filters?.includes("stale") ? true : filters?.includes("fresh") ? false : void 0; const asyncStatus = filters?.includes("loading") ? "loading" : filters?.includes("idle") ? "idle" : void 0; payload.rootNodes = queryCache.getEntries({ active, stale, // TODO: if there is an exact match, we should put it at the top exact: false, // we also filter many predicate(entry) { if (asyncStatus && entry.asyncStatus.value !== asyncStatus) return false; if (filter) { return entry.key.some((key) => String(key).includes(filter)); } return true; } }).map((entry) => { const id = entry.key.join(ID_SEPARATOR); const label = entry.key.join("/"); const asyncStatus2 = entry.asyncStatus.value; const state = entry.state.value; const tags = [ ASYNC_STATUS_TAG[asyncStatus2], STATUS_TAG[state.status] // useful for testing colors // ASYNC_STATUS_TAG.idle, // ASYNC_STATUS_TAG.fetching, // STATUS_TAG.pending, // STATUS_TAG.success, // STATUS_TAG.error, ]; if (!entry.active) { tags.push({ label: "inactive", textColor: 0, backgroundColor: 11184810, tooltip: "The query is not being used anywhere" }); } return { id, label, name: label, tags }; }); }); queryCache.$onAction(({ name, after, onError }) => { if (name === "invalidate" || name === "fetch" || name === "setEntryState" || name === "remove" || name === "untrack" || name === "track" || name === "ensure") { updateQueryInspectorTree(); after(updateQueryInspectorTree); onError(updateQueryInspectorTree); } }); api.notifyComponentUpdate(); api.sendInspectorTree(QUERY_INSPECTOR_ID); api.sendInspectorState(QUERY_INSPECTOR_ID); } ); } var STATUS_TAG = { pending: { label: "pending", textColor: 0, backgroundColor: 16751907, tooltip: `The query hasn't resolved yet` }, success: { label: "success", textColor: 0, backgroundColor: 1492095, tooltip: "The query resolved successfully" }, error: { label: "error", textColor: 0, backgroundColor: 16332839, tooltip: "The query rejected with an error" } }; var ASYNC_STATUS_TAG = { idle: { label: "idle", textColor: 0, backgroundColor: 11184810, tooltip: "The query is not fetching" }, loading: { label: "fetching", textColor: 16777215, backgroundColor: 5738442, tooltip: "The query is currently fetching" } }; // src/pinia-colada.ts var PiniaColada = (app, options = {}) => { const { pinia = app.config.globalProperties.$pinia, plugins, queryOptions, mutationOptions = {} } = options; app.provide(USE_QUERY_OPTIONS_KEY, { ...USE_QUERY_DEFAULTS, ...queryOptions }); app.provide(USE_MUTATION_OPTIONS_KEY, { ...USE_MUTATION_DEFAULTS, ...mutationOptions }); if (process.env.NODE_ENV !== "production" && !pinia) { throw new Error( '[@pinia/colada] root pinia plugin not detected. Make sure you install pinia before installing the "PiniaColada" plugin or to manually pass the pinia instance.' ); } if (typeof document !== "undefined" && process.env.NODE_ENV === "development") { addDevtools(app, pinia); } const queryCache = useQueryCache(pinia); plugins?.forEach( (plugin) => plugin({ scope: queryCache._s, queryCache, pinia }) ); }; // src/plugins/query-hooks.ts function PiniaColadaQueryHooksPlugin(options) { return ({ queryCache }) => { queryCache.$onAction(({ name, after, onError, args }) => { if (name === "fetch") { const [entry] = args; after(async ({ data }) => { await options.onSuccess?.(data, entry); options.onSettled?.(data, null, entry); }); onError(async (error) => { await options.onError?.(error, entry); options.onSettled?.(void 0, error, entry); }); } }); }; } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { PiniaColada, PiniaColadaQueryHooksPlugin, defineMutation, defineQuery, defineQueryOptions, hydrateQueryCache, isQueryCache, serializeQueryCache, useInfiniteQuery, useMutation, useQuery, useQueryCache, useQueryState }); //# sourceMappingURL=index.cjs.map