sushi-fetch 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sushi-Fetch Project
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,210 @@
1
+ # 🍣 sushi-fetch
2
+
3
+ > **Simple, fast, and powerful data fetching with built-in caching,
4
+ > deduplication, and retry --- for modern JavaScript.**
5
+
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)
13
+
14
+ ------------------------------------------------------------------------
15
+
16
+ ## ✨ Features
17
+
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
27
+
28
+ ------------------------------------------------------------------------
29
+
30
+ ## 📦 Installation
31
+
32
+ ``` bash
33
+ npm install sushi-fetch
34
+ ```
35
+
36
+ or
37
+
38
+ ``` bash
39
+ yarn add sushi-fetch
40
+ ```
41
+
42
+ ------------------------------------------------------------------------
43
+
44
+ ## 🚀 Quick Start
45
+
46
+ ``` ts
47
+ import { sushiFetch } from "sushi-fetch"
48
+
49
+ const users = await sushiFetch("https://jsonplaceholder.typicode.com/users", {
50
+ cache: true,
51
+ ttl: 10000,
52
+ retries: 2
53
+ })
54
+
55
+ console.log(users)
56
+ ```
57
+
58
+ ------------------------------------------------------------------------
59
+
60
+ ## ⚙️ API
61
+
62
+ ### sushiFetch(url, options?)
63
+
64
+ Fetch data with powerful built-in features.
65
+
66
+ #### Parameters
67
+
68
+ --------------------------------------------------------------------------
69
+ Name Type Default Description
70
+ --------------- ------------ ----------------- ---------------------------
71
+ url string --- API endpoint
72
+
73
+ cache boolean true Enable caching
74
+
75
+ ttl number 5000 Cache lifetime (ms)
76
+
77
+ revalidate boolean false Return cached data &
78
+ revalidate in background
79
+
80
+ timeout number --- Request timeout in ms
81
+
82
+ retries number 0 Retry attempts
83
+
84
+ retryDelay number 500 Delay between retries
85
+
86
+ retryStrategy "fixed" "exponential" Retry strategy
87
+
88
+ parseJson boolean true Parse response as JSON
89
+
90
+ onSuccess (data) =\> --- Success callback
91
+ void
92
+
93
+ onError (error) =\> --- Error callback
94
+ void
95
+
96
+ cacheKey string auto Custom cache key
97
+ --------------------------------------------------------------------------
98
+
99
+ ------------------------------------------------------------------------
100
+
101
+ ## 🧠 Caching Example
102
+
103
+ ``` ts
104
+ await sushiFetch("/api/data", {
105
+ cache: true,
106
+ ttl: 10000
107
+ })
108
+ ```
109
+
110
+ ------------------------------------------------------------------------
111
+
112
+ ## ♻️ Stale-While-Revalidate
113
+
114
+ ``` ts
115
+ await sushiFetch("/api/data", {
116
+ cache: true,
117
+ revalidate: true
118
+ })
119
+ ```
120
+
121
+ ------------------------------------------------------------------------
122
+
123
+ ## 🔁 Retry Example
124
+
125
+ ``` ts
126
+ await sushiFetch("/api/data", {
127
+ retries: 3,
128
+ retryStrategy: "exponential",
129
+ retryDelay: 500
130
+ })
131
+ ```
132
+
133
+ ------------------------------------------------------------------------
134
+
135
+ ## ⏱️ Timeout Example
136
+
137
+ ``` ts
138
+ await sushiFetch("/api/data", {
139
+ timeout: 3000
140
+ })
141
+ ```
142
+
143
+ ------------------------------------------------------------------------
144
+
145
+ ## 📦 Cache Utilities
146
+
147
+ ``` ts
148
+ import { sushiCache } from "sushi-fetch"
149
+
150
+ sushiCache.has(key)
151
+ sushiCache.delete(key)
152
+ sushiCache.clear()
153
+ ```
154
+
155
+ ------------------------------------------------------------------------
156
+
157
+ ## 🧩 Advanced Example
158
+
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
+ })
169
+ ```
170
+
171
+ ------------------------------------------------------------------------
172
+
173
+ ## 🛠️ Roadmap
174
+
175
+ - AbortController support
176
+ - Middleware / interceptor system
177
+ - Polling / auto re-fetch
178
+ - React hooks (useSushiFetch)
179
+ - Devtools debugging mode
180
+ - SSR utilities
181
+
182
+ ------------------------------------------------------------------------
183
+
184
+ ## 🤝 Contributing
185
+
186
+ Contributions, issues, and feature requests are welcome!
187
+
188
+ Feel free to open a PR or issue 💛
189
+
190
+ ------------------------------------------------------------------------
191
+
192
+ ## 📄 License
193
+
194
+ MIT © 2026 --- Sushi-Fetch Project
195
+
196
+ ------------------------------------------------------------------------
197
+
198
+ ## 🌟 Support
199
+
200
+ If you like this project:
201
+
202
+ - ⭐ Star this repo
203
+ - 🍣 Share it with others
204
+ - 🐛 Report bugs & ideas
205
+
206
+ ------------------------------------------------------------------------
207
+
208
+ # 🔥 Tagline
209
+
210
+ > sushi-fetch --- fetching data should be simple, fast, and delicious 🍣
package/dist/index.js ADDED
@@ -0,0 +1,315 @@
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
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,275 @@
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
+ };
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "sushi-fetch",
3
+ "version": "0.1.0",
4
+ "description": "🍣 A tiny but powerful data-fetching & caching library for modern JavaScript & TypeScript apps",
5
+ "keywords": [
6
+ "fetch",
7
+ "http",
8
+ "cache",
9
+ "data-fetching",
10
+ "typescript",
11
+ "javascript",
12
+ "api",
13
+ "client",
14
+ "request",
15
+ "swr",
16
+ "axios-alternative"
17
+ ],
18
+ "homepage": "https://github.com/sushilibdev/sushi-fetch",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/sushilibdev/sushi-fetch.git"
22
+ },
23
+ "bugs": {
24
+ "url": "https://github.com/sushilibdev/sushi-fetch/issues"
25
+ },
26
+ "author": "sushilibdev",
27
+ "license": "MIT",
28
+
29
+ "type": "commonjs",
30
+ "main": "./dist/index.cjs",
31
+ "module": "./dist/index.mjs",
32
+ "types": "./dist/index.d.ts",
33
+
34
+ "exports": {
35
+ ".": {
36
+ "types": "./dist/index.d.ts",
37
+ "import": "./dist/index.mjs",
38
+ "require": "./dist/index.cjs"
39
+ }
40
+ },
41
+
42
+ "files": [
43
+ "dist"
44
+ ],
45
+
46
+ "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",
50
+ "typecheck": "tsc --noEmit",
51
+ "clean": "rm -rf dist",
52
+ "prepare": "npm run build"
53
+ },
54
+
55
+ "dependencies": {
56
+ "node-fetch": "^2.7.0"
57
+ },
58
+
59
+ "devDependencies": {
60
+ "@types/node-fetch": "^2.6.13",
61
+ "ts-node": "^10.9.2",
62
+ "typescript": "^5.9.3",
63
+ "tsup": "^8.0.1"
64
+ }
65
+ }