@kawaiininja/fetch 1.0.39 → 1.0.41

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/README.md CHANGED
@@ -4,17 +4,20 @@ A production-grade, hybrid-native HTTP client designed for the Onyx Framework.
4
4
 
5
5
  **Rating: S-Tier Utility 🏆**
6
6
 
7
- This package automates enterprise-grade security, CSRF handling, and platform detection (Web vs. Native Mobile), allowing you to focus on building features rather than handling HTTP boilerplate.
7
+ This package automates enterprise-grade security, CSRF handling, and platform detection (Web vs. Native Mobile), allowing you to focus on building features rather than handling HTTP boilerplate. It is optimized for high-performance React applications with built-in caching and retry logic.
8
8
 
9
9
  ## 🚀 Features
10
10
 
11
11
  - **Hybrid Intelligence**: Automatically detects if running on Web or Native Mobile (Capacitor).
12
+ - **🛡️ Session Memory Vault**: High-performance token caching for native apps that avoids expensive SecureStorage calls on every request.
12
13
  - **Pro Security**:
13
- - **Native**: Uses `SecureStoragePlugin` for token management.
14
- - **Web**: Uses `HttpOnly` cookies + Auto-CSRF token rotation.
15
- - **Smart Retries**: Automatically handles `403 CSRF` errors by fetching a new token and retrying the request.
16
- - **Optimistic Updates**: Update your UI instantly before the server responds.
17
- - **Type-Safe**: Full TypeScript support, but works efficiently in JS too.
14
+ - **Native**: Uses `SecureStoragePlugin` (with localStorage fallback) for secure token management.
15
+ - **Web**: Uses `HttpOnly` cookies + Automatic CSRF token rotation.
16
+ - **⚡ Strict Reliability**:
17
+ - **3s Execution Deadline**: Requests that take too long are automatically aborted to prevent UI hangs.
18
+ - **3x Auto-Retries**: Transparently retries failed network requests before surfacing an error.
19
+ - **📦 Caching & SWR**: Built-in support for Stale-While-Revalidate, custom dedupe keys, and focus-based revalidation.
20
+ - **Type-Safe & Efficient**: Optimized for both TypeScript and standard JavaScript.
18
21
 
19
22
  ---
20
23
 
@@ -28,9 +31,13 @@ import { ApiProvider } from "@kawaiininja/fetch";
28
31
 
29
32
  const apiConfig = {
30
33
  baseUrl: "https://api.myapp.com",
31
- version: "v1",
34
+ version: "v1", // Appends /v1 to all requests
35
+ debug: true, // Enables detailed logging for network and security flows
32
36
  // Optional: Global error handler (e.g., for Toast notifications)
33
- onError: (msg, status) => console.error(`[API Error ${status}]: ${msg}`),
37
+ onError: (msg, status) => {
38
+ // Trigger global snackbar/toast here
39
+ console.error(`[API Error ${status}]: ${msg}`);
40
+ },
34
41
  };
35
42
 
36
43
  export default function App() {
@@ -44,129 +51,142 @@ export default function App() {
44
51
 
45
52
  ---
46
53
 
47
- ## 📖 Usage Patterns (Efficient JS)
54
+ ## 📖 Usage Patterns
48
55
 
49
- ### 1. Lazy Action (Forms & Buttons)
56
+ ### 1. Simple Data Fetching (SWR Style)
50
57
 
51
- Best for login forms, submitting data, or manual triggers. No need for `useState` or `try/catch` boilerplate.
58
+ By default, `useFetch` can handle loading states and caching automatically.
52
59
 
53
60
  ```jsx
54
61
  import { useFetch } from "@kawaiininja/fetch";
55
62
 
56
- export const LoginForm = () => {
57
- // Extract helpers directly
58
- const { post, isLoading, error } = useFetch("/auth/login");
63
+ export const Profile = () => {
64
+ const { data, isLoading, error, get } = useFetch("/user/me", {
65
+ swr: true, // Return cached data immediately, then fetch updates
66
+ staleTime: 60000, // Consider data fresh for 1 minute
67
+ revalidateOnFocus: true, // Refetch when user switches tabs back
68
+ });
59
69
 
60
- const handleLogin = async (e) => {
61
- e.preventDefault();
62
- const formData = new FormData(e.target);
63
- const payload = Object.fromEntries(formData);
64
-
65
- // ✅ Efficient: helper handles JSON stringify, headers, and loading state
66
- const user = await post(payload);
67
-
68
- if (user) {
69
- console.log("Welcome", user.name);
70
- // Redirect or update context here
71
- }
72
- };
70
+ if (isLoading && !data) return <p>Loading...</p>;
73
71
 
74
72
  return (
75
- <form onSubmit={handleLogin}>
76
- {error && <div className="error-banner">{error}</div>}
77
-
78
- <input name="email" type="email" placeholder="Email" />
79
- <input name="password" type="password" placeholder="Password" />
80
-
81
- <button disabled={isLoading}>
82
- {isLoading ? "Logging in..." : "Log In"}
83
- </button>
84
- </form>
73
+ <div>
74
+ <h1>{data?.name}</h1>
75
+ <button onClick={() => get()}>Refresh Profile</button>
76
+ </div>
85
77
  );
86
78
  };
87
79
  ```
88
80
 
89
- ### 2. Optimistic Updates (Instant UX)
81
+ ### 2. Actions (POST/PUT/PATCH/DELETE)
90
82
 
91
- Make your app feel zero-latency by updating the local data _before_ the server responds.
83
+ The hook returns simple helper methods that handle JSON stringification and loading states for you.
92
84
 
93
85
  ```jsx
94
- const UserProfile = () => {
95
- // 'data' is your state, 'setData' lets you modify it instantly
96
- const { data, setData, patch } = useFetch("/user/me");
97
-
98
- const updateName = async (newName) => {
99
- // 1. Instant UI update (Optimistic)
100
- // We assume it will succeed to make it feel snappy
101
- setData((prev) => ({ ...prev, name: newName }));
102
-
103
- // 2. Send request in background
104
- // If it fails, the error state will update automatically and revert (if you handle it)
105
- await patch({ name: newName });
86
+ const LoginForm = () => {
87
+ const { post, isLoading } = useFetch("/auth/login");
88
+
89
+ const onSubmit = async (payload) => {
90
+ // helpers return the parsed data directly
91
+ const user = await post(payload);
92
+ if (user) login(user);
106
93
  };
107
94
 
108
95
  return (
109
- <div>
110
- <h1>Hello, {data?.name || "Guest"}</h1>
111
- <button onClick={() => updateName("Vinay")}>Update Name</button>
112
- </div>
96
+ <button onClick={() => onSubmit({ user: "..." })} disabled={isLoading}>
97
+ {isLoading ? "Logging in..." : "Submit"}
98
+ </button>
113
99
  );
114
100
  };
115
101
  ```
116
102
 
117
- ### 3. Fetch on Load (Page Data)
103
+ ### 3. Advanced Request Options
118
104
 
119
- For standard "load data when page opens" behavior, simply pair with `useEffect`.
105
+ You can override defaults or pass extra headers/parameters on a per-call basis. Every method (`get`, `post`, `request`, etc.) accepts an options object.
120
106
 
121
107
  ```jsx
122
- import { useEffect } from "react";
123
- import { useFetch } from "@kawaiininja/fetch";
108
+ const { get, post, upload } = useFetch("/data");
109
+
110
+ // Overriding URL or passing custom headers
111
+ get({
112
+ url: "/custom-endpoint",
113
+ headers: { "X-Custom-Header": "value" },
114
+ timeout: 5000, // Override default 3s timeout
115
+ });
116
+
117
+ // Handling non-JSON responses
118
+ const textData = await get({ parseAs: "text" });
119
+
120
+ // File Uploads
121
+ const handleUpload = (file) => {
122
+ const fd = new FormData();
123
+ fd.append("file", file);
124
+ upload(fd);
125
+ };
126
+ ```
124
127
 
125
- export const Dashboard = () => {
126
- // 'get' is the trigger function
127
- const { data, isLoading, get } = useFetch("/dashboard");
128
+ ### 4. Reactive Search (Watcher Pattern)
128
129
 
129
- useEffect(() => {
130
- // Trigger the fetch when component mounts
131
- get();
132
- }, []); // Empty array ensures it only runs once
130
+ Trigger a fetch whenever a specific dependency changes.
133
131
 
134
- if (isLoading) return <div>Loading...</div>;
132
+ ```jsx
133
+ const SearchResult = ({ query }) => {
134
+ const { data, get } = useFetch("/search", { enabled: false });
135
135
 
136
- return (
137
- <div>
138
- <h1>Dashboard Stats</h1>
139
- <pre>{JSON.stringify(data, null, 2)}</pre>
140
- </div>
141
- );
136
+ useEffect(() => {
137
+ if (query) get({ url: `/search?q=${query}` });
138
+ }, [query]);
139
+
140
+ return <List items={data} />;
142
141
  };
143
142
  ```
144
143
 
145
- ### 4. Reactive Fetch (Search & Filters)
144
+ ---
146
145
 
147
- Trigger a fetch automatically whenever a variable changes (like a filter button or search bar).
146
+ ## ⚙️ Configuration Reference
148
147
 
149
- ```jsx
150
- import { useEffect, useState } from "react";
151
- import { useFetch } from "@kawaiininja/fetch";
148
+ ### `useFetch(endpoint, options)`
152
149
 
153
- export const FilterList = () => {
154
- const [filter, setFilter] = useState("all");
155
- const [search, setSearch] = useState("");
156
- const { data, get } = useFetch("/items");
150
+ Initial options configured when calling the hook:
157
151
 
158
- // The "Watcher" Pattern
159
- useEffect(() => {
160
- // Overrides the URL with new params whenever dependencies change
161
- get({ url: `/items?status=${filter}&q=${search}` });
162
- }, [filter, search]); // 👈 DEPENDENCY ARRAY controls the trigger
152
+ | Option | Type | Default | Description |
153
+ | :------------------ | :-------- | :--------- | :------------------------------------------------ |
154
+ | `enabled` | `boolean` | `true` | Set to false to prevent automatic fetch on mount. |
155
+ | `swr` | `boolean` | `false` | Enable Stale-While-Revalidate caching. |
156
+ | `staleTime` | `number` | `300000` | MS until cache is considered stale (5 mins). |
157
+ | `revalidateOnFocus` | `boolean` | `false` | Refetch data when window gains focus. |
158
+ | `refreshInterval` | `number` | `0` | Auto-refresh data every X milliseconds. |
159
+ | `dedupeKey` | `string` | `endpoint` | Custom key for caching. |
163
160
 
164
- return (
165
- <div>
166
- <button onClick={() => setFilter("active")}>Active</button>
167
- <input onChange={(e) => setSearch(e.target.value)} />
168
- <List items={data} />
169
- </div>
170
- );
171
- };
172
- ```
161
+ ### Per-Request Options (passed to `get`, `post`, etc.)
162
+
163
+ | Option | Type | Default | Description |
164
+ | :-------- | :------- | :--------- | :------------------------------------------------------ |
165
+ | `url` | `string` | `endpoint` | Override the default endpoint. |
166
+ | `method` | `string` | `"GET"` | HTTP Method. |
167
+ | `headers` | `object` | `{}` | Custom headers. |
168
+ | `timeout` | `number` | `3000` | Per-request timeout limit. |
169
+ | `retries` | `number` | `3` | Number of times to retry on network failure. |
170
+ | `parseAs` | `string` | `"auto"` | How to parse response (`json`, `text`, `blob`, `auto`). |
171
+
172
+ ### Returned Surface
173
+
174
+ - `data`: The parsed response body.
175
+ - `isLoading`: Boolean loading state.
176
+ - `error`: Error message string (if any).
177
+ - `get(options)`, `post(body, options)`, `put(body, options)`, `patch(body, options)`, `del(options)`: HTTP methods.
178
+ - `upload(formData, options)`: Dedicated multipart/form-data helper.
179
+ - `text(options)` / `blob(options)`: Shortcuts for formatted responses.
180
+ - `setData(updater)`: Manually update `data` state.
181
+ - `updateData(partial)`: Merge partial updates into `data`.
182
+
183
+ ---
184
+
185
+ ## 🛡️ Security Logic
186
+
187
+ 1. **Native**: Checks `SecureStorage` -> `localStorage`. If found, injects `Authorization: Bearer <token>` and `X-Session-ID: <id>`. CSRF is skipped.
188
+ 2. **Web**: Fetches CSRF token from `/auth/csrf-token` on first boot. Injects `X-CSRF-Token` in all internal requests. Automatically refreshes token on 403 errors.
189
+
190
+ ---
191
+
192
+ &copy; 2024 KawaiiNinja. Built for High-Performance Onyx Apps.
@@ -16,7 +16,7 @@ export interface ApiContextValue {
16
16
  debug: boolean;
17
17
  }
18
18
  export declare const ApiContext: React.Context<ApiContextValue | null>;
19
- export declare function ApiProvider({ config, children }: {
19
+ export declare function ApiProvider({ config, children, }: {
20
20
  config: ApiConfig;
21
21
  children: React.ReactNode;
22
22
  }): import("react/jsx-runtime").JSX.Element;
@@ -1,16 +1,20 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { createContext, useCallback, useEffect, useMemo, useRef } from "react";
3
- export const INTERNAL_HEADER = { "x-internal-service": "zevrinix-main" };
2
+ import { createContext, useCallback, useEffect, useMemo, useRef, } from "react";
3
+ export const INTERNAL_HEADER = {
4
+ "x-internal-service": "zevrinix-main",
5
+ };
4
6
  export const ApiContext = createContext(null);
5
- export function ApiProvider({ config, children }) {
7
+ export function ApiProvider({ config, children, }) {
6
8
  const configRef = useRef(config);
7
9
  useEffect(() => {
8
10
  configRef.current = config;
9
11
  }, [config]);
10
12
  const apiUrl = useCallback((path) => {
11
- const base = configRef.current.baseUrl;
12
- if (path.startsWith("http"))
13
+ if (!path)
14
+ return configRef.current.baseUrl;
15
+ if (path.startsWith("http://") || path.startsWith("https://"))
13
16
  return path;
17
+ const base = configRef.current.baseUrl;
14
18
  const cleanBase = base.endsWith("/") ? base.slice(0, -1) : base;
15
19
  const cleanPath = path.startsWith("/") ? path : `/${path}`;
16
20
  return `${cleanBase}${cleanPath}`;
@@ -0,0 +1,10 @@
1
+ type CacheEntry = {
2
+ data: any;
3
+ timestamp: number;
4
+ promise?: Promise<any>;
5
+ };
6
+ export declare const getCache: (key: string) => CacheEntry | undefined;
7
+ export declare const setCache: (key: string, data: any, promise?: Promise<any>) => void;
8
+ export declare const isStale: (key: string, staleTime?: number) => boolean;
9
+ export declare const clearCache: () => void;
10
+ export {};
@@ -0,0 +1,16 @@
1
+ const globalCache = new Map();
2
+ export const getCache = (key) => globalCache.get(key);
3
+ export const setCache = (key, data, promise) => {
4
+ globalCache.set(key, {
5
+ data,
6
+ timestamp: Date.now(),
7
+ promise,
8
+ });
9
+ };
10
+ export const isStale = (key, staleTime = 300000) => {
11
+ const entry = globalCache.get(key);
12
+ if (!entry)
13
+ return true;
14
+ return Date.now() - entry.timestamp > staleTime;
15
+ };
16
+ export const clearCache = () => globalCache.clear();
@@ -1,2 +1,11 @@
1
+ import { RequestOptions } from "./types";
1
2
  import { ApiSurface } from "./utils";
2
- export declare const useFetch: <T = any>(endpoint: string, baseOptions?: any) => ApiSurface<T>;
3
+ export interface SSSOptions extends RequestOptions {
4
+ swr?: boolean;
5
+ revalidateOnFocus?: boolean;
6
+ refreshInterval?: number;
7
+ dedupeKey?: string;
8
+ staleTime?: number;
9
+ enabled?: boolean;
10
+ }
11
+ export declare const useFetch: <T = any>(endpoint: string, baseOptions?: SSSOptions) => ApiSurface<T>;
@@ -1,6 +1,7 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
2
  import { useApiConfig } from "./useApiConfig";
3
3
  import { useCsrf } from "./useCsrf";
4
+ import { getCache, isStale, setCache } from "./useFetch.cache";
4
5
  import { parseResponse, performFetch } from "./useFetch.executor";
5
6
  import { createApiMethods } from "./utils";
6
7
  export const useFetch = (endpoint, baseOptions = {}) => {
@@ -8,58 +9,97 @@ export const useFetch = (endpoint, baseOptions = {}) => {
8
9
  const { fetchCSRF } = useCsrf();
9
10
  const abortRef = useRef(new AbortController());
10
11
  const mounted = useRef(true);
11
- // 🛡️ LOCK IDENTITY: Ignore baseOptions identity shifts
12
- const optionsRef = useRef(baseOptions);
13
- useEffect(() => {
14
- optionsRef.current = baseOptions;
15
- }, [JSON.stringify(baseOptions)]);
12
+ const { enabled = true, swr = false, staleTime = 300000 } = baseOptions;
13
+ const cacheKey = useMemo(() => baseOptions.dedupeKey || endpoint, [endpoint, baseOptions.dedupeKey]);
14
+ const cached = getCache(cacheKey);
16
15
  const [state, setState] = useState({
17
- data: null,
18
- loading: false,
16
+ data: cached?.data || null,
17
+ loading: enabled && !cached?.data,
19
18
  error: null,
20
19
  status: null,
21
20
  });
22
21
  const safeSet = useCallback((fn) => {
23
22
  if (!mounted.current)
24
23
  return;
25
- // 🚀 STACK FLATTENER: setTimeout(0) physically clears the memory stack
26
24
  setTimeout(() => {
27
25
  if (mounted.current)
28
26
  setState((prev) => ({ ...prev, ...fn(prev) }));
29
27
  }, 0);
30
28
  }, []);
31
29
  const request = useCallback(async (params = {}) => {
32
- if (debug)
33
- console.log(`[API] Starting: ${endpoint}`);
34
- safeSet(() => ({ loading: true, error: null }));
35
- try {
36
- const token = await fetchCSRF();
37
- let finalUrl = params.url || endpoint;
38
- if (!finalUrl.startsWith("http")) {
39
- finalUrl = apiUrl(finalUrl);
30
+ const isRead = !params.method || params.method === "GET";
31
+ const currentCache = getCache(cacheKey);
32
+ if (isRead && currentCache?.promise)
33
+ return currentCache.promise;
34
+ if (!currentCache?.data)
35
+ safeSet(() => ({ loading: true, error: null }));
36
+ const execution = (async () => {
37
+ try {
38
+ const token = await fetchCSRF();
39
+ const finalUrl = apiUrl(params.url || endpoint);
40
+ const res = await performFetch(finalUrl, params.method || "GET", token, params.body, params.headers, { ...baseOptions, ...params }, debug, abortRef.current.signal, apiUrl);
41
+ const parsed = await parseResponse(res, params.parseAs || "auto");
42
+ if (!res.ok)
43
+ throw new Error(parsed?.message || res.statusText);
44
+ if (isRead)
45
+ setCache(cacheKey, parsed);
46
+ safeSet(() => ({ data: parsed, status: res.status, loading: false }));
47
+ return parsed;
40
48
  }
41
- const res = await performFetch(finalUrl, params.method || "GET", token, params.body, params.headers, { ...optionsRef.current, ...params }, debug, abortRef.current.signal, apiUrl);
42
- const parsed = await parseResponse(res, params.parseAs || "auto");
43
- if (!res.ok)
44
- throw new Error(parsed?.message || res.statusText);
45
- if (debug)
46
- console.log(`[API] Success: ${endpoint}`);
47
- safeSet(() => ({ data: parsed, status: res.status }));
48
- return parsed;
49
- }
50
- catch (err) {
51
- if (err.name !== "AbortError") {
52
- const msg = err.message || "Network error";
53
- safeSet(() => ({ error: msg, status: null }));
54
- if (onError)
55
- onError(msg, null);
56
- throw err;
49
+ catch (err) {
50
+ if (err.name !== "AbortError") {
51
+ const msg = err.message || "Network error";
52
+ safeSet(() => ({ error: msg, status: null, loading: false }));
53
+ if (onError)
54
+ onError(msg, null);
55
+ throw err;
56
+ }
57
+ }
58
+ finally {
59
+ if (isRead) {
60
+ const entry = getCache(cacheKey);
61
+ if (entry)
62
+ entry.promise = undefined;
63
+ }
57
64
  }
65
+ })();
66
+ if (isRead) {
67
+ const entry = getCache(cacheKey);
68
+ if (entry)
69
+ entry.promise = execution;
58
70
  }
59
- finally {
60
- safeSet(() => ({ loading: false }));
71
+ return execution;
72
+ }, [
73
+ endpoint,
74
+ cacheKey,
75
+ fetchCSRF,
76
+ apiUrl,
77
+ onError,
78
+ debug,
79
+ safeSet,
80
+ baseOptions,
81
+ ]);
82
+ useEffect(() => {
83
+ if (!enabled)
84
+ return;
85
+ const shouldFetch = !cached?.data || (swr && isStale(cacheKey, staleTime));
86
+ if (shouldFetch) {
87
+ request();
61
88
  }
62
- }, [endpoint, fetchCSRF, apiUrl, onError, debug, safeSet]);
89
+ }, [enabled, cacheKey, swr, staleTime]);
90
+ useEffect(() => {
91
+ if (!baseOptions.revalidateOnFocus || !enabled)
92
+ return;
93
+ const onFocus = () => request();
94
+ window.addEventListener("focus", onFocus);
95
+ return () => window.removeEventListener("focus", onFocus);
96
+ }, [request, baseOptions.revalidateOnFocus, enabled]);
97
+ useEffect(() => {
98
+ if (!baseOptions.refreshInterval || !enabled)
99
+ return;
100
+ const timer = setInterval(() => request(), baseOptions.refreshInterval);
101
+ return () => clearInterval(timer);
102
+ }, [request, baseOptions.refreshInterval, enabled]);
63
103
  useEffect(() => {
64
104
  mounted.current = true;
65
105
  return () => {
@@ -69,7 +109,7 @@ export const useFetch = (endpoint, baseOptions = {}) => {
69
109
  }, []);
70
110
  const methods = useMemo(() => createApiMethods({
71
111
  request,
72
- baseUrl: endpoint.startsWith("http") ? endpoint : apiUrl(endpoint),
112
+ baseUrl: apiUrl(endpoint),
73
113
  safeSet,
74
114
  }), [request, endpoint, apiUrl, safeSet]);
75
115
  return useMemo(() => ({
@@ -1,7 +1,4 @@
1
1
  import { FetchState, RequestOptions } from "./types";
2
- /**
3
- * API surface interface with all request methods
4
- */
5
2
  export interface ApiSurface<T = any> {
6
3
  state: FetchState<T>;
7
4
  isLoading: boolean;
@@ -21,19 +18,13 @@ export interface ApiSurface<T = any> {
21
18
  blob: (config?: RequestOptions) => Promise<Blob | undefined>;
22
19
  upload: <R = T>(formData: FormData, config?: RequestOptions) => Promise<R | undefined>;
23
20
  }
24
- /**
25
- * Parameters for creating API surface
26
- */
27
21
  interface CreateApiSurfaceParams<T> {
28
22
  state: FetchState<T>;
29
23
  request: <R = T>(options?: RequestOptions) => Promise<R | undefined>;
30
24
  baseUrl: string;
31
25
  safeSet: (fn: (prev: FetchState<T>) => Partial<FetchState<T>>) => void;
32
26
  }
33
- /**
34
- * Helper to construct the STABLE methods of the API surface.
35
- */
36
- export declare const createApiMethods: <T = any>({ request, baseUrl, safeSet }: Omit<CreateApiSurfaceParams<T>, "state">) => {
27
+ export declare const createApiMethods: <T = any>({ request, baseUrl, safeSet, }: Omit<CreateApiSurfaceParams<T>, "state">) => {
37
28
  request: <R = T>(options?: RequestOptions) => Promise<R | undefined>;
38
29
  refetch: () => Promise<T | undefined>;
39
30
  setData: (value: T | null | ((prev: T | null) => T | null)) => void;
@@ -48,8 +39,5 @@ export declare const createApiMethods: <T = any>({ request, baseUrl, safeSet }:
48
39
  blob: (config?: RequestOptions) => Promise<Blob | undefined>;
49
40
  upload: <R_4 = T>(formData: FormData, config?: RequestOptions) => Promise<R_4 | undefined>;
50
41
  };
51
- /**
52
- * Helper to construct the API surface object.
53
- */
54
- export declare const createApiSurface: <T = any>({ state, request, baseUrl, safeSet }: CreateApiSurfaceParams<T>) => ApiSurface<T>;
42
+ export declare const createApiSurface: <T = any>({ state, request, baseUrl, safeSet, }: CreateApiSurfaceParams<T>) => ApiSurface<T>;
55
43
  export {};
@@ -1,108 +1,85 @@
1
1
  import { useApiConfig } from "./useApiConfig";
2
- /**
3
- * Helper to construct the STABLE methods of the API surface.
4
- */
5
- export const createApiMethods = ({ request, baseUrl, safeSet }) => {
6
- // little helper for JSON methods
2
+ export const createApiMethods = ({ request, baseUrl, safeSet, }) => {
3
+ const { apiUrl } = useApiConfig();
7
4
  const withJsonBody = (data) => ({
8
5
  body: data != null ? JSON.stringify(data) : undefined,
9
6
  headers: { "Content-Type": "application/json" },
10
7
  parseAs: "json",
11
8
  });
12
- const { apiUrl } = useApiConfig();
9
+ const resolve = (path) => (path ? apiUrl(path) : baseUrl);
13
10
  return {
14
- // 🔁 core
15
11
  request,
16
12
  refetch: () => request({ url: baseUrl }),
17
- // 🎯 data helpers
18
13
  setData: (value) => {
19
14
  safeSet((prev) => ({
20
- data: (typeof value === "function" ? value(prev.data) : value),
15
+ data: (typeof value === "function"
16
+ ? value(prev.data)
17
+ : value),
21
18
  }));
22
19
  },
23
20
  updateData: (partial) => safeSet((prev) => ({
24
21
  data: {
25
22
  ...prev.data,
26
- ...(typeof partial === "function" ? partial(prev.data) : partial),
23
+ ...(typeof partial === "function"
24
+ ? partial(prev.data)
25
+ : partial),
27
26
  },
28
27
  })),
29
- // 🗡 CRUD methods
30
- get: (config = {}) => request({ ...config, url: config.url || baseUrl, method: "GET" }),
28
+ get: (config = {}) => request({ ...config, url: resolve(config.url), method: "GET" }),
31
29
  post: (data, config = {}) => request({
32
30
  ...config,
33
- url: apiUrl(config.url || "") || baseUrl,
31
+ url: resolve(config.url),
34
32
  method: "POST",
35
33
  ...withJsonBody(data),
36
- headers: {
37
- ...(config.headers || {}),
38
- "Content-Type": "application/json",
39
- },
40
34
  }),
41
35
  put: (data, config = {}) => request({
42
36
  ...config,
43
- url: config.url || baseUrl,
37
+ url: resolve(config.url),
44
38
  method: "PUT",
45
39
  ...withJsonBody(data),
46
- headers: {
47
- ...(config.headers || {}),
48
- "Content-Type": "application/json",
49
- },
50
40
  }),
51
41
  patch: (data, config = {}) => request({
52
42
  ...config,
53
- url: config.url || baseUrl,
43
+ url: resolve(config.url),
54
44
  method: "PATCH",
55
45
  ...withJsonBody(data),
56
- headers: {
57
- ...(config.headers || {}),
58
- "Content-Type": "application/json",
59
- },
60
46
  }),
61
47
  delete: (config = {}) => request({
62
48
  ...config,
63
- url: config.url || baseUrl,
49
+ url: resolve(config.url),
64
50
  method: "DELETE",
65
51
  parseAs: config.parseAs || "json",
66
52
  }),
67
- // 🎭 type-focused helpers
68
53
  json: (data, config = {}) => request({
69
54
  ...config,
70
- url: config.url || baseUrl,
55
+ url: resolve(config.url),
71
56
  method: config.method || "POST",
72
57
  ...withJsonBody(data),
73
- headers: {
74
- ...(config.headers || {}),
75
- "Content-Type": "application/json",
76
- },
77
58
  }),
78
59
  text: (config = {}) => request({
79
60
  ...config,
80
- url: config.url || baseUrl,
61
+ url: resolve(config.url),
81
62
  method: config.method || "GET",
82
63
  parseAs: "text",
83
64
  }),
84
65
  blob: (config = {}) => request({
85
66
  ...config,
86
- url: config.url || baseUrl,
67
+ url: resolve(config.url),
87
68
  method: config.method || "GET",
88
69
  parseAs: "blob",
89
70
  }),
90
71
  upload: (formData, config = {}) => request({
91
72
  ...config,
92
- url: config.url || baseUrl,
93
- method: config.method || "POST",
73
+ url: resolve(config.url),
74
+ method: "POST",
94
75
  body: formData,
95
76
  parseAs: config.parseAs || "json",
96
77
  }),
97
78
  };
98
79
  };
99
- /**
100
- * Helper to construct the API surface object.
101
- */
102
- export const createApiSurface = ({ state, request, baseUrl, safeSet }) => {
80
+ export const createApiSurface = ({ state, request, baseUrl, safeSet, }) => {
103
81
  const methods = createApiMethods({ request, baseUrl, safeSet });
104
82
  return {
105
- // 🔍 state
106
83
  state,
107
84
  isLoading: state.loading,
108
85
  isError: !!state.error,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kawaiininja/fetch",
3
- "version": "1.0.39",
3
+ "version": "1.0.41",
4
4
  "description": "Core fetch utility for Onyx Framework",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",