@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.
- package/README.md +43 -564
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,615 +1,94 @@
|
|
|
1
|
-
# endpoint-fetcher
|
|
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
|
|
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** -
|
|
8
|
-
* ⚡ **
|
|
9
|
-
* 🔄 **
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
|
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
|
-
//
|
|
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 }) //
|
|
36
|
+
cache({ ttl: 300 }) // Global TTL: 5 minutes
|
|
44
37
|
]
|
|
45
38
|
});
|
|
46
39
|
|
|
47
|
-
//
|
|
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
|
-
//
|
|
55
|
-
|
|
56
|
-
console.log(
|
|
43
|
+
console.log(result.data); // User[]
|
|
44
|
+
console.log(result.isStale); // false
|
|
45
|
+
console.log(result.expiresAt); // Date
|
|
57
46
|
|
|
58
|
-
//
|
|
59
|
-
const
|
|
60
|
-
console.log(refreshed.data); // Fresh data from API
|
|
47
|
+
// Force a network refresh
|
|
48
|
+
const fresh = await result.refresh();
|
|
61
49
|
|
|
62
|
-
//
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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.
|
|
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
|
+
}
|