@kawaiininja/fetch 1.0.38 → 1.0.40
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 -1
- package/dist/context/ApiContext.js +9 -5
- package/dist/hooks/useFetch.cache.d.ts +10 -0
- package/dist/hooks/useFetch.cache.js +16 -0
- package/dist/hooks/useFetch.d.ts +9 -1
- package/dist/hooks/useFetch.js +77 -36
- package/dist/hooks/utils.d.ts +0 -12
- package/dist/hooks/utils.js +13 -38
- package/package.json +1 -1
|
@@ -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 = {
|
|
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
|
-
|
|
12
|
-
|
|
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();
|
package/dist/hooks/useFetch.d.ts
CHANGED
|
@@ -1,2 +1,10 @@
|
|
|
1
|
+
import { RequestOptions } from "./types";
|
|
1
2
|
import { ApiSurface } from "./utils";
|
|
2
|
-
export
|
|
3
|
+
export interface SSSOptions extends RequestOptions {
|
|
4
|
+
swr?: boolean;
|
|
5
|
+
revalidateOnFocus?: boolean;
|
|
6
|
+
refreshInterval?: number;
|
|
7
|
+
dedupeKey?: string;
|
|
8
|
+
staleTime?: number;
|
|
9
|
+
}
|
|
10
|
+
export declare const useFetch: <T = any>(endpoint: string, baseOptions?: SSSOptions) => ApiSurface<T>;
|
package/dist/hooks/useFetch.js
CHANGED
|
@@ -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,98 @@ export const useFetch = (endpoint, baseOptions = {}) => {
|
|
|
8
9
|
const { fetchCSRF } = useCsrf();
|
|
9
10
|
const abortRef = useRef(new AbortController());
|
|
10
11
|
const mounted = useRef(true);
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
useEffect(() => {
|
|
14
|
-
optionsRef.current = baseOptions;
|
|
15
|
-
}, [JSON.stringify(baseOptions)]);
|
|
12
|
+
const cacheKey = useMemo(() => baseOptions.dedupeKey || endpoint, [endpoint, baseOptions.dedupeKey]);
|
|
13
|
+
const cached = getCache(cacheKey);
|
|
16
14
|
const [state, setState] = useState({
|
|
17
|
-
data: null,
|
|
18
|
-
loading:
|
|
15
|
+
data: cached?.data || null,
|
|
16
|
+
loading: !cached?.data,
|
|
19
17
|
error: null,
|
|
20
18
|
status: null,
|
|
21
19
|
});
|
|
22
20
|
const safeSet = useCallback((fn) => {
|
|
23
21
|
if (!mounted.current)
|
|
24
22
|
return;
|
|
25
|
-
// 🚀 STACK FLATTENER: setTimeout(0) physically clears the memory stack
|
|
26
23
|
setTimeout(() => {
|
|
27
24
|
if (mounted.current)
|
|
28
25
|
setState((prev) => ({ ...prev, ...fn(prev) }));
|
|
29
26
|
}, 0);
|
|
30
27
|
}, []);
|
|
31
28
|
const request = useCallback(async (params = {}) => {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
29
|
+
const isRead = !params.method || params.method === "GET";
|
|
30
|
+
const currentCache = getCache(cacheKey);
|
|
31
|
+
if (isRead && currentCache?.promise)
|
|
32
|
+
return currentCache.promise;
|
|
33
|
+
if (!currentCache?.data)
|
|
34
|
+
safeSet(() => ({ loading: true, error: null }));
|
|
35
|
+
const execution = (async () => {
|
|
36
|
+
try {
|
|
37
|
+
const token = await fetchCSRF();
|
|
38
|
+
const finalUrl = apiUrl(params.url || endpoint);
|
|
39
|
+
const res = await performFetch(finalUrl, params.method || "GET", token, params.body, params.headers, { ...baseOptions, ...params }, debug, abortRef.current.signal, apiUrl);
|
|
40
|
+
const parsed = await parseResponse(res, params.parseAs || "auto");
|
|
41
|
+
if (!res.ok)
|
|
42
|
+
throw new Error(parsed?.message || res.statusText);
|
|
43
|
+
if (isRead)
|
|
44
|
+
setCache(cacheKey, parsed);
|
|
45
|
+
safeSet(() => ({ data: parsed, status: res.status, loading: false }));
|
|
46
|
+
return parsed;
|
|
40
47
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
throw err;
|
|
48
|
+
catch (err) {
|
|
49
|
+
if (err.name !== "AbortError") {
|
|
50
|
+
const msg = err.message || "Network error";
|
|
51
|
+
safeSet(() => ({ error: msg, status: null, loading: false }));
|
|
52
|
+
if (onError)
|
|
53
|
+
onError(msg, null);
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
if (isRead) {
|
|
59
|
+
const entry = getCache(cacheKey);
|
|
60
|
+
if (entry)
|
|
61
|
+
entry.promise = undefined;
|
|
62
|
+
}
|
|
57
63
|
}
|
|
64
|
+
})();
|
|
65
|
+
if (isRead) {
|
|
66
|
+
const entry = getCache(cacheKey);
|
|
67
|
+
if (entry)
|
|
68
|
+
entry.promise = execution;
|
|
69
|
+
}
|
|
70
|
+
return execution;
|
|
71
|
+
}, [
|
|
72
|
+
endpoint,
|
|
73
|
+
cacheKey,
|
|
74
|
+
fetchCSRF,
|
|
75
|
+
apiUrl,
|
|
76
|
+
onError,
|
|
77
|
+
debug,
|
|
78
|
+
safeSet,
|
|
79
|
+
baseOptions,
|
|
80
|
+
]);
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (baseOptions.swr &&
|
|
83
|
+
cached?.data &&
|
|
84
|
+
isStale(cacheKey, baseOptions.staleTime)) {
|
|
85
|
+
request();
|
|
58
86
|
}
|
|
59
|
-
|
|
60
|
-
|
|
87
|
+
else if (!cached?.data) {
|
|
88
|
+
request();
|
|
61
89
|
}
|
|
62
|
-
}, [
|
|
90
|
+
}, []);
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (!baseOptions.revalidateOnFocus)
|
|
93
|
+
return;
|
|
94
|
+
const onFocus = () => request();
|
|
95
|
+
window.addEventListener("focus", onFocus);
|
|
96
|
+
return () => window.removeEventListener("focus", onFocus);
|
|
97
|
+
}, [request, baseOptions.revalidateOnFocus]);
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (!baseOptions.refreshInterval)
|
|
100
|
+
return;
|
|
101
|
+
const timer = setInterval(() => request(), baseOptions.refreshInterval);
|
|
102
|
+
return () => clearInterval(timer);
|
|
103
|
+
}, [request, baseOptions.refreshInterval]);
|
|
63
104
|
useEffect(() => {
|
|
64
105
|
mounted.current = true;
|
|
65
106
|
return () => {
|
|
@@ -69,7 +110,7 @@ export const useFetch = (endpoint, baseOptions = {}) => {
|
|
|
69
110
|
}, []);
|
|
70
111
|
const methods = useMemo(() => createApiMethods({
|
|
71
112
|
request,
|
|
72
|
-
baseUrl:
|
|
113
|
+
baseUrl: apiUrl(endpoint),
|
|
73
114
|
safeSet,
|
|
74
115
|
}), [request, endpoint, apiUrl, safeSet]);
|
|
75
116
|
return useMemo(() => ({
|
package/dist/hooks/utils.d.ts
CHANGED
|
@@ -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,18 +18,12 @@ 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
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>;
|
|
@@ -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
42
|
export declare const createApiSurface: <T = any>({ state, request, baseUrl, safeSet, }: CreateApiSurfaceParams<T>) => ApiSurface<T>;
|
|
55
43
|
export {};
|
package/dist/hooks/utils.js
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
* Helper to construct the STABLE methods of the API surface.
|
|
3
|
-
*/
|
|
1
|
+
import { useApiConfig } from "./useApiConfig";
|
|
4
2
|
export const createApiMethods = ({ request, baseUrl, safeSet, }) => {
|
|
5
|
-
|
|
3
|
+
const { apiUrl } = useApiConfig();
|
|
6
4
|
const withJsonBody = (data) => ({
|
|
7
5
|
body: data != null ? JSON.stringify(data) : undefined,
|
|
8
6
|
headers: { "Content-Type": "application/json" },
|
|
9
7
|
parseAs: "json",
|
|
10
8
|
});
|
|
9
|
+
const resolve = (path) => (path ? apiUrl(path) : baseUrl);
|
|
11
10
|
return {
|
|
12
|
-
// 🔁 core
|
|
13
11
|
request,
|
|
14
12
|
refetch: () => request({ url: baseUrl }),
|
|
15
|
-
// 🎯 data helpers
|
|
16
13
|
setData: (value) => {
|
|
17
14
|
safeSet((prev) => ({
|
|
18
15
|
data: (typeof value === "function"
|
|
@@ -28,83 +25,61 @@ export const createApiMethods = ({ request, baseUrl, safeSet, }) => {
|
|
|
28
25
|
: partial),
|
|
29
26
|
},
|
|
30
27
|
})),
|
|
31
|
-
|
|
32
|
-
get: (config = {}) => request({ ...config, url: config.url || baseUrl, method: "GET" }),
|
|
28
|
+
get: (config = {}) => request({ ...config, url: resolve(config.url), method: "GET" }),
|
|
33
29
|
post: (data, config = {}) => request({
|
|
34
30
|
...config,
|
|
35
|
-
url: config.url
|
|
31
|
+
url: resolve(config.url),
|
|
36
32
|
method: "POST",
|
|
37
33
|
...withJsonBody(data),
|
|
38
|
-
headers: {
|
|
39
|
-
...(config.headers || {}),
|
|
40
|
-
"Content-Type": "application/json",
|
|
41
|
-
},
|
|
42
34
|
}),
|
|
43
35
|
put: (data, config = {}) => request({
|
|
44
36
|
...config,
|
|
45
|
-
url: config.url
|
|
37
|
+
url: resolve(config.url),
|
|
46
38
|
method: "PUT",
|
|
47
39
|
...withJsonBody(data),
|
|
48
|
-
headers: {
|
|
49
|
-
...(config.headers || {}),
|
|
50
|
-
"Content-Type": "application/json",
|
|
51
|
-
},
|
|
52
40
|
}),
|
|
53
41
|
patch: (data, config = {}) => request({
|
|
54
42
|
...config,
|
|
55
|
-
url: config.url
|
|
43
|
+
url: resolve(config.url),
|
|
56
44
|
method: "PATCH",
|
|
57
45
|
...withJsonBody(data),
|
|
58
|
-
headers: {
|
|
59
|
-
...(config.headers || {}),
|
|
60
|
-
"Content-Type": "application/json",
|
|
61
|
-
},
|
|
62
46
|
}),
|
|
63
47
|
delete: (config = {}) => request({
|
|
64
48
|
...config,
|
|
65
|
-
url: config.url
|
|
49
|
+
url: resolve(config.url),
|
|
66
50
|
method: "DELETE",
|
|
67
51
|
parseAs: config.parseAs || "json",
|
|
68
52
|
}),
|
|
69
|
-
// 🎭 type-focused helpers
|
|
70
53
|
json: (data, config = {}) => request({
|
|
71
54
|
...config,
|
|
72
|
-
url: config.url
|
|
55
|
+
url: resolve(config.url),
|
|
73
56
|
method: config.method || "POST",
|
|
74
57
|
...withJsonBody(data),
|
|
75
|
-
headers: {
|
|
76
|
-
...(config.headers || {}),
|
|
77
|
-
"Content-Type": "application/json",
|
|
78
|
-
},
|
|
79
58
|
}),
|
|
80
59
|
text: (config = {}) => request({
|
|
81
60
|
...config,
|
|
82
|
-
url: config.url
|
|
61
|
+
url: resolve(config.url),
|
|
83
62
|
method: config.method || "GET",
|
|
84
63
|
parseAs: "text",
|
|
85
64
|
}),
|
|
86
65
|
blob: (config = {}) => request({
|
|
87
66
|
...config,
|
|
88
|
-
url: config.url
|
|
67
|
+
url: resolve(config.url),
|
|
89
68
|
method: config.method || "GET",
|
|
90
69
|
parseAs: "blob",
|
|
91
70
|
}),
|
|
92
71
|
upload: (formData, config = {}) => request({
|
|
93
72
|
...config,
|
|
94
|
-
url: config.url
|
|
95
|
-
method:
|
|
73
|
+
url: resolve(config.url),
|
|
74
|
+
method: "POST",
|
|
96
75
|
body: formData,
|
|
97
76
|
parseAs: config.parseAs || "json",
|
|
98
77
|
}),
|
|
99
78
|
};
|
|
100
79
|
};
|
|
101
|
-
/**
|
|
102
|
-
* Helper to construct the API surface object.
|
|
103
|
-
*/
|
|
104
80
|
export const createApiSurface = ({ state, request, baseUrl, safeSet, }) => {
|
|
105
81
|
const methods = createApiMethods({ request, baseUrl, safeSet });
|
|
106
82
|
return {
|
|
107
|
-
// 🔍 state
|
|
108
83
|
state,
|
|
109
84
|
isLoading: state.loading,
|
|
110
85
|
isError: !!state.error,
|