@kawaiininja/fetch 1.0.13 → 1.0.14

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.
@@ -59,31 +59,52 @@ export function useCsrf() {
59
59
  // Construct CSRF URL using the context's baseUrl
60
60
  console.log("[useCsrf] Fetching new CSRF token from server...");
61
61
  const csrfUrl = apiUrl("auth/csrf-token");
62
- const res = await fetch(csrfUrl, {
63
- method: "GET",
64
- headers: INTERNAL_HEADER,
65
- credentials: "include",
66
- });
67
- const json = await res.json();
68
- const token = json?.csrfToken;
69
- if (!token)
70
- throw new Error("Missing CSRF token");
71
- csrfRef.current = token;
72
- // Store in storage
73
- if (typeof window !== "undefined") {
74
- const _isNative = isNative();
75
- if (_isNative) {
76
- try {
77
- await SecureStoragePlugin.set({ key: "csrf_token", value: token });
78
- console.log("[useCsrf] Stored in SecureStorage.");
79
- }
80
- catch (e) {
81
- console.warn("[useCsrf] SecureStorage set failed, falling back to localStorage:", e);
62
+ // ⏱️ TIMEOUT HANDLING for CSRF
63
+ // If the server is unreachable (e.g. paused dev tunnel), this MUST fail fast
64
+ const CSRF_TIMEOUT_MS = 15000;
65
+ const controller = new AbortController();
66
+ const timeoutId = setTimeout(() => controller.abort(), CSRF_TIMEOUT_MS);
67
+ try {
68
+ const res = await fetch(csrfUrl, {
69
+ method: "GET",
70
+ headers: INTERNAL_HEADER,
71
+ credentials: "include",
72
+ signal: controller.signal,
73
+ });
74
+ clearTimeout(timeoutId);
75
+ if (!res.ok) {
76
+ throw new Error(`CSRF Fetch failed with status: ${res.status}`);
77
+ }
78
+ const json = await res.json();
79
+ const token = json?.csrfToken;
80
+ if (!token)
81
+ throw new Error("Missing CSRF token in response");
82
+ csrfRef.current = token;
83
+ // Store in storage
84
+ if (typeof window !== "undefined") {
85
+ const _isNative = isNative();
86
+ if (_isNative) {
87
+ try {
88
+ await SecureStoragePlugin.set({ key: "csrf_token", value: token });
89
+ console.log("[useCsrf] Stored in SecureStorage.");
90
+ }
91
+ catch (e) {
92
+ console.warn("[useCsrf] SecureStorage set failed, falling back to localStorage:", e);
93
+ }
82
94
  }
95
+ localStorage.setItem("csrf_token", token);
96
+ }
97
+ return token;
98
+ }
99
+ catch (error) {
100
+ clearTimeout(timeoutId);
101
+ console.error("[useCsrf] CSRF Fetch Failed/Timed-out:", error);
102
+ let failReason = error.message;
103
+ if (error.name === "AbortError") {
104
+ failReason = `Connection Timed Out (${CSRF_TIMEOUT_MS}ms). Server unreachable.`;
83
105
  }
84
- localStorage.setItem("csrf_token", token);
106
+ throw new Error(`CSRF Handshake failed: ${failReason}`);
85
107
  }
86
- return token;
87
108
  }, [apiUrl]);
88
109
  return useMemo(() => ({ fetchCSRF, clearCsrf }), [fetchCSRF, clearCsrf]);
89
110
  }
@@ -73,14 +73,12 @@ export const useFetch = (endpoint, baseOptions = {}) => {
73
73
  };
74
74
  // 🔒 SECURITY STRATEGY: NATIVE (MOBILE)
75
75
  if (_isNative && isInternal) {
76
- // Mobile relies on manual headers ("Active Courier")
77
- // 🛡️ S-RANK UPGRADE: Use Session Memory Vault (In-Memory Singleton)
78
- // console.log(`[useFetch] [Native] Strategy: Active Courier for ${url}`);
79
76
  if (!nativeAuthVault.loaded) {
80
- // If an initialization is already in progress, await it
77
+ console.log("[useFetch] [Native] Vault not loaded, initializing...");
81
78
  if (!nativeAuthVault.initPromise) {
82
79
  nativeAuthVault.initPromise = (async () => {
83
80
  try {
81
+ console.log("[useFetch] [Native] Reading SecureStorage...");
84
82
  const { value: t } = await SecureStoragePlugin.get({
85
83
  key: "token",
86
84
  });
@@ -89,8 +87,8 @@ export const useFetch = (endpoint, baseOptions = {}) => {
89
87
  });
90
88
  nativeAuthVault.token = t || undefined;
91
89
  nativeAuthVault.sessionId = s || undefined;
92
- // Fallback to localStorage if SecureStorage is empty
93
90
  if (!nativeAuthVault.token) {
91
+ console.log("[useFetch] [Native] SecureStorage empty, trying localStorage fallback");
94
92
  nativeAuthVault.token =
95
93
  localStorage.getItem("token") || undefined;
96
94
  nativeAuthVault.sessionId =
@@ -113,6 +111,7 @@ export const useFetch = (endpoint, baseOptions = {}) => {
113
111
  })();
114
112
  }
115
113
  await nativeAuthVault.initPromise;
114
+ console.log("[useFetch] [Native] Vault ready.");
116
115
  }
117
116
  const authToken = nativeAuthVault.token;
118
117
  const sessionId = nativeAuthVault.sessionId;
@@ -128,15 +127,52 @@ export const useFetch = (endpoint, baseOptions = {}) => {
128
127
  headersConfig["X-CSRF-Token"] = token;
129
128
  }
130
129
  }
131
- return fetch(url, {
132
- ...optionsRef.current,
133
- ...rest,
134
- method,
135
- signal: abortRef.current.signal,
136
- credentials: "include",
137
- headers: headersConfig,
138
- body,
139
- });
130
+ // 🛡️ RETRY LOGIC (3 Attempts)
131
+ // We try 3 times. If all 3 fail, we throw.
132
+ let attempt = 0;
133
+ const maxAttempts = 3;
134
+ let lastError = null;
135
+ while (attempt < maxAttempts) {
136
+ attempt++;
137
+ const TIMEOUT_MS = 15000;
138
+ const controller = new AbortController();
139
+ const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS);
140
+ // Link to component lifecycle
141
+ const onUnmount = () => controller.abort();
142
+ abortRef.current.signal.addEventListener("abort", onUnmount);
143
+ try {
144
+ const response = await Promise.race([
145
+ fetch(url, {
146
+ ...optionsRef.current,
147
+ ...rest,
148
+ method,
149
+ signal: controller.signal,
150
+ credentials: "include",
151
+ headers: headersConfig,
152
+ body,
153
+ }),
154
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`Timeout of ${TIMEOUT_MS}ms exceeded`)), TIMEOUT_MS)),
155
+ ]);
156
+ clearTimeout(timeoutId);
157
+ abortRef.current.signal.removeEventListener("abort", onUnmount);
158
+ return response; // Success!
159
+ }
160
+ catch (error) {
161
+ clearTimeout(timeoutId);
162
+ abortRef.current.signal.removeEventListener("abort", onUnmount);
163
+ lastError = error;
164
+ // If aborted by user/unmount, DO NOT RETRY
165
+ if (error.name === "AbortError" || abortRef.current.signal.aborted) {
166
+ throw error;
167
+ }
168
+ console.warn(`[useFetch] Attempt ${attempt} failed: ${error.message}. Retrying...`);
169
+ // Wait 1s before retry
170
+ if (attempt < maxAttempts) {
171
+ await new Promise((r) => setTimeout(r, 1000));
172
+ }
173
+ }
174
+ }
175
+ throw lastError || new Error("Request failed after 3 attempts");
140
176
  };
141
177
  const parseResponse = async (res, parseAs) => {
142
178
  const type = res.headers.get("content-type") || "";
@@ -156,8 +192,12 @@ export const useFetch = (endpoint, baseOptions = {}) => {
156
192
  const request = useCallback(async (params = {}) => {
157
193
  const { url = endpoint, method = "GET", body, headers, parseAs = "auto", ...rest } = params;
158
194
  let finalUrl = url.startsWith("http") ? url : apiUrl(url);
159
- if (!finalUrl)
195
+ console.log(`[useFetch] Request initiated: ${method} ${finalUrl}`);
196
+ console.log(`[useFetch] Config: disableCsrf=${disableCsrf}, isNative=${isNative()}`);
197
+ if (!finalUrl) {
198
+ console.error("[useFetch] Aborted: Empty URL");
160
199
  return;
200
+ }
161
201
  safeSet(() => ({ loading: true, error: null }));
162
202
  let lastStatus = null;
163
203
  try {
@@ -192,11 +232,16 @@ export const useFetch = (endpoint, baseOptions = {}) => {
192
232
  }
193
233
  catch (err) {
194
234
  if (err.name !== "AbortError") {
235
+ console.error("[useFetch] Request Failed:", err);
195
236
  safeSet(() => ({ error: err.message, status: lastStatus }));
196
237
  if (onError)
197
238
  onError(err.message, lastStatus);
198
239
  throw err;
199
240
  }
241
+ else {
242
+ // Silently ignore component unmount aborts
243
+ console.log("[useFetch] Request aborted (component unmounted).");
244
+ }
200
245
  }
201
246
  finally {
202
247
  safeSet(() => ({ loading: false }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kawaiininja/fetch",
3
- "version": "1.0.13",
3
+ "version": "1.0.14",
4
4
  "description": "Core fetch utility for Onyx Framework",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -14,7 +14,8 @@
14
14
  "README.md"
15
15
  ],
16
16
  "scripts": {
17
- "build": "tsc"
17
+ "build": "tsc",
18
+ "rebuild": "npm run build"
18
19
  },
19
20
  "keywords": [
20
21
  "react",