UNPKG

@hyper-fetch/react

Version:

React hooks and utils for the hyper-fetch

1,574 lines 60.5 kB
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