UNPKG

@pinia/colada

Version:

The smart data fetching layer for Pinia

1,214 lines (1,198 loc) 38.6 kB
// src/mutation-store.ts import { defineStore, skipHydrate } from "pinia"; import { customRef, getCurrentScope as getCurrentScope2, shallowRef } from "vue"; // src/tree-map.ts var TreeMapNode = class _TreeMapNode { value; children; constructor(...args) { if (args.length) { this.set(...args); } } /** * Sets the value while building the tree * * @param keys - key as an array * @param value - value to set */ set(keys, value) { if (keys.length === 0) { this.value = value; } else { const [top, ...otherKeys] = keys; const node = this.children?.get(top); if (node) { node.set(otherKeys, value); } else { this.children ??= /* @__PURE__ */ new Map(); this.children.set(top, new _TreeMapNode(otherKeys, value)); } } } /** * Finds the node at the given path of keys. * * @param keys - path of keys */ find(keys) { if (keys.length === 0) { return this; } else { const [top, ...otherKeys] = keys; return this.children?.get(top)?.find(otherKeys); } } /** * Gets the value at the given path of keys. * * @param keys - path of keys */ get(keys) { return this.find(keys)?.value; } /** * Delete the node at the given path of keys and all its children. * * @param keys - path of keys */ delete(keys) { if (keys.length === 1) { this.children?.delete(keys[0]); } else { const [top, ...otherKeys] = keys; this.children?.get(top)?.delete(otherKeys); } } /** * Iterates over the node values if not null or undefined and all its children. Goes in depth first order. Allows a `for (const of node)` loop. */ *[Symbol.iterator]() { if (this.value != null) { yield this.value; } if (this.children) { for (const child of this.children.values()) { yield* child; } } } }; function appendSerializedNodeToTree(parent, [key, value, children], createNodeValue, parentKey = []) { parent.children ??= /* @__PURE__ */ new Map(); const entryKey = [...parentKey, key]; const node = new TreeMapNode( [], // NOTE: this could happen outside of an effect scope but since it's only for client side hydration, it should be // fine to have global shallowRefs as they can still be cleared when needed value && createNodeValue(entryKey, null, ...value) ); parent.children.set(key, node); if (children) { for (const child of children) { appendSerializedNodeToTree(node, child, createNodeValue, entryKey); } } } // src/utils.ts import { computed, getCurrentScope, onScopeDispose } from "vue"; function useEventListener(target, event, listener, options) { target.addEventListener(event, listener, options); if (getCurrentScope()) { onScopeDispose(() => { target.removeEventListener(event, listener); }); } } var IS_CLIENT = typeof window !== "undefined"; function toValueWithArgs(valFn, ...args) { return typeof valFn === "function" ? valFn(...args) : valFn; } function stringifyFlatObject(obj) { return obj && typeof obj === "object" ? JSON.stringify(obj, Object.keys(obj).sort()) : String(obj); } var toCacheKey = (key) => key.map(stringifyFlatObject); var noop = () => { }; function isSameArray(arr1, arr2) { if (arr1.length !== arr2.length) return false; for (let i = 0; i < arr1.length; i++) { if (arr1[i] !== arr2[i]) return false; } return true; } var warnedMessages = /* @__PURE__ */ new Set(); function warnOnce(message, id = message) { if (warnedMessages.has(id)) return; warnedMessages.add(id); console.warn(`[@pinia/colada]: ${message}`); } // src/mutation-options.ts import { inject } from "vue"; var USE_MUTATION_OPTIONS_KEY = process.env.NODE_ENV !== "production" ? Symbol("useMutationOptions") : Symbol(); var useMutationOptions = () => inject(USE_MUTATION_OPTIONS_KEY, {}); // src/mutation-store.ts function createMutationEntry(options, key, vars) { return { state: shallowRef({ status: "pending", data: void 0, error: null }), asyncStatus: shallowRef("idle"), when: 0, vars: shallowRef(vars), key, options, pending: null }; } var useMutationCache = /* @__PURE__ */ defineStore("_pc_mutation", ({ action }) => { const cachesRaw = new TreeMapNode(); let triggerCache; const caches = skipHydrate( 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 = getCurrentScope2(); const globalOptions = useMutationOptions(); const defineMutationMap = /* @__PURE__ */ new WeakMap(); function ensure(options, entry, vars) { const key = vars && toValueWithArgs(options.key, vars)?.map(stringifyFlatObject); if (!entry) { entry = createMutationEntry(options, key); if (key) { cachesRaw.set( key, // @ts-expect-error: function types with generics are incompatible entry ); triggerCache(); } return createMutationEntry(options, key); } if (key) { if (!entry.key) { entry.key = key; } else if (!isSameArray(entry.key, key)) { entry = createMutationEntry( options, key, // the type NonNullable<TVars> is not assignable to TVars vars ); cachesRaw.set( key, // @ts-expect-error: function types with generics are incompatible 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.key != null) { cachesRaw.delete(entry.key); triggerCache(); } }); const getEntries = action((filters = {}) => { const node = filters.key ? caches.value.find(toCacheKey(filters.key)) : caches.value; if (!node) return []; return (filters.exact ? node.value ? [node.value] : [] : [...node]).filter( (entry) => (filters.status == null || entry.state.value.status === filters.status) && (!filters.predicate || filters.predicate(entry)) ); }); async function mutate(currentEntry, vars) { currentEntry.asyncStatus.value = "loading"; currentEntry.vars.value = vars; let currentData; let currentError; const { options } = currentEntry; let context = {}; const currentCall = currentEntry.pending = Symbol(); 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 ); if (currentEntry.pending === currentCall) { setEntryState(currentEntry, { status: "success", data: newData, error: null }); } } catch (newError) { currentError = newError; await globalOptions.onError?.(currentError, vars, context); await options.onError?.(currentError, vars, context); if (currentEntry.pending === currentCall) { setEntryState(currentEntry, { status: "error", data: currentEntry.state.value.data, error: currentError }); } throw newError; } finally { await globalOptions.onSettled?.(currentData, currentError, vars, context); await options.onSettled?.(currentData, currentError, vars, context); if (currentEntry.pending === currentCall) { currentEntry.asyncStatus.value = "idle"; } } return currentData; } return { caches, ensure, ensureDefinedMutation, mutate, remove, setEntryState, getEntries }; }); // src/use-mutation.ts import { computed as computed2, shallowRef as shallowRef2 } from "vue"; function useMutation(options) { const mutationCache = useMutationCache(); const entry = shallowRef2( mutationCache.ensure(options) ); const state = computed2(() => entry.value.state.value); const status = computed2(() => state.value.status); const data = computed2(() => state.value.data); const error = computed2(() => state.value.error); const asyncStatus = computed2(() => entry.value.asyncStatus.value); const variables = computed2(() => entry.value.vars.value); async function mutateAsync(vars) { return mutationCache.mutate( entry.value = mutationCache.ensure(options, entry.value, vars), vars ); } function mutate(vars) { mutateAsync(vars).catch(noop); } function reset() { mutationCache.setEntryState(entry.value, { status: "pending", data: void 0, error: null }); entry.value.asyncStatus.value = "idle"; } return { state, data, isLoading: computed2(() => 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/define-query.ts import { getCurrentInstance as getCurrentInstance3, getCurrentScope as getCurrentScope5, onScopeDispose as onScopeDispose3, toValue as toValue3 } from "vue"; // src/query-store.ts import { defineStore as defineStore2, getActivePinia, skipHydrate as skipHydrate2 } from "pinia"; import { customRef as customRef2, effectScope, getCurrentInstance, getCurrentScope as getCurrentScope3, hasInjectionContext, markRaw, shallowRef as shallowRef3, toValue } from "vue"; // src/query-options.ts import { inject as inject2 } from "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 = () => inject2(USE_QUERY_OPTIONS_KEY, USE_QUERY_DEFAULTS); // src/query-store.ts 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, when ]; var QUERY_STORE_ID = "_pc_query"; var useQueryCache = /* @__PURE__ */ defineStore2(QUERY_STORE_ID, ({ action }) => { const cachesRaw = new TreeMapNode(); let triggerCache; const caches = skipHydrate2( customRef2( (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 = getCurrentScope3(); const app = getActivePinia()._a; if (process.env.NODE_ENV !== "production") { if (!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 = shallowRef3( // @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 = shallowRef3("idle"); return markRaw({ key, state, placeholderData: null, when, asyncStatus, pending: null, // this set can contain components and effects and worsen the performance // and create weird warnings deps: 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.when || Date.now() >= this.when + this.options.staleTime; }, get active() { return this.deps.size > 0; } }); }) ); let currentDefineQueryEntry; const defineQueryMap = /* @__PURE__ */ new WeakMap(); const ensureDefinedQuery = action((fn) => { let defineQueryEntry = defineQueryMap.get(fn); if (!defineQueryEntry) { currentDefineQueryEntry = defineQueryEntry = [[], null, scope.run(() => effectScope())]; defineQueryEntry[1] = app.runWithContext(() => defineQueryEntry[2].run(fn)); currentDefineQueryEntry = null; defineQueryMap.set(fn, defineQueryEntry); } else { defineQueryEntry[2].resume(); 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; entry.deps.delete(effect); triggerCache(); 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) => { return Promise.all( getEntries({ active: true, ...filters }).map((entry) => { invalidate(entry); return toValue(entry.options?.enabled) && fetch(entry); }) ); }); const getEntries = action((filters = {}) => { const node = filters.key ? caches.value.find(toCacheKey(filters.key)) : caches.value; if (!node) return []; return (filters.exact ? node.value ? [node.value] : [] : [...node]).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 = toCacheKey(toValue(options.key)); if (process.env.NODE_ENV !== "production" && key.length === 0) { throw new Error( `useQuery() was called with an empty array as the key. It must have at least one element.` ); } let entry = cachesRaw.get(key); if (!entry) { cachesRaw.set(key, 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 // NOTE: the build needs a cast or it thinks it's never isEntryUsingPlaceholderData(previousEntry) ? previousEntry.placeholderData : previousEntry?.state.value.data ); } triggerCache(); } if (process.env.NODE_ENV !== "production") { const currentInstance = getCurrentInstance(); if (currentInstance) { entry.__hmr ??= {}; entry.__hmr.deps ??= /* @__PURE__ */ new Set(); entry.__hmr.id = currentInstance.type?.__hmrId ?? currentInstance.proxy?._uid; if (entry.__hmr.id == null && process.env.NODE_ENV !== "test" && typeof document !== "undefined") { warnOnce( `Found a nullish hmr id. This is probably a bug. Please report it to pinia-colada with a boiled down reproduction. Thank you!` ); } } } 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(); } ); const setQueryData = action( (key, data) => { const cacheKey = toCacheKey(key); let entry = cachesRaw.get(cacheKey); if (!entry) { cachesRaw.set(cacheKey, entry = create(cacheKey)); } setEntryState(entry, { // if we don't cast, this is not technically correct // the user is responsible for setting the data ...entry.state.value, data: toValueWithArgs(data, entry.state.value.data) }); triggerCache(); } ); function getQueryData(key) { return caches.value.get(toCacheKey(key))?.state.value.data; } const remove = action((entry) => { cachesRaw.delete(entry.key); triggerCache(); }); return { caches, ensureDefinedQuery, /** * Scope to track effects and components that use the query cache. * @internal */ _s: markRaw(scope), setQueryData, getQueryData, invalidateQueries, cancelQueries, // Actions for entries invalidate, fetch, refresh, ensure, extend, track, untrack, cancel, create, remove, setEntryState, getEntries }; }); function serializeTreeMap(root) { return root.children ? [...root.children.entries()].map(_serialize) : []; } function _serialize([key, tree]) { return [ key, tree.value && queryEntry_toJSON(tree.value), tree.children && [...tree.children.entries()].map(_serialize) ]; } function hydrateQueryCache(queryCache, serializedCache) { for (const entryData of serializedCache) { appendSerializedNodeToTree(queryCache.caches, entryData, queryCache.create); } } function serializeQueryCache(queryCache) { return serializeTreeMap(queryCache.caches); } // src/use-query.ts import { computed as computed3, getCurrentInstance as getCurrentInstance2, getCurrentScope as getCurrentScope4, isRef, onMounted, onScopeDispose as onScopeDispose2, onServerPrefetch, onUnmounted, toValue as toValue2, watch } from "vue"; function useQuery(_options) { const queryCache = useQueryCache(); const optionDefaults = useQueryOptions(); const hasCurrentInstance = getCurrentInstance2(); const currentEffect = getCurrentDefineQueryEffect() || getCurrentScope4(); const options = { ...optionDefaults, ..._options }; const { refetchOnMount, refetchOnReconnect, refetchOnWindowFocus, enabled } = options; let lastEntry; const entry = computed3( () => ( // 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 unedfined values // https://github.com/posva/pinia-colada/issues/227 // @ts-expect-error: _isPaused is private currentEffect?._isPaused ? lastEntry : lastEntry = queryCache.ensure(options, lastEntry) ) ); lastEntry = entry.value; const errorCatcher = () => entry.value.state.value; const refresh = (throwOnError) => queryCache.refresh(entry.value, options).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).catch( // same as above throwOnError || errorCatcher ); const isPlaceholderData = computed3(() => isEntryUsingPlaceholderData(entry.value)); const state = computed3( () => isPlaceholderData.value ? { status: "success", data: entry.value.placeholderData, error: null } : entry.value.state.value ); const extensions = {}; for (const key in lastEntry.ext) { extensions[key] = computed3({ get: () => toValue2(entry.value.ext[key]), set(value) { const target = entry.value.ext[key]; if (isRef(target)) { ; target.value = value; } else { ; entry.value.ext[key] = value; } } }); } const queryReturn = { ...extensions, state, status: computed3(() => state.value.status), data: computed3(() => state.value.data), error: computed3(() => entry.value.state.value.error), asyncStatus: computed3(() => entry.value.asyncStatus.value), isPlaceholderData, isPending: computed3(() => state.value.status === "pending"), isLoading: computed3(() => entry.value.asyncStatus.value === "loading"), refresh, refetch }; if (hasCurrentInstance) { onServerPrefetch(async () => { if (toValue2(enabled)) await refresh(true); }); } let isActive = false; if (hasCurrentInstance) { onMounted(() => { isActive = true; queryCache.track(lastEntry, hasCurrentInstance); }); onUnmounted(() => { queryCache.untrack(lastEntry, hasCurrentInstance); }); } else { isActive = true; if (currentEffect) { queryCache.track(lastEntry, currentEffect); onScopeDispose2(() => { queryCache.untrack(lastEntry, currentEffect); }); } } watch( entry, (entry2, previousEntry) => { if (!isActive) return; if (previousEntry) { queryCache.untrack(previousEntry, hasCurrentInstance); queryCache.untrack(previousEntry, currentEffect); } queryCache.track(entry2, hasCurrentInstance); queryCache.track(entry2, currentEffect); if (toValue2(enabled)) refresh(); }, { immediate: true } ); if (typeof enabled !== "boolean") { watch(enabled, (newEnabled) => { if (newEnabled) refresh(); }); } if (hasCurrentInstance) { onMounted(() => { if ((refetchOnMount || queryReturn.status.value === "pending") && toValue2(enabled)) { if (refetchOnMount === "always") { refetch(); } else { refresh(); } } }); } if (IS_CLIENT) { if (refetchOnWindowFocus) { useEventListener(document, "visibilitychange", () => { if (document.visibilityState === "visible" && toValue2(enabled)) { if (toValue2(refetchOnWindowFocus) === "always") { refetch(); } else { refresh(); } } }); } if (refetchOnReconnect) { useEventListener(window, "online", () => { if (toValue2(enabled)) { if (toValue2(refetchOnReconnect) === "always") { refetch(); } else { refresh(); } } }); } } return queryReturn; } // src/define-query.ts var currentDefineQueryEffect; function getCurrentDefineQueryEffect() { return currentDefineQueryEffect; } function defineQuery(optionsOrSetup) { const setupFn = typeof optionsOrSetup === "function" ? optionsOrSetup : () => useQuery(optionsOrSetup); let hasBeenEnsured; return () => { const queryCache = useQueryCache(); const previousEffect = currentDefineQueryEffect; const currentScope = getCurrentInstance3() || (currentDefineQueryEffect = getCurrentScope5()); const [entries, ret, scope] = queryCache.ensureDefinedQuery(setupFn); if (hasBeenEnsured) { entries.forEach((entry) => { if (entry.options?.refetchOnMount && toValue3(entry.options.enabled)) { if (toValue3(entry.options.refetchOnMount) === "always") { queryCache.fetch(entry); } else { queryCache.refresh(entry); } } }); } hasBeenEnsured = true; if (currentScope) { entries.forEach((entry) => { queryCache.track(entry, currentScope); if (process.env.NODE_ENV !== "production") { entry.__hmr ??= {}; entry.__hmr.skip = true; } }); onScopeDispose3(() => { if (entries.every((entry) => { queryCache.untrack(entry, currentScope); return !entry.active; })) { scope.pause(); } }); } currentDefineQueryEffect = previousEffect; return ret; }; } // src/devtools/plugin.ts import { setupDevtoolsPlugin } from "@vue/devtools-api"; import { watch as watch2 } from "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); 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 = watch2( () => [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) return; if (payload.inspectorId === QUERY_INSPECTOR_ID) { 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, exact: filters?.includes("exact"), 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, 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); }); } }); }; } // src/infinite-query.ts import { toValue as toValue4 } from "vue"; function useInfiniteQuery(options) { let pages = toValue4(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() }; } export { PiniaColada, PiniaColadaQueryHooksPlugin, TreeMapNode, defineMutation, defineQuery, hydrateQueryCache, serializeQueryCache, serializeTreeMap, toCacheKey, useInfiniteQuery, useMutation, useQuery, useQueryCache }; //# sourceMappingURL=index.js.map