@mlyrmdhnn/use-fetch 1.0.0

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,62 @@
1
+ import { HttpOptions, HttpHeaders, RequestInterceptorFn, ResponseInterceptorFn, ErrorInterceptorFn } from '../lib/fetch/types';
2
+ import { createInterceptor } from '../lib/fetch/interceptor';
3
+ import { FetchConfigInstance } from '../lib/fetch/config';
4
+ export interface UseFetchStatic {
5
+ baseURL: (url: string) => void;
6
+ headers: (headers: HttpHeaders) => void;
7
+ onError: (status: number, fn: (error: any) => void) => void;
8
+ interceptor: {
9
+ request: {
10
+ use: (fn: RequestInterceptorFn) => number;
11
+ eject: (id: number) => void;
12
+ };
13
+ response: {
14
+ use: (fn: ResponseInterceptorFn) => number;
15
+ eject: (id: number) => void;
16
+ };
17
+ error: {
18
+ use: (fn: ErrorInterceptorFn) => number;
19
+ eject: (id: number) => void;
20
+ };
21
+ };
22
+ create: (config: Partial<FetchConfigInstance>) => UseFetchInstance;
23
+ }
24
+ export type UseFetchInstance = {
25
+ <T>(endpoint: string, options?: HttpOptions): ReturnType<typeof buildUseFetch<T>>;
26
+ baseURL: (url: string) => void;
27
+ headers: (headers: HttpHeaders) => void;
28
+ onError: (status: number, fn: (error: any) => void) => void;
29
+ interceptor: UseFetchStatic["interceptor"];
30
+ };
31
+ declare function buildUseFetch<T>(endpoint: string, options?: HttpOptions, instanceConfig?: FetchConfigInstance, instanceInterceptors?: ReturnType<typeof createInterceptor>): {
32
+ data: any;
33
+ error: any;
34
+ pending: any;
35
+ status: any;
36
+ links: any;
37
+ from: any;
38
+ to: any;
39
+ total: any;
40
+ currentPage: any;
41
+ execute: () => Promise<any>;
42
+ refresh: () => Promise<any>;
43
+ clear: () => void;
44
+ abort: () => void;
45
+ };
46
+ declare function useFetch<T>(endpoint: string, options?: HttpOptions): {
47
+ data: any;
48
+ error: any;
49
+ pending: any;
50
+ status: any;
51
+ links: any;
52
+ from: any;
53
+ to: any;
54
+ total: any;
55
+ currentPage: any;
56
+ execute: () => Promise<any>;
57
+ refresh: () => Promise<any>;
58
+ clear: () => void;
59
+ abort: () => void;
60
+ };
61
+ declare const _useFetch: typeof useFetch & UseFetchStatic;
62
+ export { _useFetch as useFetch };
@@ -0,0 +1,2 @@
1
+ export * from './http/useFetch'
2
+ export {}
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("vue");var t=e=>new Promise(t=>setTimeout(t,e)),n=e=>{let t={};return Object.entries(e||{}).forEach(([e,n])=>{n!=null&&(t[e]=String(n))}),new URLSearchParams(t).toString()},r=e=>{let t=sessionStorage.getItem(e);return t?JSON.parse(t):null},i=(e,t)=>{sessionStorage.setItem(e,JSON.stringify(t))},a={baseURL:`your domain`,headers:{},statusHooks:[]},o=e=>({baseURL:e?.baseURL??`your domain`??``,headers:e?.headers??{},statusHooks:[]}),s={baseURL(e){a.baseURL=e},getBaseURL(){return a.baseURL},headers(e){a.headers={...a.headers,...e}},getHeaders(){return a.headers},onError(e,t){a.statusHooks.push({status:e,fn:t})}},c=[],l=async(e,t)=>{for(let n of c)n.status===e&&await n.fn(t)},u=new Map,d=(e,t,n)=>`${t}:${e}:${JSON.stringify(n)}`,f=e=>u.get(e),p=(e,t)=>{u.set(e,t)},m=e=>{u.delete(e)},h=async({endpoint:e,options:o,controller:s,instanceConfig:c,instanceRequestInterceptors:u,instanceResponseInterceptors:h,instanceErrorInterceptors:g})=>{let _=c??a,{method:v=`GET`,params:y={},payload:b=null,credentials:x=!1,headers:S={},timeout:C=1e4,retry:w=0,retryDelay:T=1e3,cache:E=!1,cacheKey:D,baseURL:O=_.baseURL,dedup:k=!1}=o,A=u??[],j=h??[],M=g??[],N=n(y),P=async()=>{let n=0;for(;n<=w;)try{let t=typeof b==`function`?await b():b,n=t instanceof FormData,o=E&&r(D);if(o)return o;s=new AbortController;let u=setTimeout(()=>s.abort(),C),d={method:v,headers:{Accept:`application/json`,...n?{}:{"Content-Type":`application/json`},"X-Requested-With":`XMLHttpRequest`,...a.headers,..._.headers,...S},credentials:x?`include`:`same-origin`,signal:s.signal,body:v===`GET`?void 0:n?t:JSON.stringify(t)};for(let e of A)d=await e.fn(d);let f=``;try{let t=e.startsWith(`/`)?e.slice(1):e;f=new URL(t,O).toString()}catch{throw Error(`
2
+ [useFetch] Failed to create URL.
3
+ Base URL : ${O}
4
+ Endpoint : ${e}
5
+ Example valid:
6
+ baseURL -> http://localhost:8080/api/
7
+ endpoint -> /users
8
+ `)}let p=await fetch(`${f}${N?`?${N}`:``}`,d);clearTimeout(u);let m=await p.json();for(let e of j)m=await e.fn(m);if(!p.ok){await l(p.status,m);let e=c?.statusHooks??[];for(let t of e)t.status===p.status&&await t.fn(m);throw m}return E&&i(D,m),m}catch(e){if(n++,n<=w){await t(T);continue}let r=e;for(let e of M)r=await e.fn(r);throw r}};if(k&&v===`GET`){let t=d(e,v,y),n=f(t);if(n)return n;let r=P().finally(()=>m(t));return p(t,r),r}return P()},g=0,_=()=>{let e=[],t=[],n=[];return{interceptor:{request:{use(t){let n=++g;return e.push({id:n,fn:t}),n},eject(t){let n=e.findIndex(e=>e.id===t);n!==-1&&e.splice(n,1)}},response:{use(e){let n=++g;return t.push({id:n,fn:e}),n},eject(e){let n=t.findIndex(t=>t.id===e);n!==-1&&t.splice(n,1)}},error:{use(e){let t=++g;return n.push({id:t,fn:e}),t},eject(e){let t=n.findIndex(t=>t.id===e);t!==-1&&n.splice(t,1)}}},requestInterceptors:e,responseInterceptors:t,errorInterceptors:n}},{interceptor:v,requestInterceptors:y,responseInterceptors:b,errorInterceptors:x}=_(),S=(t,n)=>{if(t)if((0,e.isRef)(t)){(0,e.watch)(t,n,{deep:!0});return}else if((0,e.isReactive)(t)){(0,e.watch)(t,n,{deep:!0});return}else (0,e.watch)(()=>t,n,{deep:!0})};function C(t,n={},r,i){let{immediate:a=!0,watchParams:o=!1,pagination:s=!1,pick:c=``,transform:l,cacheKey:u=t,onBeforeRequest:d,onSuccess:f,onError:p,onFinally:m,...g}=n,_=(0,e.ref)(null),v=(0,e.ref)(null),y=(0,e.ref)(!1),b=(0,e.ref)(`idle`),x=(0,e.ref)([]),C=(0,e.ref)(0),w=(0,e.ref)(0),T=(0,e.ref)(0),E=(0,e.ref)(1),D=new AbortController,O=async()=>{try{y.value=!0,b.value=`pending`,v.value=null,d?.(n.payload);let e=await h({endpoint:t,controller:D,options:{...g,endpoint:t,cacheKey:u},instanceConfig:r,instanceRequestInterceptors:i?.requestInterceptors,instanceResponseInterceptors:i?.responseInterceptors,instanceErrorInterceptors:i?.errorInterceptors});if(s)return _.value=e.result.data,x.value=e.result.links,C.value=e.result.from,w.value=e.result.to,T.value=e.result.total,E.value=e.result.current_page,b.value=`success`,f?.(e),e;let a=c?e[c]:e;return l&&(a=l(a)),_.value=a,b.value=`success`,f?.(a),a}catch(e){b.value=`error`,v.value=e,p?.(e)}finally{y.value=!1,m?.()}};return a&&O(),o&&S(n.params,()=>O()),{data:_,error:v,pending:y,status:b,links:x,from:C,to:w,total:T,currentPage:E,execute:O,refresh:()=>O(),clear:()=>{_.value=null,v.value=null,b.value=`idle`},abort:()=>D.abort()}}function w(e,t={}){return C(e,t)}var T=w;T.baseURL=s.baseURL,T.headers=s.headers,T.interceptor=v,T.onError=s.onError,T.create=e=>{let t=o(e),n=_(),r=(e,r={})=>C(e,r,t,n);return r.baseURL=e=>{t.baseURL=e},r.headers=e=>{t.headers={...t.headers,...e}},r.onError=(e,n)=>{t.statusHooks.push({status:e,fn:n})},r.interceptor=n.interceptor,r},exports.useFetch=T;
package/dist/index.mjs ADDED
@@ -0,0 +1,237 @@
1
+ import { isReactive as e, isRef as t, ref as n, watch as r } from "vue";
2
+ //#region lib/fetch/helpers.ts
3
+ var i = (e) => new Promise((t) => setTimeout(t, e)), a = (e) => {
4
+ let t = {};
5
+ return Object.entries(e || {}).forEach(([e, n]) => {
6
+ n != null && (t[e] = String(n));
7
+ }), new URLSearchParams(t).toString();
8
+ }, o = (e) => {
9
+ let t = sessionStorage.getItem(e);
10
+ return t ? JSON.parse(t) : null;
11
+ }, s = (e, t) => {
12
+ sessionStorage.setItem(e, JSON.stringify(t));
13
+ }, c = {
14
+ baseURL: "your domain",
15
+ headers: {},
16
+ statusHooks: []
17
+ }, l = (e) => ({
18
+ baseURL: e?.baseURL ?? "your domain" ?? "",
19
+ headers: e?.headers ?? {},
20
+ statusHooks: []
21
+ }), u = {
22
+ baseURL(e) {
23
+ c.baseURL = e;
24
+ },
25
+ getBaseURL() {
26
+ return c.baseURL;
27
+ },
28
+ headers(e) {
29
+ c.headers = {
30
+ ...c.headers,
31
+ ...e
32
+ };
33
+ },
34
+ getHeaders() {
35
+ return c.headers;
36
+ },
37
+ onError(e, t) {
38
+ c.statusHooks.push({
39
+ status: e,
40
+ fn: t
41
+ });
42
+ }
43
+ }, d = [], f = async (e, t) => {
44
+ for (let n of d) n.status === e && await n.fn(t);
45
+ }, p = /* @__PURE__ */ new Map(), m = (e, t, n) => `${t}:${e}:${JSON.stringify(n)}`, h = (e) => p.get(e), g = (e, t) => {
46
+ p.set(e, t);
47
+ }, _ = (e) => {
48
+ p.delete(e);
49
+ }, v = async ({ endpoint: e, options: t, controller: n, instanceConfig: r, instanceRequestInterceptors: l, instanceResponseInterceptors: u, instanceErrorInterceptors: d }) => {
50
+ let p = r ?? c, { method: v = "GET", params: y = {}, payload: b = null, credentials: x = !1, headers: S = {}, timeout: C = 1e4, retry: w = 0, retryDelay: T = 1e3, cache: E = !1, cacheKey: D, baseURL: O = p.baseURL, dedup: k = !1 } = t, A = l ?? [], j = u ?? [], M = d ?? [], N = a(y), P = async () => {
51
+ let t = 0;
52
+ for (; t <= w;) try {
53
+ let t = typeof b == "function" ? await b() : b, i = t instanceof FormData, a = E && o(D);
54
+ if (a) return a;
55
+ n = new AbortController();
56
+ let l = setTimeout(() => n.abort(), C), u = {
57
+ method: v,
58
+ headers: {
59
+ Accept: "application/json",
60
+ ...i ? {} : { "Content-Type": "application/json" },
61
+ "X-Requested-With": "XMLHttpRequest",
62
+ ...c.headers,
63
+ ...p.headers,
64
+ ...S
65
+ },
66
+ credentials: x ? "include" : "same-origin",
67
+ signal: n.signal,
68
+ body: v === "GET" ? void 0 : i ? t : JSON.stringify(t)
69
+ };
70
+ for (let e of A) u = await e.fn(u);
71
+ let d = "";
72
+ try {
73
+ let t = e.startsWith("/") ? e.slice(1) : e;
74
+ d = new URL(t, O).toString();
75
+ } catch {
76
+ throw Error(`
77
+ [useFetch] Failed to create URL.
78
+ Base URL : ${O}
79
+ Endpoint : ${e}
80
+ Example valid:
81
+ baseURL -> http://localhost:8080/api/
82
+ endpoint -> /users
83
+ `);
84
+ }
85
+ let m = await fetch(`${d}${N ? `?${N}` : ""}`, u);
86
+ clearTimeout(l);
87
+ let h = await m.json();
88
+ for (let e of j) h = await e.fn(h);
89
+ if (!m.ok) {
90
+ await f(m.status, h);
91
+ let e = r?.statusHooks ?? [];
92
+ for (let t of e) t.status === m.status && await t.fn(h);
93
+ throw h;
94
+ }
95
+ return E && s(D, h), h;
96
+ } catch (e) {
97
+ if (t++, t <= w) {
98
+ await i(T);
99
+ continue;
100
+ }
101
+ let n = e;
102
+ for (let e of M) n = await e.fn(n);
103
+ throw n;
104
+ }
105
+ };
106
+ if (k && v === "GET") {
107
+ let t = m(e, v, y), n = h(t);
108
+ if (n) return n;
109
+ let r = P().finally(() => _(t));
110
+ return g(t, r), r;
111
+ }
112
+ return P();
113
+ }, y = 0, b = () => {
114
+ let e = [], t = [], n = [];
115
+ return {
116
+ interceptor: {
117
+ request: {
118
+ use(t) {
119
+ let n = ++y;
120
+ return e.push({
121
+ id: n,
122
+ fn: t
123
+ }), n;
124
+ },
125
+ eject(t) {
126
+ let n = e.findIndex((e) => e.id === t);
127
+ n !== -1 && e.splice(n, 1);
128
+ }
129
+ },
130
+ response: {
131
+ use(e) {
132
+ let n = ++y;
133
+ return t.push({
134
+ id: n,
135
+ fn: e
136
+ }), n;
137
+ },
138
+ eject(e) {
139
+ let n = t.findIndex((t) => t.id === e);
140
+ n !== -1 && t.splice(n, 1);
141
+ }
142
+ },
143
+ error: {
144
+ use(e) {
145
+ let t = ++y;
146
+ return n.push({
147
+ id: t,
148
+ fn: e
149
+ }), t;
150
+ },
151
+ eject(e) {
152
+ let t = n.findIndex((t) => t.id === e);
153
+ t !== -1 && n.splice(t, 1);
154
+ }
155
+ }
156
+ },
157
+ requestInterceptors: e,
158
+ responseInterceptors: t,
159
+ errorInterceptors: n
160
+ };
161
+ }, { interceptor: x, requestInterceptors: S, responseInterceptors: C, errorInterceptors: w } = b(), T = (n, i) => {
162
+ if (n) if (t(n)) {
163
+ r(n, i, { deep: !0 });
164
+ return;
165
+ } else if (e(n)) {
166
+ r(n, i, { deep: !0 });
167
+ return;
168
+ } else r(() => n, i, { deep: !0 });
169
+ };
170
+ //#endregion
171
+ //#region http/useFetch.ts
172
+ function E(e, t = {}, r, i) {
173
+ let { immediate: a = !0, watchParams: o = !1, pagination: s = !1, pick: c = "", transform: l, cacheKey: u = e, onBeforeRequest: d, onSuccess: f, onError: p, onFinally: m, ...h } = t, g = n(null), _ = n(null), y = n(!1), b = n("idle"), x = n([]), S = n(0), C = n(0), w = n(0), E = n(1), D = new AbortController(), O = async () => {
174
+ try {
175
+ y.value = !0, b.value = "pending", _.value = null, d?.(t.payload);
176
+ let n = await v({
177
+ endpoint: e,
178
+ controller: D,
179
+ options: {
180
+ ...h,
181
+ endpoint: e,
182
+ cacheKey: u
183
+ },
184
+ instanceConfig: r,
185
+ instanceRequestInterceptors: i?.requestInterceptors,
186
+ instanceResponseInterceptors: i?.responseInterceptors,
187
+ instanceErrorInterceptors: i?.errorInterceptors
188
+ });
189
+ if (s) return g.value = n.result.data, x.value = n.result.links, S.value = n.result.from, C.value = n.result.to, w.value = n.result.total, E.value = n.result.current_page, b.value = "success", f?.(n), n;
190
+ let a = c ? n[c] : n;
191
+ return l && (a = l(a)), g.value = a, b.value = "success", f?.(a), a;
192
+ } catch (e) {
193
+ b.value = "error", _.value = e, p?.(e);
194
+ } finally {
195
+ y.value = !1, m?.();
196
+ }
197
+ };
198
+ return a && O(), o && T(t.params, () => O()), {
199
+ data: g,
200
+ error: _,
201
+ pending: y,
202
+ status: b,
203
+ links: x,
204
+ from: S,
205
+ to: C,
206
+ total: w,
207
+ currentPage: E,
208
+ execute: O,
209
+ refresh: () => O(),
210
+ clear: () => {
211
+ g.value = null, _.value = null, b.value = "idle";
212
+ },
213
+ abort: () => D.abort()
214
+ };
215
+ }
216
+ function D(e, t = {}) {
217
+ return E(e, t);
218
+ }
219
+ var O = D;
220
+ O.baseURL = u.baseURL, O.headers = u.headers, O.interceptor = x, O.onError = u.onError, O.create = (e) => {
221
+ let t = l(e), n = b(), r = (e, r = {}) => E(e, r, t, n);
222
+ return r.baseURL = (e) => {
223
+ t.baseURL = e;
224
+ }, r.headers = (e) => {
225
+ t.headers = {
226
+ ...t.headers,
227
+ ...e
228
+ };
229
+ }, r.onError = (e, n) => {
230
+ t.statusHooks.push({
231
+ status: e,
232
+ fn: n
233
+ });
234
+ }, r.interceptor = n.interceptor, r;
235
+ };
236
+ //#endregion
237
+ export { O as useFetch };
@@ -0,0 +1,2 @@
1
+ export declare const getCache: (key: string) => any;
2
+ export declare const setCache: (key: string, value: any) => void;
@@ -0,0 +1,20 @@
1
+ import { HttpHeaders } from './types';
2
+ type StatusHookFn = (error: any) => void | Promise<void>;
3
+ export interface FetchConfigInstance {
4
+ baseURL: string;
5
+ headers: HttpHeaders;
6
+ statusHooks: {
7
+ status: number;
8
+ fn: StatusHookFn;
9
+ }[];
10
+ }
11
+ export declare const globalConfig: FetchConfigInstance;
12
+ export declare const createConfig: (initial?: Partial<FetchConfigInstance>) => FetchConfigInstance;
13
+ export declare const fetchConfig: {
14
+ baseURL(url: string): void;
15
+ getBaseURL(): string;
16
+ headers(headers: HttpHeaders): void;
17
+ getHeaders(): HttpHeaders;
18
+ onError(status: number, fn: StatusHookFn): void;
19
+ };
20
+ export {};
@@ -0,0 +1,13 @@
1
+ import { FetchConfigInstance } from './config';
2
+ import { InterceptorHandler, RequestInterceptorFn, ResponseInterceptorFn, ErrorInterceptorFn } from './types';
3
+ interface FetchCoreParams {
4
+ endpoint: string;
5
+ options: any;
6
+ controller: AbortController;
7
+ instanceConfig?: FetchConfigInstance;
8
+ instanceRequestInterceptors?: InterceptorHandler<RequestInterceptorFn>[];
9
+ instanceResponseInterceptors?: InterceptorHandler<ResponseInterceptorFn>[];
10
+ instanceErrorInterceptors?: InterceptorHandler<ErrorInterceptorFn>[];
11
+ }
12
+ export declare const fetchCore: ({ endpoint, options, controller, instanceConfig, instanceRequestInterceptors, instanceResponseInterceptors, instanceErrorInterceptors, }: FetchCoreParams) => Promise<any>;
13
+ export {};
@@ -0,0 +1,4 @@
1
+ export declare const buildDedupKey: (endpoint: string, method: string, params: Record<string, any>) => string;
2
+ export declare const getDedupRequest: (key: string) => Promise<any> | undefined;
3
+ export declare const setDedupRequest: (key: string, promise: Promise<any>) => void;
4
+ export declare const deleteDedupRequest: (key: string) => void;
@@ -0,0 +1,2 @@
1
+ export declare const sleep: (ms: number) => Promise<unknown>;
2
+ export declare const buildQueryString: (params: Record<string, any>) => string;
@@ -0,0 +1,34 @@
1
+ import { RequestInterceptorFn, ResponseInterceptorFn, ErrorInterceptorFn, InterceptorHandler } from './types';
2
+ export declare const createInterceptor: () => {
3
+ interceptor: {
4
+ request: {
5
+ use(fn: RequestInterceptorFn): number;
6
+ eject(id: number): void;
7
+ };
8
+ response: {
9
+ use(fn: ResponseInterceptorFn): number;
10
+ eject(id: number): void;
11
+ };
12
+ error: {
13
+ use(fn: ErrorInterceptorFn): number;
14
+ eject(id: number): void;
15
+ };
16
+ };
17
+ requestInterceptors: InterceptorHandler<RequestInterceptorFn>[];
18
+ responseInterceptors: InterceptorHandler<ResponseInterceptorFn>[];
19
+ errorInterceptors: InterceptorHandler<ErrorInterceptorFn>[];
20
+ };
21
+ export declare const fetchInterceptor: {
22
+ request: {
23
+ use(fn: RequestInterceptorFn): number;
24
+ eject(id: number): void;
25
+ };
26
+ response: {
27
+ use(fn: ResponseInterceptorFn): number;
28
+ eject(id: number): void;
29
+ };
30
+ error: {
31
+ use(fn: ErrorInterceptorFn): number;
32
+ eject(id: number): void;
33
+ };
34
+ }, requestInterceptors: InterceptorHandler<RequestInterceptorFn>[], responseInterceptors: InterceptorHandler<ResponseInterceptorFn>[], errorInterceptors: InterceptorHandler<ErrorInterceptorFn>[];
@@ -0,0 +1,4 @@
1
+ type StatusHookFn = (error: any) => void | Promise<void>;
2
+ export declare const registerStatusHook: (status: number, fn: StatusHookFn) => void;
3
+ export declare const runStatusHooks: (status: number, error: any) => Promise<void>;
4
+ export {};
@@ -0,0 +1,45 @@
1
+ export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
2
+ export interface HttpHeaders {
3
+ Authorization?: string;
4
+ Accept?: string;
5
+ "Content-Type"?: string;
6
+ "X-CSRF-TOKEN"?: string;
7
+ "X-XSRF-TOKEN"?: string;
8
+ "X-Requested-With"?: string;
9
+ "X-API-KEY"?: string;
10
+ "Accept-Language"?: string;
11
+ "Cache-Control"?: string;
12
+ [key: string]: string | undefined;
13
+ }
14
+ export interface HttpOptions {
15
+ method?: HttpMethod;
16
+ params?: Record<string, any>;
17
+ payload?: any;
18
+ immediate?: boolean;
19
+ pagination?: boolean;
20
+ watchParams?: boolean;
21
+ credentials?: boolean;
22
+ baseURL?: string;
23
+ pick?: string;
24
+ transform?: (data: any) => any;
25
+ timeout?: number;
26
+ cache?: boolean;
27
+ cacheKey?: string;
28
+ retry?: number;
29
+ retryDelay?: number;
30
+ onBeforeRequest?: (payload: any) => void;
31
+ onSuccess?: (data: any) => void;
32
+ onError?: (error: any) => void;
33
+ onFinally?: () => void;
34
+ headers?: HttpHeaders;
35
+ xsrfCookieName?: string;
36
+ xsrfHeaderName?: string;
37
+ dedup?: boolean;
38
+ }
39
+ export type RequestInterceptorFn = (config: RequestInit) => RequestInit | Promise<RequestInit>;
40
+ export type ResponseInterceptorFn = (response: any) => any | Promise<any>;
41
+ export type ErrorInterceptorFn = (error: any) => any | Promise<any>;
42
+ export interface InterceptorHandler<T> {
43
+ id: number;
44
+ fn: T;
45
+ }
@@ -0,0 +1 @@
1
+ export declare const createWatcher: (params: any, callback: () => void) => void;
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@mlyrmdhnn/use-fetch",
3
+ "version": "1.0.0",
4
+ "description": "Lightweight zero-dependency HTTP composable for Vue 3.5+",
5
+ "main": "./dist/index.cjs",
6
+ "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "require": "./dist/index.cjs",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "keywords": [
19
+ "vue",
20
+ "vue3",
21
+ "composable",
22
+ "fetch",
23
+ "typescript",
24
+ "http"
25
+ ],
26
+ "author": "mlyrmdhnn",
27
+ "license": "MIT",
28
+ "peerDependencies": {
29
+ "vue": ">=3.5.0"
30
+ },
31
+ "scripts": {
32
+ "build": "vite build",
33
+ "prepublishOnly": "npm run build"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^25.9.1",
37
+ "typescript": "^6.0.3",
38
+ "vite": "^8.0.14",
39
+ "vite-plugin-dts": "^5.0.1"
40
+ }
41
+ }
package/readme.md ADDED
@@ -0,0 +1,548 @@
1
+ # useFetch
2
+
3
+ A lightweight, modular, zero-dependency HTTP composable for Vue 3.5+, inspired by Nuxt's `useFetch`. Built with TypeScript, works with both **JavaScript** and **TypeScript** Vue projects.
4
+
5
+ ---
6
+
7
+ ## Features
8
+
9
+ - Full **TypeScript** support — works with JS too
10
+ - **Modular architecture** — core, cache, interceptors, watcher, dedup all separated
11
+ - **Auto-execute** on mount with `immediate`
12
+ - **Retry** failed requests with configurable delay
13
+ - **Laravel pagination** support out of the box
14
+ - **Reactive params** watcher (`ref`, `reactive`, or plain object)
15
+ - **Session cache** with custom cache key
16
+ - **Timeout** & **AbortController** support
17
+ - **Pick** & **transform** response data
18
+ - **Global interceptors** — request, response, and error with `eject` support
19
+ - **Status hooks** — global HTTP status code error handling
20
+ - **Request deduplication** — prevent duplicate in-flight requests
21
+ - **Multiple instances** — isolated configs per API service
22
+ - **Credentials** / cookie support
23
+ - **FormData** auto-detection
24
+ - Lifecycle callbacks: `onBeforeRequest`, `onSuccess`, `onError`, `onFinally`
25
+ - **Zero dependencies**
26
+
27
+ ---
28
+
29
+ ## Requirements
30
+
31
+ - Vue `3.5+`
32
+ - Vite (uses `import.meta.env.VITE_API_URL` as default base URL)
33
+
34
+ ---
35
+
36
+ ## Installation
37
+
38
+ ```bash
39
+ npm install @mlyrmdhnn/usefetch
40
+ ```
41
+
42
+ ---
43
+
44
+ ## Setup
45
+
46
+ Set your base API URL in `.env`:
47
+
48
+ ```env
49
+ VITE_API_URL=https://your-api.com/api/
50
+ ```
51
+
52
+ Or set it manually in `main.ts` / `main.js`:
53
+
54
+ ```ts
55
+ import { useFetch } from "@mlyrmdhnn/usefetch";
56
+
57
+ useFetch.baseURL("https://your-api.com/api/");
58
+ ```
59
+
60
+ ---
61
+
62
+ ## Project Structure
63
+
64
+ ```
65
+ lib/
66
+ └── fetch/
67
+ ├── cache.ts # SessionStorage cache handler
68
+ ├── config.ts # Global config & instance factory
69
+ ├── core.ts # Core fetch logic
70
+ ├── dedup.ts # Request deduplication
71
+ ├── helpers.ts # Utility functions
72
+ ├── interceptor.ts # Global & instance interceptors
73
+ ├── statusHooks.ts # HTTP status code hooks
74
+ ├── types.ts # TypeScript types & interfaces
75
+ └── watcher.ts # Reactive params watcher
76
+ http/
77
+ └── useFetch.ts # Main composable entry point
78
+ ```
79
+
80
+ ---
81
+
82
+ ## Basic Usage
83
+
84
+ ### JavaScript
85
+
86
+ ```js
87
+ import { useFetch } from "@mlyrmdhnn/usefetch";
88
+
89
+ const { data, pending, error } = useFetch("/users");
90
+ ```
91
+
92
+ ### TypeScript
93
+
94
+ ```ts
95
+ import { useFetch } from "@mlyrmdhnn/usefetch";
96
+
97
+ const { data, pending, error } = useFetch<User[]>("/users");
98
+ ```
99
+
100
+ ---
101
+
102
+ ## API
103
+
104
+ ### Signature
105
+
106
+ ```ts
107
+ useFetch<T>(endpoint: string, options?: HttpOptions)
108
+ ```
109
+
110
+ ### Options
111
+
112
+ | Option | Type | Default | Description |
113
+ | ----------------- | --------------------- | -------------- | --------------------------------- |
114
+ | `method` | `HttpMethod` | `'GET'` | HTTP method |
115
+ | `params` | `Record<string, any>` | `{}` | URL query parameters |
116
+ | `payload` | `any` | `null` | Request body |
117
+ | `immediate` | `boolean` | `true` | Auto-execute on composable call |
118
+ | `pagination` | `boolean` | `false` | Enable Laravel pagination mode |
119
+ | `watchParams` | `boolean` | `false` | Re-fetch when params change |
120
+ | `credentials` | `boolean` | `false` | Include cookies/credentials |
121
+ | `baseURL` | `string` | `VITE_API_URL` | Override base URL per request |
122
+ | `pick` | `string` | `''` | Pick a specific key from response |
123
+ | `transform` | `(data: any) => any` | - | Transform response data |
124
+ | `timeout` | `number` | `10000` | Request timeout in ms |
125
+ | `cache` | `boolean` | `false` | Enable sessionStorage cache |
126
+ | `cacheKey` | `string` | `endpoint` | Custom cache key |
127
+ | `retry` | `number` | `0` | Number of retry attempts |
128
+ | `retryDelay` | `number` | `1000` | Delay between retries in ms |
129
+ | `dedup` | `boolean` | `false` | Enable request deduplication |
130
+ | `headers` | `HttpHeaders` | `{}` | Custom request headers |
131
+ | `onBeforeRequest` | `(payload) => void` | - | Callback before request fires |
132
+ | `onSuccess` | `(data) => void` | - | Callback on success |
133
+ | `onError` | `(error) => void` | - | Callback on error |
134
+ | `onFinally` | `() => void` | - | Callback after request completes |
135
+
136
+ ### Return Values
137
+
138
+ | Value | Type | Description |
139
+ | ------------- | -------------------------------------------------- | --------------------------------- |
140
+ | `data` | `Ref<T \| null>` | Response data |
141
+ | `error` | `Ref<any>` | Error object if request failed |
142
+ | `pending` | `Ref<boolean>` | Loading state |
143
+ | `status` | `Ref<'idle' \| 'pending' \| 'success' \| 'error'>` | Request status |
144
+ | `links` | `Ref<any[]>` | Pagination links (Laravel) |
145
+ | `from` | `Ref<number>` | Pagination from (Laravel) |
146
+ | `to` | `Ref<number>` | Pagination to (Laravel) |
147
+ | `total` | `Ref<number>` | Total records (Laravel) |
148
+ | `currentPage` | `Ref<number>` | Current page (Laravel) |
149
+ | `execute` | `() => Promise` | Manually trigger request |
150
+ | `refresh` | `() => Promise` | Alias for execute |
151
+ | `clear` | `() => void` | Reset data, error, status to idle |
152
+ | `abort` | `() => void` | Cancel the active request |
153
+
154
+ ---
155
+
156
+ ## Examples
157
+
158
+ ### GET Request
159
+
160
+ ```ts
161
+ const { data, pending, error } = useFetch<Product[]>("/products");
162
+ ```
163
+
164
+ ---
165
+
166
+ ### POST Request
167
+
168
+ ```ts
169
+ const { execute, pending } = useFetch("/products", {
170
+ method: "POST",
171
+ payload: {
172
+ name: "New Product",
173
+ price: 50000,
174
+ },
175
+ immediate: false,
176
+ onSuccess(data) {
177
+ console.log("Created:", data);
178
+ },
179
+ onError(err) {
180
+ console.error("Failed:", err);
181
+ },
182
+ });
183
+
184
+ await execute();
185
+ ```
186
+
187
+ ---
188
+
189
+ ### PUT / PATCH / DELETE
190
+
191
+ ```ts
192
+ // PUT
193
+ const { execute } = useFetch("/products/1", {
194
+ method: "PUT",
195
+ payload: { name: "Updated" },
196
+ immediate: false,
197
+ });
198
+
199
+ // PATCH
200
+ const { execute } = useFetch("/products/1", {
201
+ method: "PATCH",
202
+ payload: { price: 60000 },
203
+ immediate: false,
204
+ });
205
+
206
+ // DELETE
207
+ const { execute } = useFetch("/products/1", {
208
+ method: "DELETE",
209
+ immediate: false,
210
+ });
211
+ ```
212
+
213
+ ---
214
+
215
+ ### With Query Params
216
+
217
+ ```ts
218
+ const { data } = useFetch("/orders", {
219
+ params: {
220
+ status: "active",
221
+ limit: 10,
222
+ },
223
+ });
224
+ ```
225
+
226
+ > `null` and `undefined` param values are automatically stripped from the query string.
227
+
228
+ ---
229
+
230
+ ### Reactive Params (Auto Re-fetch)
231
+
232
+ ```ts
233
+ import { reactive } from "vue";
234
+
235
+ const params = reactive({ page: 1, search: "" });
236
+
237
+ const { data } = useFetch("/users", {
238
+ params,
239
+ watchParams: true,
240
+ });
241
+
242
+ // changing params will automatically re-fetch
243
+ params.page = 2;
244
+ params.search = "john";
245
+ ```
246
+
247
+ Supports `ref`, `reactive`, and plain objects.
248
+
249
+ ---
250
+
251
+ ### Laravel Pagination
252
+
253
+ ```ts
254
+ const { data, total, currentPage, links } = useFetch("/invoices", {
255
+ pagination: true,
256
+ params: { page: 1 },
257
+ });
258
+ ```
259
+
260
+ Expects this response structure from Laravel:
261
+
262
+ ```json
263
+ {
264
+ "result": {
265
+ "data": [],
266
+ "links": [],
267
+ "from": 1,
268
+ "to": 10,
269
+ "total": 100,
270
+ "current_page": 1
271
+ }
272
+ }
273
+ ```
274
+
275
+ ---
276
+
277
+ ### Pick Specific Key
278
+
279
+ ```ts
280
+ // Response: { status: true, result: [...] }
281
+ const { data } = useFetch("/users", {
282
+ pick: "result",
283
+ });
284
+ // data.value → [...]
285
+ ```
286
+
287
+ ---
288
+
289
+ ### Transform Response
290
+
291
+ ```ts
292
+ const { data } = useFetch("/users", {
293
+ transform: (res) => res.map((user) => user.name),
294
+ });
295
+ ```
296
+
297
+ ---
298
+
299
+ ### Upload FormData
300
+
301
+ ```ts
302
+ const form = new FormData();
303
+ form.append("file", file);
304
+ form.append("name", "document.pdf");
305
+
306
+ const { execute } = useFetch("/upload", {
307
+ method: "POST",
308
+ payload: form,
309
+ immediate: false,
310
+ });
311
+ ```
312
+
313
+ > `Content-Type` is automatically omitted for `FormData` so the browser sets the correct multipart boundary.
314
+
315
+ ---
316
+
317
+ ### Session Cache
318
+
319
+ ```ts
320
+ const { data } = useFetch("/config", {
321
+ cache: true,
322
+ cacheKey: "app-config",
323
+ });
324
+ ```
325
+
326
+ > Cached in `sessionStorage`. Clears when the browser tab is closed.
327
+
328
+ ---
329
+
330
+ ### Retry on Failure
331
+
332
+ ```ts
333
+ const { data } = useFetch("/unstable-endpoint", {
334
+ retry: 3,
335
+ retryDelay: 2000, // wait 2s between each retry
336
+ });
337
+ ```
338
+
339
+ ---
340
+
341
+ ### Request Deduplication
342
+
343
+ Prevents the same request from firing multiple times simultaneously. Useful when multiple components mount at the same time and need the same data.
344
+
345
+ ```ts
346
+ // ComponentA.vue
347
+ const { data } = useFetch("/user/profile", { dedup: true });
348
+
349
+ // ComponentB.vue — mounts at the same time
350
+ const { data } = useFetch("/user/profile", { dedup: true });
351
+
352
+ // Result: only ONE actual HTTP request fires, both components get the same result ✅
353
+ ```
354
+
355
+ > Deduplication only applies to `GET` requests.
356
+
357
+ ---
358
+
359
+ ### Manual Execute + Abort
360
+
361
+ ```ts
362
+ const { execute, abort } = useFetch("/long-request", {
363
+ immediate: false,
364
+ });
365
+
366
+ execute();
367
+ abort(); // cancel anytime
368
+ ```
369
+
370
+ ---
371
+
372
+ ## Global Configuration
373
+
374
+ Set once in `main.ts` or `main.js`, applies to every request automatically.
375
+
376
+ ### Base URL
377
+
378
+ ```ts
379
+ useFetch.baseURL("https://api.example.com/api/");
380
+ ```
381
+
382
+ ### Default Headers
383
+
384
+ ```ts
385
+ useFetch.headers({
386
+ "X-API-KEY": "your-api-key",
387
+ "Accept-Language": "id",
388
+ });
389
+ ```
390
+
391
+ > Per-request headers always override global headers.
392
+
393
+ ---
394
+
395
+ ## Global Interceptors
396
+
397
+ ### Request Interceptor
398
+
399
+ Runs before every request. Useful for injecting auth tokens globally.
400
+
401
+ ```ts
402
+ const id = useFetch.interceptor.request.use(async (config) => {
403
+ const token = localStorage.getItem("token");
404
+ if (token) {
405
+ config.headers["Authorization"] = `Bearer ${token}`;
406
+ }
407
+ return config;
408
+ });
409
+
410
+ // remove later if needed
411
+ useFetch.interceptor.request.eject(id);
412
+ ```
413
+
414
+ ### Response Interceptor
415
+
416
+ Runs after every successful response.
417
+
418
+ ```ts
419
+ const id = useFetch.interceptor.response.use(async (result) => {
420
+ console.log("Response received:", result);
421
+ return result;
422
+ });
423
+
424
+ useFetch.interceptor.response.eject(id);
425
+ ```
426
+
427
+ ### Error Interceptor
428
+
429
+ Runs when a request throws or response is not ok.
430
+
431
+ ```ts
432
+ const id = useFetch.interceptor.error.use(async (error) => {
433
+ console.error("Request failed:", error);
434
+ return error;
435
+ });
436
+
437
+ useFetch.interceptor.error.eject(id);
438
+ ```
439
+
440
+ ---
441
+
442
+ ## Status Hooks
443
+
444
+ Handle specific HTTP status codes globally — no need to repeat error handling in every component.
445
+
446
+ ```ts
447
+ // main.ts / main.js
448
+ useFetch.onError(401, () => {
449
+ router.push("/login");
450
+ });
451
+
452
+ useFetch.onError(403, () => {
453
+ toast.error("You are not authorized!");
454
+ });
455
+
456
+ useFetch.onError(404, () => {
457
+ toast.error("Resource not found!");
458
+ });
459
+
460
+ useFetch.onError(500, () => {
461
+ toast.error("Server error, please try again later");
462
+ });
463
+ ```
464
+
465
+ ---
466
+
467
+ ## Multiple Instances
468
+
469
+ Create isolated instances with their own base URL, headers, interceptors, and status hooks. Perfect for projects that communicate with multiple APIs.
470
+
471
+ ```ts
472
+ // main.ts / main.js
473
+ import { useFetch } from "@mlyrmdhnn/usefetch";
474
+
475
+ // global instance
476
+ useFetch.baseURL("https://api.example.com/api/");
477
+
478
+ // isolated instance for payment service
479
+ export const paymentApi = useFetch.create({
480
+ baseURL: "https://payment.example.com/api/",
481
+ headers: { "X-API-KEY": "payment-key" },
482
+ });
483
+
484
+ // isolated instance for reporting service
485
+ export const reportApi = useFetch.create({
486
+ baseURL: "https://report.example.com/api/",
487
+ });
488
+ ```
489
+
490
+ Each instance has its own `baseURL`, `headers`, `interceptor`, and `onError`:
491
+
492
+ ```ts
493
+ // instance specific interceptor
494
+ paymentApi.interceptor.request.use(async (config) => {
495
+ config.headers["X-Signature"] = generateSignature();
496
+ return config;
497
+ });
498
+
499
+ // instance specific status hook
500
+ paymentApi.onError(422, (err) => {
501
+ toast.error("Payment validation failed");
502
+ });
503
+ ```
504
+
505
+ Then use anywhere:
506
+
507
+ ```ts
508
+ // global instance
509
+ const { data } = useFetch("/users");
510
+
511
+ // payment instance
512
+ const { data } = paymentApi("/transactions");
513
+
514
+ // report instance
515
+ const { data } = reportApi("/summary");
516
+ ```
517
+
518
+ ---
519
+
520
+ ## HTTP Methods
521
+
522
+ ```ts
523
+ type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
524
+ ```
525
+
526
+ ---
527
+
528
+ ## TypeScript Support
529
+
530
+ All types are exported and available:
531
+
532
+ ```ts
533
+ import type {
534
+ HttpOptions,
535
+ HttpHeaders,
536
+ HttpMethod,
537
+ RequestInterceptorFn,
538
+ ResponseInterceptorFn,
539
+ ErrorInterceptorFn,
540
+ InterceptorHandler,
541
+ } from "@mlyrmdhnn/usefetch";
542
+ ```
543
+
544
+ ---
545
+
546
+ ## License
547
+
548
+ MIT