mirror of
https://github.com/hanzoai/agency.git
synced 2026-11-02 19:13:28 +00:00
434 lines
12 KiB
Plaintext
434 lines
12 KiB
Plaintext
// src/query.ts
|
|
import {
|
|
ensureQueryFn,
|
|
noop,
|
|
replaceData,
|
|
resolveEnabled,
|
|
resolveStaleTime,
|
|
skipToken,
|
|
timeUntilStale
|
|
} from "./utils.js";
|
|
import { notifyManager } from "./notifyManager.js";
|
|
import { CancelledError, canFetch, createRetryer } from "./retryer.js";
|
|
import { Removable } from "./removable.js";
|
|
var Query = class extends Removable {
|
|
#initialState;
|
|
#revertState;
|
|
#cache;
|
|
#client;
|
|
#retryer;
|
|
#defaultOptions;
|
|
#abortSignalConsumed;
|
|
constructor(config) {
|
|
super();
|
|
this.#abortSignalConsumed = false;
|
|
this.#defaultOptions = config.defaultOptions;
|
|
this.setOptions(config.options);
|
|
this.observers = [];
|
|
this.#client = config.client;
|
|
this.#cache = this.#client.getQueryCache();
|
|
this.queryKey = config.queryKey;
|
|
this.queryHash = config.queryHash;
|
|
this.#initialState = getDefaultState(this.options);
|
|
this.state = config.state ?? this.#initialState;
|
|
this.scheduleGc();
|
|
}
|
|
get meta() {
|
|
return this.options.meta;
|
|
}
|
|
get promise() {
|
|
return this.#retryer?.promise;
|
|
}
|
|
setOptions(options) {
|
|
this.options = { ...this.#defaultOptions, ...options };
|
|
this.updateGcTime(this.options.gcTime);
|
|
if (this.state && this.state.data === void 0) {
|
|
const defaultState = getDefaultState(this.options);
|
|
if (defaultState.data !== void 0) {
|
|
this.setState(
|
|
successState(defaultState.data, defaultState.dataUpdatedAt)
|
|
);
|
|
this.#initialState = defaultState;
|
|
}
|
|
}
|
|
}
|
|
optionalRemove() {
|
|
if (!this.observers.length && this.state.fetchStatus === "idle") {
|
|
this.#cache.remove(this);
|
|
}
|
|
}
|
|
setData(newData, options) {
|
|
const data = replaceData(this.state.data, newData, this.options);
|
|
this.#dispatch({
|
|
data,
|
|
type: "success",
|
|
dataUpdatedAt: options?.updatedAt,
|
|
manual: options?.manual
|
|
});
|
|
return data;
|
|
}
|
|
setState(state, setStateOptions) {
|
|
this.#dispatch({ type: "setState", state, setStateOptions });
|
|
}
|
|
cancel(options) {
|
|
const promise = this.#retryer?.promise;
|
|
this.#retryer?.cancel(options);
|
|
return promise ? promise.then(noop).catch(noop) : Promise.resolve();
|
|
}
|
|
destroy() {
|
|
super.destroy();
|
|
this.cancel({ silent: true });
|
|
}
|
|
reset() {
|
|
this.destroy();
|
|
this.setState(this.#initialState);
|
|
}
|
|
isActive() {
|
|
return this.observers.some(
|
|
(observer) => resolveEnabled(observer.options.enabled, this) !== false
|
|
);
|
|
}
|
|
isDisabled() {
|
|
if (this.getObserversCount() > 0) {
|
|
return !this.isActive();
|
|
}
|
|
return this.options.queryFn === skipToken || this.state.dataUpdateCount + this.state.errorUpdateCount === 0;
|
|
}
|
|
isStatic() {
|
|
if (this.getObserversCount() > 0) {
|
|
return this.observers.some(
|
|
(observer) => resolveStaleTime(observer.options.staleTime, this) === "static"
|
|
);
|
|
}
|
|
return false;
|
|
}
|
|
isStale() {
|
|
if (this.getObserversCount() > 0) {
|
|
return this.observers.some(
|
|
(observer) => observer.getCurrentResult().isStale
|
|
);
|
|
}
|
|
return this.state.data === void 0 || this.state.isInvalidated;
|
|
}
|
|
isStaleByTime(staleTime = 0) {
|
|
if (this.state.data === void 0) {
|
|
return true;
|
|
}
|
|
if (staleTime === "static") {
|
|
return false;
|
|
}
|
|
if (this.state.isInvalidated) {
|
|
return true;
|
|
}
|
|
return !timeUntilStale(this.state.dataUpdatedAt, staleTime);
|
|
}
|
|
onFocus() {
|
|
const observer = this.observers.find((x) => x.shouldFetchOnWindowFocus());
|
|
observer?.refetch({ cancelRefetch: false });
|
|
this.#retryer?.continue();
|
|
}
|
|
onOnline() {
|
|
const observer = this.observers.find((x) => x.shouldFetchOnReconnect());
|
|
observer?.refetch({ cancelRefetch: false });
|
|
this.#retryer?.continue();
|
|
}
|
|
addObserver(observer) {
|
|
if (!this.observers.includes(observer)) {
|
|
this.observers.push(observer);
|
|
this.clearGcTimeout();
|
|
this.#cache.notify({ type: "observerAdded", query: this, observer });
|
|
}
|
|
}
|
|
removeObserver(observer) {
|
|
if (this.observers.includes(observer)) {
|
|
this.observers = this.observers.filter((x) => x !== observer);
|
|
if (!this.observers.length) {
|
|
if (this.#retryer) {
|
|
if (this.#abortSignalConsumed) {
|
|
this.#retryer.cancel({ revert: true });
|
|
} else {
|
|
this.#retryer.cancelRetry();
|
|
}
|
|
}
|
|
this.scheduleGc();
|
|
}
|
|
this.#cache.notify({ type: "observerRemoved", query: this, observer });
|
|
}
|
|
}
|
|
getObserversCount() {
|
|
return this.observers.length;
|
|
}
|
|
invalidate() {
|
|
if (!this.state.isInvalidated) {
|
|
this.#dispatch({ type: "invalidate" });
|
|
}
|
|
}
|
|
async fetch(options, fetchOptions) {
|
|
if (this.state.fetchStatus !== "idle" && // If the promise in the retryer is already rejected, we have to definitely
|
|
// re-start the fetch; there is a chance that the query is still in a
|
|
// pending state when that happens
|
|
this.#retryer?.status() !== "rejected") {
|
|
if (this.state.data !== void 0 && fetchOptions?.cancelRefetch) {
|
|
this.cancel({ silent: true });
|
|
} else if (this.#retryer) {
|
|
this.#retryer.continueRetry();
|
|
return this.#retryer.promise;
|
|
}
|
|
}
|
|
if (options) {
|
|
this.setOptions(options);
|
|
}
|
|
if (!this.options.queryFn) {
|
|
const observer = this.observers.find((x) => x.options.queryFn);
|
|
if (observer) {
|
|
this.setOptions(observer.options);
|
|
}
|
|
}
|
|
if (process.env.NODE_ENV !== "production") {
|
|
if (!Array.isArray(this.options.queryKey)) {
|
|
console.error(
|
|
`As of v4, queryKey needs to be an Array. If you are using a string like 'repoData', please change it to an Array, e.g. ['repoData']`
|
|
);
|
|
}
|
|
}
|
|
const abortController = new AbortController();
|
|
const addSignalProperty = (object) => {
|
|
Object.defineProperty(object, "signal", {
|
|
enumerable: true,
|
|
get: () => {
|
|
this.#abortSignalConsumed = true;
|
|
return abortController.signal;
|
|
}
|
|
});
|
|
};
|
|
const fetchFn = () => {
|
|
const queryFn = ensureQueryFn(this.options, fetchOptions);
|
|
const createQueryFnContext = () => {
|
|
const queryFnContext2 = {
|
|
client: this.#client,
|
|
queryKey: this.queryKey,
|
|
meta: this.meta
|
|
};
|
|
addSignalProperty(queryFnContext2);
|
|
return queryFnContext2;
|
|
};
|
|
const queryFnContext = createQueryFnContext();
|
|
this.#abortSignalConsumed = false;
|
|
if (this.options.persister) {
|
|
return this.options.persister(
|
|
queryFn,
|
|
queryFnContext,
|
|
this
|
|
);
|
|
}
|
|
return queryFn(queryFnContext);
|
|
};
|
|
const createFetchContext = () => {
|
|
const context2 = {
|
|
fetchOptions,
|
|
options: this.options,
|
|
queryKey: this.queryKey,
|
|
client: this.#client,
|
|
state: this.state,
|
|
fetchFn
|
|
};
|
|
addSignalProperty(context2);
|
|
return context2;
|
|
};
|
|
const context = createFetchContext();
|
|
this.options.behavior?.onFetch(context, this);
|
|
this.#revertState = this.state;
|
|
if (this.state.fetchStatus === "idle" || this.state.fetchMeta !== context.fetchOptions?.meta) {
|
|
this.#dispatch({ type: "fetch", meta: context.fetchOptions?.meta });
|
|
}
|
|
this.#retryer = createRetryer({
|
|
initialPromise: fetchOptions?.initialPromise,
|
|
fn: context.fetchFn,
|
|
onCancel: (error) => {
|
|
if (error instanceof CancelledError && error.revert) {
|
|
this.setState({
|
|
...this.#revertState,
|
|
fetchStatus: "idle"
|
|
});
|
|
}
|
|
abortController.abort();
|
|
},
|
|
onFail: (failureCount, error) => {
|
|
this.#dispatch({ type: "failed", failureCount, error });
|
|
},
|
|
onPause: () => {
|
|
this.#dispatch({ type: "pause" });
|
|
},
|
|
onContinue: () => {
|
|
this.#dispatch({ type: "continue" });
|
|
},
|
|
retry: context.options.retry,
|
|
retryDelay: context.options.retryDelay,
|
|
networkMode: context.options.networkMode,
|
|
canRun: () => true
|
|
});
|
|
try {
|
|
const data = await this.#retryer.start();
|
|
if (data === void 0) {
|
|
if (process.env.NODE_ENV !== "production") {
|
|
console.error(
|
|
`Query data cannot be undefined. Please make sure to return a value other than undefined from your query function. Affected query key: ${this.queryHash}`
|
|
);
|
|
}
|
|
throw new Error(`${this.queryHash} data is undefined`);
|
|
}
|
|
this.setData(data);
|
|
this.#cache.config.onSuccess?.(data, this);
|
|
this.#cache.config.onSettled?.(
|
|
data,
|
|
this.state.error,
|
|
this
|
|
);
|
|
return data;
|
|
} catch (error) {
|
|
if (error instanceof CancelledError) {
|
|
if (error.silent) {
|
|
return this.#retryer.promise;
|
|
} else if (error.revert) {
|
|
if (this.state.data === void 0) {
|
|
throw error;
|
|
}
|
|
return this.state.data;
|
|
}
|
|
}
|
|
this.#dispatch({
|
|
type: "error",
|
|
error
|
|
});
|
|
this.#cache.config.onError?.(
|
|
error,
|
|
this
|
|
);
|
|
this.#cache.config.onSettled?.(
|
|
this.state.data,
|
|
error,
|
|
this
|
|
);
|
|
throw error;
|
|
} finally {
|
|
this.scheduleGc();
|
|
}
|
|
}
|
|
#dispatch(action) {
|
|
const reducer = (state) => {
|
|
switch (action.type) {
|
|
case "failed":
|
|
return {
|
|
...state,
|
|
fetchFailureCount: action.failureCount,
|
|
fetchFailureReason: action.error
|
|
};
|
|
case "pause":
|
|
return {
|
|
...state,
|
|
fetchStatus: "paused"
|
|
};
|
|
case "continue":
|
|
return {
|
|
...state,
|
|
fetchStatus: "fetching"
|
|
};
|
|
case "fetch":
|
|
return {
|
|
...state,
|
|
...fetchState(state.data, this.options),
|
|
fetchMeta: action.meta ?? null
|
|
};
|
|
case "success":
|
|
const newState = {
|
|
...state,
|
|
...successState(action.data, action.dataUpdatedAt),
|
|
dataUpdateCount: state.dataUpdateCount + 1,
|
|
...!action.manual && {
|
|
fetchStatus: "idle",
|
|
fetchFailureCount: 0,
|
|
fetchFailureReason: null
|
|
}
|
|
};
|
|
this.#revertState = action.manual ? newState : void 0;
|
|
return newState;
|
|
case "error":
|
|
const error = action.error;
|
|
return {
|
|
...state,
|
|
error,
|
|
errorUpdateCount: state.errorUpdateCount + 1,
|
|
errorUpdatedAt: Date.now(),
|
|
fetchFailureCount: state.fetchFailureCount + 1,
|
|
fetchFailureReason: error,
|
|
fetchStatus: "idle",
|
|
status: "error",
|
|
// flag existing data as invalidated if we get a background error
|
|
// note that "no data" always means stale so we can set unconditionally here
|
|
isInvalidated: true
|
|
};
|
|
case "invalidate":
|
|
return {
|
|
...state,
|
|
isInvalidated: true
|
|
};
|
|
case "setState":
|
|
return {
|
|
...state,
|
|
...action.state
|
|
};
|
|
}
|
|
};
|
|
this.state = reducer(this.state);
|
|
notifyManager.batch(() => {
|
|
this.observers.forEach((observer) => {
|
|
observer.onQueryUpdate();
|
|
});
|
|
this.#cache.notify({ query: this, type: "updated", action });
|
|
});
|
|
}
|
|
};
|
|
function fetchState(data, options) {
|
|
return {
|
|
fetchFailureCount: 0,
|
|
fetchFailureReason: null,
|
|
fetchStatus: canFetch(options.networkMode) ? "fetching" : "paused",
|
|
...data === void 0 && {
|
|
error: null,
|
|
status: "pending"
|
|
}
|
|
};
|
|
}
|
|
function successState(data, dataUpdatedAt) {
|
|
return {
|
|
data,
|
|
dataUpdatedAt: dataUpdatedAt ?? Date.now(),
|
|
error: null,
|
|
isInvalidated: false,
|
|
status: "success"
|
|
};
|
|
}
|
|
function getDefaultState(options) {
|
|
const data = typeof options.initialData === "function" ? options.initialData() : options.initialData;
|
|
const hasData = data !== void 0;
|
|
const initialDataUpdatedAt = hasData ? typeof options.initialDataUpdatedAt === "function" ? options.initialDataUpdatedAt() : options.initialDataUpdatedAt : 0;
|
|
return {
|
|
data,
|
|
dataUpdateCount: 0,
|
|
dataUpdatedAt: hasData ? initialDataUpdatedAt ?? Date.now() : 0,
|
|
error: null,
|
|
errorUpdateCount: 0,
|
|
errorUpdatedAt: 0,
|
|
fetchFailureCount: 0,
|
|
fetchFailureReason: null,
|
|
fetchMeta: null,
|
|
isInvalidated: false,
|
|
status: hasData ? "success" : "pending",
|
|
fetchStatus: "idle"
|
|
};
|
|
}
|
|
export {
|
|
Query,
|
|
fetchState
|
|
};
|
|
//# sourceMappingURL=query.js.map |