@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.
@@ -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
- // 🛡️ STABILITY: Hash the config to detect REAL changes vs identity shifts
18
- const configHash = useMemo(() => {
19
- try {
20
- return JSON.stringify({
21
- baseUrl: config?.baseUrl,
22
- version: config?.version,
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]);
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
- * 🛡️ HYPER-STABLE URL CONSTRUCTOR
41
- * Memoized independently of the context value to prevent downstream loop invalidation.
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 = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
27
+ const cleanBase = base.endsWith("/") ? base.slice(0, -1) : base;
47
28
  const cleanPath = path.startsWith("/") ? path : `/${path}`;
48
29
  return `${cleanBase}${cleanPath}`;
49
- }, [baseUrl]);
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
- }), [baseUrl, version, debug, onError, apiUrl]);
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
  }
@@ -2,6 +2,6 @@ import { ApiSurface } from "./utils";
2
2
  export { clearNativeAuthVault } from "./useFetch.vault";
3
3
  /**
4
4
  * useFetch Hook
5
- * 🛡️ STABILITY UPGRADE: Reference Stability + Internal Recursion Guard
5
+ * 🛡️ STACK PROTECTION: Logic flattening + Identity Locking
6
6
  */
7
7
  export declare const useFetch: <T = any>(endpoint: string, baseOptions?: any) => ApiSurface<T>;
@@ -6,113 +6,64 @@ import { createApiMethods } from "./utils";
6
6
  export { clearNativeAuthVault } from "./useFetch.vault";
7
7
  /**
8
8
  * useFetch Hook
9
- * 🛡️ STABILITY UPGRADE: Reference Stability + Internal Recursion Guard
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, clearCsrf } = useCsrf();
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
- // 🛡️ STABILITY: Options hashing to prevent unnecessary effect triggers from object literals
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
- }, [optionsHash, endpoint, debug]);
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
- // 🛡️ RECURSION GUARD: Prevent synchronous state-update cascades
42
- const isUpdatingRef = useRef(false);
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
- if (isUpdatingRef.current)
47
- return; // Block synchronous recursion
48
- isUpdatingRef.current = true;
49
- try {
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
- // 🛡️ RECURSION GUARD: Instant rejection of concurrent loops
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
- let token = await fetchCSRF();
89
- // Merge baseOptions from Ref to maintain identity stability
90
- let res = await performFetch(finalUrl, method, token, body, headers, { ...optionsRef.current, ...rest }, debug, abortRef.current.signal, apiUrlRef.current);
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
- // Auto-retry on 403 CSRF Error
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
- safeSet(() => ({ error: err.message, status: lastStatus }));
63
+ const msg = err.message || "Network error";
64
+ safeSet(() => ({ error: msg, status: lastStatus }));
114
65
  if (onError)
115
- onError(err.message, lastStatus);
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 resolvedUrl = useMemo(() => (endpoint.startsWith("http") ? endpoint : apiUrl(endpoint)), [endpoint, apiUrl]);
134
- // methods is now stable across renders because request is stable
135
- const methods = useMemo(() => createApiMethods({ request, baseUrl: resolvedUrl, safeSet }), [request, resolvedUrl, safeSet]);
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kawaiininja/fetch",
3
- "version": "1.0.24",
3
+ "version": "1.0.26",
4
4
  "description": "Core fetch utility for Onyx Framework",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",