@endpoint-fetcher/cache 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lorenzo Giovanni Vecchio
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,615 @@
1
+ # endpoint-fetcher-cache
2
+
3
+ A caching plugin for [endpoint-fetcher](https://github.com/lorenzo-vecchio/endpoint-fetcher) that adds intelligent caching with type-safe wrapper support.
4
+
5
+ ## Features
6
+
7
+ * 🔒 **Fully Type-Safe** - TypeScript support with complete type inference
8
+ * ⚡ **Smart Caching** - Automatic caching with TTL support
9
+ * 🔄 **Manual Refresh** - Programmatically refresh cached data
10
+ * 🗑️ **Cache Invalidation** - Clear specific cache entries
11
+ * 📊 **Cache Metadata** - Know when data was cached and when it expires
12
+ * 🎯 **LRU Eviction** - Automatic eviction of old entries when cache is full
13
+ * 🔧 **Customizable** - Custom TTL, methods, key generation, and storage
14
+ * 💾 **Storage Adapters** - Bring your own storage (localStorage, Redis, etc.)
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install @endpoint-fetcher/cache
20
+ ```
21
+
22
+ **Peer dependency:** `endpoint-fetcher` ^2.0.0
23
+
24
+ ## Quick Start
25
+
26
+ ```typescript
27
+ import { createApiClient, get } from 'endpoint-fetcher';
28
+ import { cache, CachingWrapper } from 'endpoint-fetcher-cache';
29
+
30
+ type User = { id: string; name: string; email: string };
31
+
32
+ const api = createApiClient({
33
+ users: {
34
+ endpoints: {
35
+ // Important: Output type must be CachingWrapper<T>
36
+ getAll: get<void, CachingWrapper<User[]>>('/users'),
37
+ getById: get<{ id: string }, CachingWrapper<User>>((input) => `/users/${input.id}`)
38
+ }
39
+ }
40
+ }, {
41
+ baseUrl: 'https://api.example.com',
42
+ plugins: [
43
+ cache({ ttl: 300 }) // Cache for 5 minutes
44
+ ]
45
+ });
46
+
47
+ // First call - fetches from API
48
+ const result = await api.users.getAll();
49
+ console.log(result.data); // User[]
50
+ console.log(result.cachedAt); // Date when cached
51
+ console.log(result.expiresAt); // Date when expires
52
+ console.log(result.isStale); // false
53
+
54
+ // Second call - returns from cache (if within TTL)
55
+ const cached = await api.users.getAll();
56
+ console.log(cached.cachedAt === result.cachedAt); // true (same cache)
57
+
58
+ // Manually refresh
59
+ const refreshed = await result.refresh();
60
+ console.log(refreshed.data); // Fresh data from API
61
+
62
+ // Invalidate cache
63
+ result.invalidate();
64
+ ```
65
+
66
+ ## API Reference
67
+
68
+ ### `cache(config?)`
69
+
70
+ Creates a caching plugin for endpoint-fetcher.
71
+
72
+ **Parameters:**
73
+
74
+ ```typescript
75
+ {
76
+ ttl?: number; // Time to live in seconds (default: 300)
77
+ methods?: string[]; // HTTP methods to cache (default: ['GET'])
78
+ maxSize?: number; // Max cache entries (default: Infinity)
79
+ keyGenerator?: (method: string, path: string, input: any) => string;
80
+ storage?: CacheStorage; // Custom storage adapter
81
+ }
82
+ ```
83
+
84
+ **Returns:** Plugin instance for use with endpoint-fetcher
85
+
86
+ ### `CachingWrapper<T>`
87
+
88
+ The wrapper type that adds caching metadata to your response type.
89
+
90
+ ```typescript
91
+ type CachingWrapper<T> = {
92
+ data: T; // The actual data
93
+ cachedAt: Date; // When it was cached
94
+ expiresAt: Date; // When it expires
95
+ isStale: boolean; // Whether it's expired
96
+ refresh: () => Promise<CachingWrapper<T>>; // Refresh the data
97
+ invalidate: () => void; // Remove from cache
98
+ };
99
+ ```
100
+
101
+ ### `CacheStorage` Interface
102
+
103
+ Implement this interface to create custom storage adapters:
104
+
105
+ ```typescript
106
+ interface CacheStorage {
107
+ get(key: string): CacheEntry | undefined;
108
+ set(key: string, value: CacheEntry): void;
109
+ delete(key: string): void;
110
+ clear(): void;
111
+ keys(): string[];
112
+ }
113
+
114
+ interface CacheEntry {
115
+ data: any;
116
+ cachedAt: Date;
117
+ expiresAt: Date;
118
+ }
119
+ ```
120
+
121
+ ## Usage Examples
122
+
123
+ ### Basic Caching
124
+
125
+ ```typescript
126
+ import { createApiClient, get } from 'endpoint-fetcher';
127
+ import { cache, CachingWrapper } from 'endpoint-fetcher-cache';
128
+
129
+ type Post = { id: string; title: string; content: string };
130
+
131
+ const api = createApiClient({
132
+ posts: {
133
+ endpoints: {
134
+ getAll: get<void, CachingWrapper<Post[]>>('/posts'),
135
+ getById: get<{ id: string }, CachingWrapper<Post>>((input) => `/posts/${input.id}`)
136
+ }
137
+ }
138
+ }, {
139
+ baseUrl: 'https://api.example.com',
140
+ plugins: [cache({ ttl: 600 })] // 10 minutes
141
+ });
142
+
143
+ const posts = await api.posts.getAll();
144
+ console.log('Cached at:', posts.cachedAt);
145
+ console.log('Expires at:', posts.expiresAt);
146
+ console.log('Data:', posts.data);
147
+ ```
148
+
149
+ ### Custom TTL Per Endpoint
150
+
151
+ ```typescript
152
+ // You can use multiple cache plugin instances for different TTLs
153
+ const api = createApiClient({
154
+ fastChanging: {
155
+ endpoints: {
156
+ getData: get<void, CachingWrapper<Data>>('/fast')
157
+ }
158
+ },
159
+ slowChanging: {
160
+ endpoints: {
161
+ getData: get<void, CachingWrapper<Data>>('/slow')
162
+ }
163
+ }
164
+ }, {
165
+ baseUrl: 'https://api.example.com',
166
+ plugins: [
167
+ cache({ ttl: 60 }) // Default: 1 minute
168
+ ]
169
+ });
170
+
171
+ // Or apply at group level
172
+ const api2 = createApiClient({
173
+ fast: group({
174
+ endpoints: {
175
+ getData: get<void, CachingWrapper<Data>>('/fast')
176
+ }
177
+ }),
178
+ slow: group({
179
+ endpoints: {
180
+ getData: get<void, CachingWrapper<Data>>('/slow')
181
+ }
182
+ })
183
+ }, {
184
+ baseUrl: 'https://api.example.com',
185
+ plugins: [cache({ ttl: 300 })]
186
+ });
187
+ ```
188
+
189
+ ### Checking Cache Freshness
190
+
191
+ ```typescript
192
+ const result = await api.users.getById({ id: '123' });
193
+
194
+ if (result.isStale) {
195
+ console.log('Data is stale, consider refreshing');
196
+ const fresh = await result.refresh();
197
+ console.log('Refreshed data:', fresh.data);
198
+ } else {
199
+ console.log('Data is fresh:', result.data);
200
+ }
201
+
202
+ // Calculate remaining cache time
203
+ const now = new Date();
204
+ const remainingMs = result.expiresAt.getTime() - now.getTime();
205
+ const remainingSec = Math.floor(remainingMs / 1000);
206
+ console.log(`Cache expires in ${remainingSec} seconds`);
207
+ ```
208
+
209
+ ### Manual Cache Management
210
+
211
+ ```typescript
212
+ const result = await api.users.getAll();
213
+
214
+ // Force refresh (bypasses cache)
215
+ const refreshed = await result.refresh();
216
+ console.log('Fresh data:', refreshed.data);
217
+
218
+ // Invalidate specific cache entry
219
+ result.invalidate();
220
+
221
+ // Next call will fetch from API
222
+ const fresh = await api.users.getAll();
223
+ console.log('Fetched from API:', fresh.data);
224
+ ```
225
+
226
+ ### Cache Specific Methods
227
+
228
+ ```typescript
229
+ const api = createApiClient({
230
+ users: {
231
+ endpoints: {
232
+ getAll: get<void, CachingWrapper<User[]>>('/users'),
233
+ create: post<CreateUserInput, CachingWrapper<User>>('/users')
234
+ }
235
+ }
236
+ }, {
237
+ baseUrl: 'https://api.example.com',
238
+ plugins: [
239
+ cache({
240
+ ttl: 300,
241
+ methods: ['GET', 'POST'] // Cache both GET and POST
242
+ })
243
+ ]
244
+ });
245
+ ```
246
+
247
+ ### Custom Cache Key Generation
248
+
249
+ ```typescript
250
+ const api = createApiClient({
251
+ users: {
252
+ endpoints: {
253
+ getAll: get<{ page?: number }, CachingWrapper<User[]>>('/users')
254
+ }
255
+ }
256
+ }, {
257
+ baseUrl: 'https://api.example.com',
258
+ plugins: [
259
+ cache({
260
+ ttl: 300,
261
+ // Custom key generator ignores page parameter
262
+ keyGenerator: (method, path, input) => {
263
+ return `${method}:${path}`; // Ignores input
264
+ }
265
+ })
266
+ ]
267
+ });
268
+
269
+ // Both calls use same cache entry
270
+ const page1 = await api.users.getAll({ page: 1 });
271
+ const page2 = await api.users.getAll({ page: 2 });
272
+ console.log(page1.cachedAt === page2.cachedAt); // true
273
+ ```
274
+
275
+ ### Limited Cache Size (LRU)
276
+
277
+ ```typescript
278
+ const api = createApiClient({
279
+ users: {
280
+ endpoints: {
281
+ getById: get<{ id: string }, CachingWrapper<User>>((input) => `/users/${input.id}`)
282
+ }
283
+ }
284
+ }, {
285
+ baseUrl: 'https://api.example.com',
286
+ plugins: [
287
+ cache({
288
+ ttl: 600,
289
+ maxSize: 100 // Keep only 100 most recently used entries
290
+ })
291
+ ]
292
+ });
293
+
294
+ // When cache exceeds 100 entries, oldest entries are removed
295
+ for (let i = 0; i < 150; i++) {
296
+ await api.users.getById({ id: i.toString() });
297
+ }
298
+ // Only last 100 users are cached
299
+ ```
300
+
301
+ ### Custom Storage (localStorage)
302
+
303
+ ```typescript
304
+ import { cache, CacheStorage, CacheEntry } from 'endpoint-fetcher-cache';
305
+
306
+ class LocalStorageCacheAdapter implements CacheStorage {
307
+ private prefix = 'api-cache:';
308
+
309
+ get(key: string): CacheEntry | undefined {
310
+ const item = localStorage.getItem(this.prefix + key);
311
+ if (!item) return undefined;
312
+
313
+ const entry = JSON.parse(item);
314
+ return {
315
+ data: entry.data,
316
+ cachedAt: new Date(entry.cachedAt),
317
+ expiresAt: new Date(entry.expiresAt)
318
+ };
319
+ }
320
+
321
+ set(key: string, value: CacheEntry): void {
322
+ localStorage.setItem(
323
+ this.prefix + key,
324
+ JSON.stringify({
325
+ data: value.data,
326
+ cachedAt: value.cachedAt.toISOString(),
327
+ expiresAt: value.expiresAt.toISOString()
328
+ })
329
+ );
330
+ }
331
+
332
+ delete(key: string): void {
333
+ localStorage.removeItem(this.prefix + key);
334
+ }
335
+
336
+ clear(): void {
337
+ const keys = this.keys();
338
+ keys.forEach(key => localStorage.removeItem(this.prefix + key));
339
+ }
340
+
341
+ keys(): string[] {
342
+ const keys: string[] = [];
343
+ for (let i = 0; i < localStorage.length; i++) {
344
+ const key = localStorage.key(i);
345
+ if (key?.startsWith(this.prefix)) {
346
+ keys.push(key.substring(this.prefix.length));
347
+ }
348
+ }
349
+ return keys;
350
+ }
351
+ }
352
+
353
+ const api = createApiClient({
354
+ users: {
355
+ endpoints: {
356
+ getAll: get<void, CachingWrapper<User[]>>('/users')
357
+ }
358
+ }
359
+ }, {
360
+ baseUrl: 'https://api.example.com',
361
+ plugins: [
362
+ cache({
363
+ ttl: 3600,
364
+ storage: new LocalStorageCacheAdapter()
365
+ })
366
+ ]
367
+ });
368
+
369
+ // Cache persists across page reloads!
370
+ ```
371
+
372
+ ### Combining with Other Plugins
373
+
374
+ ```typescript
375
+ import { createApiClient, get } from 'endpoint-fetcher';
376
+ import { cache, CachingWrapper } from 'endpoint-fetcher-cache';
377
+ import { retry } from 'endpoint-fetcher-retry'; // hypothetical
378
+
379
+ const api = createApiClient({
380
+ users: {
381
+ endpoints: {
382
+ getAll: get<void, CachingWrapper<User[]>>('/users')
383
+ }
384
+ }
385
+ }, {
386
+ baseUrl: 'https://api.example.com',
387
+ plugins: [
388
+ retry({ maxRetries: 3 }), // Retry failed requests
389
+ cache({ ttl: 300 }) // Then cache successful responses
390
+ ]
391
+ });
392
+ ```
393
+
394
+ ### React Hook Example
395
+
396
+ ```typescript
397
+ import { useState, useEffect } from 'react';
398
+ import { api } from './api';
399
+ import type { CachingWrapper } from 'endpoint-fetcher-cache';
400
+
401
+ function useUsers() {
402
+ const [result, setResult] = useState<CachingWrapper<User[]> | null>(null);
403
+ const [loading, setLoading] = useState(true);
404
+
405
+ useEffect(() => {
406
+ api.users.getAll().then(data => {
407
+ setResult(data);
408
+ setLoading(false);
409
+ });
410
+ }, []);
411
+
412
+ const refresh = async () => {
413
+ if (result) {
414
+ setLoading(true);
415
+ const fresh = await result.refresh();
416
+ setResult(fresh);
417
+ setLoading(false);
418
+ }
419
+ };
420
+
421
+ return {
422
+ users: result?.data,
423
+ cachedAt: result?.cachedAt,
424
+ isStale: result?.isStale,
425
+ loading,
426
+ refresh
427
+ };
428
+ }
429
+
430
+ // Usage in component
431
+ function UsersPage() {
432
+ const { users, cachedAt, isStale, loading, refresh } = useUsers();
433
+
434
+ return (
435
+ <div>
436
+ <button onClick={refresh}>Refresh</button>
437
+ {loading && <p>Loading...</p>}
438
+ {cachedAt && <p>Last updated: {cachedAt.toLocaleString()}</p>}
439
+ {isStale && <p>⚠️ Data is stale</p>}
440
+ <ul>
441
+ {users?.map(user => <li key={user.id}>{user.name}</li>)}
442
+ </ul>
443
+ </div>
444
+ );
445
+ }
446
+ ```
447
+
448
+ ## How It Works
449
+
450
+ ### Type Transformation
451
+
452
+ The plugin uses TypeScript's type system to transform your endpoint's return type:
453
+
454
+ 1. **You specify** : `get<void, CachingWrapper<User[]>>('/users')`
455
+ 2. **API returns** : `User[]`
456
+ 3. **Plugin wraps it** : Adds metadata and methods
457
+ 4. **You receive** : `CachingWrapper<User[]>` with full type safety
458
+
459
+ ### Cache Key Generation
460
+
461
+ By default, cache keys are generated from:
462
+
463
+ * HTTP method
464
+ * Request path
465
+ * Request input (JSON stringified)
466
+
467
+ Example: `GET:/users/123:{"includeProfile":true}`
468
+
469
+ ### LRU Eviction
470
+
471
+ When `maxSize` is set, the cache uses Least Recently Used (LRU) eviction:
472
+
473
+ 1. When cache reaches `maxSize` and a new entry is added
474
+ 2. The least recently accessed entry is removed
475
+ 3. Access order is updated on every `get()` operation
476
+
477
+ ## TypeScript
478
+
479
+ The plugin is written in TypeScript and provides complete type safety:
480
+
481
+ ```typescript
482
+ import { cache, CachingWrapper } from 'endpoint-fetcher-cache';
483
+
484
+ // ✅ Correct - output type is CachingWrapper<User[]>
485
+ const getUsers = get<void, CachingWrapper<User[]>>('/users');
486
+
487
+ // ❌ Type error - output type must be CachingWrapper<T>
488
+ const getUsers = get<void, User[]>('/users');
489
+ // If you use the cache plugin, TypeScript will complain because
490
+ // User[] !== CachingWrapper<User[]>
491
+ ```
492
+
493
+ ## Best Practices
494
+
495
+ ### 1. Use Appropriate TTL
496
+
497
+ Choose TTL based on how frequently your data changes:
498
+
499
+ * **Static data** : 3600+ seconds (1+ hour)
500
+ * **Slow-changing** : 600-1800 seconds (10-30 minutes)
501
+ * **Medium** : 300-600 seconds (5-10 minutes)
502
+ * **Fast-changing** : 60-300 seconds (1-5 minutes)
503
+ * **Real-time** : Don't cache or use very short TTL (10-30 seconds)
504
+
505
+ ### 2. Selective Caching
506
+
507
+ Only cache GET requests by default:
508
+
509
+ ```typescript
510
+ cache({ methods: ['GET'] }) // Default
511
+ ```
512
+
513
+ Only cache POST if responses are deterministic and safe to cache.
514
+
515
+ ### 3. Monitor Cache Size
516
+
517
+ Set `maxSize` to prevent unbounded memory growth:
518
+
519
+ ```typescript
520
+ cache({
521
+ ttl: 300,
522
+ maxSize: 1000 // Reasonable limit
523
+ })
524
+ ```
525
+
526
+ ### 4. Invalidate on Mutations
527
+
528
+ Invalidate relevant cache entries after mutations:
529
+
530
+ ```typescript
531
+ const users = await api.users.getAll();
532
+
533
+ // After creating a user
534
+ await api.users.create({ name: 'John', email: 'john@example.com' });
535
+ users.invalidate(); // Clear cached users list
536
+
537
+ // Fetch fresh list
538
+ const updated = await api.users.getAll();
539
+ ```
540
+
541
+ ### 5. Handle Stale Data
542
+
543
+ Check `isStale` and refresh when needed:
544
+
545
+ ```typescript
546
+ const result = await api.users.getAll();
547
+
548
+ if (result.isStale) {
549
+ const fresh = await result.refresh();
550
+ // Use fresh.data
551
+ } else {
552
+ // Use result.data
553
+ }
554
+ ```
555
+
556
+ ## Advanced Patterns
557
+
558
+ ### Automatic Refresh on Stale
559
+
560
+ ```typescript
561
+ async function getUsers() {
562
+ const result = await api.users.getAll();
563
+
564
+ if (result.isStale) {
565
+ return (await result.refresh()).data;
566
+ }
567
+
568
+ return result.data;
569
+ }
570
+ ```
571
+
572
+ ### Cache Warming
573
+
574
+ ```typescript
575
+ // Warm cache on app startup
576
+ async function warmCache() {
577
+ await api.users.getAll();
578
+ await api.posts.getAll();
579
+ console.log('Cache warmed');
580
+ }
581
+
582
+ warmCache();
583
+ ```
584
+
585
+ ### Conditional Caching
586
+
587
+ ```typescript
588
+ const api = createApiClient({
589
+ users: {
590
+ endpoints: {
591
+ // Cache list
592
+ getAll: get<void, CachingWrapper<User[]>>('/users'),
593
+ // Don't cache individual users (use different plugin instance)
594
+ getById: get<{ id: string }, User>((input) => `/users/${input.id}`)
595
+ }
596
+ }
597
+ }, {
598
+ baseUrl: 'https://api.example.com',
599
+ plugins: [
600
+ cache({ ttl: 300, methods: ['GET'] })
601
+ ]
602
+ });
603
+ ```
604
+
605
+ ## License
606
+
607
+ MIT
608
+
609
+ ## Contributing
610
+
611
+ Contributions are welcome! Please feel free to submit a Pull Request.
612
+
613
+ ## Related
614
+
615
+ * [endpoint-fetcher](https://github.com/lorenzo-vecchio/endpoint-fetcher) - The main library
package/dist/index.cjs ADDED
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ cache: () => cache,
24
+ clearCache: () => clearCache,
25
+ default: () => index_default
26
+ });
27
+ module.exports = __toCommonJS(index_exports);
28
+ var import_endpoint_fetcher = require("endpoint-fetcher");
29
+ var InMemoryCacheStorage = class {
30
+ constructor(maxSize = Infinity) {
31
+ this.maxSize = maxSize;
32
+ this.cache = /* @__PURE__ */ new Map();
33
+ this.accessOrder = [];
34
+ }
35
+ get(key) {
36
+ const entry = this.cache.get(key);
37
+ if (entry) {
38
+ this.accessOrder = this.accessOrder.filter((k) => k !== key);
39
+ this.accessOrder.push(key);
40
+ }
41
+ return entry;
42
+ }
43
+ set(key, value) {
44
+ if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
45
+ const oldestKey = this.accessOrder.shift();
46
+ if (oldestKey) {
47
+ this.cache.delete(oldestKey);
48
+ }
49
+ }
50
+ this.cache.set(key, value);
51
+ this.accessOrder = this.accessOrder.filter((k) => k !== key);
52
+ this.accessOrder.push(key);
53
+ }
54
+ delete(key) {
55
+ this.cache.delete(key);
56
+ this.accessOrder = this.accessOrder.filter((k) => k !== key);
57
+ }
58
+ clear() {
59
+ this.cache.clear();
60
+ this.accessOrder = [];
61
+ }
62
+ keys() {
63
+ return Array.from(this.cache.keys());
64
+ }
65
+ };
66
+ var defaultKeyGenerator = (method, path, input) => {
67
+ const inputStr = input === void 0 || input === null ? "" : JSON.stringify(input);
68
+ return `${method}:${path}:${inputStr}`;
69
+ };
70
+ var cache = (0, import_endpoint_fetcher.createPlugin)((config = {}) => {
71
+ const {
72
+ ttl = 300,
73
+ methods = ["GET"],
74
+ maxSize = Infinity,
75
+ keyGenerator = defaultKeyGenerator,
76
+ storage = new InMemoryCacheStorage(maxSize)
77
+ } = config;
78
+ return {
79
+ handlerWrapper: (originalHandler) => {
80
+ return async (input, context) => {
81
+ if (!methods.includes(context.method)) {
82
+ return originalHandler(input, context);
83
+ }
84
+ const cacheKey = keyGenerator(context.method, context.path, input);
85
+ const createWrapper = (data, cachedAt2, expiresAt2) => {
86
+ const wrapper = {
87
+ data,
88
+ cachedAt: cachedAt2,
89
+ expiresAt: expiresAt2,
90
+ get isStale() {
91
+ return /* @__PURE__ */ new Date() > expiresAt2;
92
+ },
93
+ refresh: async () => {
94
+ storage.delete(cacheKey);
95
+ const freshData = await originalHandler(input, context);
96
+ const newCachedAt = /* @__PURE__ */ new Date();
97
+ const newExpiresAt = new Date(newCachedAt.getTime() + ttl * 1e3);
98
+ storage.set(cacheKey, {
99
+ data: freshData,
100
+ cachedAt: newCachedAt,
101
+ expiresAt: newExpiresAt
102
+ });
103
+ return createWrapper(freshData, newCachedAt, newExpiresAt);
104
+ },
105
+ invalidate: () => {
106
+ storage.delete(cacheKey);
107
+ }
108
+ };
109
+ return wrapper;
110
+ };
111
+ const cached = storage.get(cacheKey);
112
+ const now = /* @__PURE__ */ new Date();
113
+ if (cached && now < cached.expiresAt) {
114
+ return createWrapper(cached.data, cached.cachedAt, cached.expiresAt);
115
+ }
116
+ const result = await originalHandler(input, context);
117
+ const cachedAt = /* @__PURE__ */ new Date();
118
+ const expiresAt = new Date(cachedAt.getTime() + ttl * 1e3);
119
+ storage.set(cacheKey, {
120
+ data: result,
121
+ cachedAt,
122
+ expiresAt
123
+ });
124
+ return createWrapper(result, cachedAt, expiresAt);
125
+ };
126
+ }
127
+ };
128
+ });
129
+ var clearCache = (storage) => {
130
+ if (storage) {
131
+ storage.clear();
132
+ }
133
+ };
134
+ var index_default = cache;
135
+ // Annotate the CommonJS export names for ESM import in node:
136
+ 0 && (module.exports = {
137
+ cache,
138
+ clearCache
139
+ });
@@ -0,0 +1,186 @@
1
+ import { PluginOptions } from 'endpoint-fetcher';
2
+
3
+ /**
4
+ * Wrapper type that adds caching metadata and methods to the response
5
+ *
6
+ * @template T - The actual data type being cached
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * const api = createApiClient({
11
+ * users: {
12
+ * endpoints: {
13
+ * getAll: get<void, CachingWrapper<User[]>>('/users')
14
+ * }
15
+ * }
16
+ * }, {
17
+ * baseUrl: 'https://api.example.com',
18
+ * plugins: [cache({ ttl: 300 })]
19
+ * });
20
+ *
21
+ * const result = await api.users.getAll();
22
+ * console.log(result.data); // User[]
23
+ * console.log(result.cachedAt); // Date
24
+ * console.log(result.isStale); // boolean
25
+ * await result.refresh(); // Refresh the cache
26
+ * result.invalidate(); // Clear from cache
27
+ * ```
28
+ */
29
+ type CachingWrapper<T> = {
30
+ /**
31
+ * The actual cached data
32
+ */
33
+ data: T;
34
+ /**
35
+ * When this data was cached
36
+ */
37
+ cachedAt: Date;
38
+ /**
39
+ * When this cache entry will expire
40
+ */
41
+ expiresAt: Date;
42
+ /**
43
+ * Whether this cache entry has expired
44
+ */
45
+ isStale: boolean;
46
+ /**
47
+ * Refresh the data by fetching it again and updating the cache
48
+ */
49
+ refresh: () => Promise<CachingWrapper<T>>;
50
+ /**
51
+ * Remove this entry from the cache
52
+ */
53
+ invalidate: () => void;
54
+ };
55
+ /**
56
+ * Configuration options for the caching plugin
57
+ */
58
+ type CachePluginConfig = {
59
+ /**
60
+ * Time to live in seconds - how long cached data remains valid
61
+ * @default 300 (5 minutes)
62
+ */
63
+ ttl?: number;
64
+ /**
65
+ * HTTP methods to cache
66
+ * @default ['GET']
67
+ */
68
+ methods?: string[];
69
+ /**
70
+ * Maximum number of cache entries to store
71
+ * When exceeded, oldest entries are removed (LRU)
72
+ * @default Infinity (no limit)
73
+ */
74
+ maxSize?: number;
75
+ /**
76
+ * Custom cache key generator
77
+ * By default uses: `${method}:${path}:${JSON.stringify(input)}`
78
+ *
79
+ * @param method - HTTP method
80
+ * @param path - Request path
81
+ * @param input - Request input/body
82
+ * @returns Cache key string
83
+ */
84
+ keyGenerator?: (method: string, path: string, input: any) => string;
85
+ /**
86
+ * Custom storage adapter (useful for persistent caching)
87
+ * @default In-memory Map
88
+ */
89
+ storage?: CacheStorage;
90
+ };
91
+ /**
92
+ * Cache storage interface
93
+ */
94
+ interface CacheStorage {
95
+ get(key: string): CacheEntry | undefined;
96
+ set(key: string, value: CacheEntry): void;
97
+ delete(key: string): void;
98
+ clear(): void;
99
+ keys(): string[];
100
+ }
101
+ /**
102
+ * Cache entry structure
103
+ */
104
+ interface CacheEntry {
105
+ data: any;
106
+ cachedAt: Date;
107
+ expiresAt: Date;
108
+ }
109
+ /**
110
+ * Caching plugin for endpoint-fetcher
111
+ *
112
+ * Wraps API responses with caching metadata and methods, allowing you to:
113
+ * - Cache responses in memory
114
+ * - Check cache freshness
115
+ * - Manually refresh cached data
116
+ * - Invalidate specific cache entries
117
+ *
118
+ * @param config - Plugin configuration
119
+ * @returns Plugin options for endpoint-fetcher
120
+ *
121
+ * @example
122
+ * Basic usage:
123
+ * ```typescript
124
+ * import { createApiClient, get } from 'endpoint-fetcher';
125
+ * import { cache, CachingWrapper } from 'endpoint-fetcher-cache';
126
+ *
127
+ * const api = createApiClient({
128
+ * users: {
129
+ * endpoints: {
130
+ * // Output type must be CachingWrapper<T>
131
+ * getAll: get<void, CachingWrapper<User[]>>('/users'),
132
+ * getById: get<{ id: string }, CachingWrapper<User>>((input) => `/users/${input.id}`)
133
+ * }
134
+ * }
135
+ * }, {
136
+ * baseUrl: 'https://api.example.com',
137
+ * plugins: [
138
+ * cache({ ttl: 300 }) // Cache for 5 minutes
139
+ * ]
140
+ * });
141
+ *
142
+ * const result = await api.users.getAll();
143
+ * console.log(result.data); // User[]
144
+ * console.log(result.cachedAt); // Date
145
+ * console.log(result.isStale); // boolean
146
+ * await result.refresh(); // Force refresh
147
+ * result.invalidate(); // Clear from cache
148
+ * ```
149
+ *
150
+ * @example
151
+ * Advanced usage with custom storage:
152
+ * ```typescript
153
+ * import { cache } from 'endpoint-fetcher-cache';
154
+ *
155
+ * const api = createApiClient({
156
+ * // ... endpoints
157
+ * }, {
158
+ * baseUrl: 'https://api.example.com',
159
+ * plugins: [
160
+ * cache({
161
+ * ttl: 600, // 10 minutes
162
+ * methods: ['GET', 'POST'], // Cache GET and POST
163
+ * maxSize: 100, // Store max 100 entries
164
+ * keyGenerator: (method, path, input) => {
165
+ * // Custom key generation
166
+ * return `custom:${method}:${path}`;
167
+ * }
168
+ * })
169
+ * ]
170
+ * });
171
+ * ```
172
+ */
173
+ declare const cache: (() => PluginOptions) | ((config: CachePluginConfig) => PluginOptions);
174
+ /**
175
+ * Clears all cache entries
176
+ *
177
+ * @example
178
+ * ```typescript
179
+ * import { clearCache } from 'endpoint-fetcher-cache';
180
+ *
181
+ * clearCache(); // Clears all cached data
182
+ * ```
183
+ */
184
+ declare const clearCache: (storage?: CacheStorage) => void;
185
+
186
+ export { type CacheEntry, type CachePluginConfig, type CacheStorage, type CachingWrapper, cache, clearCache, cache as default };
@@ -0,0 +1,186 @@
1
+ import { PluginOptions } from 'endpoint-fetcher';
2
+
3
+ /**
4
+ * Wrapper type that adds caching metadata and methods to the response
5
+ *
6
+ * @template T - The actual data type being cached
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * const api = createApiClient({
11
+ * users: {
12
+ * endpoints: {
13
+ * getAll: get<void, CachingWrapper<User[]>>('/users')
14
+ * }
15
+ * }
16
+ * }, {
17
+ * baseUrl: 'https://api.example.com',
18
+ * plugins: [cache({ ttl: 300 })]
19
+ * });
20
+ *
21
+ * const result = await api.users.getAll();
22
+ * console.log(result.data); // User[]
23
+ * console.log(result.cachedAt); // Date
24
+ * console.log(result.isStale); // boolean
25
+ * await result.refresh(); // Refresh the cache
26
+ * result.invalidate(); // Clear from cache
27
+ * ```
28
+ */
29
+ type CachingWrapper<T> = {
30
+ /**
31
+ * The actual cached data
32
+ */
33
+ data: T;
34
+ /**
35
+ * When this data was cached
36
+ */
37
+ cachedAt: Date;
38
+ /**
39
+ * When this cache entry will expire
40
+ */
41
+ expiresAt: Date;
42
+ /**
43
+ * Whether this cache entry has expired
44
+ */
45
+ isStale: boolean;
46
+ /**
47
+ * Refresh the data by fetching it again and updating the cache
48
+ */
49
+ refresh: () => Promise<CachingWrapper<T>>;
50
+ /**
51
+ * Remove this entry from the cache
52
+ */
53
+ invalidate: () => void;
54
+ };
55
+ /**
56
+ * Configuration options for the caching plugin
57
+ */
58
+ type CachePluginConfig = {
59
+ /**
60
+ * Time to live in seconds - how long cached data remains valid
61
+ * @default 300 (5 minutes)
62
+ */
63
+ ttl?: number;
64
+ /**
65
+ * HTTP methods to cache
66
+ * @default ['GET']
67
+ */
68
+ methods?: string[];
69
+ /**
70
+ * Maximum number of cache entries to store
71
+ * When exceeded, oldest entries are removed (LRU)
72
+ * @default Infinity (no limit)
73
+ */
74
+ maxSize?: number;
75
+ /**
76
+ * Custom cache key generator
77
+ * By default uses: `${method}:${path}:${JSON.stringify(input)}`
78
+ *
79
+ * @param method - HTTP method
80
+ * @param path - Request path
81
+ * @param input - Request input/body
82
+ * @returns Cache key string
83
+ */
84
+ keyGenerator?: (method: string, path: string, input: any) => string;
85
+ /**
86
+ * Custom storage adapter (useful for persistent caching)
87
+ * @default In-memory Map
88
+ */
89
+ storage?: CacheStorage;
90
+ };
91
+ /**
92
+ * Cache storage interface
93
+ */
94
+ interface CacheStorage {
95
+ get(key: string): CacheEntry | undefined;
96
+ set(key: string, value: CacheEntry): void;
97
+ delete(key: string): void;
98
+ clear(): void;
99
+ keys(): string[];
100
+ }
101
+ /**
102
+ * Cache entry structure
103
+ */
104
+ interface CacheEntry {
105
+ data: any;
106
+ cachedAt: Date;
107
+ expiresAt: Date;
108
+ }
109
+ /**
110
+ * Caching plugin for endpoint-fetcher
111
+ *
112
+ * Wraps API responses with caching metadata and methods, allowing you to:
113
+ * - Cache responses in memory
114
+ * - Check cache freshness
115
+ * - Manually refresh cached data
116
+ * - Invalidate specific cache entries
117
+ *
118
+ * @param config - Plugin configuration
119
+ * @returns Plugin options for endpoint-fetcher
120
+ *
121
+ * @example
122
+ * Basic usage:
123
+ * ```typescript
124
+ * import { createApiClient, get } from 'endpoint-fetcher';
125
+ * import { cache, CachingWrapper } from 'endpoint-fetcher-cache';
126
+ *
127
+ * const api = createApiClient({
128
+ * users: {
129
+ * endpoints: {
130
+ * // Output type must be CachingWrapper<T>
131
+ * getAll: get<void, CachingWrapper<User[]>>('/users'),
132
+ * getById: get<{ id: string }, CachingWrapper<User>>((input) => `/users/${input.id}`)
133
+ * }
134
+ * }
135
+ * }, {
136
+ * baseUrl: 'https://api.example.com',
137
+ * plugins: [
138
+ * cache({ ttl: 300 }) // Cache for 5 minutes
139
+ * ]
140
+ * });
141
+ *
142
+ * const result = await api.users.getAll();
143
+ * console.log(result.data); // User[]
144
+ * console.log(result.cachedAt); // Date
145
+ * console.log(result.isStale); // boolean
146
+ * await result.refresh(); // Force refresh
147
+ * result.invalidate(); // Clear from cache
148
+ * ```
149
+ *
150
+ * @example
151
+ * Advanced usage with custom storage:
152
+ * ```typescript
153
+ * import { cache } from 'endpoint-fetcher-cache';
154
+ *
155
+ * const api = createApiClient({
156
+ * // ... endpoints
157
+ * }, {
158
+ * baseUrl: 'https://api.example.com',
159
+ * plugins: [
160
+ * cache({
161
+ * ttl: 600, // 10 minutes
162
+ * methods: ['GET', 'POST'], // Cache GET and POST
163
+ * maxSize: 100, // Store max 100 entries
164
+ * keyGenerator: (method, path, input) => {
165
+ * // Custom key generation
166
+ * return `custom:${method}:${path}`;
167
+ * }
168
+ * })
169
+ * ]
170
+ * });
171
+ * ```
172
+ */
173
+ declare const cache: (() => PluginOptions) | ((config: CachePluginConfig) => PluginOptions);
174
+ /**
175
+ * Clears all cache entries
176
+ *
177
+ * @example
178
+ * ```typescript
179
+ * import { clearCache } from 'endpoint-fetcher-cache';
180
+ *
181
+ * clearCache(); // Clears all cached data
182
+ * ```
183
+ */
184
+ declare const clearCache: (storage?: CacheStorage) => void;
185
+
186
+ export { type CacheEntry, type CachePluginConfig, type CacheStorage, type CachingWrapper, cache, clearCache, cache as default };
package/dist/index.js ADDED
@@ -0,0 +1,113 @@
1
+ // src/index.ts
2
+ import { createPlugin } from "endpoint-fetcher";
3
+ var InMemoryCacheStorage = class {
4
+ constructor(maxSize = Infinity) {
5
+ this.maxSize = maxSize;
6
+ this.cache = /* @__PURE__ */ new Map();
7
+ this.accessOrder = [];
8
+ }
9
+ get(key) {
10
+ const entry = this.cache.get(key);
11
+ if (entry) {
12
+ this.accessOrder = this.accessOrder.filter((k) => k !== key);
13
+ this.accessOrder.push(key);
14
+ }
15
+ return entry;
16
+ }
17
+ set(key, value) {
18
+ if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
19
+ const oldestKey = this.accessOrder.shift();
20
+ if (oldestKey) {
21
+ this.cache.delete(oldestKey);
22
+ }
23
+ }
24
+ this.cache.set(key, value);
25
+ this.accessOrder = this.accessOrder.filter((k) => k !== key);
26
+ this.accessOrder.push(key);
27
+ }
28
+ delete(key) {
29
+ this.cache.delete(key);
30
+ this.accessOrder = this.accessOrder.filter((k) => k !== key);
31
+ }
32
+ clear() {
33
+ this.cache.clear();
34
+ this.accessOrder = [];
35
+ }
36
+ keys() {
37
+ return Array.from(this.cache.keys());
38
+ }
39
+ };
40
+ var defaultKeyGenerator = (method, path, input) => {
41
+ const inputStr = input === void 0 || input === null ? "" : JSON.stringify(input);
42
+ return `${method}:${path}:${inputStr}`;
43
+ };
44
+ var cache = createPlugin((config = {}) => {
45
+ const {
46
+ ttl = 300,
47
+ methods = ["GET"],
48
+ maxSize = Infinity,
49
+ keyGenerator = defaultKeyGenerator,
50
+ storage = new InMemoryCacheStorage(maxSize)
51
+ } = config;
52
+ return {
53
+ handlerWrapper: (originalHandler) => {
54
+ return async (input, context) => {
55
+ if (!methods.includes(context.method)) {
56
+ return originalHandler(input, context);
57
+ }
58
+ const cacheKey = keyGenerator(context.method, context.path, input);
59
+ const createWrapper = (data, cachedAt2, expiresAt2) => {
60
+ const wrapper = {
61
+ data,
62
+ cachedAt: cachedAt2,
63
+ expiresAt: expiresAt2,
64
+ get isStale() {
65
+ return /* @__PURE__ */ new Date() > expiresAt2;
66
+ },
67
+ refresh: async () => {
68
+ storage.delete(cacheKey);
69
+ const freshData = await originalHandler(input, context);
70
+ const newCachedAt = /* @__PURE__ */ new Date();
71
+ const newExpiresAt = new Date(newCachedAt.getTime() + ttl * 1e3);
72
+ storage.set(cacheKey, {
73
+ data: freshData,
74
+ cachedAt: newCachedAt,
75
+ expiresAt: newExpiresAt
76
+ });
77
+ return createWrapper(freshData, newCachedAt, newExpiresAt);
78
+ },
79
+ invalidate: () => {
80
+ storage.delete(cacheKey);
81
+ }
82
+ };
83
+ return wrapper;
84
+ };
85
+ const cached = storage.get(cacheKey);
86
+ const now = /* @__PURE__ */ new Date();
87
+ if (cached && now < cached.expiresAt) {
88
+ return createWrapper(cached.data, cached.cachedAt, cached.expiresAt);
89
+ }
90
+ const result = await originalHandler(input, context);
91
+ const cachedAt = /* @__PURE__ */ new Date();
92
+ const expiresAt = new Date(cachedAt.getTime() + ttl * 1e3);
93
+ storage.set(cacheKey, {
94
+ data: result,
95
+ cachedAt,
96
+ expiresAt
97
+ });
98
+ return createWrapper(result, cachedAt, expiresAt);
99
+ };
100
+ }
101
+ };
102
+ });
103
+ var clearCache = (storage) => {
104
+ if (storage) {
105
+ storage.clear();
106
+ }
107
+ };
108
+ var index_default = cache;
109
+ export {
110
+ cache,
111
+ clearCache,
112
+ index_default as default
113
+ };
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@endpoint-fetcher/cache",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "Caching plugin for endpoint-fetcher with type-safe wrapper support",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.js",
12
+ "require": "./dist/index.cjs",
13
+ "types": "./dist/index.d.ts"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
23
+ "watch": "tsup src/index.ts --format cjs,esm --dts --watch",
24
+ "prepublishOnly": "npm run build"
25
+ },
26
+ "keywords": [
27
+ "endpoint-fetcher",
28
+ "plugin",
29
+ "cache",
30
+ "caching",
31
+ "api",
32
+ "fetch",
33
+ "typescript",
34
+ "type-safe"
35
+ ],
36
+ "author": "Lorenzo Vecchio",
37
+ "license": "MIT",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/lorenzo-vecchio/endpoint-fetcher-cache.git"
41
+ },
42
+ "bugs": {
43
+ "url": "https://github.com/lorenzo-vecchio/endpoint-fetcher-cache/issues"
44
+ },
45
+ "homepage": "https://github.com/lorenzo-vecchio/endpoint-fetcher-cache#readme",
46
+ "engines": {
47
+ "node": ">=16.0.0"
48
+ },
49
+ "peerDependencies": {
50
+ "endpoint-fetcher": "^2.0.0"
51
+ },
52
+ "devDependencies": {
53
+ "@types/node": "^20.0.0",
54
+ "endpoint-fetcher": "^2.1.1",
55
+ "tsup": "^8.5.1",
56
+ "typescript": "^5.0.0"
57
+ }
58
+ }