@hyper-fetch/react
Version:
React hooks and utils for the hyper-fetch
1,574 lines • 60.5 kB
JavaScript
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
import { Request, Time, getRequestDispatcher, scopeKey, sendRequest } from "@hyper-fetch/core";
import { jsx } from "react/jsx-runtime";
//#region ../../node_modules/@better-hooks/lifecycle/dist/index.esm.js
var o = function(t) {
var u = useRef(!1);
useEffect((function() {
if (!u.current) return u.current = !0, t();
}), []);
}, i = function(t, u, c) {
void 0 === c && (c = !1);
var e = useRef(c);
useEffect((function() {
if (e.current) return t();
e.current = !0;
}), u);
}, f$1 = function() {
var r = function(r, n) {
var t = "function" == typeof Symbol && r[Symbol.iterator];
if (!t) return r;
var u, c, e = t.call(r), o = [];
try {
for (; (void 0 === n || n-- > 0) && !(u = e.next()).done;) o.push(u.value);
} catch (r) {
c = { error: r };
} finally {
try {
u && !u.done && (t = e.return) && t.call(e);
} finally {
if (c) throw c.error;
}
}
return o;
}(useState(0), 2)[1];
return useCallback((function() {
r((function(r) {
return r + 1;
}));
}), []);
}, d$1 = function(r) {
useEffect((function() {
return r;
}), []);
}, f = function(e) {
var u = (e || {}).delay, c = void 0 === u ? 400 : u, i = useRef(!1), l = useRef(null), f = f$1(), a = useCallback((function() {
i.current && f();
}), [f]), v = function() {
null !== l.current && clearTimeout(l.current), l.current = null;
}, d = useCallback((function(n, t) {
var r = null != t ? t : c;
v(), l.current = setTimeout((function() {
l.current = null, n(), a();
}), r), a();
}), [c, a]);
return useEffect((function() {
return v;
}), []), {
get active() {
return i.current = !0, !!l.current;
},
debounce: d,
reset: v
};
};
function v(n, t) {
var r = "function" == typeof Symbol && n[Symbol.iterator];
if (!r) return n;
var e, u, c = r.call(n), o = [];
try {
for (; (void 0 === t || t-- > 0) && !(e = c.next()).done;) o.push(e.value);
} catch (n) {
u = { error: n };
} finally {
try {
e && !e.done && (r = c.return) && r.call(c);
} finally {
if (u) throw u.error;
}
}
return o;
}
var y = function(n) {
var t = Object.prototype.toString.call(n);
return Array.isArray(n) ? !n.length : "object" == typeof n && null !== n && "[object Object]" === t && !Object.keys(n).length;
}, m = function(n, t) {
var r, e = Object.prototype.toString.call(n), u = Object.prototype.toString.call(t), c = typeof n, o = typeof t, i = function(n) {
return c === n && o === n;
};
return e === u && (null === n && null === t || !!(i("number") && Number.isNaN(n) && Number.isNaN(t)) || !(!y(n) || !y(t)) || (Array.isArray(n) && Array.isArray(t) ? n.length === t.length && !n.some((function(n, r) {
return !m(n, t[r]);
})) : i("object") && e === (r = "[object Object]") && u === r ? Object.keys(n).length === Object.keys(t).length && !Object.entries(n).some((function(n) {
var r = v(n, 2), e = r[0], u = r[1];
return !m(u, t[e]);
})) : n instanceof Date && t instanceof Date ? +n == +t : n === t));
}, p = function(t) {
var e = t || {}, u = e.interval, c = void 0 === u ? 200 : u, i = e.timeout, l = void 0 === i ? 200 : i, f = useRef(0), a = useRef(!0), v = useRef(!1), d = useRef(null), y = f$1(), m = function() {
a.current && (a.current = !1), v.current && y();
}, s = function() {
null !== d.current && clearTimeout(d.current), d.current = null;
};
return useEffect((function() {
return s;
}), []), {
get active() {
return v.current = !0, !!d.current;
},
throttle: function(n, t) {
var r, e, u = function() {
f.current = Date.now(), n(), m();
}, o = null !== (r = null == t ? void 0 : t.interval) && void 0 !== r ? r : c, i = null !== (e = null == t ? void 0 : t.timeout) && void 0 !== e ? e : l, v = Date.now() >= f.current + o;
a.current && m(), d.current && s(), v ? u() : i && (d.current = setTimeout((function() {
d.current = null, a.current = !0, u();
}), i));
},
reset: s
};
};
"undefined" == typeof window || void 0 === window.document || window.document.createElement;
//#endregion
//#region src/helpers/use-request-events/use-request-events.hooks.ts
/**
* This is helper hook that handles main Hyper-Fetch event/data flow
* @internal
* @param options
* @returns
*/
var useRequestEvents = ({ request, dispatcher, logger, actions, setCacheData, getIsDataProcessing }) => {
const { unstable_responseMapper } = request;
const { cache, requestManager } = request.client;
const onSuccessCallback = useRef(null);
const onErrorCallback = useRef(null);
const onAbortCallback = useRef(null);
const onOfflineErrorCallback = useRef(null);
const onFinishedCallback = useRef(null);
const onRequestStartCallback = useRef(null);
const onResponseStartCallback = useRef(null);
const onDownloadProgressCallback = useRef(null);
const onUploadProgressCallback = useRef(null);
const lifecycleEvents = useRef(/* @__PURE__ */ new Map());
const dataEvents = useRef(null);
const removeLifecycleListener = (requestId) => {
lifecycleEvents.current.get(requestId)?.unmount();
lifecycleEvents.current.delete(requestId);
};
const clearLifecycleListeners = () => {
const events = lifecycleEvents.current;
Array.from(events.values()).forEach((value) => {
value.unmount();
});
events.clear();
};
const optimisticResultsRef = useRef(/* @__PURE__ */ new Map());
const handleResponseCallbacks = (values) => {
const { success } = values.response;
const { isOffline, isCanceled, willRetry } = values.details;
const opt = optimisticResultsRef.current.get(values.requestId);
const paramsWithContext = {
...values,
mutationContext: opt?.context
};
if (request.offline && isOffline && !success) {
logger.debug({
title: "Performing offline error callback",
type: "system",
extra: values
});
onOfflineErrorCallback.current?.(paramsWithContext);
} else if (isCanceled) {
logger.debug({
title: "Performing abort callback",
type: "system",
extra: values
});
try {
opt?.rollback?.();
} catch {}
onAbortCallback.current?.(paramsWithContext);
} else if (success) {
logger.debug({
title: "Performing success callback",
type: "system",
extra: values
});
if (opt?.invalidate) opt.invalidate.forEach((req) => cache.invalidate(req));
onSuccessCallback.current?.(paramsWithContext);
} else {
logger.debug({
title: "Performing error callback",
type: "system",
extra: values
});
if (!willRetry) try {
opt?.rollback?.();
} catch {}
onErrorCallback.current?.(paramsWithContext);
}
onFinishedCallback.current?.(paramsWithContext);
if (!willRetry) optimisticResultsRef.current.delete(values.requestId);
};
const handleGetLoadingEvent = (req) => {
return ({ loading }) => {
if (getIsDataProcessing(req.cacheKey)) return;
const canDisableLoading = !loading && !dispatcher.hasRunningRequests(scopeKey(req.queryKey, req.scope));
if (loading || canDisableLoading) actions.setLoading(loading);
};
};
const handleDownloadProgress = (data) => {
onDownloadProgressCallback.current?.(data);
};
const handleUploadProgress = (data) => {
onUploadProgressCallback.current?.(data);
};
const handleRequestStart = () => {
return (details) => {
onRequestStartCallback.current?.(details);
};
};
const handleResponseStart = () => {
return (details) => {
onResponseStartCallback.current?.(details);
};
};
const handleResponse = () => {
return (values) => {
const data = unstable_responseMapper ? unstable_responseMapper(values.response) : values.response;
if (data instanceof Promise) return (async () => {
handleResponseCallbacks({
...values,
response: await data
});
})();
return handleResponseCallbacks(values);
};
};
const handleRemove = ({ requestId }) => {
const opt = optimisticResultsRef.current.get(requestId);
if (opt) {
try {
opt.rollback?.();
} catch {}
optimisticResultsRef.current.delete(requestId);
}
removeLifecycleListener(requestId);
};
const clearCacheDataListener = () => {
dataEvents.current?.unmount();
dataEvents.current = null;
};
const addCacheDataListener = (req) => {
const loadingUnmount = requestManager.events.onLoadingByQueue(scopeKey(req.queryKey, req.scope), handleGetLoadingEvent(req));
const getResponseUnmount = cache.events.onDataByKey(req.cacheKey, setCacheData);
const unmount = () => {
loadingUnmount();
getResponseUnmount();
};
clearCacheDataListener();
dataEvents.current = { unmount };
return unmount;
};
const addLifecycleListeners = (req, requestId, optimisticResult) => {
/**
* useFetch handles requesting by general keys
* This makes it possible to deduplicate requests from different places and share data
*/
if (!requestId) {
clearLifecycleListeners();
const { queryKey, cacheKey } = req;
const requestStartUnmount = requestManager.events.onRequestStartByQueue(queryKey, handleRequestStart());
const responseStartUnmount = requestManager.events.onResponseStartByQueue(queryKey, handleResponseStart());
const uploadUnmount = requestManager.events.onUploadProgressByQueue(queryKey, handleUploadProgress);
const downloadUnmount = requestManager.events.onDownloadProgressByQueue(queryKey, handleDownloadProgress);
const responseUnmount = requestManager.events.onResponseByCache(cacheKey, handleResponse());
const unmount = () => {
downloadUnmount();
uploadUnmount();
requestStartUnmount();
responseStartUnmount();
responseUnmount();
};
lifecycleEvents.current.set(queryKey, { unmount });
return unmount;
}
/**
* useSubmit handles requesting by requestIds, this makes it possible to track single requests
*/
if (optimisticResult) optimisticResultsRef.current.set(requestId, optimisticResult);
const requestRemove = requestManager.events.onRemoveById(requestId, handleRemove);
const requestStartUnmount = requestManager.events.onRequestStartById(requestId, handleRequestStart());
const responseStartUnmount = requestManager.events.onResponseStartById(requestId, handleResponseStart());
const responseUnmount = requestManager.events.onResponseById(requestId, handleResponse());
const uploadUnmount = requestManager.events.onUploadProgressById(requestId, handleUploadProgress);
const downloadUnmount = requestManager.events.onDownloadProgressById(requestId, handleDownloadProgress);
const unmount = () => {
requestRemove();
downloadUnmount();
uploadUnmount();
requestStartUnmount();
responseStartUnmount();
responseUnmount();
};
lifecycleEvents.current.set(requestId, { unmount });
return unmount;
};
const abort = () => {
const ak = scopeKey(request.abortKey, request.scope);
dispatcher.getAllRunningRequests().forEach((requestData) => {
const reqAk = scopeKey(requestData.request.abortKey, requestData.request.scope);
if (reqAk === ak) {
const qk = scopeKey(requestData.request.queryKey, requestData.request.scope);
dispatcher.delete(qk, requestData.requestId, reqAk);
}
});
};
/**
* On unmount we want to clear all the listeners to prevent memory leaks
*/
d$1(() => {
clearLifecycleListeners();
clearCacheDataListener();
});
return [{
abort,
onSuccess: (callback) => {
onSuccessCallback.current = callback;
},
onError: (callback) => {
onErrorCallback.current = callback;
},
onAbort: (callback) => {
onAbortCallback.current = callback;
},
onOfflineError: (callback) => {
onOfflineErrorCallback.current = callback;
},
onFinished: (callback) => {
onFinishedCallback.current = callback;
},
onRequestStart: (callback) => {
onRequestStartCallback.current = callback;
},
onResponseStart: (callback) => {
onResponseStartCallback.current = callback;
},
onDownloadProgress: (callback) => {
onDownloadProgressCallback.current = callback;
},
onUploadProgress: (callback) => {
onUploadProgressCallback.current = callback;
}
}, {
addCacheDataListener,
clearCacheDataListener,
addLifecycleListeners,
removeLifecycleListener,
clearLifecycleListeners
}];
};
//#endregion
//#region src/helpers/use-tracked-state/use-tracked-state.constants.ts
var initialState = {
data: null,
error: null,
status: null,
extra: {},
success: false,
loading: false,
retries: 0,
responseTimestamp: null,
requestTimestamp: null
};
//#endregion
//#region src/utils/deep-equal.utils.ts
/**
* Check if value is empty
* @param value any object or primitive
* @returns true when value is empty
*/
var isEmpty = (value) => {
const valueType = Object.prototype.toString.call(value);
if (Array.isArray(value)) return !value.length;
if (typeof value === "object" && value !== null && valueType === "[object Object]") return !Object.keys(value).length;
return false;
};
/**
* Allow to deep compare any passed values
* @param firstValue unknown
* @param secondValue unknown
* @returns true when elements are equal
*/
var isEqual = (firstValue, secondValue) => {
if (firstValue === secondValue) return true;
try {
const firstValueType = Object.prototype.toString.call(firstValue);
const secondValueType = Object.prototype.toString.call(secondValue);
const firstType = typeof firstValue;
const secondType = typeof secondValue;
const isType = (type) => firstType === type && secondType === type;
const isTypeValue = (type) => firstValueType === type && secondValueType === type;
if (firstValueType !== secondValueType) return false;
if (isType("number") && Number.isNaN(firstValue) && Number.isNaN(secondValue)) return true;
if (isEmpty(firstValue) && isEmpty(secondValue)) return true;
if (Array.isArray(firstValue) && Array.isArray(secondValue)) {
if (firstValue.length !== secondValue.length) return false;
return !firstValue.some((element, i) => !isEqual(element, secondValue[i]));
}
if (isType("object") && isTypeValue("[object Object]")) {
if (Object.keys(firstValue).length !== Object.keys(secondValue).length) return false;
return !Object.entries(firstValue).some(([key, value]) => !isEqual(value, secondValue[key]));
}
if (firstValue instanceof Date && secondValue instanceof Date) return +firstValue === +secondValue;
return firstValue === secondValue;
} catch (err) {
console.error(err);
return false;
}
};
//#endregion
//#region src/utils/bounce.utils.ts
var getBounceData = (bounceData) => {
return {
...bounceData,
throttle: void 0,
debounce: void 0
};
};
//#endregion
//#region src/utils/tracked-proxy.utils.ts
/**
* Wraps a plain object in a Proxy that calls `setRenderKey` when tracked properties are accessed.
* Unlike getter-based tracking, Proxy-wrapped objects display their actual values in console.log,
* making debugging significantly easier while preserving field-level dependency tracking.
*/
var createTrackedProxy = (target, trackedKeys, setRenderKey) => {
return new Proxy(target, { get(obj, prop, receiver) {
if (typeof prop === "string" && trackedKeys.includes(prop)) setRenderKey(prop);
return Reflect.get(obj, prop, receiver);
} });
};
//#endregion
//#region src/helpers/use-tracked-state/use-tracked-state.utils.ts
/**
* 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`.
*/
var getCacheKeyIdentity = (cacheKey) => {
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
*/
var getShouldClearState = (mode, oldCacheKey, newCacheKey) => {
if (oldCacheKey === newCacheKey) return false;
if (mode === "clean") return true;
if (mode === "preserve") return false;
return getCacheKeyIdentity(oldCacheKey) !== getCacheKeyIdentity(newCacheKey);
};
var getDetailsState = (state, details) => {
return {
retries: state?.retries || 0,
isCanceled: false,
isOffline: false,
willRetry: false,
addedTimestamp: +/* @__PURE__ */ new Date(),
triggerTimestamp: +/* @__PURE__ */ new Date(),
requestTimestamp: +/* @__PURE__ */ new Date(),
responseTimestamp: +/* @__PURE__ */ new Date(),
...details
};
};
var isStaleCacheData = (staleTime, staleTimestamp) => {
if (!staleTimestamp) return true;
return +/* @__PURE__ */ new Date() > +staleTimestamp + staleTime;
};
var getValidCacheData = (request, initialResponse, cacheData) => {
if (cacheData) return cacheData;
if (initialResponse) return {
data: null,
error: null,
status: null,
success: true,
extra: null,
cached: !!request.cache,
...initialResponse,
...getDetailsState(),
staleTime: 1e3,
version: request.client.cache.version,
cacheKey: request.cacheKey,
scope: request.scope,
cacheTime: request.cacheTime,
requestTimestamp: initialResponse?.requestTimestamp ?? +/* @__PURE__ */ new Date(),
responseTimestamp: initialResponse?.responseTimestamp ?? +/* @__PURE__ */ new Date()
};
return null;
};
var getTimestamp = (timestamp) => {
return timestamp ? new Date(timestamp) : null;
};
var getIsInitiallyLoading = ({ queryKey, dispatcher, hasState, revalidate, disabled }) => {
if (!revalidate && hasState) return false;
const queue = dispatcher.getQueue(queryKey);
return dispatcher.hasRunningRequests(queryKey) || !queue.stopped && disabled === false;
};
var getInitialState = ({ initialResponse, dispatcher, request, disabled, revalidate }) => {
const { client, cacheKey, unstable_responseMapper } = request;
const { cache } = client;
const cacheState = getValidCacheData(request, initialResponse, cache.get(scopeKey(cacheKey, request.scope)));
const initialLoading = getIsInitiallyLoading({
queryKey: scopeKey(request.queryKey, request.scope),
dispatcher,
disabled,
revalidate,
hasState: !!cacheState
});
if (cacheState) {
const mappedData = unstable_responseMapper ? unstable_responseMapper(cacheState) : cacheState;
if (mappedData instanceof Promise) return initialState;
return {
data: mappedData.data,
error: mappedData.error,
status: mappedData.status,
success: mappedData.success,
extra: mappedData.extra || client.adapter.defaultExtra,
retries: cacheState.retries,
requestTimestamp: getTimestamp(cacheState.requestTimestamp),
responseTimestamp: getTimestamp(cacheState.responseTimestamp),
loading: initialLoading
};
}
return {
data: initialState.data,
error: initialState.error,
status: initialState.status,
success: initialState.success,
extra: request.client.adapter.defaultExtra,
retries: initialState.retries,
requestTimestamp: getTimestamp(initialState.requestTimestamp),
responseTimestamp: getTimestamp(initialState.responseTimestamp),
loading: initialLoading
};
};
//#endregion
//#region src/helpers/use-tracked-state/use-tracked-state.hooks.ts
/**
*
* @param request
* @param initialResponse
* @param dispatcher
* @param dependencies
* @internal
*/
var useTrackedState = ({ request, dispatcher, initialResponse, deepCompare, dependencyTracking, keepPreviousData = "auto", disabled, revalidate }) => {
const { client, cacheKey, queryKey, staleTime, unstable_responseMapper } = request;
const { cache } = client;
const state = useRef(getInitialState({
initialResponse,
dispatcher,
request,
disabled
}));
const renderKeys = useRef([]);
const isProcessingData = useRef("");
const previousCacheKey = useRef(cacheKey);
const versionRef = useRef(0);
const listenerRef = useRef(null);
const subscribe = useCallback((listener) => {
listenerRef.current = listener;
return () => {
listenerRef.current = null;
};
}, []);
const getSnapshot = useCallback(() => versionRef.current, []);
useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
const emitChange = () => {
versionRef.current += 1;
listenerRef.current?.();
};
const getStaleStatus = () => {
const cacheData = cache.get(scopeKey(cacheKey, request.scope));
return !cacheData || isStaleCacheData(staleTime, cacheData?.responseTimestamp);
};
const renderKeyTrigger = (keys) => {
if (renderKeys.current.some((renderKey) => keys.includes(renderKey))) emitChange();
};
const setRenderKey = (renderKey) => {
if (!renderKeys.current.includes(renderKey)) renderKeys.current.push(renderKey);
};
i(() => {
const oldKey = previousCacheKey.current;
previousCacheKey.current = cacheKey;
const cacheState = getValidCacheData(request, initialResponse, cache.get(cacheKey));
if (getShouldClearState(keepPreviousData, oldKey, cacheKey) && !cacheState) {
state.current = getInitialState({
initialResponse,
dispatcher,
request,
disabled,
revalidate
});
renderKeyTrigger(Object.keys(state.current));
} else if (cacheState) {
state.current.loading = getIsInitiallyLoading({
queryKey: scopeKey(request.queryKey, request.scope),
dispatcher,
disabled,
revalidate,
hasState: true
});
setCacheData(cacheState);
}
}, [cacheKey, queryKey], true);
i(() => {
const handleDependencyTracking = () => {
if (!dependencyTracking) Object.keys(state.current).forEach((key) => setRenderKey(key));
};
handleDependencyTracking();
}, [dependencyTracking], true);
const handleCompare = (firstValue, secondValue) => {
if (typeof deepCompare === "function") return deepCompare(firstValue, secondValue);
if (deepCompare) return isEqual(firstValue, secondValue);
return false;
};
const handleCacheData = (cacheData) => {
const newStateValues = {
data: cacheData.data,
error: cacheData.error,
status: cacheData.status,
success: cacheData.success,
extra: cacheData.extra,
retries: cacheData.retries,
responseTimestamp: new Date(cacheData.responseTimestamp),
requestTimestamp: new Date(cacheData.requestTimestamp),
loading: dispatcher.hasRunningRequests(queryKey)
};
const changedKeys = Object.keys(newStateValues).filter((key) => {
const keyValue = key;
const firstValue = state.current[keyValue];
const secondValue = newStateValues[keyValue];
return !handleCompare(firstValue, secondValue);
});
state.current = {
...state.current,
...newStateValues
};
renderKeyTrigger(changedKeys);
};
const setIsDataProcessing = ({ processingCacheKey, isProcessing }) => {
if (isProcessing) isProcessingData.current = processingCacheKey;
else if (isProcessingData.current === cacheKey) isProcessingData.current = "";
};
const getIsDataProcessing = (processingCacheKey) => {
return isProcessingData.current === processingCacheKey;
};
const setCacheData = (cacheData) => {
setIsDataProcessing({
processingCacheKey: cacheKey,
isProcessing: true
});
const data = unstable_responseMapper ? unstable_responseMapper(cacheData) : cacheData;
if (data instanceof Promise) return (async () => {
const promiseData = await data;
handleCacheData({
...cacheData,
...promiseData
});
setIsDataProcessing({
processingCacheKey: cacheKey,
isProcessing: false
});
})();
setIsDataProcessing({
processingCacheKey: cacheKey,
isProcessing: false
});
return handleCacheData({
...cacheData,
...data
});
};
return [
state.current,
{
setData: (data) => {
state.current.data = data instanceof Function ? data(state.current.data || null) : data;
renderKeyTrigger(["data"]);
},
setError: (error) => {
state.current.error = error instanceof Function ? error(state.current.error || null) : error;
renderKeyTrigger(["error"]);
},
setLoading: (loading) => {
const value = loading instanceof Function ? loading(state.current.loading) : loading;
if (value === state.current.loading) return;
state.current.loading = value;
renderKeyTrigger(["loading"]);
},
setStatus: (status) => {
if ((status instanceof Function ? status(state.current.status) : status) === state.current.status) return;
state.current.status = status instanceof Function ? status(state.current.status || null) : status;
renderKeyTrigger(["status"]);
},
setSuccess: (success) => {
if ((success instanceof Function ? success(state.current.success || false) : success) === state.current.success) return;
state.current.success = success instanceof Function ? success(state.current.success || false) : success;
renderKeyTrigger(["success"]);
},
setExtra: (extra) => {
if ((extra instanceof Function ? extra(state.current.extra) : extra) === state.current.extra) return;
state.current.extra = extra instanceof Function ? extra(state.current.extra) : extra;
renderKeyTrigger(["extra"]);
},
setRetries: (retries) => {
if ((retries instanceof Function ? retries(state.current.retries || 0) : retries) === state.current.retries) return;
state.current.retries = retries instanceof Function ? retries(state.current.retries || 0) : retries;
renderKeyTrigger(["retries"]);
},
setResponseTimestamp: (timestamp) => {
if ((timestamp instanceof Function ? timestamp(state.current.responseTimestamp) : timestamp) === state.current.responseTimestamp) return;
const getTimestamp = (prev) => {
return timestamp instanceof Function ? timestamp(prev ? new Date(prev) : null) : timestamp;
};
state.current.responseTimestamp = getTimestamp(state.current.responseTimestamp);
renderKeyTrigger(["responseTimestamp"]);
},
setRequestTimestamp: (timestamp) => {
if ((timestamp instanceof Function ? timestamp(state.current.requestTimestamp) : timestamp) === state.current.requestTimestamp) return;
const getTimestamp = (prev) => {
return timestamp instanceof Function ? timestamp(prev ? new Date(prev) : null) : timestamp;
};
state.current.requestTimestamp = getTimestamp(state.current.requestTimestamp);
renderKeyTrigger(["requestTimestamp"]);
},
clearState: () => {
state.current = {
data: null,
error: null,
loading: false,
status: null,
success: false,
extra: null,
retries: 0,
responseTimestamp: null,
requestTimestamp: null
};
renderKeyTrigger(Object.keys(state.current));
}
},
{
setRenderKey,
setCacheData,
getStaleStatus,
getIsDataProcessing
}
];
};
//#endregion
//#region src/helpers/use-socket-state/use-socket-state.hooks.ts
var useSocketState = (socket, { dependencyTracking }) => {
const onDisconnectCallback = useRef(null);
const onErrorCallback = useRef(null);
const onConnectedCallback = useRef(null);
const onConnectingCallback = useRef(null);
const onReconnectingCallback = useRef(null);
const onReconnectingFailedCallback = useRef(null);
const state = useRef({
data: null,
extra: null,
connected: socket.adapter.connected,
connecting: socket.adapter.connecting,
timestamp: null
});
const renderKeys = useRef([]);
const versionRef = useRef(0);
const listenerRef = useRef(null);
const subscribe = useCallback((listener) => {
listenerRef.current = listener;
return () => {
listenerRef.current = null;
};
}, []);
const getSnapshot = useCallback(() => versionRef.current, []);
useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
const emitChange = () => {
versionRef.current += 1;
listenerRef.current?.();
};
const renderKeyTrigger = (keys) => {
if (renderKeys.current.some((renderKey) => keys.includes(renderKey))) emitChange();
};
const setRenderKey = (renderKey) => {
if (!renderKeys.current.includes(renderKey)) renderKeys.current.push(renderKey);
};
i(() => {
const handleDependencyTracking = () => {
if (!dependencyTracking) Object.keys(state.current).forEach((key) => setRenderKey(key));
};
handleDependencyTracking();
}, [dependencyTracking], true);
const actions = {
setData: (data) => {
state.current.data = data;
renderKeyTrigger(["data"]);
},
setExtra: (extra) => {
state.current.extra = extra;
renderKeyTrigger(["extra"]);
},
setConnected: (connected) => {
state.current.connected = connected;
renderKeyTrigger(["connected"]);
},
setConnecting: (connecting) => {
state.current.connecting = connecting;
renderKeyTrigger(["connecting"]);
},
setTimestamp: (timestamp) => {
state.current.timestamp = timestamp;
renderKeyTrigger(["timestamp"]);
},
clearState: () => {
state.current = {
data: null,
extra: null,
connected: false,
connecting: false,
timestamp: null
};
renderKeyTrigger(Object.keys(state.current));
}
};
const callbacks = {
onConnected: (callback) => {
onConnectedCallback.current = callback;
},
onDisconnected: (callback) => {
onDisconnectCallback.current = callback;
},
onError: (callback) => {
onErrorCallback.current = callback;
},
onConnecting: (callback) => {
onConnectingCallback.current = callback;
},
onReconnecting: (callback) => {
onReconnectingCallback.current = callback;
},
onReconnectingFailed: (callback) => {
onReconnectingFailedCallback.current = callback;
}
};
o(() => {
const umountOnError = socket.events.onError((event) => {
onErrorCallback.current?.(event);
});
const umountOnConnecting = socket.events.onConnecting(({ connecting }) => {
actions.setConnecting(connecting);
onConnectingCallback.current?.();
});
const umountOnOpen = socket.events.onConnected(() => {
actions.setConnected(true);
onConnectedCallback.current?.();
});
const umountOnClose = socket.events.onDisconnected(() => {
actions.setConnected(false);
onDisconnectCallback.current?.();
});
const umountOnReconnecting = socket.events.onReconnecting(({ attempts }) => {
onReconnectingCallback.current?.({ attempts });
});
const umountOnReconnectingFailed = socket.events.onReconnectingFailed(({ attempts }) => {
onReconnectingFailedCallback.current?.({ attempts });
});
return () => {
umountOnError();
umountOnConnecting();
umountOnOpen();
umountOnClose();
umountOnReconnecting();
umountOnReconnectingFailed();
};
});
return [
state.current,
actions,
callbacks,
{ setRenderKey }
];
};
//#endregion
//#region src/helpers/use-socket-state/use-socket-state.constants.ts
var initialSocketState = {
data: null,
extra: null,
connected: false,
connecting: false,
timestamp: null
};
//#endregion
//#region src/provider/provider.tsx
var ConfigContext = React.createContext({
config: {},
setConfig: () => null
});
/**
* Provider with configuration for hooks
* @param options
* @returns
*/
var Provider = ({ children, config }) => {
const [currentConfig, setConfig] = useState(config || {});
const value = useMemo(() => {
return {
config: currentConfig,
setConfig
};
}, [currentConfig]);
return /* @__PURE__ */ jsx(ConfigContext.Provider, {
value,
children
});
};
/**
* Hook to allow reading current context config
* @returns
*/
var useProvider = () => {
return useContext(ConfigContext);
};
//#endregion
//#region src/hooks/use-fetch/use-fetch.hooks.ts
var suspensePromiseMap = /* @__PURE__ */ new Map();
var suspenseResultMap = /* @__PURE__ */ new Map();
/**
* This hook aims to retrieve data from the server. It automatically fetches on mount and
* refetches based on dependencies, with support for caching, polling, and suspense.
* @param request Request instance
* @param options Hook options
* @returns
*/
var useFetch = (request, options) => {
const { config: globalConfig } = useProvider();
const { suspense, dependencies, disabled, dependencyTracking, revalidate, initialResponse, keepPreviousData, refresh, refreshTime, refetchBlurred, refetchOnBlur, refetchOnFocus, refetchOnReconnect, bounce, bounceType, bounceTime, bounceTimeout, deepCompare } = {
...useFetchDefaultOptions,
...globalConfig.useFetchConfig,
...options
};
const updateKey = JSON.stringify(request.toJSON());
const requestDebounce = f({ delay: bounceTime });
const requestThrottle = p({
interval: bounceTime,
timeout: bounceTimeout
});
const refreshDebounce = f({ delay: refreshTime });
const { cacheKey, queryKey, client } = request;
const { cache, fetchDispatcher: dispatcher, appManager, loggerManager } = client;
const ignoreReact18DoubleRender = useRef(true);
const logger = useRef(loggerManager.initialize(client, "useFetch")).current;
const bounceData = bounceType === "throttle" ? requestThrottle : requestDebounce;
const bounceFunction = bounceType === "throttle" ? requestThrottle.throttle : requestDebounce.debounce;
/**
* State handler with optimization for re-rendering, that hooks into the cache state and dispatchers queues
*/
const [state, actions, { setRenderKey, setCacheData, getStaleStatus, getIsDataProcessing }] = useTrackedState({
logger,
request,
dispatcher,
initialResponse,
deepCompare,
dependencyTracking,
keepPreviousData,
disabled,
revalidate
});
/**
* Handles the data exchange with the core logic - responses, loading, downloading etc
*/
const [callbacks, listeners] = useRequestEvents({
logger,
actions,
request,
dispatcher,
setCacheData,
getIsDataProcessing
});
const { addCacheDataListener, addLifecycleListeners, clearCacheDataListener } = listeners;
const handleFetch = () => {
if (!disabled) {
logger.debug({
title: `Fetching data`,
type: "system",
extra: { request }
});
dispatcher.add(request);
} else logger.debug({
title: `Cannot add to fetch queue`,
type: "system",
extra: { disabled }
});
};
function handleRefresh() {
if (!refresh || disabled) {
refreshDebounce.reset();
return;
}
refreshDebounce.debounce(() => {
const isBlurred = !appManager.isFocused;
const scopedQueryKey = scopeKey(request.queryKey, request.scope);
const isFetching = dispatcher.hasRunningRequests(scopedQueryKey);
const isQueued = dispatcher.getIsActiveQueue(scopedQueryKey);
const isActive = isFetching || isQueued;
if (isBlurred && refetchBlurred && !isActive || !isBlurred && !isActive) {
handleFetch();
logger.debug({
title: `Performing refresh request`,
type: "system",
extra: { request }
});
}
handleRefresh();
});
}
const refetch = () => {
handleFetch();
handleRefresh();
};
const getIsFetchingIdentity = () => {
return dispatcher.getRunningRequests(queryKey).some((running) => running.request.cacheKey === cacheKey);
};
const initialFetchData = () => {
const hasStaleData = getStaleStatus();
const isFetching = getIsFetchingIdentity();
if ((revalidate || hasStaleData) && !isFetching) handleFetch();
};
const updateFetchData = () => {
const hasStaleData = getStaleStatus();
const shouldUpdate = !revalidate ? hasStaleData : true;
/**
* This is a hack to avoid double rendering in React 18
* It renders initial mount event and allow us to consume only hook updates
*/
if (!ignoreReact18DoubleRender.current && shouldUpdate)
/**
* While debouncing we need to make sure that first request is not debounced when the cache is not available
* This way it will not wait for debouncing but fetch data right away
*/
if (bounce) {
logger.debug({
title: `Bounce request with ${bounceType}`,
type: "system",
extra: {
queryKey,
request
}
});
bounceFunction(() => handleFetch());
} else handleFetch();
else ignoreReact18DoubleRender.current = false;
};
const handleMountEvents = () => {
addCacheDataListener(request);
addLifecycleListeners(request);
const focusUnmount = appManager.events.onFocus(() => {
if (refetchOnFocus && !disabled) {
handleFetch();
handleRefresh();
}
});
const blurUnmount = appManager.events.onBlur(() => {
if (refetchOnBlur && !disabled) {
handleFetch();
handleRefresh();
}
});
const onlineUnmount = appManager.events.onOnline(() => {
if (refetchOnReconnect && !disabled) {
handleFetch();
handleRefresh();
}
});
const invalidateUnmount = cache.events.onInvalidateByKey(cacheKey, handleFetch);
const deletionUnmount = cache.events.onDeleteByKey(cacheKey, handleFetch);
const unmount = () => {
clearCacheDataListener();
focusUnmount();
blurUnmount();
onlineUnmount();
invalidateUnmount();
deletionUnmount();
};
return unmount;
};
/**
* Initialization of the events related to data exchange with cache and queue
* This allows to share the state with other hooks and keep it related
*/
i(handleMountEvents, [updateKey, disabled], true);
/**
* Initial fetch triggered once data is stale or we use the refetch strategy
*/
o(initialFetchData);
/**
* Fetching logic for updates handling
*/
i(updateFetchData, [
updateKey,
disabled,
...dependencies
], true);
/**
* Refresh lifecycle handler
*/
i(handleRefresh, [
updateKey,
...dependencies,
disabled,
refresh,
refreshTime
], true);
/**
* Reset the ignore flag for React 18 strict mode
*/
d$1(() => {
ignoreReact18DoubleRender.current = true;
});
if (suspense && !disabled) {
const suspenseResult = suspenseResultMap.get(cacheKey);
if (suspenseResult) {
suspenseResultMap.delete(cacheKey);
if (suspenseResult.data !== null) state.data = suspenseResult.data;
if (suspenseResult.error !== null) state.error = suspenseResult.error;
if (suspenseResult.status !== null) state.status = suspenseResult.status;
if (suspenseResult.extra !== null) state.extra = suspenseResult.extra;
state.success = suspenseResult.success;
}
if (!(state.data !== null || state.error !== null)) {
let entry = suspensePromiseMap.get(cacheKey);
if (!entry) {
let resolvePromise;
const promise = new Promise((r) => {
resolvePromise = r;
});
const unsubscribe = cache.events.onDataByKey(cacheKey, (cacheData) => {
suspenseResultMap.set(cacheKey, {
data: cacheData.data ?? null,
error: cacheData.error ?? null,
status: cacheData.status ?? null,
extra: cacheData.extra ?? null,
success: cacheData.success ?? false
});
resolvePromise();
unsubscribe();
suspensePromiseMap.delete(cacheKey);
});
entry = {
promise,
resolve: resolvePromise,
cleanup: unsubscribe
};
suspensePromiseMap.set(cacheKey, entry);
}
if (!dispatcher.hasRunningRequests(queryKey)) dispatcher.add(request);
throw entry.promise;
}
const entry = suspensePromiseMap.get(cacheKey);
if (entry) {
entry.cleanup();
suspensePromiseMap.delete(cacheKey);
}
}
const trackedKeys = [
"data",
"error",
"loading",
"status",
"success",
"extra",
"retries",
"responseTimestamp",
"requestTimestamp"
];
return createTrackedProxy({
data: state.data,
error: state.error,
loading: state.loading,
status: state.status,
success: state.success,
extra: state.extra,
retries: state.retries,
responseTimestamp: state.responseTimestamp,
requestTimestamp: state.requestTimestamp,
bounce: getBounceData(bounceData),
...actions,
...callbacks,
refetch
}, trackedKeys, setRenderKey);
};
//#endregion
//#region src/hooks/use-fetch/use-fetch.utils.ts
var getRefreshTime = (refreshTime, dataTimestamp) => {
if (dataTimestamp) {
const timeDiff = Date.now() - +dataTimestamp;
return timeDiff < refreshTime ? refreshTime - timeDiff : refreshTime;
}
return refreshTime;
};
//#endregion
//#region src/hooks/use-fetch/use-fetch.constants.ts
var useFetchDefaultOptions = {
suspense: false,
dependencies: [],
disabled: false,
dependencyTracking: true,
revalidate: true,
initialResponse: null,
keepPreviousData: "auto",
refresh: false,
refreshTime: Time.HOUR,
refetchBlurred: true,
refetchOnBlur: false,
refetchOnFocus: false,
refetchOnReconnect: false,
bounce: false,
bounceType: "debounce",
bounceTime: 400,
bounceTimeout: 400,
deepCompare: true
};
//#endregion
//#region src/hooks/use-submit/use-submit.hooks.ts
/**
* This hook aims to mutate data on the server. Unlike useFetch, it does not fire automatically -
* call the returned `submit` function to trigger the request (e.g., on button click).
* @param request
* @param options
* @returns
*/
var useSubmit = (request, options) => {
const { config: globalConfig } = useProvider();
const mergedOptions = useMemo(() => ({
...useSubmitDefaultOptions,
...globalConfig.useSubmitConfig,
...options
}), [
globalConfig.useSubmitConfig,
JSON.stringify(options),
options?.deepCompare
]);
const { disabled, dependencyTracking, initialResponse, bounce, bounceType, bounceTime, deepCompare } = mergedOptions;
/**
* Because of the dynamic cacheKey / queryKey signing within the request we need to store it's latest instance
* so the events got triggered properly and show the latest result without mixing it up
*/
const { client } = request;
const { cache, submitDispatcher: dispatcher, loggerManager } = client;
const logger = useRef(loggerManager.initialize(client, "useSubmit")).current;
const requestDebounce = f({ delay: bounceTime });
const requestThrottle = p({
interval: bounceTime,
timeout: "bounceTimeout" in mergedOptions ? mergedOptions.bounceTimeout : bounceTime
});
const bounceResolver = useRef(() => null);
const bounceData = bounceType === "throttle" ? requestThrottle : requestDebounce;
const bounceFunction = bounceType === "throttle" ? requestThrottle.throttle : requestDebounce.debounce;
/**
* State handler with optimization for rerendering, that hooks into the cache state and dispatchers queues
*/
const [state, actions, { setRenderKey, setCacheData, getIsDataProcessing }] = useTrackedState({
logger,
request,
dispatcher,
initialResponse,
deepCompare,
dependencyTracking
});
/**
* Handles the data exchange with the core logic - responses, loading, downloading etc
*/
const [callbacks, listeners] = useRequestEvents({
logger,
actions,
request,
dispatcher,
setCacheData,
getIsDataProcessing
});
const { addCacheDataListener, addLifecycleListeners } = listeners;
const handleSubmit = (submitOptions) => {
const requestClone = request.clone(submitOptions);
if (disabled) {
logger.warning({
title: `Cannot submit request`,
type: "system",
extra: {
disabled,
submitOptions
}
});
return Promise.resolve({
data: null,
error: /* @__PURE__ */ new Error("Cannot submit request. Option 'disabled' is enabled"),
status: null,
extra: request.client.adapter.defaultExtra
});
}
const triggerRequest = () => {
addCacheDataListener(requestClone);
return sendRequest(requestClone, {
dispatcherType: "submit",
...submitOptions,
onBeforeSent: (data) => {
addLifecycleListeners(requestClone, data.requestId, data.mutationContext);
submitOptions?.onBeforeSent?.(data);
}
});
};
return new Promise((resolve) => {
const performSubmit = async () => {
logger.debug({
title: `Submitting request`,
type: "system",
extra: {
disabled,
submitOptions
}
});
if (bounce) {
const bouncedResolve = bounceResolver.current;
bounceResolver.current = (value) => {
bouncedResolve(value);
resolve(value);
};
bounceFunction(async () => {
const callback = bounceResolver.current;
bounceResolver.current = () => null;
callback(await triggerRequest());
});
} else resolve(await triggerRequest());
};
performSubmit();
});
};
const refetch = () => {
cache.invalidate(request);
};
const handlers = {
onSubmitSuccess: callbacks.onSuccess,
onSubmitError: callbacks.onError,
onSubmitFinished: callbacks.onFinished,
onSubmitRequestStart: callbacks.onRequestStart,
onSubmitResponseStart: callbacks.onResponseStart,
onSubmitDownloadProgress: callbacks.onDownloadProgress,
onSubmitUploadProgress: callbacks.onUploadProgress,
onSubmitOfflineError: callbacks.onOfflineError,
onSubmitAbort: callbacks.onAbort
};
o(() => {
addCacheDataListener(request);
});
const setSubmitRenderKey = (key) => {
setRenderKey(key === "submitting" ? "loading" : key);
};
const trackedKeys = [
"data",
"error",
"submitting",
"status",
"success",
"extra",
"retries",
"responseTimestamp",
"requestTimestamp"
];
return createTrackedProxy({
submit: handleSubmit,
data: state.data,
error: state.error,
submitting: state.loading,
status: state.status,
success: state.success,
extra: state.extra,
retries: state.retries,
responseTimestamp: state.responseTimestamp,
requestTimestamp: state.requestTimestamp,
abort: callbacks.abort,
...actions,
...handlers,
bounce: getBounceData(bounceData),
refetch
}, trackedKeys, setSubmitRenderKey);
};
//#endregion
//#region src/hooks/use-submit/use-submit.constants.ts
var useSubmitDefaultOptions = {
disabled: false,
dependencyTracking: true,
initialResponse: null,
bounce: false,
bounceType: "debounce",
bounceTime: 400,
deepCompare: true
};
//#endregion
//#region src/hooks/use-queue/use-queue.hooks.ts
var canUpdate = (item) => {
if (item.success || item.failed) return false;
return true;
};
/**
* This hook allows you to monitor and control dispatcher request queues for a given request,
* including starting, stopping, pausing, and inspecting pending/in-flight items.
* @param request
* @param options
* @returns
*/
var useQueue = (request, options) => {
const { config: globalConfig } = useProvider();
const { dispatcherType, keepFinishedRequests } = {
...useQueueDefaultOptions,
...globalConfig.useQueueConfig,
...options
};
const { abortKey: rawAbortKey, queryKey: rawQueryKey, scope, client } = request;
const abortKey = scopeKey(rawAbortKey, scope);
const queryKey = scopeKey(rawQueryKey, scope);
const { requestManager } = client;
const [dispatcher] = getRequestDispatcher(request, dispatcherType);
const [stopped, setStopped] = useState(false);
const [requests, setRequests] = useState([]);
const createRequestsArray = useCallback((queueElements, prevRequests) => {
const newRequests = queueElements.filter((el) => !prevRequests?.some((prevEl) => prevEl.requestId === el.requestId)).map((req) => ({
failed: false,
canceled: false,
removed: false,
success: false,
...req,
downloading: {
progress: 0,
timeLeft: 0,
sizeLeft: 0,
total: 0,
loaded: 0,
startTimestamp: 0
},
uploading: {
progress: 0,
timeLeft: 0,
sizeLeft: 0,
total: 0,
loaded: 0,
startTimestamp: 0
},
stopRequest: () => dispatcher.stopRequest(queryKey, req.requestId),
startRequest: () => dispatcher.startRequest(queryKey, req.requestId),
deleteRequest: () => dispatcher.delete(queryKey, req.requestId, abortKey)
}));
if (keepFinishedRequests && prevRequests) return [...prevRequests, ...newRequests];
return newRequests;
}, [
abortKey,
dispatcher,
queryKey,
keepFinishedRequests
]);
const mergePayloadType = useCallback((requestId, data) => {
setRequests((prev) => prev.map((el) => {
if (el.requestId === requestId && canUpdate(el)) return {
...el,
...data
};
return el;
}));
}, []);
const getInitialState = () => {
const requestQueue = dispatcher.getQueue(queryKey);
setStopped(requestQueue.stopped);
setRequests(createRequestsArray(requestQueue.requests));
};
const resolveQueueItems = useCallback((items) => {
return items.map((item) => {
if (item.request instanceof Request) return item;
return {
...item,
request: client.fromJSON(item.request)
};
});
}, [client]);
const updateQueueState = useCallback((values) => {
setStopped(values.stopped);
setRequests((prev) => createRequestsArray(resolveQueueItems(values.requests), prev));
}, [createRequestsArray, resolveQueueItems]);
const mountEvents = () => {
const unmountChange = dispatcher.events.onQueueChangeByKey(queryKey, updateQueueState);
const unmountStatus = dispatcher.events.onQueueStatusChangeByKey(queryKey, updateQueueState);
const unmountFailed = requestManager.events.onResponse(({ requestId, response }) => {
if (!response.success) setRequests((prev) => prev.map((el) => el.requestId === requestId ? {
...el,
failed: true
} : el));
else setRequests((prev) => prev.map((el) => el.requestId === requestId ? {
...el,
success: true
} : el));
});
const unmountCanceled = requestManager.events.onAbort(({ requestId }) => {
setRequests((prev) => prev.map((el) => el.requestId === requestId ? {
...el,
canceled: true
} : el));
});
const unmountRemoved = requestManager.events.onRemove(({ requestId }) => {
setRequests((prev) => prev.map((el) => el.requestId === requestId && canUpdate(el) ? {
...el,
removed: true
} : el));
});
const unmountDownload = requestManager.events.onDownloadProgress(({ progress, timeLeft, sizeLeft, total, loaded, startTimestamp, requestId }) => {
mergePayloadType(requestId, { downloading: {
progress,
timeLeft,
sizeLeft,
total,
loaded,
startTimestamp
} });
});
const unmountUpload = requestManager.events.onUploadProgress(({ progress, timeLeft, sizeLeft, total, loaded, startTimestamp, requestId }) => {
mergePayloadType(requestId, { uploading: {
progress,
timeLeft,
sizeLeft,
total,
loaded,
startTimestamp
} });
});
const unmount = () => {
unmountStatus();
unmountChange();
unmountDownload();
unmountUpload();
unmountFailed();
unmountCanceled();
unmountRemoved();
};
return unmount;
};
useEffect(getInitialState, [
createRequestsArray,
dispatcher,
queryKey
]);
useEffect(mountEvents, [
stopped,
requests,
setRequests,
setStopped,
queryKey,
dispatcher.events,
requestManager.events,
updateQueueState,
mergePayloadType
]);
return {
stopped,
requests,
dispatcher,
stop: () => dispatcher.stop(queryKey),
pause: () => dispatcher.pause(queryKey),
start: () => dispatcher.start(query