sushi-fetch 0.1.0 → 0.2.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.
package/README.md CHANGED
@@ -1,210 +1,220 @@
1
- # ðŸĢ sushi-fetch
1
+ <div align="center">
2
+ <h1>ðŸĢ sushi-fetch</h1>
3
+ <p><strong>Data fetching should be simple, fast, and delicious.</strong></p>
4
+ <p>A tiny, zero-dependency, and highly-optimized data-fetching & caching library for modern JavaScript and TypeScript apps.</p>
2
5
 
3
- > **Simple, fast, and powerful data fetching with built-in caching,
4
- > deduplication, and retry --- for modern JavaScript.**
6
+ <p>
7
+ <a href="https://www.npmjs.com/package/sushi-fetch"><img src="https://img.shields.io/npm/v/sushi-fetch?color=33cd56&logo=npm" alt="NPM Version" /></a>
8
+ <a href="https://www.npmjs.com/package/sushi-fetch"><img src="https://img.shields.io/npm/dm/sushi-fetch?color=blue" alt="NPM Downloads" /></a>
9
+ <a href="https://bundlephobia.com/package/sushi-fetch"><img src="https://img.shields.io/bundlephobia/minzip/sushi-fetch?color=success&label=size" alt="Bundle Size" /></a>
10
+ <img src="https://img.shields.io/node/v/sushi-fetch" alt="Node Version" />
11
+ <img src="https://img.shields.io/badge/TypeScript-Ready-blue?logo=typescript" alt="TypeScript" />
12
+ <img src="https://img.shields.io/badge/dependencies-0-brightgreen" alt="Zero Dependencies" />
13
+ </p>
14
+ </div>
5
15
 
6
- ![npm](https://img.shields.io/npm/v/sushi-fetch)
7
- ![downloads](https://img.shields.io/npm/dm/sushi-fetch)
8
- ![license](https://img.shields.io/npm/l/sushi-fetch)
9
- ![typescript](https://img.shields.io/badge/types-TypeScript-blue)
10
- ![bundle](https://img.shields.io/bundlephobia/min/sushi-fetch)
11
- ![node](https://img.shields.io/node/v/sushi-fetch)
12
- ![stars](https://img.shields.io/github/stars/sushilibdev/sushi-fetch?style=social)
16
+ ---
13
17
 
14
- ------------------------------------------------------------------------
18
+ ## ðŸĪ” Why sushi-fetch?
15
19
 
16
- ## âœĻ Features
20
+ Most HTTP clients give you only the basics. You still end up writing your own wrappers for caching, retries, and request deduplication. **sushi-fetch** is designed to solve that out-of-the-box without bloating your bundle size.
17
21
 
18
- - ⚡ Fast & Lightweight
19
- - ðŸ“Ķ Built-in Cache (TTL support)
20
- - 🔁 Request Deduplication
21
- - 🔄 Retry System (fixed & exponential)
22
- - ⏱ïļ Timeout Control
23
- - â™ŧïļ Stale-While-Revalidate support
24
- - ðŸŽŊ Fully Typed with TypeScript
25
- - 🧠 Smart & Minimal API
26
- - 🔌 Works in Node.js & modern environments
22
+ Built on top of the native `globalThis.fetch`, it provides the intelligence of massive libraries (like SWR or React Query) in a fraction of the size.
27
23
 
28
- ------------------------------------------------------------------------
24
+ ### âœĻ The Superpowers
25
+ * ðŸ“Ķ **Built-in Smart Caching (TTL + LRU):** Responses are automatically cached and reused.
26
+ * ⚡ **Stale-While-Revalidate (SWR):** Instant UI updates with background revalidation.
27
+ * 🔁 **Request Deduplication:** Prevents "Cache Stampedes". 100 identical parallel requests will result in exactly **1** network call.
28
+ * ðŸ“Ą **Reactivity (Pub/Sub):** Subscribe to cache keys and mutate data for Optimistic Updates.
29
+ * 🏷ïļ **Cache Tagging:** Group related requests and invalidate them instantly by tag.
30
+ * 🔄 **Smart Retries:** Handle flaky networks gracefully with fixed or exponential backoff strategies.
31
+ * 🔌 **Global Middleware:** Intercept requests, responses, and errors globally.
32
+ * ðŸŠķ **Zero Dependencies:** Pure, modern JavaScript.
33
+
34
+ ---
29
35
 
30
36
  ## ðŸ“Ķ Installation
31
37
 
32
- ``` bash
38
+ ```bash
33
39
  npm install sushi-fetch
34
40
  ```
35
41
 
36
- or
37
-
38
- ``` bash
39
- yarn add sushi-fetch
40
- ```
42
+ Also works perfectly with `yarn`, `pnpm`, and `bun`.
41
43
 
42
- ------------------------------------------------------------------------
44
+ ---
43
45
 
44
46
  ## 🚀 Quick Start
45
47
 
46
- ``` ts
48
+ **1. Basic Fetch & Cache (Node / Vanilla JS)**
49
+
50
+ ```ts
47
51
  import { sushiFetch } from "sushi-fetch"
48
52
 
49
- const users = await sushiFetch("https://jsonplaceholder.typicode.com/users", {
50
- cache: true,
51
- ttl: 10000,
53
+ // First request: Hits the network
54
+ const users = await sushiFetch("[https://api.example.com/users](https://api.example.com/users)", {
55
+ ttl: 60000, // Cache for 60 seconds
52
56
  retries: 2
53
57
  })
54
58
 
55
- console.log(users)
59
+ // Second request (immediately after): INSTANT (0-1ms) from memory!
60
+ const cachedUsers = await sushiFetch("[https://api.example.com/users](https://api.example.com/users)")
56
61
  ```
57
62
 
58
- ------------------------------------------------------------------------
59
-
60
- ## ⚙ïļ API
61
-
62
- ### sushiFetch(url, options?)
63
-
64
- Fetch data with powerful built-in features.
63
+ **2. React Integration (Reactivity & Hooks)**
65
64
 
66
- #### Parameters
65
+ sushi-fetch exposes a powerful `subscribe` and `mutate` API, making it trivial to create reactive components.
67
66
 
68
- --------------------------------------------------------------------------
69
- Name Type Default Description
70
- --------------- ------------ ----------------- ---------------------------
71
- url string --- API endpoint
67
+ ```ts
68
+ import { useEffect, useState } from "react"
69
+ import { sushiFetch, sushiCache } from "sushi-fetch"
72
70
 
73
- cache boolean true Enable caching
71
+ export function useSushi<T>(url: string) {
72
+ const [data, setData] = useState<T | null>(() => sushiCache.get(url))
74
73
 
75
- ttl number 5000 Cache lifetime (ms)
74
+ useEffect(() => {
75
+ // 1. Fetch and revalidate in background
76
+ sushiFetch<T>(url, { revalidate: true })
76
77
 
77
- revalidate boolean false Return cached data &
78
- revalidate in background
78
+ // 2. Subscribe to cache mutations
79
+ const unsubscribe = sushiCache.subscribe<T>(url, setData)
80
+ return () => unsubscribe()
81
+ }, [url])
79
82
 
80
- timeout number --- Request timeout in ms
83
+ return { data }
84
+ }
81
85
 
82
- retries number 0 Retry attempts
83
-
84
- retryDelay number 500 Delay between retries
86
+ // In your component:
87
+ // Mutate cache directly for Optimistic Updates!
88
+ // sushiCache.mutate("/api/users", [...newData])
89
+ ```
85
90
 
86
- retryStrategy "fixed" "exponential" Retry strategy
91
+ **3. Request Deduplication**
87
92
 
88
- parseJson boolean true Parse response as JSON
93
+ Stop spamming your servers. sushi-fetch automatically groups identical requests made at the exact same time.
89
94
 
90
- onSuccess (data) =\> --- Success callback
91
- void
95
+ ```ts
96
+ // Only ONE network request is actually sent to the server.
97
+ await Promise.all([
98
+ sushiFetch("[https://api.example.com/data](https://api.example.com/data)"),
99
+ sushiFetch("[https://api.example.com/data](https://api.example.com/data)"),
100
+ sushiFetch("[https://api.example.com/data](https://api.example.com/data)"),
101
+ ])
102
+ ```
92
103
 
93
- onError (error) =\> --- Error callback
94
- void
104
+ **4. Cache Tags & Invalidation**
95
105
 
96
- cacheKey string auto Custom cache key
97
- --------------------------------------------------------------------------
106
+ Easily manage complex caches by grouping them with tags.
98
107
 
99
- ------------------------------------------------------------------------
108
+ ```ts
109
+ import { sushiFetch, sushiCache } from "sushi-fetch"
100
110
 
101
- ## 🧠 Caching Example
111
+ // Assign tags during fetch
112
+ await sushiFetch("/api/posts/1", { cacheTags: ["posts-group"] })
113
+ await sushiFetch("/api/posts/2", { cacheTags: ["posts-group"] })
102
114
 
103
- ``` ts
104
- await sushiFetch("/api/data", {
105
- cache: true,
106
- ttl: 10000
107
- })
115
+ // Later, invalidate all posts instantly:
116
+ sushiCache.invalidateTag("posts-group")
108
117
  ```
109
118
 
110
- ------------------------------------------------------------------------
119
+ **5. Global Middleware**
111
120
 
112
- ## â™ŧïļ Stale-While-Revalidate
113
-
114
- ``` ts
115
- await sushiFetch("/api/data", {
116
- cache: true,
117
- revalidate: true
118
- })
119
- ```
121
+ Log requests, add auth headers, or handle errors globally.
120
122
 
121
- ------------------------------------------------------------------------
123
+ ```ts
124
+ import { addSushiMiddleware } from "sushi-fetch"
122
125
 
123
- ## 🔁 Retry Example
124
-
125
- ``` ts
126
- await sushiFetch("/api/data", {
127
- retries: 3,
128
- retryStrategy: "exponential",
129
- retryDelay: 500
126
+ addSushiMiddleware({
127
+ onRequest: (ctx) => {
128
+ ctx.options.headers = { ...ctx.options.headers, Authorization: "Bearer token" }
129
+ },
130
+ onResponse: (res) => console.log(`✅ Success: ${res.status}`),
131
+ onError: (err) => console.error(`❌ Fetch failed:`, err),
130
132
  })
131
133
  ```
132
134
 
133
- ------------------------------------------------------------------------
134
-
135
- ## ⏱ïļ Timeout Example
135
+ ---
136
136
 
137
- ``` ts
138
- await sushiFetch("/api/data", {
139
- timeout: 3000
140
- })
141
- ```
137
+ ## ⚙ïļ API Reference
142
138
 
143
- ------------------------------------------------------------------------
139
+ `sushiFetch(url, options?)`
144
140
 
145
- ## ðŸ“Ķ Cache Utilities
141
+ | Option | Type | Default | Description |
142
+ | ---------- | ---------- | ---------- | ---------- |
143
+ | `cache` | `boolean` | `true` | Enable/disable cache entirely |
144
+ | `ttl` | `number` | `5000` | Cache lifetime (in milliseconds) |
145
+ | `revalidate` | `boolean` | `false` | Return cached data instantly, but refresh in background |
146
+ | `timeout` | `number` | `-` | Request timeout (aborts if exceeded) |
147
+ | `retries` | `number` | `0` | Number of retry attempts on failure |
148
+ | `retryDelay` | `number` | `500` | Delay between retries (in ms) |
149
+ | `retryStrategy` | `"fixed" | "exponential"` | `"exponential"` | Backoff algorithm for retries |
150
+ | `cacheTags` | `string[]` | `[]` | Tags for grouped cache invalidation |
151
+ | `transform` | `(data) => any` | `-` | Format data before caching it |
152
+ | `onSuccess` | `(data) => void` | `-` | Hook triggered on successful fetch |
153
+ | `onError` | `(error) => void` | `-` | Hook triggered on failed fetch |
146
154
 
147
- ``` ts
148
- import { sushiCache } from "sushi-fetch"
155
+ **`sushiCache` Utilities**
149
156
 
157
+ ```ts
158
+ sushiCache.get(key)
159
+ sushiCache.set(key, data, ttl?)
150
160
  sushiCache.has(key)
151
161
  sushiCache.delete(key)
152
162
  sushiCache.clear()
153
- ```
154
-
155
- ------------------------------------------------------------------------
156
-
157
- ## ðŸ§Đ Advanced Example
158
163
 
159
- ``` ts
160
- const data = await sushiFetch("https://api.example.com/posts", {
161
- cache: true,
162
- ttl: 60000,
163
- retries: 2,
164
- timeout: 5000,
165
- revalidate: true,
166
- onSuccess: (data) => console.log("Success:", data),
167
- onError: (err) => console.error("Error:", err)
168
- })
164
+ // Pub/Sub & Mutate
165
+ sushiCache.subscribe(key, listener)
166
+ sushiCache.mutate(key, mutatorData)
167
+ sushiCache.invalidateTag(tag)
169
168
  ```
170
169
 
171
- ------------------------------------------------------------------------
170
+ ---
172
171
 
173
- ## 🛠ïļ Roadmap
172
+ ## 🆚 Comparison
174
173
 
175
- - AbortController support
176
- - Middleware / interceptor system
177
- - Polling / auto re-fetch
178
- - React hooks (useSushiFetch)
179
- - Devtools debugging mode
180
- - SSR utilities
174
+ | Features | sushi-fetch | axios | swr |
175
+ | Zero Dependencies | ✅ | ❌ | ❌ |
176
+ | Built-in cache | ✅ | ❌ | ✅ |
177
+ | Request deduplication | ✅ | ❌ | ✅ |
178
+ | Retry & Timeout system | ✅ | ✅ | ❌ |
179
+ | Pub/Sub Reactivity | ✅ | ❌ | ✅ |
180
+ | Cache tags | ✅ | ❌ | ❌ |
181
+ | Bundle size | ~5kb | ~30kb | ~15kb |
181
182
 
182
- ------------------------------------------------------------------------
183
+ <div align="center">
184
+ <p>While tools like Axios have a mature ecosystem, **sushi-fetch** focuses on giving you the modern SWR-like caching and fetching experience in a drastically smaller, zero-dependency package.</p>
185
+ <div>
183
186
 
184
- ## ðŸĪ Contributing
187
+ ---
185
188
 
186
- Contributions, issues, and feature requests are welcome!
189
+ ## ðŸ›Ģ Roadmap
187
190
 
188
- Feel free to open a PR or issue 💛
191
+ - [x] Global Middleware system
192
+ - [x] Reactivity (Pub/Sub & Mutate)
193
+ - [x] Cache tagging
194
+ - [ ] Built-in React Hooks package (`@sushi-fetch/react`)
195
+ - [ ] Polling / Auto referch interval
196
+ - [ ] Devtools extensions
189
197
 
190
- ------------------------------------------------------------------------
198
+ ---
191
199
 
192
- ## 📄 License
200
+ ## ðŸĪ Contributing
201
+
202
+ Pull requests, issues, and feature ideas are highly welcome!
193
203
 
194
- MIT ÂĐ 2026 --- Sushi-Fetch Project
204
+ If you like this project, consider:
195
205
 
196
- ------------------------------------------------------------------------
206
+ - ⭐ Starring the repo
207
+ - ðŸĢ Sharing it with your team
208
+ - 🐛 Reporting bugs
197
209
 
198
- ## 🌟 Support
210
+ ---
199
211
 
200
- If you like this project:
212
+ ## 💖 Sponsors
201
213
 
202
- - ⭐ Star this repo
203
- - ðŸĢ Share it with others
204
- - 🐛 Report bugs & ideas
214
+ I’m building this project independently. If `sushi-fetch` saves you time and headache, consider supporting its development âĪïļ Every bit of support helps keep the project alive and brewing new features!
205
215
 
206
- ------------------------------------------------------------------------
216
+ ---
207
217
 
208
- # ðŸ”Ĩ Tagline
218
+ ## 📄 License
209
219
 
210
- > sushi-fetch --- fetching data should be simple, fast, and delicious ðŸĢ
220
+ MIT ÂĐ 2026 — sushilibdev
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ "use strict";var x=Object.defineProperty;var k=Object.getOwnPropertyDescriptor;var K=Object.getOwnPropertyNames;var j=Object.prototype.hasOwnProperty;var B=(s,e)=>{for(var t in e)x(s,t,{get:e[t],enumerable:!0})},J=(s,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of K(e))!j.call(s,n)&&n!==t&&x(s,n,{get:()=>e[n],enumerable:!(r=k(e,n))||r.enumerable});return s};var V=s=>J(x({},"__esModule",{value:!0}),s);var se={};B(se,{SushiCache:()=>p,VERSION:()=>te,addSushiMiddleware:()=>O,fetcher:()=>w,sushiCache:()=>L,sushiFetch:()=>D});module.exports=V(se);var p=class{store=new Map;pendingFetches=new Map;listeners=new Map;maxSize;defaultTTL;sliding;onEvict;hits=0;misses=0;cleanupTimer;constructor(e={}){this.maxSize=e.maxSize??1/0,this.defaultTTL=e.defaultTTL??5e3,this.sliding=e.sliding??!1,this.onEvict=e.onEvict,e.cleanupInterval&&(this.cleanupTimer=setInterval(()=>{this.pruneExpired()},e.cleanupInterval),typeof this.cleanupTimer.unref=="function"&&this.cleanupTimer.unref())}subscribe(e,t){return this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(t),()=>{this.listeners.get(e)?.delete(t),this.listeners.get(e)?.size===0&&this.listeners.delete(e)}}notify(e,t){this.listeners.has(e)&&this.listeners.get(e).forEach(r=>r(t))}set(e,t,r=this.defaultTTL){let n=Date.now();this.store.has(e)&&this.store.delete(e),this.store.set(e,{data:t,expiry:n+r,lastAccess:n}),this.evictIfNeeded(),this.notify(e,t)}get(e){let t=this.store.get(e);if(!t)return this.misses++,null;let r=Date.now();return r>t.expiry?(this.delete(e),this.misses++,null):(t.lastAccess=r,this.sliding&&(t.expiry=r+this.defaultTTL),this.store.delete(e),this.store.set(e,t),this.hits++,t.data)}peek(e){let t=this.store.get(e);return t?Date.now()>t.expiry?(this.delete(e),null):t.data:null}has(e){return this.peek(e)!==null}delete(e){let t=this.store.get(e);t&&(this.onEvict&&this.onEvict(e,t.data),this.store.delete(e),this.notify(e,null))}mutate(e,t,r=this.defaultTTL){let n=this.get(e),i=typeof t=="function"?t(n):t;return this.set(e,i,r),i}clear(){if(this.onEvict)for(let[e,t]of this.store.entries())this.onEvict(e,t.data);this.store.clear();for(let e of this.listeners.keys())this.notify(e,null)}evictIfNeeded(){for(;this.store.size>this.maxSize;){let e=this.store.keys().next().value;if(e!==void 0)this.delete(e);else break}}async getOrSet(e,t,r=this.defaultTTL){let n=this.get(e);if(n!==null)return n;if(this.pendingFetches.has(e))return this.pendingFetches.get(e);let i=(async()=>{try{let l=await t();return this.set(e,l,r),l}finally{this.pendingFetches.delete(e)}})();return this.pendingFetches.set(e,i),i}async getOrSetSWR(e,t,r=this.defaultTTL){let n=this.store.get(e),i=Date.now();return n?i>n.expiry?(this.revalidate(e,t,r).catch(T=>{console.error(`[SushiCache] SWR Background fetch failed for key ${e}:`,T)}),n.data):(this.hits++,n.data):this.getOrSet(e,t,r)}async revalidate(e,t,r){if(this.pendingFetches.has(e))return;let n=t().then(i=>{this.set(e,i,r)}).finally(()=>{this.pendingFetches.delete(e)});return this.pendingFetches.set(e,n),n}pruneExpired(){let e=Date.now();for(let[t,r]of this.store.entries())e>r.expiry&&this.delete(t)}destroy(){this.cleanupTimer&&clearInterval(this.cleanupTimer),this.clear(),this.listeners.clear(),this.pendingFetches.clear()}};var u=new p,m=new Map,E=new Set,g=new Map,W=5e3,G=process.env.NODE_ENV!=="production";function f(...s){G&&console.log("[sushi-fetch]",...s)}var F=[];function H(s){F.push(s)}var U=s=>new Promise(e=>setTimeout(e,s));function Q(s){if(!s)return null;let e=new AbortController,t=setTimeout(()=>e.abort(),s);return e.signal.addEventListener("abort",()=>clearTimeout(t)),e}function X(s,e,t){return t==="fixed"?e:e*Math.pow(2,s)+Math.random()*100}function Y(s){return s?JSON.stringify(Object.keys(s).sort().reduce((e,t)=>(e[t]=s[t],e),{})):""}function Z(s,e){return`${s}::${Y({method:e.method||"GET",body:e.body,headers:e.headers})}`}async function ee(s,e,t,r,n){let i=0;for(;;)try{return await s()}catch(l){let T=!l.status,y=l.status>=500,v=n?n(l.response||null,l):T||y;if(i>=e||!v)throw l;f(`Retry attempt ${i+1}/${e} for failing request...`),await U(X(i,t,r)),i++}}async function S(s,e,t){let r=[...F,...e.options.middleware||[]];for(let n of r)try{s==="onRequest"&&n.onRequest?await n.onRequest(e):s==="onResponse"&&n.onResponse?await n.onResponse(t,e):s==="onError"&&n.onError&&await n.onError(t,e)}catch(i){f("Middleware error:",i)}}async function w(s,e={}){let{cache:t=!0,ttl:r=W,timeout:n,revalidate:i=!1,force:l=!1,retries:T=0,retryDelay:y=500,retryStrategy:v="fixed",retryOn:I,cacheKey:q,cacheTags:z=[],parseJson:$=!0,parser:C,transform:R,validateStatus:N=c=>c>=200&&c<300,onSuccess:A,onError:_,...M}=e,a=q||Z(s,M),b={url:s,options:e};if(!l&&t){let c=u.get(a);if(c!==null)return f("Cache hit:",a),i&&!E.has(a)&&(E.add(a),w(s,{...e,revalidate:!1,force:!0}).finally(()=>E.delete(a)).catch(()=>f("Background revalidation failed for",a))),c}if(m.has(a))return f("Dedup hit:",a),m.get(a);f("Fetching:",s);let P=ee(async()=>{await S("onRequest",b);let c=Q(n),o=await globalThis.fetch(s,{...M,signal:c?.signal});if(await S("onResponse",b,o),!N(o.status)){let d=new Error(`HTTP Error ${o.status}`);throw d.status=o.status,d.response=o,d}let h;if(C)h=await C(o);else if($){let d=o.headers.get("content-type");d&&d.includes("application/json")?h=await o.json():h=await o.text()}else h=await o.text();if(R&&(h=R(h)),t){u.set(a,h,r);for(let d of z)g.has(d)||g.set(d,new Set),g.get(d).add(a)}return A?.(h),h},T,y,v,I).catch(async c=>{await S("onError",b,c),_?.(c);let o=u.peek(a);if(o!==null)return f("Returning stale data due to fetch error:",a),o;throw c}).finally(()=>{m.delete(a)});return m.set(a,P),P}var L={clear:()=>{u.clear(),g.clear()},delete:s=>u.delete(s),has:s=>u.has(s),set:(s,e,t)=>u.set(s,e,t),get:s=>u.get(s),mutate:(s,e,t)=>u.mutate(s,e,t),subscribe:(s,e)=>u.subscribe(s,e),invalidateTag:s=>{let e=g.get(s);if(e){for(let t of e)u.delete(t);g.delete(s),f(`Invalidated tag: ${s} (${e.size} items)`)}}},D=w,O=H;var te="0.1.0";0&&(module.exports={SushiCache,VERSION,addSushiMiddleware,fetcher,sushiCache,sushiFetch});
@@ -0,0 +1,111 @@
1
+ type RetryConfig = {
2
+ retries?: number;
3
+ retryDelay?: number;
4
+ retryStrategy?: "fixed" | "exponential";
5
+ retryOn?: (res: Response | null, err: unknown) => boolean;
6
+ };
7
+ type MiddlewareContext = {
8
+ url: string;
9
+ options: FetchOptions;
10
+ };
11
+ type Middleware = {
12
+ onRequest?: (ctx: MiddlewareContext) => Promise<void> | void;
13
+ onResponse?: (res: Response, ctx: MiddlewareContext) => Promise<void> | void;
14
+ onError?: (err: unknown, ctx: MiddlewareContext) => Promise<void> | void;
15
+ };
16
+ type FetchOptions = RequestInit & RetryConfig & {
17
+ cache?: boolean;
18
+ ttl?: number;
19
+ timeout?: number;
20
+ revalidate?: boolean;
21
+ force?: boolean;
22
+ cacheKey?: string;
23
+ cacheTags?: string[];
24
+ parseJson?: boolean;
25
+ parser?: (res: Response) => Promise<unknown>;
26
+ transform?: <T>(data: T) => T;
27
+ validateStatus?: (status: number) => boolean;
28
+ middleware?: Middleware[];
29
+ onSuccess?: (data: unknown) => void;
30
+ onError?: (error: unknown) => void;
31
+ };
32
+ declare function addMiddleware(mw: Middleware): void;
33
+ declare function fetcher<T = unknown>(url: string, options?: FetchOptions): Promise<T>;
34
+ declare const sushiCache: {
35
+ clear: () => void;
36
+ delete: (key: string) => void;
37
+ has: (key: string) => boolean;
38
+ set: <T>(key: string, data: T, ttl?: number) => void;
39
+ get: <T>(key: string) => T | null;
40
+ mutate: <T>(key: string, mutator: T | ((oldData: T | null) => T), ttl?: number) => any;
41
+ subscribe: <T>(key: string, listener: (data: T | null) => void) => () => void;
42
+ invalidateTag: (tag: string) => void;
43
+ };
44
+ declare const sushiFetch: typeof fetcher;
45
+ declare const addSushiMiddleware: typeof addMiddleware;
46
+
47
+ type CacheListener<T = any> = (data: T | null) => void;
48
+ type CacheOptions = {
49
+ maxSize?: number;
50
+ defaultTTL?: number;
51
+ sliding?: boolean;
52
+ cleanupInterval?: number;
53
+ onEvict?: (key: string, value: any) => void;
54
+ };
55
+ declare class SushiCache {
56
+ private store;
57
+ private pendingFetches;
58
+ private listeners;
59
+ private maxSize;
60
+ private defaultTTL;
61
+ private sliding;
62
+ private onEvict?;
63
+ private hits;
64
+ private misses;
65
+ private cleanupTimer?;
66
+ constructor(options?: CacheOptions);
67
+ /**
68
+ * Subscribe ke perubahan data di cache. Sangat berguna untuk React/Vue Hooks.
69
+ * Mengembalikan fungsi untuk unsubscribe.
70
+ */
71
+ subscribe<T>(key: string, listener: CacheListener<T>): () => void;
72
+ private notify;
73
+ set<T>(key: string, data: T, ttl?: number): void;
74
+ get<T>(key: string): T | null;
75
+ peek<T>(key: string): T | null;
76
+ has(key: string): boolean;
77
+ delete(key: string): void;
78
+ /**
79
+ * ðŸ’Ą FITUR BARU: Mutate data secara manual (Optimistic Updates)
80
+ */
81
+ mutate<T>(key: string, mutator: T | ((oldData: T | null) => T), ttl?: number): any;
82
+ clear(): void;
83
+ private evictIfNeeded;
84
+ getOrSet<T>(key: string, fetcher: () => Promise<T>, ttl?: number): Promise<T>;
85
+ /**
86
+ * Stale While Revalidate
87
+ */
88
+ getOrSetSWR<T>(key: string, fetcher: () => Promise<T>, ttl?: number): Promise<T>;
89
+ private revalidate;
90
+ pruneExpired(): void;
91
+ destroy(): void;
92
+ }
93
+
94
+ /**
95
+ * ðŸĢ Sushi Fetch
96
+ * A tiny but powerful data-fetching & caching library
97
+ * -----------------------------------------------
98
+ * Public API entry point
99
+ */
100
+
101
+ /**
102
+ * Example for future:
103
+ *
104
+ * export { createClient } from "./client"
105
+ * export { createStore } from "./store"
106
+ *
107
+ * export type { ClientOptions } from "./client"
108
+ */
109
+ declare const VERSION = "0.1.0";
110
+
111
+ export { type CacheListener, type FetchOptions, SushiCache, VERSION, addSushiMiddleware, fetcher, sushiCache, sushiFetch };
@@ -0,0 +1,111 @@
1
+ type RetryConfig = {
2
+ retries?: number;
3
+ retryDelay?: number;
4
+ retryStrategy?: "fixed" | "exponential";
5
+ retryOn?: (res: Response | null, err: unknown) => boolean;
6
+ };
7
+ type MiddlewareContext = {
8
+ url: string;
9
+ options: FetchOptions;
10
+ };
11
+ type Middleware = {
12
+ onRequest?: (ctx: MiddlewareContext) => Promise<void> | void;
13
+ onResponse?: (res: Response, ctx: MiddlewareContext) => Promise<void> | void;
14
+ onError?: (err: unknown, ctx: MiddlewareContext) => Promise<void> | void;
15
+ };
16
+ type FetchOptions = RequestInit & RetryConfig & {
17
+ cache?: boolean;
18
+ ttl?: number;
19
+ timeout?: number;
20
+ revalidate?: boolean;
21
+ force?: boolean;
22
+ cacheKey?: string;
23
+ cacheTags?: string[];
24
+ parseJson?: boolean;
25
+ parser?: (res: Response) => Promise<unknown>;
26
+ transform?: <T>(data: T) => T;
27
+ validateStatus?: (status: number) => boolean;
28
+ middleware?: Middleware[];
29
+ onSuccess?: (data: unknown) => void;
30
+ onError?: (error: unknown) => void;
31
+ };
32
+ declare function addMiddleware(mw: Middleware): void;
33
+ declare function fetcher<T = unknown>(url: string, options?: FetchOptions): Promise<T>;
34
+ declare const sushiCache: {
35
+ clear: () => void;
36
+ delete: (key: string) => void;
37
+ has: (key: string) => boolean;
38
+ set: <T>(key: string, data: T, ttl?: number) => void;
39
+ get: <T>(key: string) => T | null;
40
+ mutate: <T>(key: string, mutator: T | ((oldData: T | null) => T), ttl?: number) => any;
41
+ subscribe: <T>(key: string, listener: (data: T | null) => void) => () => void;
42
+ invalidateTag: (tag: string) => void;
43
+ };
44
+ declare const sushiFetch: typeof fetcher;
45
+ declare const addSushiMiddleware: typeof addMiddleware;
46
+
47
+ type CacheListener<T = any> = (data: T | null) => void;
48
+ type CacheOptions = {
49
+ maxSize?: number;
50
+ defaultTTL?: number;
51
+ sliding?: boolean;
52
+ cleanupInterval?: number;
53
+ onEvict?: (key: string, value: any) => void;
54
+ };
55
+ declare class SushiCache {
56
+ private store;
57
+ private pendingFetches;
58
+ private listeners;
59
+ private maxSize;
60
+ private defaultTTL;
61
+ private sliding;
62
+ private onEvict?;
63
+ private hits;
64
+ private misses;
65
+ private cleanupTimer?;
66
+ constructor(options?: CacheOptions);
67
+ /**
68
+ * Subscribe ke perubahan data di cache. Sangat berguna untuk React/Vue Hooks.
69
+ * Mengembalikan fungsi untuk unsubscribe.
70
+ */
71
+ subscribe<T>(key: string, listener: CacheListener<T>): () => void;
72
+ private notify;
73
+ set<T>(key: string, data: T, ttl?: number): void;
74
+ get<T>(key: string): T | null;
75
+ peek<T>(key: string): T | null;
76
+ has(key: string): boolean;
77
+ delete(key: string): void;
78
+ /**
79
+ * ðŸ’Ą FITUR BARU: Mutate data secara manual (Optimistic Updates)
80
+ */
81
+ mutate<T>(key: string, mutator: T | ((oldData: T | null) => T), ttl?: number): any;
82
+ clear(): void;
83
+ private evictIfNeeded;
84
+ getOrSet<T>(key: string, fetcher: () => Promise<T>, ttl?: number): Promise<T>;
85
+ /**
86
+ * Stale While Revalidate
87
+ */
88
+ getOrSetSWR<T>(key: string, fetcher: () => Promise<T>, ttl?: number): Promise<T>;
89
+ private revalidate;
90
+ pruneExpired(): void;
91
+ destroy(): void;
92
+ }
93
+
94
+ /**
95
+ * ðŸĢ Sushi Fetch
96
+ * A tiny but powerful data-fetching & caching library
97
+ * -----------------------------------------------
98
+ * Public API entry point
99
+ */
100
+
101
+ /**
102
+ * Example for future:
103
+ *
104
+ * export { createClient } from "./client"
105
+ * export { createStore } from "./store"
106
+ *
107
+ * export type { ClientOptions } from "./client"
108
+ */
109
+ declare const VERSION = "0.1.0";
110
+
111
+ export { type CacheListener, type FetchOptions, SushiCache, VERSION, addSushiMiddleware, fetcher, sushiCache, sushiFetch };
package/dist/index.js CHANGED
@@ -1,315 +1 @@
1
- "use strict";
2
- var __create = Object.create;
3
- var __defProp = Object.defineProperty;
4
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
- var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
- var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __export = (target, all) => {
9
- for (var name in all)
10
- __defProp(target, name, { get: all[name], enumerable: true });
11
- };
12
- var __copyProps = (to, from, except, desc) => {
13
- if (from && typeof from === "object" || typeof from === "function") {
14
- for (let key of __getOwnPropNames(from))
15
- if (!__hasOwnProp.call(to, key) && key !== except)
16
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
- }
18
- return to;
19
- };
20
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
- // If the importer is in node compatibility mode or this is not an ESM
22
- // file that has been converted to a CommonJS file using a Babel-
23
- // compatible transform (i.e. "__esModule" has not been set), then set
24
- // "default" to the CommonJS "module.exports" for node compatibility.
25
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
- mod
27
- ));
28
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
-
30
- // src/index.ts
31
- var index_exports = {};
32
- __export(index_exports, {
33
- SushiCache: () => SushiCache,
34
- fetcher: () => fetcher,
35
- sushiCache: () => sushiCache,
36
- sushiFetch: () => sushiFetch
37
- });
38
- module.exports = __toCommonJS(index_exports);
39
-
40
- // src/core/fetcher.ts
41
- var import_node_fetch = __toESM(require("node-fetch"));
42
-
43
- // src/core/cache.ts
44
- var SushiCache = class {
45
- constructor(options = {}) {
46
- this.store = /* @__PURE__ */ new Map();
47
- this.hits = 0;
48
- this.misses = 0;
49
- this.maxSize = options.maxSize ?? Infinity;
50
- this.defaultTTL = options.defaultTTL ?? 5e3;
51
- this.onEvict = options.onEvict;
52
- }
53
- // ========================
54
- // CORE
55
- // ========================
56
- set(key, data, ttl = this.defaultTTL) {
57
- const now = Date.now();
58
- const expiry = now + ttl;
59
- if (this.store.has(key)) {
60
- this.store.delete(key);
61
- }
62
- this.store.set(key, {
63
- data,
64
- expiry,
65
- lastAccess: now
66
- });
67
- this.evictIfNeeded();
68
- }
69
- get(key) {
70
- const entry = this.store.get(key);
71
- if (!entry) {
72
- this.misses++;
73
- return null;
74
- }
75
- if (Date.now() > entry.expiry) {
76
- this.store.delete(key);
77
- this.misses++;
78
- return null;
79
- }
80
- entry.lastAccess = Date.now();
81
- this.store.delete(key);
82
- this.store.set(key, entry);
83
- this.hits++;
84
- return entry.data;
85
- }
86
- peek(key) {
87
- const entry = this.store.get(key);
88
- if (!entry) return null;
89
- if (Date.now() > entry.expiry) {
90
- this.store.delete(key);
91
- return null;
92
- }
93
- return entry.data;
94
- }
95
- has(key) {
96
- return this.get(key) !== null;
97
- }
98
- delete(key) {
99
- const entry = this.store.get(key);
100
- if (entry && this.onEvict) {
101
- this.onEvict(key, entry.data);
102
- }
103
- this.store.delete(key);
104
- }
105
- deleteMany(keys) {
106
- for (const key of keys) {
107
- this.delete(key);
108
- }
109
- }
110
- clear() {
111
- if (this.onEvict) {
112
- for (const [key, entry] of this.store.entries()) {
113
- this.onEvict(key, entry.data);
114
- }
115
- }
116
- this.store.clear();
117
- }
118
- // ========================
119
- // LRU EVICTION
120
- // ========================
121
- evictIfNeeded() {
122
- if (this.store.size <= this.maxSize) return;
123
- const oldestKey = this.store.keys().next().value;
124
- if (!oldestKey) return;
125
- const entry = this.store.get(oldestKey);
126
- if (entry && this.onEvict) {
127
- this.onEvict(oldestKey, entry.data);
128
- }
129
- this.store.delete(oldestKey);
130
- }
131
- // ========================
132
- // UTILITIES
133
- // ========================
134
- async getOrSet(key, fetcher2, ttl = this.defaultTTL) {
135
- const cached = this.get(key);
136
- if (cached !== null) return cached;
137
- const data = await fetcher2();
138
- this.set(key, data, ttl);
139
- return data;
140
- }
141
- pruneExpired() {
142
- const now = Date.now();
143
- for (const [key, entry] of this.store.entries()) {
144
- if (now > entry.expiry) {
145
- this.delete(key);
146
- }
147
- }
148
- }
149
- size() {
150
- return this.store.size;
151
- }
152
- keys() {
153
- return this.store.keys();
154
- }
155
- values() {
156
- return Array.from(this.store.values()).map((v) => v.data);
157
- }
158
- entries() {
159
- return Array.from(this.store.entries()).map(([k, v]) => [k, v.data]);
160
- }
161
- stats() {
162
- return {
163
- hits: this.hits,
164
- misses: this.misses,
165
- size: this.store.size
166
- };
167
- }
168
- };
169
-
170
- // src/core/fetcher.ts
171
- var cache = new SushiCache();
172
- var pendingRequests = /* @__PURE__ */ new Map();
173
- var revalidateLocks = /* @__PURE__ */ new Set();
174
- var DEFAULT_TTL = 5e3;
175
- var globalMiddleware = [];
176
- var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
177
- function buildAbortController(timeout) {
178
- if (!timeout) return null;
179
- const controller = new AbortController();
180
- const id = setTimeout(() => controller.abort(), timeout);
181
- controller.signal.addEventListener("abort", () => clearTimeout(id));
182
- return controller;
183
- }
184
- function computeBackoff(attempt, base, strategy) {
185
- if (strategy === "fixed") return base;
186
- return base * Math.pow(2, attempt) + Math.random() * 100;
187
- }
188
- async function retryFetch(fn, retries, delay, strategy, retryOn) {
189
- let attempt = 0;
190
- while (true) {
191
- try {
192
- return await fn();
193
- } catch (err) {
194
- const shouldRetry = retryOn ? retryOn(null, err) : true;
195
- if (attempt >= retries || !shouldRetry) throw err;
196
- await sleep(computeBackoff(attempt, delay, strategy));
197
- attempt++;
198
- }
199
- }
200
- }
201
- async function runMiddleware(type, ctx, resOrErr) {
202
- const stack = [...globalMiddleware, ...ctx.options.middleware || []];
203
- for (const mw of stack) {
204
- const fn = mw[type];
205
- if (!fn) continue;
206
- if (type === "onRequest") await fn(ctx);
207
- else await fn(resOrErr, ctx);
208
- }
209
- }
210
- function buildCacheKey(url, options) {
211
- return url + JSON.stringify(options || {});
212
- }
213
- async function fetcher(url, options = {}) {
214
- const {
215
- cache: useCache = true,
216
- ttl = DEFAULT_TTL,
217
- timeout,
218
- revalidate = false,
219
- force = false,
220
- retries = 0,
221
- retryDelay = 500,
222
- retryStrategy = "fixed",
223
- retryOn,
224
- cacheKey,
225
- cacheTags = [],
226
- parseJson = true,
227
- parser,
228
- transform,
229
- validateStatus = (s) => s >= 200 && s < 300,
230
- onSuccess,
231
- onError,
232
- ...fetchOptions
233
- } = options;
234
- const key = cacheKey || buildCacheKey(url, fetchOptions);
235
- const ctx = { url, options };
236
- if (!force && useCache) {
237
- const cached = cache.get(key);
238
- if (cached !== null) {
239
- if (revalidate && !revalidateLocks.has(key)) {
240
- revalidateLocks.add(key);
241
- fetcher(url, { ...options, revalidate: false }).finally(
242
- () => revalidateLocks.delete(key)
243
- );
244
- }
245
- return cached;
246
- }
247
- }
248
- if (pendingRequests.has(key)) {
249
- return pendingRequests.get(key);
250
- }
251
- const requestPromise = retryFetch(
252
- async () => {
253
- await runMiddleware("onRequest", ctx);
254
- const controller = buildAbortController(timeout);
255
- const res = await (0, import_node_fetch.default)(url, {
256
- ...fetchOptions,
257
- signal: controller?.signal
258
- });
259
- await runMiddleware("onResponse", ctx, res);
260
- if (!validateStatus(res.status)) {
261
- throw new Error(`HTTP ${res.status}`);
262
- }
263
- let data;
264
- if (parser) data = await parser(res);
265
- else if (parseJson) data = await res.json();
266
- else data = await res.text();
267
- if (transform) {
268
- data = transform(data);
269
- }
270
- if (useCache) {
271
- cache.set(key, data, ttl);
272
- for (const tag of cacheTags) {
273
- cache.set(`__tag__:${tag}:${key}`, true, ttl);
274
- }
275
- }
276
- onSuccess?.(data);
277
- return data;
278
- },
279
- retries,
280
- retryDelay,
281
- retryStrategy,
282
- retryOn
283
- ).catch(async (err) => {
284
- await runMiddleware("onError", ctx, err);
285
- onError?.(err);
286
- throw err;
287
- }).finally(() => {
288
- pendingRequests.delete(key);
289
- });
290
- pendingRequests.set(key, requestPromise);
291
- return requestPromise;
292
- }
293
- var sushiCache = {
294
- clear: () => cache.clear(),
295
- delete: (key) => cache.delete(key),
296
- has: (key) => cache.has(key),
297
- invalidateTag: (tag) => {
298
- const prefix = `__tag__:${tag}:`;
299
- for (const k of cache.keys()) {
300
- if (k.startsWith(prefix)) {
301
- const realKey = k.slice(prefix.length);
302
- cache.delete(realKey);
303
- cache.delete(k);
304
- }
305
- }
306
- }
307
- };
308
- var sushiFetch = fetcher;
309
- // Annotate the CommonJS export names for ESM import in node:
310
- 0 && (module.exports = {
311
- SushiCache,
312
- fetcher,
313
- sushiCache,
314
- sushiFetch
315
- });
1
+ var T=class{store=new Map;pendingFetches=new Map;listeners=new Map;maxSize;defaultTTL;sliding;onEvict;hits=0;misses=0;cleanupTimer;constructor(e={}){this.maxSize=e.maxSize??1/0,this.defaultTTL=e.defaultTTL??5e3,this.sliding=e.sliding??!1,this.onEvict=e.onEvict,e.cleanupInterval&&(this.cleanupTimer=setInterval(()=>{this.pruneExpired()},e.cleanupInterval),typeof this.cleanupTimer.unref=="function"&&this.cleanupTimer.unref())}subscribe(e,t){return this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(t),()=>{this.listeners.get(e)?.delete(t),this.listeners.get(e)?.size===0&&this.listeners.delete(e)}}notify(e,t){this.listeners.has(e)&&this.listeners.get(e).forEach(r=>r(t))}set(e,t,r=this.defaultTTL){let n=Date.now();this.store.has(e)&&this.store.delete(e),this.store.set(e,{data:t,expiry:n+r,lastAccess:n}),this.evictIfNeeded(),this.notify(e,t)}get(e){let t=this.store.get(e);if(!t)return this.misses++,null;let r=Date.now();return r>t.expiry?(this.delete(e),this.misses++,null):(t.lastAccess=r,this.sliding&&(t.expiry=r+this.defaultTTL),this.store.delete(e),this.store.set(e,t),this.hits++,t.data)}peek(e){let t=this.store.get(e);return t?Date.now()>t.expiry?(this.delete(e),null):t.data:null}has(e){return this.peek(e)!==null}delete(e){let t=this.store.get(e);t&&(this.onEvict&&this.onEvict(e,t.data),this.store.delete(e),this.notify(e,null))}mutate(e,t,r=this.defaultTTL){let n=this.get(e),i=typeof t=="function"?t(n):t;return this.set(e,i,r),i}clear(){if(this.onEvict)for(let[e,t]of this.store.entries())this.onEvict(e,t.data);this.store.clear();for(let e of this.listeners.keys())this.notify(e,null)}evictIfNeeded(){for(;this.store.size>this.maxSize;){let e=this.store.keys().next().value;if(e!==void 0)this.delete(e);else break}}async getOrSet(e,t,r=this.defaultTTL){let n=this.get(e);if(n!==null)return n;if(this.pendingFetches.has(e))return this.pendingFetches.get(e);let i=(async()=>{try{let l=await t();return this.set(e,l,r),l}finally{this.pendingFetches.delete(e)}})();return this.pendingFetches.set(e,i),i}async getOrSetSWR(e,t,r=this.defaultTTL){let n=this.store.get(e),i=Date.now();return n?i>n.expiry?(this.revalidate(e,t,r).catch(g=>{console.error(`[SushiCache] SWR Background fetch failed for key ${e}:`,g)}),n.data):(this.hits++,n.data):this.getOrSet(e,t,r)}async revalidate(e,t,r){if(this.pendingFetches.has(e))return;let n=t().then(i=>{this.set(e,i,r)}).finally(()=>{this.pendingFetches.delete(e)});return this.pendingFetches.set(e,n),n}pruneExpired(){let e=Date.now();for(let[t,r]of this.store.entries())e>r.expiry&&this.delete(t)}destroy(){this.cleanupTimer&&clearInterval(this.cleanupTimer),this.clear(),this.listeners.clear(),this.pendingFetches.clear()}};var u=new T,m=new Map,b=new Set,p=new Map,$=5e3,N=process.env.NODE_ENV!=="production";function f(...s){N&&console.log("[sushi-fetch]",...s)}var P=[];function A(s){P.push(s)}var _=s=>new Promise(e=>setTimeout(e,s));function k(s){if(!s)return null;let e=new AbortController,t=setTimeout(()=>e.abort(),s);return e.signal.addEventListener("abort",()=>clearTimeout(t)),e}function K(s,e,t){return t==="fixed"?e:e*Math.pow(2,s)+Math.random()*100}function j(s){return s?JSON.stringify(Object.keys(s).sort().reduce((e,t)=>(e[t]=s[t],e),{})):""}function B(s,e){return`${s}::${j({method:e.method||"GET",body:e.body,headers:e.headers})}`}async function J(s,e,t,r,n){let i=0;for(;;)try{return await s()}catch(l){let g=!l.status,w=l.status>=500,y=n?n(l.response||null,l):g||w;if(i>=e||!y)throw l;f(`Retry attempt ${i+1}/${e} for failing request...`),await _(K(i,t,r)),i++}}async function x(s,e,t){let r=[...P,...e.options.middleware||[]];for(let n of r)try{s==="onRequest"&&n.onRequest?await n.onRequest(e):s==="onResponse"&&n.onResponse?await n.onResponse(t,e):s==="onError"&&n.onError&&await n.onError(t,e)}catch(i){f("Middleware error:",i)}}async function E(s,e={}){let{cache:t=!0,ttl:r=$,timeout:n,revalidate:i=!1,force:l=!1,retries:g=0,retryDelay:w=500,retryStrategy:y="fixed",retryOn:F,cacheKey:L,cacheTags:D=[],parseJson:O=!0,parser:S,transform:C,validateStatus:I=c=>c>=200&&c<300,onSuccess:q,onError:z,...R}=e,a=L||B(s,R),v={url:s,options:e};if(!l&&t){let c=u.get(a);if(c!==null)return f("Cache hit:",a),i&&!b.has(a)&&(b.add(a),E(s,{...e,revalidate:!1,force:!0}).finally(()=>b.delete(a)).catch(()=>f("Background revalidation failed for",a))),c}if(m.has(a))return f("Dedup hit:",a),m.get(a);f("Fetching:",s);let M=J(async()=>{await x("onRequest",v);let c=k(n),o=await globalThis.fetch(s,{...R,signal:c?.signal});if(await x("onResponse",v,o),!I(o.status)){let d=new Error(`HTTP Error ${o.status}`);throw d.status=o.status,d.response=o,d}let h;if(S)h=await S(o);else if(O){let d=o.headers.get("content-type");d&&d.includes("application/json")?h=await o.json():h=await o.text()}else h=await o.text();if(C&&(h=C(h)),t){u.set(a,h,r);for(let d of D)p.has(d)||p.set(d,new Set),p.get(d).add(a)}return q?.(h),h},g,w,y,F).catch(async c=>{await x("onError",v,c),z?.(c);let o=u.peek(a);if(o!==null)return f("Returning stale data due to fetch error:",a),o;throw c}).finally(()=>{m.delete(a)});return m.set(a,M),M}var V={clear:()=>{u.clear(),p.clear()},delete:s=>u.delete(s),has:s=>u.has(s),set:(s,e,t)=>u.set(s,e,t),get:s=>u.get(s),mutate:(s,e,t)=>u.mutate(s,e,t),subscribe:(s,e)=>u.subscribe(s,e),invalidateTag:s=>{let e=p.get(s);if(e){for(let t of e)u.delete(t);p.delete(s),f(`Invalidated tag: ${s} (${e.size} items)`)}}},W=E,G=A;var X="0.1.0";export{T as SushiCache,X as VERSION,G as addSushiMiddleware,E as fetcher,V as sushiCache,W as sushiFetch};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sushi-fetch",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "ðŸĢ A tiny but powerful data-fetching & caching library for modern JavaScript & TypeScript apps",
5
5
  "keywords": [
6
6
  "fetch",
@@ -13,53 +13,72 @@
13
13
  "client",
14
14
  "request",
15
15
  "swr",
16
- "axios-alternative"
16
+ "deduplication",
17
+ "retry",
18
+ "lightweight"
17
19
  ],
18
20
  "homepage": "https://github.com/sushilibdev/sushi-fetch",
19
21
  "repository": {
20
22
  "type": "git",
21
- "url": "https://github.com/sushilibdev/sushi-fetch.git"
23
+ "url": "git+https://github.com/sushilibdev/sushi-fetch.git"
22
24
  },
23
25
  "bugs": {
24
26
  "url": "https://github.com/sushilibdev/sushi-fetch/issues"
25
27
  },
26
- "author": "sushilibdev",
28
+ "author": {
29
+ "name": "sushilibdev",
30
+ "email": "sushilibdev@gmail.com"
31
+ },
27
32
  "license": "MIT",
28
-
29
- "type": "commonjs",
33
+ "type": "module",
34
+
30
35
  "main": "./dist/index.cjs",
31
- "module": "./dist/index.mjs",
36
+ "module": "./dist/index.js",
32
37
  "types": "./dist/index.d.ts",
33
-
34
38
  "exports": {
35
- ".": {
36
- "types": "./dist/index.d.ts",
37
- "import": "./dist/index.mjs",
38
- "require": "./dist/index.cjs"
39
+ ".": {
40
+ "types": "./dist/index.d.ts",
41
+ "import": "./dist/index.js",
42
+ "require": "./dist/index.cjs"
39
43
  }
40
44
  },
41
-
42
45
  "files": [
43
46
  "dist"
44
47
  ],
45
-
48
+ "sideEffects": false,
49
+ "engines": {
50
+ "node": ">=18.0.0"
51
+ },
52
+ "publishConfig": {
53
+ "access": "public"
54
+ },
55
+
46
56
  "scripts": {
47
- "dev": "ts-node examples/vanilla-js/app.ts",
48
- "build": "tsup src/index.ts --format esm,cjs",
49
- "watch": "tsup src/index.ts --format esm,cjs --dts --watch",
57
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean --minify",
58
+ "watch": "tsup src/index.ts --format cjs,esm --dts --watch",
50
59
  "typecheck": "tsc --noEmit",
51
- "clean": "rm -rf dist",
52
- "prepare": "npm run build"
53
- },
54
-
55
- "dependencies": {
56
- "node-fetch": "^2.7.0"
60
+ "lint": "eslint src/**/*.ts",
61
+ "test": "vitest run",
62
+
63
+ "example:vanilla": "ts-node examples/vanilla-js/app.ts",
64
+ "example:express": "ts-node examples/express/server.ts",
65
+ "example:node-cli": "ts-node examples/node-cli/app.ts",
66
+ "example:bun": "bun run examples/bun/app.ts",
67
+
68
+ "prepublishOnly": "npm run typecheck && npm run build"
57
69
  },
58
-
70
+
71
+ "dependencies": {},
72
+
59
73
  "devDependencies": {
60
- "@types/node-fetch": "^2.6.13",
74
+ "@types/express": "^4.17.21",
75
+ "@types/node": "^20.11.0",
76
+ "@types/react": "^18.2.0",
77
+ "express": "^4.18.2",
78
+ "react": "^18.2.0",
61
79
  "ts-node": "^10.9.2",
62
- "typescript": "^5.9.3",
63
- "tsup": "^8.0.1"
80
+ "tsup": "^8.0.1",
81
+ "typescript": "^5.3.3",
82
+ "vitest": "^1.2.0"
64
83
  }
65
84
  }
package/dist/index.mjs DELETED
@@ -1,275 +0,0 @@
1
- // src/core/fetcher.ts
2
- import fetch from "node-fetch";
3
-
4
- // src/core/cache.ts
5
- var SushiCache = class {
6
- constructor(options = {}) {
7
- this.store = /* @__PURE__ */ new Map();
8
- this.hits = 0;
9
- this.misses = 0;
10
- this.maxSize = options.maxSize ?? Infinity;
11
- this.defaultTTL = options.defaultTTL ?? 5e3;
12
- this.onEvict = options.onEvict;
13
- }
14
- // ========================
15
- // CORE
16
- // ========================
17
- set(key, data, ttl = this.defaultTTL) {
18
- const now = Date.now();
19
- const expiry = now + ttl;
20
- if (this.store.has(key)) {
21
- this.store.delete(key);
22
- }
23
- this.store.set(key, {
24
- data,
25
- expiry,
26
- lastAccess: now
27
- });
28
- this.evictIfNeeded();
29
- }
30
- get(key) {
31
- const entry = this.store.get(key);
32
- if (!entry) {
33
- this.misses++;
34
- return null;
35
- }
36
- if (Date.now() > entry.expiry) {
37
- this.store.delete(key);
38
- this.misses++;
39
- return null;
40
- }
41
- entry.lastAccess = Date.now();
42
- this.store.delete(key);
43
- this.store.set(key, entry);
44
- this.hits++;
45
- return entry.data;
46
- }
47
- peek(key) {
48
- const entry = this.store.get(key);
49
- if (!entry) return null;
50
- if (Date.now() > entry.expiry) {
51
- this.store.delete(key);
52
- return null;
53
- }
54
- return entry.data;
55
- }
56
- has(key) {
57
- return this.get(key) !== null;
58
- }
59
- delete(key) {
60
- const entry = this.store.get(key);
61
- if (entry && this.onEvict) {
62
- this.onEvict(key, entry.data);
63
- }
64
- this.store.delete(key);
65
- }
66
- deleteMany(keys) {
67
- for (const key of keys) {
68
- this.delete(key);
69
- }
70
- }
71
- clear() {
72
- if (this.onEvict) {
73
- for (const [key, entry] of this.store.entries()) {
74
- this.onEvict(key, entry.data);
75
- }
76
- }
77
- this.store.clear();
78
- }
79
- // ========================
80
- // LRU EVICTION
81
- // ========================
82
- evictIfNeeded() {
83
- if (this.store.size <= this.maxSize) return;
84
- const oldestKey = this.store.keys().next().value;
85
- if (!oldestKey) return;
86
- const entry = this.store.get(oldestKey);
87
- if (entry && this.onEvict) {
88
- this.onEvict(oldestKey, entry.data);
89
- }
90
- this.store.delete(oldestKey);
91
- }
92
- // ========================
93
- // UTILITIES
94
- // ========================
95
- async getOrSet(key, fetcher2, ttl = this.defaultTTL) {
96
- const cached = this.get(key);
97
- if (cached !== null) return cached;
98
- const data = await fetcher2();
99
- this.set(key, data, ttl);
100
- return data;
101
- }
102
- pruneExpired() {
103
- const now = Date.now();
104
- for (const [key, entry] of this.store.entries()) {
105
- if (now > entry.expiry) {
106
- this.delete(key);
107
- }
108
- }
109
- }
110
- size() {
111
- return this.store.size;
112
- }
113
- keys() {
114
- return this.store.keys();
115
- }
116
- values() {
117
- return Array.from(this.store.values()).map((v) => v.data);
118
- }
119
- entries() {
120
- return Array.from(this.store.entries()).map(([k, v]) => [k, v.data]);
121
- }
122
- stats() {
123
- return {
124
- hits: this.hits,
125
- misses: this.misses,
126
- size: this.store.size
127
- };
128
- }
129
- };
130
-
131
- // src/core/fetcher.ts
132
- var cache = new SushiCache();
133
- var pendingRequests = /* @__PURE__ */ new Map();
134
- var revalidateLocks = /* @__PURE__ */ new Set();
135
- var DEFAULT_TTL = 5e3;
136
- var globalMiddleware = [];
137
- var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
138
- function buildAbortController(timeout) {
139
- if (!timeout) return null;
140
- const controller = new AbortController();
141
- const id = setTimeout(() => controller.abort(), timeout);
142
- controller.signal.addEventListener("abort", () => clearTimeout(id));
143
- return controller;
144
- }
145
- function computeBackoff(attempt, base, strategy) {
146
- if (strategy === "fixed") return base;
147
- return base * Math.pow(2, attempt) + Math.random() * 100;
148
- }
149
- async function retryFetch(fn, retries, delay, strategy, retryOn) {
150
- let attempt = 0;
151
- while (true) {
152
- try {
153
- return await fn();
154
- } catch (err) {
155
- const shouldRetry = retryOn ? retryOn(null, err) : true;
156
- if (attempt >= retries || !shouldRetry) throw err;
157
- await sleep(computeBackoff(attempt, delay, strategy));
158
- attempt++;
159
- }
160
- }
161
- }
162
- async function runMiddleware(type, ctx, resOrErr) {
163
- const stack = [...globalMiddleware, ...ctx.options.middleware || []];
164
- for (const mw of stack) {
165
- const fn = mw[type];
166
- if (!fn) continue;
167
- if (type === "onRequest") await fn(ctx);
168
- else await fn(resOrErr, ctx);
169
- }
170
- }
171
- function buildCacheKey(url, options) {
172
- return url + JSON.stringify(options || {});
173
- }
174
- async function fetcher(url, options = {}) {
175
- const {
176
- cache: useCache = true,
177
- ttl = DEFAULT_TTL,
178
- timeout,
179
- revalidate = false,
180
- force = false,
181
- retries = 0,
182
- retryDelay = 500,
183
- retryStrategy = "fixed",
184
- retryOn,
185
- cacheKey,
186
- cacheTags = [],
187
- parseJson = true,
188
- parser,
189
- transform,
190
- validateStatus = (s) => s >= 200 && s < 300,
191
- onSuccess,
192
- onError,
193
- ...fetchOptions
194
- } = options;
195
- const key = cacheKey || buildCacheKey(url, fetchOptions);
196
- const ctx = { url, options };
197
- if (!force && useCache) {
198
- const cached = cache.get(key);
199
- if (cached !== null) {
200
- if (revalidate && !revalidateLocks.has(key)) {
201
- revalidateLocks.add(key);
202
- fetcher(url, { ...options, revalidate: false }).finally(
203
- () => revalidateLocks.delete(key)
204
- );
205
- }
206
- return cached;
207
- }
208
- }
209
- if (pendingRequests.has(key)) {
210
- return pendingRequests.get(key);
211
- }
212
- const requestPromise = retryFetch(
213
- async () => {
214
- await runMiddleware("onRequest", ctx);
215
- const controller = buildAbortController(timeout);
216
- const res = await fetch(url, {
217
- ...fetchOptions,
218
- signal: controller?.signal
219
- });
220
- await runMiddleware("onResponse", ctx, res);
221
- if (!validateStatus(res.status)) {
222
- throw new Error(`HTTP ${res.status}`);
223
- }
224
- let data;
225
- if (parser) data = await parser(res);
226
- else if (parseJson) data = await res.json();
227
- else data = await res.text();
228
- if (transform) {
229
- data = transform(data);
230
- }
231
- if (useCache) {
232
- cache.set(key, data, ttl);
233
- for (const tag of cacheTags) {
234
- cache.set(`__tag__:${tag}:${key}`, true, ttl);
235
- }
236
- }
237
- onSuccess?.(data);
238
- return data;
239
- },
240
- retries,
241
- retryDelay,
242
- retryStrategy,
243
- retryOn
244
- ).catch(async (err) => {
245
- await runMiddleware("onError", ctx, err);
246
- onError?.(err);
247
- throw err;
248
- }).finally(() => {
249
- pendingRequests.delete(key);
250
- });
251
- pendingRequests.set(key, requestPromise);
252
- return requestPromise;
253
- }
254
- var sushiCache = {
255
- clear: () => cache.clear(),
256
- delete: (key) => cache.delete(key),
257
- has: (key) => cache.has(key),
258
- invalidateTag: (tag) => {
259
- const prefix = `__tag__:${tag}:`;
260
- for (const k of cache.keys()) {
261
- if (k.startsWith(prefix)) {
262
- const realKey = k.slice(prefix.length);
263
- cache.delete(realKey);
264
- cache.delete(k);
265
- }
266
- }
267
- }
268
- };
269
- var sushiFetch = fetcher;
270
- export {
271
- SushiCache,
272
- fetcher,
273
- sushiCache,
274
- sushiFetch
275
- };