@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.
@@ -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,10 @@
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
+ }
10
+ 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,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
- // 🛡️ LOCK IDENTITY: Ignore baseOptions identity shifts
12
- const optionsRef = useRef(baseOptions);
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: false,
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
- 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);
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
- 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;
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
- finally {
60
- safeSet(() => ({ loading: false }));
87
+ else if (!cached?.data) {
88
+ request();
61
89
  }
62
- }, [endpoint, fetchCSRF, apiUrl, onError, debug, safeSet]);
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: endpoint.startsWith("http") ? endpoint : apiUrl(endpoint),
113
+ baseUrl: apiUrl(endpoint),
73
114
  safeSet,
74
115
  }), [request, endpoint, apiUrl, safeSet]);
75
116
  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,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 {};
@@ -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
- // little helper for JSON methods
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
- // 🗡 CRUD methods
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 || baseUrl,
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 || baseUrl,
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 || baseUrl,
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 || baseUrl,
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 || baseUrl,
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 || baseUrl,
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 || baseUrl,
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 || baseUrl,
95
- method: config.method || "POST",
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kawaiininja/fetch",
3
- "version": "1.0.38",
3
+ "version": "1.0.40",
4
4
  "description": "Core fetch utility for Onyx Framework",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",