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 +147 -137
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +111 -0
- package/dist/index.d.ts +111 -0
- package/dist/index.js +1 -315
- package/package.json +46 -27
- package/dist/index.mjs +0 -275
package/README.md
CHANGED
|
@@ -1,210 +1,220 @@
|
|
|
1
|
-
|
|
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
|
-
>
|
|
4
|
-
|
|
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
|
-
|
|
7
|
-

|
|
8
|
-

|
|
9
|
-

|
|
10
|
-

|
|
11
|
-

|
|
12
|
-

|
|
16
|
+
---
|
|
13
17
|
|
|
14
|
-
|
|
18
|
+
## ðĪ Why sushi-fetch?
|
|
15
19
|
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
38
|
+
```bash
|
|
33
39
|
npm install sushi-fetch
|
|
34
40
|
```
|
|
35
41
|
|
|
36
|
-
|
|
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
|
-
|
|
48
|
+
**1. Basic Fetch & Cache (Node / Vanilla JS)**
|
|
49
|
+
|
|
50
|
+
```ts
|
|
47
51
|
import { sushiFetch } from "sushi-fetch"
|
|
48
52
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
ttl:
|
|
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
|
-
|
|
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
|
-
|
|
65
|
+
sushi-fetch exposes a powerful `subscribe` and `mutate` API, making it trivial to create reactive components.
|
|
67
66
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
url string --- API endpoint
|
|
67
|
+
```ts
|
|
68
|
+
import { useEffect, useState } from "react"
|
|
69
|
+
import { sushiFetch, sushiCache } from "sushi-fetch"
|
|
72
70
|
|
|
73
|
-
|
|
71
|
+
export function useSushi<T>(url: string) {
|
|
72
|
+
const [data, setData] = useState<T | null>(() => sushiCache.get(url))
|
|
74
73
|
|
|
75
|
-
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
// 1. Fetch and revalidate in background
|
|
76
|
+
sushiFetch<T>(url, { revalidate: true })
|
|
76
77
|
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
// 2. Subscribe to cache mutations
|
|
79
|
+
const unsubscribe = sushiCache.subscribe<T>(url, setData)
|
|
80
|
+
return () => unsubscribe()
|
|
81
|
+
}, [url])
|
|
79
82
|
|
|
80
|
-
|
|
83
|
+
return { data }
|
|
84
|
+
}
|
|
81
85
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
86
|
+
// In your component:
|
|
87
|
+
// Mutate cache directly for Optimistic Updates!
|
|
88
|
+
// sushiCache.mutate("/api/users", [...newData])
|
|
89
|
+
```
|
|
85
90
|
|
|
86
|
-
|
|
91
|
+
**3. Request Deduplication**
|
|
87
92
|
|
|
88
|
-
|
|
93
|
+
Stop spamming your servers. sushi-fetch automatically groups identical requests made at the exact same time.
|
|
89
94
|
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
94
|
-
void
|
|
104
|
+
**4. Cache Tags & Invalidation**
|
|
95
105
|
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
138
|
-
await sushiFetch("/api/data", {
|
|
139
|
-
timeout: 3000
|
|
140
|
-
})
|
|
141
|
-
```
|
|
137
|
+
## âïļ API Reference
|
|
142
138
|
|
|
143
|
-
|
|
139
|
+
`sushiFetch(url, options?)`
|
|
144
140
|
|
|
145
|
-
|
|
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
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
##
|
|
172
|
+
## ð Comparison
|
|
174
173
|
|
|
175
|
-
-
|
|
176
|
-
|
|
177
|
-
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
187
|
+
---
|
|
185
188
|
|
|
186
|
-
|
|
189
|
+
## ðĢ Roadmap
|
|
187
190
|
|
|
188
|
-
|
|
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
|
-
##
|
|
200
|
+
## ðĪ Contributing
|
|
201
|
+
|
|
202
|
+
Pull requests, issues, and feature ideas are highly welcome!
|
|
193
203
|
|
|
194
|
-
|
|
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
|
-
|
|
210
|
+
---
|
|
199
211
|
|
|
200
|
-
|
|
212
|
+
## ð Sponsors
|
|
201
213
|
|
|
202
|
-
|
|
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
|
-
|
|
218
|
+
## ð License
|
|
209
219
|
|
|
210
|
-
|
|
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});
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
-
"
|
|
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.
|
|
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
|
-
"
|
|
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":
|
|
28
|
+
"author": {
|
|
29
|
+
"name": "sushilibdev",
|
|
30
|
+
"email": "sushilibdev@gmail.com"
|
|
31
|
+
},
|
|
27
32
|
"license": "MIT",
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
"type": "module",
|
|
34
|
+
|
|
30
35
|
"main": "./dist/index.cjs",
|
|
31
|
-
"module": "./dist/index.
|
|
36
|
+
"module": "./dist/index.js",
|
|
32
37
|
"types": "./dist/index.d.ts",
|
|
33
|
-
|
|
34
38
|
"exports": {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
"
|
|
48
|
-
"
|
|
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
|
-
"
|
|
52
|
-
"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"node-
|
|
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/
|
|
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
|
-
"
|
|
63
|
-
"
|
|
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
|
-
};
|