@pinia/colada
Version:
The smart data fetching layer for Pinia
1,232 lines (1,214 loc) • 40.5 kB
JavaScript
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,
TreeMapNode: () => TreeMapNode,
defineMutation: () => defineMutation,
defineQuery: () => defineQuery,
hydrateQueryCache: () => hydrateQueryCache,
serializeQueryCache: () => serializeQueryCache,
serializeTreeMap: () => serializeTreeMap,
toCacheKey: () => toCacheKey,
useInfiniteQuery: () => useInfiniteQuery,
useMutation: () => useMutation,
useQuery: () => useQuery,
useQueryCache: () => useQueryCache
});
module.exports = __toCommonJS(index_exports);
// src/mutation-store.ts
var import_pinia = require("pinia");
var import_vue3 = require("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
var import_vue = require("vue");
function useEventListener(target, event, listener, options) {
target.addEventListener(event, listener, options);
if ((0, import_vue.getCurrentScope)()) {
(0, import_vue.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
var import_vue2 = require("vue");
var USE_MUTATION_OPTIONS_KEY = process.env.NODE_ENV !== "production" ? Symbol("useMutationOptions") : Symbol();
var useMutationOptions = () => (0, import_vue2.inject)(USE_MUTATION_OPTIONS_KEY, {});
// src/mutation-store.ts
function createMutationEntry(options, key, vars) {
return {
state: (0, import_vue3.shallowRef)({
status: "pending",
data: void 0,
error: null
}),
asyncStatus: (0, import_vue3.shallowRef)("idle"),
when: 0,
vars: (0, import_vue3.shallowRef)(vars),
key,
options,
pending: null
};
}
var useMutationCache = /* @__PURE__ */ (0, import_pinia.defineStore)("_pc_mutation", ({ action }) => {
const cachesRaw = new TreeMapNode();
let triggerCache;
const caches = (0, import_pinia.skipHydrate)(
(0, import_vue3.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_vue3.getCurrentScope)();
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
var import_vue4 = require("vue");
function useMutation(options) {
const mutationCache = useMutationCache();
const entry = (0, import_vue4.shallowRef)(
mutationCache.ensure(options)
);
const state = (0, import_vue4.computed)(() => entry.value.state.value);
const status = (0, import_vue4.computed)(() => state.value.status);
const data = (0, import_vue4.computed)(() => state.value.data);
const error = (0, import_vue4.computed)(() => state.value.error);
const asyncStatus = (0, import_vue4.computed)(() => entry.value.asyncStatus.value);
const variables = (0, import_vue4.computed)(() => 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: (0, import_vue4.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/define-query.ts
var import_vue8 = require("vue");
// src/query-store.ts
var import_pinia2 = require("pinia");
var import_vue6 = require("vue");
// src/query-options.ts
var import_vue5 = 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_vue5.inject)(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__ */ (0, import_pinia2.defineStore)(QUERY_STORE_ID, ({ action }) => {
const cachesRaw = new TreeMapNode();
let triggerCache;
const caches = (0, import_pinia2.skipHydrate)(
(0, import_vue6.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_vue6.getCurrentScope)();
const app = (0, import_pinia2.getActivePinia)()._a;
if (process.env.NODE_ENV !== "production") {
if (!(0, import_vue6.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_vue6.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_vue6.shallowRef)("idle");
return (0, import_vue6.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: (0, import_vue6.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(() => (0, import_vue6.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 (0, import_vue6.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((0, import_vue6.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 = (0, import_vue6.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: (0, import_vue6.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
var import_vue7 = require("vue");
function useQuery(_options) {
const queryCache = useQueryCache();
const optionDefaults = useQueryOptions();
const hasCurrentInstance = (0, import_vue7.getCurrentInstance)();
const currentEffect = getCurrentDefineQueryEffect() || (0, import_vue7.getCurrentScope)();
const options = {
...optionDefaults,
..._options
};
const { refetchOnMount, refetchOnReconnect, refetchOnWindowFocus, enabled } = options;
let lastEntry;
const entry = (0, import_vue7.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 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 = (0, import_vue7.computed)(() => isEntryUsingPlaceholderData(entry.value));
const state = (0, import_vue7.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_vue7.computed)({
get: () => (0, import_vue7.toValue)(entry.value.ext[key]),
set(value) {
const target = entry.value.ext[key];
if ((0, import_vue7.isRef)(target)) {
;
target.value = value;
} else {
;
entry.value.ext[key] = value;
}
}
});
}
const queryReturn = {
...extensions,
state,
status: (0, import_vue7.computed)(() => state.value.status),
data: (0, import_vue7.computed)(() => state.value.data),
error: (0, import_vue7.computed)(() => entry.value.state.value.error),
asyncStatus: (0, import_vue7.computed)(() => entry.value.asyncStatus.value),
isPlaceholderData,
isPending: (0, import_vue7.computed)(() => state.value.status === "pending"),
isLoading: (0, import_vue7.computed)(() => entry.value.asyncStatus.value === "loading"),
refresh,
refetch
};
if (hasCurrentInstance) {
(0, import_vue7.onServerPrefetch)(async () => {
if ((0, import_vue7.toValue)(enabled)) await refresh(true);
});
}
let isActive = false;
if (hasCurrentInstance) {
(0, import_vue7.onMounted)(() => {
isActive = true;
queryCache.track(lastEntry, hasCurrentInstance);
});
(0, import_vue7.onUnmounted)(() => {
queryCache.untrack(lastEntry, hasCurrentInstance);
});
} else {
isActive = true;
if (currentEffect) {
queryCache.track(lastEntry, currentEffect);
(0, import_vue7.onScopeDispose)(() => {
queryCache.untrack(lastEntry, currentEffect);
});
}
}
(0, import_vue7.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 ((0, import_vue7.toValue)(enabled)) refresh();
},
{
immediate: true
}
);
if (typeof enabled !== "boolean") {
(0, import_vue7.watch)(enabled, (newEnabled) => {
if (newEnabled) refresh();
});
}
if (hasCurrentInstance) {
(0, import_vue7.onMounted)(() => {
if ((refetchOnMount || queryReturn.status.value === "pending") && (0, import_vue7.toValue)(enabled)) {
if (refetchOnMount === "always") {
refetch();
} else {
refresh();
}
}
});
}
if (IS_CLIENT) {
if (refetchOnWindowFocus) {
useEventListener(document, "visibilitychange", () => {
if (document.visibilityState === "visible" && (0, import_vue7.toValue)(enabled)) {
if ((0, import_vue7.toValue)(refetchOnWindowFocus) === "always") {
refetch();
} else {
refresh();
}
}
});
}
if (refetchOnReconnect) {
useEventListener(window, "online", () => {
if ((0, import_vue7.toValue)(enabled)) {
if ((0, import_vue7.toValue)(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 = (0, import_vue8.getCurrentInstance)() || (currentDefineQueryEffect = (0, import_vue8.getCurrentScope)());
const [entries, ret, scope] = queryCache.ensureDefinedQuery(setupFn);
if (hasBeenEnsured) {
entries.forEach((entry) => {
if (entry.options?.refetchOnMount && (0, import_vue8.toValue)(entry.options.enabled)) {
if ((0, import_vue8.toValue)(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;
}
});
(0, import_vue8.onScopeDispose)(() => {
if (entries.every((entry) => {
queryCache.untrack(entry, currentScope);
return !entry.active;
})) {
scope.pause();
}
});
}
currentDefineQueryEffect = previousEffect;
return ret;
};
}
// src/devtools/plugin.ts
var import_devtools_api = require("@vue/devtools-api");
var import_vue9 = 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_vue9.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) 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
var import_vue10 = require("vue");
function useInfiniteQuery(options) {
let pages = (0, import_vue10.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()
};
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
PiniaColada,
PiniaColadaQueryHooksPlugin,
TreeMapNode,
defineMutation,
defineQuery,
hydrateQueryCache,
serializeQueryCache,
serializeTreeMap,
toCacheKey,
useInfiniteQuery,
useMutation,
useQuery,
useQueryCache
});
//# sourceMappingURL=index.cjs.map