@pinia/colada
Version:
The smart data fetching layer for Pinia
1,214 lines (1,198 loc) • 38.6 kB
JavaScript
// 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