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