/* eslint-disable @typescript-eslint/naming-convention */ import type { CacheValueType, NullableType, RequestInstance, ExtractResponseType, ExtractErrorType, ExtractAdapterResolvedType, Dispatcher, ExtractAdapterType, ExtractAdapterExtraType, ResponseDetailsType, ExtractAdapterStatusType, ResponseType, } from "@hyper-fetch/core"; import { scopeKey } from "@hyper-fetch/core"; import type { UseTrackedStateType } from "helpers"; import { initialState } from "helpers"; /** * Extracts the "identity" portion of a cache key (method + endpoint with resolved params), * stripping the query params part. Cache keys follow the format: `method_endpoint_queryParams`. */ const getCacheKeyIdentity = (cacheKey: string): string => { const lastUnderscoreIndex = cacheKey.lastIndexOf("_"); if (lastUnderscoreIndex === -1) return cacheKey; return cacheKey.substring(0, lastUnderscoreIndex); }; /** * Determines whether state should be cleared when the cache key changes. * * - `"clean"` — always clear * - `"preserve"` — never clear * - `"auto"` — clear when the resource identity changed (URL params), preserve when only query params changed */ export const getShouldClearState = ( mode: "auto" | "preserve" | "clean", oldCacheKey: string, newCacheKey: string, ): boolean => { if (oldCacheKey === newCacheKey) return false; if (mode === "clean") return true; if (mode === "preserve") return false; // "auto" — compare the identity part (method + endpoint with params) return getCacheKeyIdentity(oldCacheKey) !== getCacheKeyIdentity(newCacheKey); }; export const getDetailsState = ( state?: UseTrackedStateType, details?: Partial, ): ResponseDetailsType => { return { retries: state?.retries || 0, isCanceled: false, isOffline: false, willRetry: false, addedTimestamp: +new Date(), triggerTimestamp: +new Date(), requestTimestamp: +new Date(), responseTimestamp: +new Date(), ...details, }; }; export const isStaleCacheData = (staleTime: number, staleTimestamp: NullableType) => { if (!staleTimestamp) return true; return +new Date() > +staleTimestamp + staleTime; }; export const getValidCacheData = ( request: T, initialResponse: NullableType>>, cacheData: NullableType, ExtractErrorType, ExtractAdapterType>>, ): CacheValueType, ExtractErrorType, ExtractAdapterType> | null => { const isStale = isStaleCacheData(request.staleTime, cacheData?.responseTimestamp); if (!isStale && cacheData) { return cacheData; } if (initialResponse) { return { data: null, error: null, status: null, success: true, extra: null, cached: !!request.cache, ...(initialResponse as Partial>), ...getDetailsState(), staleTime: 1000, version: request.client.cache.version, cacheKey: request.cacheKey, scope: request.scope, cacheTime: request.cacheTime, requestTimestamp: initialResponse?.requestTimestamp ?? +new Date(), responseTimestamp: initialResponse?.responseTimestamp ?? +new Date(), }; } return null; }; export const getTimestamp = (timestamp?: NullableType) => { return timestamp ? new Date(timestamp) : null; }; export const getIsInitiallyLoading = ({ queryKey, dispatcher, hasState, revalidate, disabled, }: { queryKey: string; dispatcher: Dispatcher>; hasState: boolean; revalidate?: boolean; disabled?: boolean; }) => { if (!revalidate && hasState) { return false; } const queue = dispatcher.getQueue(queryKey); const isInitiallyLoading = dispatcher.hasRunningRequests(queryKey) || (!queue.stopped && disabled === false); return isInitiallyLoading; }; export const getInitialState = ({ initialResponse, dispatcher, request, disabled, revalidate, }: { initialResponse: NullableType>>; dispatcher: Dispatcher>; request: T; disabled?: boolean; // useFetch only revalidate?: boolean; // useFetch only }): UseTrackedStateType => { const { client, cacheKey, unstable_responseMapper } = request; const { cache } = client; const cacheData = cache.get, ExtractErrorType>(scopeKey(cacheKey, request.scope)); const cacheState = getValidCacheData(request, initialResponse, cacheData); const initialLoading = getIsInitiallyLoading({ queryKey: scopeKey(request.queryKey, request.scope), dispatcher, disabled, revalidate, hasState: !!cacheState, }); if (cacheState) { const mappedData = unstable_responseMapper ? unstable_responseMapper(cacheState as ResponseType>) : cacheState; if (mappedData instanceof Promise) { // For the async mapper we cannot return async values // So we have return the initial state instead return initialState; } return { data: mappedData.data, error: mappedData.error, status: mappedData.status as ExtractAdapterStatusType>, success: mappedData.success, extra: (mappedData.extra || client.adapter.defaultExtra) as ExtractAdapterExtraType>, retries: cacheState.retries, requestTimestamp: getTimestamp(cacheState.requestTimestamp), responseTimestamp: getTimestamp(cacheState.responseTimestamp), loading: initialLoading, }; } return { data: initialState.data, error: initialState.error, status: initialState.status as ExtractAdapterStatusType>, success: initialState.success, extra: request.client.adapter.defaultExtra as ExtractAdapterExtraType>, retries: initialState.retries, requestTimestamp: getTimestamp(initialState.requestTimestamp), responseTimestamp: getTimestamp(initialState.responseTimestamp), loading: initialLoading, }; };