@kawaiininja/fetch 1.0.23 → 1.0.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/context/ApiContext.js +15 -28
- package/dist/hooks/useFetch.js +62 -49
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { createContext, useCallback, useMemo } from "react";
|
|
2
|
+
import { createContext, useCallback, useEffect, useMemo, useRef } from "react";
|
|
3
3
|
/**
|
|
4
4
|
* Internal service header for API requests
|
|
5
5
|
*/
|
|
@@ -14,39 +14,26 @@ export const ApiContext = createContext(null);
|
|
|
14
14
|
* Constructs full API URLs from relative paths using the baseUrl.
|
|
15
15
|
*/
|
|
16
16
|
export function ApiProvider({ config, children }) {
|
|
17
|
-
//
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
debug: config?.debug,
|
|
24
|
-
// We can't stringify functions, so we use their existence/identity
|
|
25
|
-
hasOnError: !!config?.onError,
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
return Math.random();
|
|
30
|
-
}
|
|
31
|
-
}, [config?.baseUrl, config?.version, config?.debug, config?.onError]);
|
|
32
|
-
// Extract stable values based on the hash
|
|
33
|
-
const { baseUrl, version, debug, onError } = useMemo(() => ({
|
|
34
|
-
baseUrl: config?.baseUrl || "",
|
|
35
|
-
version: config?.version || "v1",
|
|
36
|
-
debug: !!config?.debug,
|
|
37
|
-
onError: config?.onError,
|
|
38
|
-
}), [configHash]);
|
|
39
|
-
/**
|
|
40
|
-
* 🛡️ HYPER-STABLE URL CONSTRUCTOR
|
|
41
|
-
* Memoized independently of the context value to prevent downstream loop invalidation.
|
|
42
|
-
*/
|
|
17
|
+
// 1. STABILITY: Use a Ref to hold the baseUrl to avoid apiUrl re-creations
|
|
18
|
+
const baseUrlRef = useRef(config?.baseUrl || "");
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
baseUrlRef.current = config?.baseUrl || "";
|
|
21
|
+
}, [config?.baseUrl]);
|
|
22
|
+
// 2. STABLE URL CONSTRUCTOR: This function now NEVER changes identity
|
|
43
23
|
const apiUrl = useCallback((path) => {
|
|
24
|
+
const baseUrl = baseUrlRef.current;
|
|
44
25
|
if (path.startsWith("http"))
|
|
45
26
|
return path;
|
|
46
27
|
const cleanBase = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
47
28
|
const cleanPath = path.startsWith("/") ? path : `/${path}`;
|
|
48
29
|
return `${cleanBase}${cleanPath}`;
|
|
49
|
-
}, [
|
|
30
|
+
}, []);
|
|
31
|
+
// 3. INTERNAL STABILITY: Memoize primitive values strictly
|
|
32
|
+
const baseUrl = config?.baseUrl || "";
|
|
33
|
+
const version = config?.version || "v1";
|
|
34
|
+
const debug = !!config?.debug;
|
|
35
|
+
const onError = config?.onError;
|
|
36
|
+
// 4. VALUE MEMOIZATION: Only re-create context if primitives change
|
|
50
37
|
const value = useMemo(() => ({
|
|
51
38
|
baseUrl,
|
|
52
39
|
version,
|
package/dist/hooks/useFetch.js
CHANGED
|
@@ -43,8 +43,12 @@ export const useFetch = (endpoint, baseOptions = {}) => {
|
|
|
43
43
|
const safeSet = useCallback((fn) => {
|
|
44
44
|
if (!mounted.current)
|
|
45
45
|
return;
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
// If we are already in an update cycle, queue the next one
|
|
47
|
+
// instead of running it synchronously.
|
|
48
|
+
if (isUpdatingRef.current) {
|
|
49
|
+
setTimeout(() => safeSet(fn), 0);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
48
52
|
isUpdatingRef.current = true;
|
|
49
53
|
try {
|
|
50
54
|
setState((prev) => {
|
|
@@ -58,65 +62,74 @@ export const useFetch = (endpoint, baseOptions = {}) => {
|
|
|
58
62
|
});
|
|
59
63
|
}
|
|
60
64
|
finally {
|
|
61
|
-
//
|
|
65
|
+
// Ensure the lock is released in the next microtask
|
|
66
|
+
// This clears the stack and prevents the "Background" pile-up
|
|
62
67
|
Promise.resolve().then(() => {
|
|
63
68
|
isUpdatingRef.current = false;
|
|
64
69
|
});
|
|
65
70
|
}
|
|
66
71
|
}, [endpoint, debug]);
|
|
72
|
+
// 🛡️ INTERNAL STABILITY: Decouple from context identity
|
|
73
|
+
const apiUrlRef = useRef(apiUrl);
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
apiUrlRef.current = apiUrl;
|
|
76
|
+
}, [apiUrl]);
|
|
67
77
|
const request = useCallback(async (params = {}) => {
|
|
68
|
-
|
|
69
|
-
if (isRequestingRef.current) {
|
|
70
|
-
if (debug)
|
|
71
|
-
console.warn("[useFetch] 🔥 RECURSION GUARD: Blocking concurrent call.");
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
const { url = endpoint, method = "GET", body, headers, parseAs = "auto", ...rest } = params;
|
|
75
|
-
const finalUrl = url.startsWith("http") ? url : apiUrl(url);
|
|
76
|
-
if (!finalUrl)
|
|
78
|
+
if (isRequestingRef.current)
|
|
77
79
|
return;
|
|
78
80
|
isRequestingRef.current = true;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
81
|
+
// 🛡️ STACK BREAK: Use a microtask to start the request
|
|
82
|
+
// This ensures the calling function can finish and "End" before fetch starts
|
|
83
|
+
return await Promise.resolve().then(async () => {
|
|
84
|
+
const { url = endpoint, method = "GET", body, headers, parseAs = "auto", ...rest } = params;
|
|
85
|
+
// Use Ref to prevent re-creation of request when Context changes
|
|
86
|
+
const finalUrl = url.startsWith("http") ? url : apiUrlRef.current(url);
|
|
87
|
+
if (!finalUrl) {
|
|
88
|
+
isRequestingRef.current = false;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
safeSet(() => ({ loading: true, error: null }));
|
|
92
|
+
let lastStatus = null;
|
|
93
|
+
try {
|
|
94
|
+
let token = await fetchCSRF();
|
|
95
|
+
// Merge baseOptions from Ref to maintain identity stability
|
|
96
|
+
let res = await performFetch(finalUrl, method, token, body, headers, { ...optionsRef.current, ...rest }, debug, abortRef.current.signal, apiUrlRef.current);
|
|
97
|
+
lastStatus = res.status;
|
|
98
|
+
// Auto-retry on 403 CSRF Error
|
|
99
|
+
if (res.status === 403) {
|
|
100
|
+
const bodyClone = await res
|
|
101
|
+
.clone()
|
|
102
|
+
.json()
|
|
103
|
+
.catch(() => ({}));
|
|
104
|
+
if (bodyClone?.code === "CSRF_ERROR" || bodyClone?.message?.includes("CSRF")) {
|
|
105
|
+
await clearCsrf();
|
|
106
|
+
token = await fetchCSRF();
|
|
107
|
+
res = await performFetch(finalUrl, method, token, body, headers, { ...optionsRef.current, ...rest }, debug, abortRef.current.signal, apiUrlRef.current);
|
|
108
|
+
lastStatus = res.status;
|
|
109
|
+
}
|
|
97
110
|
}
|
|
111
|
+
const parsed = await parseResponse(res, parseAs);
|
|
112
|
+
if (!res.ok)
|
|
113
|
+
throw new Error(parsed?.message || res.statusText);
|
|
114
|
+
safeSet(() => ({ data: parsed, status: res.status }));
|
|
115
|
+
return parsed;
|
|
98
116
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (err.name !== "AbortError") {
|
|
107
|
-
safeSet(() => ({ error: err.message, status: lastStatus }));
|
|
108
|
-
if (onError)
|
|
109
|
-
onError(err.message, lastStatus);
|
|
110
|
-
throw err;
|
|
117
|
+
catch (err) {
|
|
118
|
+
if (err.name !== "AbortError") {
|
|
119
|
+
safeSet(() => ({ error: err.message, status: lastStatus }));
|
|
120
|
+
if (onError)
|
|
121
|
+
onError(err.message, lastStatus);
|
|
122
|
+
throw err;
|
|
123
|
+
}
|
|
111
124
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
125
|
+
finally {
|
|
126
|
+
safeSet(() => ({ loading: false }));
|
|
127
|
+
isRequestingRef.current = false;
|
|
128
|
+
}
|
|
129
|
+
});
|
|
117
130
|
},
|
|
118
|
-
//
|
|
119
|
-
[endpoint, fetchCSRF, clearCsrf,
|
|
131
|
+
// apiUrl REMOVED specific dependency to stop context-based invalidations
|
|
132
|
+
[endpoint, fetchCSRF, clearCsrf, safeSet, onError, debug]);
|
|
120
133
|
useEffect(() => {
|
|
121
134
|
mounted.current = true;
|
|
122
135
|
return () => {
|