@kawaiininja/fetch 1.0.1

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.
@@ -0,0 +1,41 @@
1
+ import React from "react";
2
+ /**
3
+ * Internal service header for API requests
4
+ */
5
+ export declare const INTERNAL_HEADER: {
6
+ readonly "x-internal-service": "zevrinix-main";
7
+ };
8
+ /**
9
+ * API configuration interface
10
+ */
11
+ export interface ApiConfig {
12
+ baseUrl: string;
13
+ version?: string;
14
+ disableCsrf?: boolean;
15
+ onError?: (error: string, status: number | null) => void;
16
+ }
17
+ /**
18
+ * API context value type
19
+ */
20
+ export interface ApiContextValue {
21
+ baseUrl: string;
22
+ version: string;
23
+ disableCsrf: boolean;
24
+ apiUrl: (path: string) => string;
25
+ onError?: (error: string, status: number | null) => void;
26
+ }
27
+ /**
28
+ * Props for ApiProvider component
29
+ */
30
+ export interface ApiProviderProps {
31
+ config: ApiConfig;
32
+ children: React.ReactNode;
33
+ }
34
+ export declare const ApiContext: React.Context<ApiContextValue | null>;
35
+ /**
36
+ * ApiProvider
37
+ *
38
+ * Provides API configuration to the component tree.
39
+ * Constructs full API URLs from relative paths using the baseUrl.
40
+ */
41
+ export declare function ApiProvider({ config, children }: ApiProviderProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,37 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useMemo } from "react";
3
+ /**
4
+ * Internal service header for API requests
5
+ */
6
+ export const INTERNAL_HEADER = {
7
+ "x-internal-service": "zevrinix-main",
8
+ };
9
+ export const ApiContext = createContext(null);
10
+ /**
11
+ * ApiProvider
12
+ *
13
+ * Provides API configuration to the component tree.
14
+ * Constructs full API URLs from relative paths using the baseUrl.
15
+ */
16
+ export function ApiProvider({ config, children }) {
17
+ const value = useMemo(() => {
18
+ // Ensure valid config structure
19
+ const baseUrl = config?.baseUrl || "";
20
+ const version = config?.version || "v1";
21
+ return {
22
+ baseUrl,
23
+ version,
24
+ disableCsrf: !!config?.disableCsrf,
25
+ onError: config?.onError,
26
+ // Helper to construct URLs
27
+ apiUrl: (path) => {
28
+ const cleanBase = baseUrl.endsWith("/")
29
+ ? baseUrl.slice(0, -1)
30
+ : baseUrl;
31
+ const cleanPath = path.startsWith("/") ? path : `/${path}`;
32
+ return `${cleanBase}${cleanPath}`;
33
+ },
34
+ };
35
+ }, [config]);
36
+ return _jsx(ApiContext.Provider, { value: value, children: children });
37
+ }
@@ -0,0 +1 @@
1
+ export * from "./ApiContext";
@@ -0,0 +1 @@
1
+ export * from "./ApiContext";
@@ -0,0 +1,5 @@
1
+ export * from "./types";
2
+ export * from "./useApiConfig";
3
+ export * from "./useCsrf";
4
+ export * from "./useFetch";
5
+ export * from "./utils";
@@ -0,0 +1,5 @@
1
+ export * from "./types";
2
+ export * from "./useApiConfig";
3
+ export * from "./useCsrf";
4
+ export * from "./useFetch";
5
+ export * from "./utils";
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Fetch state interface
3
+ */
4
+ export interface FetchState<T = any> {
5
+ data: T | null;
6
+ loading: boolean;
7
+ error: string | null;
8
+ status: number | null;
9
+ }
10
+ /**
11
+ * Base fetch options
12
+ */
13
+ export interface BaseFetchOptions extends RequestInit {
14
+ parseAs?: "auto" | "json" | "text" | "blob";
15
+ }
16
+ /**
17
+ * Request options for the request function
18
+ */
19
+ export interface RequestOptions extends BaseFetchOptions {
20
+ url?: string;
21
+ method?: string;
22
+ body?: BodyInit;
23
+ headers?: HeadersInit;
24
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ import { ApiContextValue } from "../context/ApiContext";
2
+ /**
3
+ * Hook to consume API configuration
4
+ *
5
+ * @returns API context value with baseUrl, version, and apiUrl helper
6
+ * @throws Error if used outside ApiProvider
7
+ */
8
+ export declare function useApiConfig(): ApiContextValue;
@@ -0,0 +1,15 @@
1
+ import { useContext } from "react";
2
+ import { ApiContext } from "../context/ApiContext";
3
+ /**
4
+ * Hook to consume API configuration
5
+ *
6
+ * @returns API context value with baseUrl, version, and apiUrl helper
7
+ * @throws Error if used outside ApiProvider
8
+ */
9
+ export function useApiConfig() {
10
+ const context = useContext(ApiContext);
11
+ if (!context) {
12
+ throw new Error("useApiConfig must be used within an ApiProvider");
13
+ }
14
+ return context;
15
+ }
@@ -0,0 +1,4 @@
1
+ export declare function useCsrf(): {
2
+ fetchCSRF: () => Promise<string>;
3
+ clearCsrf: () => void;
4
+ };
@@ -0,0 +1,43 @@
1
+ import { useCallback, useRef } from "react";
2
+ import { INTERNAL_HEADER } from "../context/ApiContext";
3
+ import { useApiConfig } from "./useApiConfig";
4
+ export function useCsrf() {
5
+ const { apiUrl } = useApiConfig();
6
+ const csrfRef = useRef(null);
7
+ const clearCsrf = useCallback(() => {
8
+ csrfRef.current = null;
9
+ if (typeof window !== "undefined") {
10
+ localStorage.removeItem("csrf_token");
11
+ }
12
+ }, []);
13
+ const fetchCSRF = useCallback(async () => {
14
+ if (csrfRef.current)
15
+ return csrfRef.current;
16
+ // Try to get from localStorage first (for Capacitor/mobile)
17
+ if (typeof window !== "undefined") {
18
+ const storedCsrf = localStorage.getItem("csrf_token");
19
+ if (storedCsrf) {
20
+ csrfRef.current = storedCsrf;
21
+ return storedCsrf;
22
+ }
23
+ }
24
+ // Construct CSRF URL using the context's baseUrl
25
+ const csrfUrl = apiUrl("auth/csrf-token");
26
+ const res = await fetch(csrfUrl, {
27
+ method: "GET",
28
+ headers: INTERNAL_HEADER,
29
+ credentials: "include",
30
+ });
31
+ const json = await res.json();
32
+ const token = json?.csrfToken;
33
+ if (!token)
34
+ throw new Error("Missing CSRF token");
35
+ csrfRef.current = token;
36
+ // Store in localStorage for Capacitor/mobile (cookies don't work cross-domain)
37
+ if (typeof window !== "undefined") {
38
+ localStorage.setItem("csrf_token", token);
39
+ }
40
+ return token;
41
+ }, [apiUrl]);
42
+ return { fetchCSRF, clearCsrf };
43
+ }
@@ -0,0 +1,9 @@
1
+ import { BaseFetchOptions } from "./types";
2
+ import { ApiSurface } from "./utils";
3
+ /**
4
+ * useFetch Hook
5
+ *
6
+ * A comprehensive hook for data fetching with automatic CSRF handling,
7
+ * request cancellation, and a rich API surface.
8
+ */
9
+ export declare const useFetch: <T = any>(endpoint: string, baseOptions?: BaseFetchOptions) => ApiSurface<T>;
@@ -0,0 +1,128 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import { INTERNAL_HEADER } from "../context/ApiContext";
3
+ import { useApiConfig } from "./useApiConfig";
4
+ import { useCsrf } from "./useCsrf";
5
+ import { createApiSurface } from "./utils";
6
+ /**
7
+ * useFetch Hook
8
+ *
9
+ * A comprehensive hook for data fetching with automatic CSRF handling,
10
+ * request cancellation, and a rich API surface.
11
+ */
12
+ export const useFetch = (endpoint, baseOptions = {}) => {
13
+ const { apiUrl, disableCsrf, onError } = useApiConfig();
14
+ const { fetchCSRF, clearCsrf } = useCsrf();
15
+ const resolvedUrl = endpoint.startsWith("http") ? endpoint : apiUrl(endpoint);
16
+ const abortRef = useRef(new AbortController());
17
+ const mounted = useRef(true);
18
+ const optionsRef = useRef(baseOptions);
19
+ const [state, setState] = useState({
20
+ data: null,
21
+ loading: false,
22
+ error: null,
23
+ status: null,
24
+ });
25
+ useEffect(() => {
26
+ optionsRef.current = baseOptions;
27
+ }, [baseOptions]);
28
+ const safeSet = useCallback((fn) => {
29
+ if (mounted.current) {
30
+ setState((prev) => ({ ...prev, ...fn(prev) }));
31
+ }
32
+ }, []);
33
+ const performFetch = async (url, method, token, body, headers, rest) => {
34
+ const authToken = typeof window !== "undefined" ? localStorage.getItem("token") : null;
35
+ const sessionId = typeof window !== "undefined" ? localStorage.getItem("session_id") : null;
36
+ const headersConfig = {
37
+ ...(optionsRef.current.headers || {}),
38
+ ...(headers || {}),
39
+ ...INTERNAL_HEADER,
40
+ "X-CSRF-Token": token,
41
+ ...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
42
+ ...(sessionId ? { "X-Session-ID": sessionId } : {}),
43
+ };
44
+ return fetch(url, {
45
+ ...optionsRef.current,
46
+ ...rest,
47
+ method,
48
+ signal: abortRef.current.signal,
49
+ credentials: "include",
50
+ headers: headersConfig,
51
+ body,
52
+ });
53
+ };
54
+ const parseResponse = async (res, parseAs) => {
55
+ const type = res.headers.get("content-type") || "";
56
+ if (parseAs === "json" ||
57
+ (parseAs === "auto" && type.includes("application/json"))) {
58
+ return res.json();
59
+ }
60
+ else if (parseAs === "text") {
61
+ return res.text();
62
+ }
63
+ else if (parseAs === "blob") {
64
+ return res.blob();
65
+ }
66
+ return res.text();
67
+ };
68
+ const request = useCallback(async (params = {}) => {
69
+ const { url = resolvedUrl, method = "GET", body, headers, parseAs = "auto", ...rest } = params;
70
+ let finalUrl = url.startsWith("http") ? url : apiUrl(url);
71
+ if (!finalUrl)
72
+ return;
73
+ safeSet(() => ({ loading: true, error: null }));
74
+ let lastStatus = null;
75
+ try {
76
+ let csrfToken = "";
77
+ if (!disableCsrf) {
78
+ csrfToken = await fetchCSRF();
79
+ }
80
+ let res = await performFetch(finalUrl, method, csrfToken, body, headers, rest);
81
+ lastStatus = res.status;
82
+ // 🔄 Auto-retry on CSRF error
83
+ if (!disableCsrf && res.status === 403) {
84
+ try {
85
+ const errorBody = await res.clone().json();
86
+ if (errorBody?.code === "CSRF_ERROR" ||
87
+ errorBody?.message === "Invalid or missing CSRF token") {
88
+ clearCsrf();
89
+ csrfToken = await fetchCSRF();
90
+ res = await performFetch(finalUrl, method, csrfToken, body, headers, rest);
91
+ lastStatus = res.status;
92
+ }
93
+ }
94
+ catch (e) {
95
+ /* ignore parse error */
96
+ }
97
+ }
98
+ const parsed = await parseResponse(res, parseAs);
99
+ if (!res.ok) {
100
+ throw new Error(parsed?.message || res.statusText);
101
+ }
102
+ safeSet(() => ({ data: parsed, status: res.status }));
103
+ return parsed;
104
+ }
105
+ catch (err) {
106
+ if (err.name !== "AbortError") {
107
+ safeSet(() => ({ error: err.message, status: lastStatus }));
108
+ // 📣 Global Error Broadcast
109
+ if (onError) {
110
+ onError(err.message, lastStatus);
111
+ }
112
+ throw err;
113
+ }
114
+ }
115
+ finally {
116
+ safeSet(() => ({ loading: false }));
117
+ }
118
+ }, [resolvedUrl, fetchCSRF, clearCsrf, apiUrl, safeSet]);
119
+ useEffect(() => {
120
+ abortRef.current = new AbortController();
121
+ mounted.current = true;
122
+ return () => {
123
+ mounted.current = false;
124
+ abortRef.current?.abort();
125
+ };
126
+ }, []);
127
+ return useMemo(() => createApiSurface({ state, request, baseUrl: resolvedUrl, safeSet }), [state, request, resolvedUrl, safeSet]);
128
+ };
@@ -0,0 +1,38 @@
1
+ import { FetchState, RequestOptions } from "./types";
2
+ /**
3
+ * API surface interface with all request methods
4
+ */
5
+ export interface ApiSurface<T = any> {
6
+ state: FetchState<T>;
7
+ isLoading: boolean;
8
+ isError: boolean;
9
+ isSuccess: boolean;
10
+ request: <R = T>(options?: RequestOptions) => Promise<R | undefined>;
11
+ refetch: () => Promise<T | undefined>;
12
+ setData: (value: T | null | ((prev: T | null) => T | null)) => void;
13
+ updateData: (partial: Partial<T> | ((prev: T | null) => Partial<T>)) => void;
14
+ get: <R = T>(config?: RequestOptions) => Promise<R | undefined>;
15
+ post: <D = any, R = T>(data?: D, config?: RequestOptions) => Promise<R | undefined>;
16
+ put: <D = any, R = T>(data?: D, config?: RequestOptions) => Promise<R | undefined>;
17
+ patch: <D = any, R = T>(data?: D, config?: RequestOptions) => Promise<R | undefined>;
18
+ delete: <R = T>(config?: RequestOptions) => Promise<R | undefined>;
19
+ json: <D = any, R = T>(data?: D, config?: RequestOptions) => Promise<R | undefined>;
20
+ text: (config?: RequestOptions) => Promise<string | undefined>;
21
+ blob: (config?: RequestOptions) => Promise<Blob | undefined>;
22
+ upload: <R = T>(formData: FormData, config?: RequestOptions) => Promise<R | undefined>;
23
+ }
24
+ /**
25
+ * Parameters for creating API surface
26
+ */
27
+ interface CreateApiSurfaceParams<T> {
28
+ state: FetchState<T>;
29
+ request: <R = T>(options?: RequestOptions) => Promise<R | undefined>;
30
+ baseUrl: string;
31
+ safeSet: (fn: (prev: FetchState<T>) => Partial<FetchState<T>>) => void;
32
+ }
33
+ /**
34
+ * Helper to construct the API surface object.
35
+ * Extracts the massive object creation from the main hook to keep it clean.
36
+ */
37
+ export declare const createApiSurface: <T = any>({ state, request, baseUrl, safeSet, }: CreateApiSurfaceParams<T>) => ApiSurface<T>;
38
+ export {};
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Helper to construct the API surface object.
3
+ * Extracts the massive object creation from the main hook to keep it clean.
4
+ */
5
+ export const createApiSurface = ({ state, request, baseUrl, safeSet, }) => {
6
+ // little helper for JSON methods
7
+ const withJsonBody = (data) => ({
8
+ body: data != null ? JSON.stringify(data) : undefined,
9
+ headers: { "Content-Type": "application/json" },
10
+ parseAs: "json",
11
+ });
12
+ return {
13
+ // 🔍 state
14
+ state,
15
+ isLoading: state.loading,
16
+ isError: !!state.error,
17
+ isSuccess: !!state.data && !state.error,
18
+ // 🔁 core
19
+ request,
20
+ refetch: () => request({ url: baseUrl }),
21
+ // 🎯 data helpers
22
+ setData: (value) => {
23
+ safeSet((prev) => ({
24
+ data: (typeof value === "function"
25
+ ? value(prev.data)
26
+ : value),
27
+ }));
28
+ },
29
+ updateData: (partial) => safeSet((prev) => ({
30
+ data: {
31
+ ...prev.data,
32
+ ...(typeof partial === "function" ? partial(prev.data) : partial),
33
+ },
34
+ })),
35
+ // 🗡 CRUD methods
36
+ get: (config = {}) => request({ ...config, url: config.url || baseUrl, method: "GET" }),
37
+ post: (data, config = {}) => request({
38
+ ...config,
39
+ url: config.url || baseUrl,
40
+ method: "POST",
41
+ ...withJsonBody(data),
42
+ headers: {
43
+ ...(config.headers || {}),
44
+ "Content-Type": "application/json",
45
+ },
46
+ }),
47
+ put: (data, config = {}) => request({
48
+ ...config,
49
+ url: config.url || baseUrl,
50
+ method: "PUT",
51
+ ...withJsonBody(data),
52
+ headers: {
53
+ ...(config.headers || {}),
54
+ "Content-Type": "application/json",
55
+ },
56
+ }),
57
+ patch: (data, config = {}) => request({
58
+ ...config,
59
+ url: config.url || baseUrl,
60
+ method: "PATCH",
61
+ ...withJsonBody(data),
62
+ headers: {
63
+ ...(config.headers || {}),
64
+ "Content-Type": "application/json",
65
+ },
66
+ }),
67
+ delete: (config = {}) => request({
68
+ ...config,
69
+ url: config.url || baseUrl,
70
+ method: "DELETE",
71
+ parseAs: config.parseAs || "json",
72
+ }),
73
+ // 🎭 type-focused helpers
74
+ json: (data, config = {}) => request({
75
+ ...config,
76
+ url: config.url || baseUrl,
77
+ method: config.method || "POST",
78
+ ...withJsonBody(data),
79
+ headers: {
80
+ ...(config.headers || {}),
81
+ "Content-Type": "application/json",
82
+ },
83
+ }),
84
+ text: (config = {}) => request({
85
+ ...config,
86
+ url: config.url || baseUrl,
87
+ method: config.method || "GET",
88
+ parseAs: "text",
89
+ }),
90
+ blob: (config = {}) => request({
91
+ ...config,
92
+ url: config.url || baseUrl,
93
+ method: config.method || "GET",
94
+ parseAs: "blob",
95
+ }),
96
+ upload: (formData, config = {}) => request({
97
+ ...config,
98
+ url: config.url || baseUrl,
99
+ method: config.method || "POST",
100
+ body: formData,
101
+ parseAs: config.parseAs || "json",
102
+ }),
103
+ };
104
+ };
@@ -0,0 +1,2 @@
1
+ export * from "./context/ApiContext";
2
+ export * from "./hooks/index";
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./context/ApiContext";
2
+ export * from "./hooks/index";
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@kawaiininja/fetch",
3
+ "version": "1.0.1",
4
+ "description": "Core fetch utility for Onyx Framework",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "module": "dist/index.js",
8
+ "type": "module",
9
+ "exports": {
10
+ ".": "./dist/index.js"
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "README.md"
15
+ ],
16
+ "scripts": {
17
+ "build": "tsc"
18
+ },
19
+ "keywords": [
20
+ "react",
21
+ "fetch",
22
+ "onyx",
23
+ "kawaiininja",
24
+ "http"
25
+ ],
26
+ "author": "Vinay (4kawaiininja)",
27
+ "license": "MIT",
28
+ "peerDependencies": {
29
+ "react": "^18.0.0 || ^19.0.0",
30
+ "react-dom": "^18.0.0 || ^19.0.0"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "devDependencies": {
36
+ "@types/react": "^19.2.7",
37
+ "@types/react-dom": "^19.2.3",
38
+ "typescript": "^5.7.0"
39
+ }
40
+ }