@endpoint-fetcher/cache 1.0.0 → 1.0.1

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.
Files changed (2) hide show
  1. package/README.md +43 -564
  2. package/package.json +2 -2
package/README.md CHANGED
@@ -1,615 +1,94 @@
1
- # endpoint-fetcher-cache
1
+ # @endpoint-fetcher/cache
2
2
 
3
- A caching plugin for [endpoint-fetcher](https://github.com/lorenzo-vecchio/endpoint-fetcher) that adds intelligent caching with type-safe wrapper support.
3
+ A caching plugin for [endpoint-fetcher](https://github.com/lorenzo-vecchio/endpoint-fetcher) that adds intelligent caching with type-safe metadata.
4
4
 
5
5
  ## Features
6
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.)
7
+ * 🔒 **Fully Type-Safe** - Complete inference for your cached data.
8
+ * ⚡ **Metadata Aware** - Know exactly when data was `cachedAt` or if it `isStale`.
9
+ * 🔄 **Built-in Actions** - Programmatically `refresh()` or `invalidate()` from the response.
10
+ * 💾 **Storage Adapters** - Plug-and-play storage (Memory, localStorage, etc.).
11
+ * 🎯 **LRU Eviction** - Automatically cleans up old entries when the limit is reached.
15
12
 
16
13
  ## Installation
17
14
 
18
15
  ```bash
19
16
  npm install @endpoint-fetcher/cache
20
17
  ```
21
-
22
- **Peer dependency:** `endpoint-fetcher` ^2.0.0
18
+ *Requires `endpoint-fetcher` ^2.0.0*
23
19
 
24
20
  ## Quick Start
25
21
 
26
22
  ```typescript
27
23
  import { createApiClient, get } from 'endpoint-fetcher';
28
- import { cache, CachingWrapper } from 'endpoint-fetcher-cache';
29
-
30
- type User = { id: string; name: string; email: string };
24
+ import { cache, CachingWrapper } from '@endpoint-fetcher/cache';
31
25
 
32
26
  const api = createApiClient({
33
27
  users: {
34
28
  endpoints: {
35
- // Important: Output type must be CachingWrapper<T>
29
+ // Wrap your return type with CachingWrapper<T>
36
30
  getAll: get<void, CachingWrapper<User[]>>('/users'),
37
- getById: get<{ id: string }, CachingWrapper<User>>((input) => `/users/${input.id}`)
38
31
  }
39
32
  }
40
33
  }, {
41
- baseUrl: 'https://api.example.com',
34
+ baseUrl: '[https://api.example.com](https://api.example.com)',
42
35
  plugins: [
43
- cache({ ttl: 300 }) // Cache for 5 minutes
36
+ cache({ ttl: 300 }) // Global TTL: 5 minutes
44
37
  ]
45
38
  });
46
39
 
47
- // First call - fetches from API
40
+ // Usage
48
41
  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
42
 
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)
43
+ console.log(result.data); // User[]
44
+ console.log(result.isStale); // false
45
+ console.log(result.expiresAt); // Date
57
46
 
58
- // Manually refresh
59
- const refreshed = await result.refresh();
60
- console.log(refreshed.data); // Fresh data from API
47
+ // Force a network refresh
48
+ const fresh = await result.refresh();
61
49
 
62
- // Invalidate cache
50
+ // Remove this specific entry from cache
63
51
  result.invalidate();
64
52
  ```
65
53
 
66
54
  ## API Reference
67
55
 
68
56
  ### `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
57
+ | Option | Type | Default | Description |
58
+ | :--- | :--- | :--- | :--- |
59
+ | `ttl` | `number` | `300` | Global time-to-live in seconds. |
60
+ | `maxSize` | `number` | `Infinity` | Max number of entries (LRU). |
61
+ | `methods` | `string[]` | `['GET']` | HTTP methods to cache. |
62
+ | `storage` | `CacheStorage`| `Memory` | Custom storage (e.g., localStorage). |
85
63
 
86
64
  ### `CachingWrapper<T>`
65
+ The response object returned by your API calls:
66
+ * `data: T` - The actual API response.
67
+ * `cachedAt: Date` - Timestamp of the original fetch.
68
+ * `expiresAt: Date` - When the entry will be considered stale.
69
+ * `isStale: boolean` - Helper to check if TTL has passed.
70
+ * `refresh(): Promise<CachingWrapper<T>>` - Re-fetches from network.
71
+ * `invalidate(): void` - Clears this entry from cache.
87
72
 
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
73
+ ## Custom Storage
74
+ You can persist cache across sessions using `localStorage`:
210
75
 
211
76
  ```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',
77
+ const api = createApiClient({...}, {
361
78
  plugins: [
362
79
  cache({
363
80
  ttl: 3600,
364
- storage: new LocalStorageCacheAdapter()
81
+ storage: {
82
+ get: (key) => JSON.parse(localStorage.getItem(key)),
83
+ set: (key, val) => localStorage.setItem(key, JSON.stringify(val)),
84
+ delete: (key) => localStorage.removeItem(key),
85
+ keys: () => Object.keys(localStorage),
86
+ clear: () => localStorage.clear()
87
+ }
365
88
  })
366
89
  ]
367
90
  });
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
91
  ```
604
92
 
605
93
  ## 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
94
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@endpoint-fetcher/cache",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "type": "module",
5
5
  "description": "Caching plugin for endpoint-fetcher with type-safe wrapper support",
6
6
  "main": "./dist/index.cjs",
@@ -55,4 +55,4 @@
55
55
  "tsup": "^8.5.1",
56
56
  "typescript": "^5.0.0"
57
57
  }
58
- }
58
+ }