@kawaiininja/fetch 1.0.24 → 1.0.26
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.d.ts +1 -12
- package/dist/context/ApiContext.js +20 -35
- package/dist/hooks/useFetch.d.ts +1 -1
- package/dist/hooks/useFetch.js +31 -80
- package/package.json +1 -1
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
/**
|
|
3
|
-
* Internal service header for API requests
|
|
4
|
-
*/
|
|
5
2
|
export declare const INTERNAL_HEADER: {
|
|
6
3
|
readonly "x-internal-service": "zevrinix-main";
|
|
7
4
|
};
|
|
@@ -11,9 +8,6 @@ export interface ApiConfig {
|
|
|
11
8
|
onError?: (error: string, status: number | null) => void;
|
|
12
9
|
debug?: boolean;
|
|
13
10
|
}
|
|
14
|
-
/**
|
|
15
|
-
* API context value type
|
|
16
|
-
*/
|
|
17
11
|
export interface ApiContextValue {
|
|
18
12
|
baseUrl: string;
|
|
19
13
|
version: string;
|
|
@@ -21,9 +15,6 @@ export interface ApiContextValue {
|
|
|
21
15
|
onError?: (error: string, status: number | null) => void;
|
|
22
16
|
debug: boolean;
|
|
23
17
|
}
|
|
24
|
-
/**
|
|
25
|
-
* Props for ApiProvider component
|
|
26
|
-
*/
|
|
27
18
|
export interface ApiProviderProps {
|
|
28
19
|
config: ApiConfig;
|
|
29
20
|
children: React.ReactNode;
|
|
@@ -31,8 +22,6 @@ export interface ApiProviderProps {
|
|
|
31
22
|
export declare const ApiContext: React.Context<ApiContextValue | null>;
|
|
32
23
|
/**
|
|
33
24
|
* ApiProvider
|
|
34
|
-
*
|
|
35
|
-
* Provides API configuration to the component tree.
|
|
36
|
-
* Constructs full API URLs from relative paths using the baseUrl.
|
|
25
|
+
* 🛡️ GOD-LEVEL STABILITY: Identity is decoupled from config reference
|
|
37
26
|
*/
|
|
38
27
|
export declare function ApiProvider({ config, children }: ApiProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,58 +1,43 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { createContext, useCallback, useMemo } from "react";
|
|
3
|
-
/**
|
|
4
|
-
* Internal service header for API requests
|
|
5
|
-
*/
|
|
2
|
+
import { createContext, useCallback, useEffect, useMemo, useRef } from "react";
|
|
6
3
|
export const INTERNAL_HEADER = {
|
|
7
4
|
"x-internal-service": "zevrinix-main",
|
|
8
5
|
};
|
|
9
6
|
export const ApiContext = createContext(null);
|
|
10
7
|
/**
|
|
11
8
|
* ApiProvider
|
|
12
|
-
*
|
|
13
|
-
* Provides API configuration to the component tree.
|
|
14
|
-
* Constructs full API URLs from relative paths using the baseUrl.
|
|
9
|
+
* 🛡️ GOD-LEVEL STABILITY: Identity is decoupled from config reference
|
|
15
10
|
*/
|
|
16
11
|
export function ApiProvider({ config, children }) {
|
|
17
|
-
//
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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]);
|
|
12
|
+
// Use refs for values that change but shouldn't break the identity of helpers
|
|
13
|
+
const baseUrlRef = useRef(config?.baseUrl || "");
|
|
14
|
+
const onErrorRef = useRef(config?.onError);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
baseUrlRef.current = config?.baseUrl || "";
|
|
17
|
+
onErrorRef.current = config?.onError;
|
|
18
|
+
}, [config?.baseUrl, config?.onError]);
|
|
39
19
|
/**
|
|
40
|
-
* 🛡️
|
|
41
|
-
*
|
|
20
|
+
* 🛡️ PERMANENT IDENTITY URL CONSTRUCTOR
|
|
21
|
+
* This function's reference NEVER changes, stopping the downstream loop chain.
|
|
42
22
|
*/
|
|
43
23
|
const apiUrl = useCallback((path) => {
|
|
24
|
+
const base = baseUrlRef.current;
|
|
44
25
|
if (path.startsWith("http"))
|
|
45
26
|
return path;
|
|
46
|
-
const cleanBase =
|
|
27
|
+
const cleanBase = base.endsWith("/") ? base.slice(0, -1) : base;
|
|
47
28
|
const cleanPath = path.startsWith("/") ? path : `/${path}`;
|
|
48
29
|
return `${cleanBase}${cleanPath}`;
|
|
49
|
-
}, [
|
|
30
|
+
}, []);
|
|
31
|
+
// Memoize primitive values only
|
|
32
|
+
const baseUrl = config?.baseUrl || "";
|
|
33
|
+
const version = config?.version || "v1";
|
|
34
|
+
const debug = !!config?.debug;
|
|
50
35
|
const value = useMemo(() => ({
|
|
51
36
|
baseUrl,
|
|
52
37
|
version,
|
|
53
38
|
debug,
|
|
54
|
-
onError,
|
|
55
39
|
apiUrl,
|
|
56
|
-
|
|
40
|
+
onError: (err, status) => onErrorRef.current?.(err, status),
|
|
41
|
+
}), [baseUrl, version, debug, apiUrl]);
|
|
57
42
|
return _jsx(ApiContext.Provider, { value: value, children: children });
|
|
58
43
|
}
|
package/dist/hooks/useFetch.d.ts
CHANGED
|
@@ -2,6 +2,6 @@ import { ApiSurface } from "./utils";
|
|
|
2
2
|
export { clearNativeAuthVault } from "./useFetch.vault";
|
|
3
3
|
/**
|
|
4
4
|
* useFetch Hook
|
|
5
|
-
* 🛡️
|
|
5
|
+
* 🛡️ STACK PROTECTION: Logic flattening + Identity Locking
|
|
6
6
|
*/
|
|
7
7
|
export declare const useFetch: <T = any>(endpoint: string, baseOptions?: any) => ApiSurface<T>;
|
package/dist/hooks/useFetch.js
CHANGED
|
@@ -6,113 +6,64 @@ import { createApiMethods } from "./utils";
|
|
|
6
6
|
export { clearNativeAuthVault } from "./useFetch.vault";
|
|
7
7
|
/**
|
|
8
8
|
* useFetch Hook
|
|
9
|
-
* 🛡️
|
|
9
|
+
* 🛡️ STACK PROTECTION: Logic flattening + Identity Locking
|
|
10
10
|
*/
|
|
11
11
|
export const useFetch = (endpoint, baseOptions = {}) => {
|
|
12
12
|
const { apiUrl, onError, debug } = useApiConfig();
|
|
13
|
-
const { fetchCSRF
|
|
14
|
-
const runsRef = useRef(0);
|
|
13
|
+
const { fetchCSRF } = useCsrf();
|
|
15
14
|
const abortRef = useRef(new AbortController());
|
|
16
15
|
const mounted = useRef(true);
|
|
17
16
|
const isRequestingRef = useRef(false);
|
|
18
|
-
// 🛡️
|
|
17
|
+
// 🛡️ IDENTITY LOCK: Decouple options from render cycle
|
|
19
18
|
const optionsRef = useRef(baseOptions);
|
|
20
|
-
const optionsHash = useMemo(() => {
|
|
21
|
-
try {
|
|
22
|
-
return JSON.stringify(baseOptions);
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
25
|
-
return Math.random();
|
|
26
|
-
}
|
|
27
|
-
}, [baseOptions]);
|
|
28
19
|
useEffect(() => {
|
|
29
|
-
if (debug)
|
|
30
|
-
console.log(`[useFetch] [${endpoint}] ⚙️ baseOptions updated`);
|
|
31
20
|
optionsRef.current = baseOptions;
|
|
32
|
-
}, [
|
|
33
|
-
if (debug)
|
|
34
|
-
console.log(`[useFetch] [${endpoint}] 🔄 Hook Render`);
|
|
21
|
+
}, [JSON.stringify(baseOptions)]); // Only update ref if content actually changes
|
|
35
22
|
const [state, setState] = useState({
|
|
36
23
|
data: null,
|
|
37
24
|
loading: false,
|
|
38
25
|
error: null,
|
|
39
26
|
status: null,
|
|
40
27
|
});
|
|
41
|
-
|
|
42
|
-
|
|
28
|
+
/**
|
|
29
|
+
* 🛡️ STACK FLATTENER
|
|
30
|
+
* Pushes state updates to the microtask queue.
|
|
31
|
+
* This forces the current function to "Return" and clear the stack before next render.
|
|
32
|
+
*/
|
|
43
33
|
const safeSet = useCallback((fn) => {
|
|
44
34
|
if (!mounted.current)
|
|
45
35
|
return;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
setState((prev) => {
|
|
51
|
-
const updates = fn(prev);
|
|
52
|
-
if (debug) {
|
|
53
|
-
console.groupCollapsed(`[useFetch] [${endpoint}] 🔄 State Update`);
|
|
54
|
-
console.log("Updates:", updates);
|
|
55
|
-
console.groupEnd();
|
|
56
|
-
}
|
|
57
|
-
return { ...prev, ...updates };
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
finally {
|
|
61
|
-
// Use a microtask to release the lock after the current execution frame
|
|
62
|
-
Promise.resolve().then(() => {
|
|
63
|
-
isUpdatingRef.current = false;
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
}, [endpoint, debug]);
|
|
67
|
-
// 🛡️ INTERNAL STABILITY: Decouple from context identity
|
|
68
|
-
const apiUrlRef = useRef(apiUrl);
|
|
69
|
-
useEffect(() => {
|
|
70
|
-
apiUrlRef.current = apiUrl;
|
|
71
|
-
}, [apiUrl]);
|
|
36
|
+
queueMicrotask(() => {
|
|
37
|
+
setState((prev) => ({ ...prev, ...fn(prev) }));
|
|
38
|
+
});
|
|
39
|
+
}, []);
|
|
72
40
|
const request = useCallback(async (params = {}) => {
|
|
73
|
-
|
|
74
|
-
if (isRequestingRef.current) {
|
|
75
|
-
if (debug)
|
|
76
|
-
console.warn("[useFetch] 🔥 RECURSION GUARD: Blocking concurrent call.");
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
const { url = endpoint, method = "GET", body, headers, parseAs = "auto", ...rest } = params;
|
|
80
|
-
// Use Ref to prevent re-creation of request when Context changes
|
|
81
|
-
const finalUrl = url.startsWith("http") ? url : apiUrlRef.current(url);
|
|
82
|
-
if (!finalUrl)
|
|
41
|
+
if (isRequestingRef.current)
|
|
83
42
|
return;
|
|
84
43
|
isRequestingRef.current = true;
|
|
44
|
+
if (debug)
|
|
45
|
+
console.log(`[useFetch] [${endpoint}] 🚀 Starting Request`);
|
|
85
46
|
safeSet(() => ({ loading: true, error: null }));
|
|
86
47
|
let lastStatus = null;
|
|
87
48
|
try {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
49
|
+
const token = await fetchCSRF();
|
|
50
|
+
const finalUrl = params.url || (endpoint.startsWith("http") ? endpoint : apiUrl(endpoint));
|
|
51
|
+
const res = await performFetch(finalUrl, params.method || "GET", token, params.body, params.headers, { ...optionsRef.current, ...params }, debug, abortRef.current.signal, apiUrl);
|
|
91
52
|
lastStatus = res.status;
|
|
92
|
-
|
|
93
|
-
if (res.status === 403) {
|
|
94
|
-
const bodyClone = await res
|
|
95
|
-
.clone()
|
|
96
|
-
.json()
|
|
97
|
-
.catch(() => ({}));
|
|
98
|
-
if (bodyClone?.code === "CSRF_ERROR" || bodyClone?.message?.includes("CSRF")) {
|
|
99
|
-
await clearCsrf();
|
|
100
|
-
token = await fetchCSRF();
|
|
101
|
-
res = await performFetch(finalUrl, method, token, body, headers, { ...optionsRef.current, ...rest }, debug, abortRef.current.signal, apiUrlRef.current);
|
|
102
|
-
lastStatus = res.status;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
const parsed = await parseResponse(res, parseAs);
|
|
53
|
+
const parsed = await parseResponse(res, params.parseAs || "auto");
|
|
106
54
|
if (!res.ok)
|
|
107
55
|
throw new Error(parsed?.message || res.statusText);
|
|
56
|
+
if (debug)
|
|
57
|
+
console.log(`[useFetch] [${endpoint}] ✅ Success`);
|
|
108
58
|
safeSet(() => ({ data: parsed, status: res.status }));
|
|
109
59
|
return parsed;
|
|
110
60
|
}
|
|
111
61
|
catch (err) {
|
|
112
62
|
if (err.name !== "AbortError") {
|
|
113
|
-
|
|
63
|
+
const msg = err.message || "Network error";
|
|
64
|
+
safeSet(() => ({ error: msg, status: lastStatus }));
|
|
114
65
|
if (onError)
|
|
115
|
-
onError(
|
|
66
|
+
onError(msg, lastStatus);
|
|
116
67
|
throw err;
|
|
117
68
|
}
|
|
118
69
|
}
|
|
@@ -120,9 +71,7 @@ export const useFetch = (endpoint, baseOptions = {}) => {
|
|
|
120
71
|
safeSet(() => ({ loading: false }));
|
|
121
72
|
isRequestingRef.current = false;
|
|
122
73
|
}
|
|
123
|
-
},
|
|
124
|
-
// apiUrl REMOVED specific dependency to stop context-based invalidations
|
|
125
|
-
[endpoint, fetchCSRF, clearCsrf, safeSet, onError, debug]);
|
|
74
|
+
}, [endpoint, fetchCSRF, apiUrl, onError, debug, safeSet]);
|
|
126
75
|
useEffect(() => {
|
|
127
76
|
mounted.current = true;
|
|
128
77
|
return () => {
|
|
@@ -130,9 +79,11 @@ export const useFetch = (endpoint, baseOptions = {}) => {
|
|
|
130
79
|
abortRef.current?.abort();
|
|
131
80
|
};
|
|
132
81
|
}, []);
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
82
|
+
const methods = useMemo(() => createApiMethods({
|
|
83
|
+
request,
|
|
84
|
+
baseUrl: endpoint.startsWith("http") ? endpoint : apiUrl(endpoint),
|
|
85
|
+
safeSet,
|
|
86
|
+
}), [request, endpoint, apiUrl, safeSet]);
|
|
136
87
|
return useMemo(() => ({
|
|
137
88
|
state,
|
|
138
89
|
isLoading: state.loading,
|