@bloomneo/appkit 1.5.1 โ 1.5.2
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/AGENTS.md +195 -0
- package/CHANGELOG.md +253 -0
- package/README.md +147 -799
- package/bin/commands/generate.js +7 -7
- package/cookbook/README.md +26 -0
- package/cookbook/api-key-service.ts +106 -0
- package/cookbook/auth-protected-crud.ts +112 -0
- package/cookbook/file-upload-pipeline.ts +113 -0
- package/cookbook/multi-tenant-saas.ts +87 -0
- package/cookbook/real-time-chat.ts +121 -0
- package/dist/auth/auth.d.ts +21 -4
- package/dist/auth/auth.d.ts.map +1 -1
- package/dist/auth/auth.js +56 -44
- package/dist/auth/auth.js.map +1 -1
- package/dist/auth/defaults.d.ts +1 -1
- package/dist/auth/defaults.js +35 -35
- package/dist/cache/cache.d.ts +29 -6
- package/dist/cache/cache.d.ts.map +1 -1
- package/dist/cache/cache.js +72 -44
- package/dist/cache/cache.js.map +1 -1
- package/dist/cache/defaults.js +25 -25
- package/dist/cache/index.d.ts +19 -10
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +21 -18
- package/dist/cache/index.js.map +1 -1
- package/dist/config/defaults.d.ts +1 -1
- package/dist/config/defaults.js +8 -8
- package/dist/config/index.d.ts +3 -3
- package/dist/config/index.js +4 -4
- package/dist/database/adapters/mongoose.js +2 -2
- package/dist/database/adapters/prisma.js +2 -2
- package/dist/database/defaults.d.ts +1 -1
- package/dist/database/defaults.js +4 -4
- package/dist/database/index.js +2 -2
- package/dist/database/index.js.map +1 -1
- package/dist/email/defaults.js +20 -20
- package/dist/error/defaults.d.ts +1 -1
- package/dist/error/defaults.js +12 -12
- package/dist/error/error.d.ts +12 -0
- package/dist/error/error.d.ts.map +1 -1
- package/dist/error/error.js +19 -0
- package/dist/error/error.js.map +1 -1
- package/dist/error/index.d.ts +14 -3
- package/dist/error/index.d.ts.map +1 -1
- package/dist/error/index.js +14 -3
- package/dist/error/index.js.map +1 -1
- package/dist/event/defaults.js +30 -30
- package/dist/logger/defaults.d.ts +1 -1
- package/dist/logger/defaults.js +40 -40
- package/dist/logger/index.d.ts +1 -0
- package/dist/logger/index.d.ts.map +1 -1
- package/dist/logger/index.js.map +1 -1
- package/dist/logger/logger.d.ts +8 -0
- package/dist/logger/logger.d.ts.map +1 -1
- package/dist/logger/logger.js +13 -3
- package/dist/logger/logger.js.map +1 -1
- package/dist/logger/transports/console.js +1 -1
- package/dist/logger/transports/http.d.ts +1 -1
- package/dist/logger/transports/http.js +1 -1
- package/dist/logger/transports/webhook.d.ts +1 -1
- package/dist/logger/transports/webhook.js +1 -1
- package/dist/queue/defaults.d.ts +2 -2
- package/dist/queue/defaults.js +38 -38
- package/dist/security/defaults.d.ts +1 -1
- package/dist/security/defaults.js +29 -29
- package/dist/security/index.d.ts +1 -1
- package/dist/security/index.js +3 -3
- package/dist/security/security.d.ts +1 -1
- package/dist/security/security.js +4 -4
- package/dist/storage/defaults.js +19 -19
- package/dist/util/defaults.d.ts +1 -1
- package/dist/util/defaults.js +34 -34
- package/dist/util/env.d.ts +35 -0
- package/dist/util/env.d.ts.map +1 -0
- package/dist/util/env.js +50 -0
- package/dist/util/env.js.map +1 -0
- package/dist/util/errors.d.ts +52 -0
- package/dist/util/errors.d.ts.map +1 -0
- package/dist/util/errors.js +82 -0
- package/dist/util/errors.js.map +1 -0
- package/examples/.env.example +80 -0
- package/examples/README.md +16 -0
- package/examples/auth.ts +228 -0
- package/examples/cache.ts +36 -0
- package/examples/config.ts +45 -0
- package/examples/database.ts +69 -0
- package/examples/email.ts +53 -0
- package/examples/error.ts +50 -0
- package/examples/event.ts +42 -0
- package/examples/logger.ts +41 -0
- package/examples/queue.ts +58 -0
- package/examples/security.ts +46 -0
- package/examples/storage.ts +44 -0
- package/examples/util.ts +47 -0
- package/llms.txt +591 -0
- package/package.json +19 -10
- package/src/auth/README.md +850 -0
- package/src/cache/README.md +756 -0
- package/src/config/README.md +604 -0
- package/src/database/README.md +818 -0
- package/src/email/README.md +759 -0
- package/src/error/README.md +660 -0
- package/src/event/README.md +729 -0
- package/src/logger/README.md +435 -0
- package/src/queue/README.md +851 -0
- package/src/security/README.md +612 -0
- package/src/storage/README.md +1008 -0
- package/src/util/README.md +955 -0
- package/bin/templates/backend/docs/APPKIT_CLI.md +0 -507
- package/bin/templates/backend/docs/APPKIT_COMMENTS_GUIDELINES.md +0 -61
- package/bin/templates/backend/docs/APPKIT_LLM_GUIDE.md +0 -2539
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
# @bloomneo/appkit - Cache Module โก
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@bloomneo/appkit)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
> Ultra-simple caching that just works - One function, automatic Redis/Memory
|
|
7
|
+
> strategy, zero configuration
|
|
8
|
+
|
|
9
|
+
**One function** returns a cache object with automatic strategy selection. Zero
|
|
10
|
+
configuration needed, production-ready performance by default.
|
|
11
|
+
|
|
12
|
+
## ๐ Why Choose This?
|
|
13
|
+
|
|
14
|
+
- โก One Function - Just cacheClass.get() (optional namespace), everything else
|
|
15
|
+
is automatic
|
|
16
|
+
- **๐ฏ Auto-Strategy** - REDIS_URL = Redis, no URL = Memory
|
|
17
|
+
- **๐ง Zero Configuration** - Smart defaults for everything
|
|
18
|
+
- **๐ Namespace Isolation** - `users`, `sessions` - completely separate
|
|
19
|
+
- **โฐ TTL Management** - Automatic expiration
|
|
20
|
+
- **๐ Production Ready** - Redis clustering, memory limits, graceful
|
|
21
|
+
degradation
|
|
22
|
+
- **๐ค AI-Ready** - Optimized for LLM code generation
|
|
23
|
+
|
|
24
|
+
## ๐ฆ Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install @bloomneo/appkit
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## ๐โโ๏ธ Quick Start (30 seconds)
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import { cacheClass } from '@bloomneo/appkit/cache';
|
|
34
|
+
|
|
35
|
+
const cache = cacheClass.get('users');
|
|
36
|
+
|
|
37
|
+
// Set data with 1 hour expiration
|
|
38
|
+
await cache.set('user:123', { name: 'John' }, 3600);
|
|
39
|
+
|
|
40
|
+
// Get data
|
|
41
|
+
const user = await cache.get('user:123');
|
|
42
|
+
console.log(user); // { name: 'John' }
|
|
43
|
+
|
|
44
|
+
// Delete data
|
|
45
|
+
await cache.delete('user:123');
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## ๐ Environment Variables
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# Development (automatic Memory cache)
|
|
52
|
+
# No environment variables needed!
|
|
53
|
+
|
|
54
|
+
# Production (automatic Redis cache)
|
|
55
|
+
REDIS_URL=redis://localhost:6379
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## ๐ค LLM Quick Reference - Copy These Patterns
|
|
59
|
+
|
|
60
|
+
### **Basic Cache Operations (Copy Exactly)**
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
// โ
CORRECT - Complete cache setup
|
|
64
|
+
import { cacheClass } from '@bloomneo/appkit/cache';
|
|
65
|
+
const cache = cacheClass.get('namespace');
|
|
66
|
+
|
|
67
|
+
// Cache operations
|
|
68
|
+
await cache.set('key', data, 3600); // Set with TTL
|
|
69
|
+
const data = await cache.get('key'); // Get (null if not found)
|
|
70
|
+
await cache.delete('key'); // Delete key
|
|
71
|
+
await cache.clear(); // Clear namespace
|
|
72
|
+
|
|
73
|
+
// Cache-aside pattern
|
|
74
|
+
const data = await cache.getOrSet(
|
|
75
|
+
'key',
|
|
76
|
+
async () => {
|
|
77
|
+
return await fetchFromDatabase();
|
|
78
|
+
},
|
|
79
|
+
3600
|
|
80
|
+
);
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### **Namespace Usage (Copy These)**
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
// โ
CORRECT - Separate namespaces for different data
|
|
87
|
+
const userCache = cacheClass.get('users');
|
|
88
|
+
const sessionCache = cacheClass.get('sessions');
|
|
89
|
+
const apiCache = cacheClass.get('external-api');
|
|
90
|
+
|
|
91
|
+
// Each namespace is completely isolated
|
|
92
|
+
await userCache.set('123', userData);
|
|
93
|
+
await sessionCache.set('123', sessionData); // Different from user:123
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### **Error Handling (Copy This Pattern)**
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
import { cacheClass, CacheError } from '@bloomneo/appkit/cache';
|
|
100
|
+
|
|
101
|
+
// โ
CORRECT - Distinguish cache failures from other errors
|
|
102
|
+
async function getUser(id: number) {
|
|
103
|
+
try {
|
|
104
|
+
const user = await userCache.get<User>(`user:${id}`);
|
|
105
|
+
if (user) return user;
|
|
106
|
+
|
|
107
|
+
// Cache miss โ fetch from DB and populate cache
|
|
108
|
+
const fresh = await database.getUser(id);
|
|
109
|
+
await userCache.set(`user:${id}`, fresh, 3600);
|
|
110
|
+
return fresh;
|
|
111
|
+
} catch (err) {
|
|
112
|
+
if (err instanceof CacheError) {
|
|
113
|
+
// Cache infrastructure failed โ fall back silently
|
|
114
|
+
logger.warn('Cache unavailable', { code: err.code, message: err.message });
|
|
115
|
+
return await database.getUser(id);
|
|
116
|
+
}
|
|
117
|
+
throw err; // DB error or other โ caller's problem
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## โ ๏ธ Common LLM Mistakes - Avoid These
|
|
123
|
+
|
|
124
|
+
### **Wrong Cache Usage**
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
// โ WRONG - Don't access strategies directly
|
|
128
|
+
import { RedisStrategy } from '@bloomneo/appkit/cache';
|
|
129
|
+
const redis = new RedisStrategy(); // Wrong!
|
|
130
|
+
|
|
131
|
+
// โ WRONG - Missing TTL for temporary data
|
|
132
|
+
await cache.set('temp', data); // Always set TTL for temp data
|
|
133
|
+
|
|
134
|
+
// โ WRONG - Using same namespace for different data types
|
|
135
|
+
const cache = cacheClass.get('data'); // Be specific
|
|
136
|
+
await cache.set('user:123', userData);
|
|
137
|
+
await cache.set('session:456', sessionData); // Use separate namespaces
|
|
138
|
+
|
|
139
|
+
// โ
CORRECT - Use cacheClass.get() with specific namespaces
|
|
140
|
+
const userCache = cacheClass.get('users');
|
|
141
|
+
const sessionCache = cacheClass.get('sessions');
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### **Wrong Error Handling**
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
// โ WRONG - Crashing on cache miss
|
|
148
|
+
const user = await cache.get('user:123');
|
|
149
|
+
console.log(user.name); // Will crash if user is null
|
|
150
|
+
|
|
151
|
+
// โ WRONG - Not handling cache failures
|
|
152
|
+
const user = await cache.get('user:123');
|
|
153
|
+
if (!user) {
|
|
154
|
+
throw new Error('User not found'); // Should fallback to database
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// โ
CORRECT - Safe cache access with fallback
|
|
158
|
+
const user = await cache.get('user:123');
|
|
159
|
+
if (!user) {
|
|
160
|
+
user = await database.getUser(123); // Fallback to database
|
|
161
|
+
await cache.set('user:123', user, 3600); // Cache result
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### **Wrong Testing**
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
// โ WRONG - No cleanup between tests
|
|
169
|
+
test('should cache user', async () => {
|
|
170
|
+
await cache.set('user:123', userData);
|
|
171
|
+
// Missing: await cacheClass.clear();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// โ
CORRECT - Proper test cleanup
|
|
175
|
+
afterEach(async () => {
|
|
176
|
+
await cacheClass.flushAll(); // flushAll() clears data; clear() disconnects instances
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## ๐จ Error Handling Patterns
|
|
181
|
+
|
|
182
|
+
Cache operations throw `CacheError` when the underlying strategy fails (Redis
|
|
183
|
+
down, serialization error, connection timeout). Use `instanceof CacheError` to
|
|
184
|
+
distinguish cache infrastructure failures from your own errors.
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
import { cacheClass, CacheError } from '@bloomneo/appkit/cache';
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### **Cache-Aside with Fallback**
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
async function getUserProfile(userId: number) {
|
|
194
|
+
const cache = cacheClass.get('profiles');
|
|
195
|
+
try {
|
|
196
|
+
const profile = await cache.get<UserProfile>(`profile:${userId}`);
|
|
197
|
+
if (profile) return profile;
|
|
198
|
+
|
|
199
|
+
const fresh = await database.getUserProfile(userId);
|
|
200
|
+
if (fresh) await cache.set(`profile:${userId}`, fresh, 1800);
|
|
201
|
+
return fresh;
|
|
202
|
+
} catch (err) {
|
|
203
|
+
if (err instanceof CacheError) {
|
|
204
|
+
logger.warn('Cache unavailable, falling back to DB', { code: err.code });
|
|
205
|
+
return database.getUserProfile(userId);
|
|
206
|
+
}
|
|
207
|
+
throw err;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### **Session Management**
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
async function getSession(sessionId: string) {
|
|
216
|
+
const cache = cacheClass.get('sessions');
|
|
217
|
+
try {
|
|
218
|
+
return await cache.get<Session>(`session:${sessionId}`);
|
|
219
|
+
// Returns null if not found โ handle that in the caller, not here
|
|
220
|
+
} catch (err) {
|
|
221
|
+
if (err instanceof CacheError) {
|
|
222
|
+
logger.warn('Session cache unavailable', { code: err.code });
|
|
223
|
+
return null; // safe to degrade โ caller will redirect to login
|
|
224
|
+
}
|
|
225
|
+
throw err;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function createSession(userId: number): Promise<string> {
|
|
230
|
+
const cache = cacheClass.get('sessions');
|
|
231
|
+
const sessionId = crypto.randomUUID();
|
|
232
|
+
try {
|
|
233
|
+
await cache.set(`session:${sessionId}`, { userId, loginTime: Date.now() }, 7200);
|
|
234
|
+
} catch (err) {
|
|
235
|
+
if (err instanceof CacheError) {
|
|
236
|
+
logger.warn('Session not cached โ Redis unavailable', { code: err.code });
|
|
237
|
+
// Session is still valid โ just not cached. Return it.
|
|
238
|
+
} else {
|
|
239
|
+
throw err;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return sessionId;
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### **API Response Caching**
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
async function getWeather(city: string) {
|
|
250
|
+
const cache = cacheClass.get('weather');
|
|
251
|
+
const key = `weather:${city.toLowerCase()}`;
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
const cached = await cache.get<WeatherData>(key);
|
|
255
|
+
if (cached) return cached;
|
|
256
|
+
|
|
257
|
+
const data = await fetchWeatherFromApi(city);
|
|
258
|
+
await cache.set(key, data, 1800); // 30 min
|
|
259
|
+
return data;
|
|
260
|
+
} catch (err) {
|
|
261
|
+
if (err instanceof CacheError) {
|
|
262
|
+
logger.warn('Cache unavailable for weather data', { code: err.code });
|
|
263
|
+
return fetchWeatherFromApi(city); // bypass cache entirely
|
|
264
|
+
}
|
|
265
|
+
throw err; // API error โ let it propagate
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## ๐ Security & Production
|
|
271
|
+
|
|
272
|
+
### **Production Configuration**
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
# โ
SECURE - Production Redis with auth
|
|
276
|
+
REDIS_URL=redis://username:password@redis-host:6379/0
|
|
277
|
+
|
|
278
|
+
# โ
SECURE - Redis with TLS
|
|
279
|
+
REDIS_URL=rediss://username:password@redis-host:6380/0
|
|
280
|
+
|
|
281
|
+
# โ
PERFORMANCE - Custom timeouts
|
|
282
|
+
BLOOM_CACHE_TTL=3600 # 1 hour default TTL
|
|
283
|
+
BLOOM_CACHE_REDIS_CONNECT_TIMEOUT=10000 # 10 second connect timeout
|
|
284
|
+
BLOOM_CACHE_REDIS_COMMAND_TIMEOUT=5000 # 5 second command timeout
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### **Production Checklist**
|
|
288
|
+
|
|
289
|
+
- โ
**Redis Connection**: Set secure `REDIS_URL` with authentication
|
|
290
|
+
- โ
**TTL Strategy**: Set appropriate `BLOOM_CACHE_TTL` for your use case
|
|
291
|
+
- โ
**Error Handling**: Implement fallback logic for cache failures
|
|
292
|
+
- โ
**Monitoring**: Log cache hit/miss rates and errors
|
|
293
|
+
- โ
**Memory Limits**: Configure Redis memory limits and eviction policies
|
|
294
|
+
- โ
**Clustering**: Use Redis Cluster for high availability
|
|
295
|
+
|
|
296
|
+
### **Security Best Practices**
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
// โ
Namespace isolation prevents key collisions
|
|
300
|
+
const userCache = cacheClass.get('users');
|
|
301
|
+
const adminCache = cacheClass.get('admin'); // Completely separate
|
|
302
|
+
|
|
303
|
+
// โ
TTL prevents indefinite data retention
|
|
304
|
+
await cache.set('temp:token', token, 300); // 5 minutes only
|
|
305
|
+
|
|
306
|
+
// โ
Safe error handling prevents information leakage
|
|
307
|
+
try {
|
|
308
|
+
const data = await cache.get('sensitive:data');
|
|
309
|
+
return data;
|
|
310
|
+
} catch (error) {
|
|
311
|
+
console.error('Cache error:', error.message);
|
|
312
|
+
return null; // Don't expose cache errors to users
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### **Memory Strategy Security**
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
# โ
SECURE - Memory limits for development
|
|
320
|
+
BLOOM_CACHE_MEMORY_MAX_ITEMS=10000 # Max items in memory
|
|
321
|
+
BLOOM_CACHE_MEMORY_MAX_SIZE=100000000 # 100MB memory limit
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## ๐ Complete API
|
|
325
|
+
|
|
326
|
+
### Core Function
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
const cache = cacheClass.get(namespace); // One function, everything you need
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Cache Operations
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
await cache.get(key); // Get value (null if not found)
|
|
336
|
+
await cache.set(key, value, ttl?); // Set value with TTL in seconds
|
|
337
|
+
await cache.delete(key); // Remove key
|
|
338
|
+
await cache.clear(); // Clear entire namespace
|
|
339
|
+
await cache.getOrSet(key, factory, ttl?); // Get cached or compute and cache
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Utility Methods
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
cache.getStrategy(); // 'redis' or 'memory'
|
|
346
|
+
cacheClass.hasRedis(); // true if REDIS_URL is set
|
|
347
|
+
cacheClass.getActiveNamespaces(); // List of active namespaces
|
|
348
|
+
cacheClass.getConfig(); // Configuration summary
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
## ๐ก Usage Examples
|
|
352
|
+
|
|
353
|
+
### **Basic User Caching**
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
import { cacheClass } from '@bloomneo/appkit/cache';
|
|
357
|
+
|
|
358
|
+
const userCache = cacheClass.get('users');
|
|
359
|
+
|
|
360
|
+
async function getUser(id) {
|
|
361
|
+
// Try cache first
|
|
362
|
+
let user = await userCache.get(`user:${id}`);
|
|
363
|
+
|
|
364
|
+
if (!user) {
|
|
365
|
+
// Get from database
|
|
366
|
+
user = await db.users.findById(id);
|
|
367
|
+
|
|
368
|
+
// Cache for 1 hour
|
|
369
|
+
await userCache.set(`user:${id}`, user, 3600);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return user;
|
|
373
|
+
}
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### **API Response Caching**
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
import { cacheClass } from '@bloomneo/appkit/cache';
|
|
380
|
+
|
|
381
|
+
const apiCache = cacheClass.get('external-api');
|
|
382
|
+
|
|
383
|
+
async function getWeather(city) {
|
|
384
|
+
return await apiCache.getOrSet(
|
|
385
|
+
`weather:${city}`,
|
|
386
|
+
async () => {
|
|
387
|
+
// This only runs on cache miss
|
|
388
|
+
const response = await fetch(
|
|
389
|
+
`https://api.weather.com/v1/weather?q=${city}`
|
|
390
|
+
);
|
|
391
|
+
return await response.json();
|
|
392
|
+
},
|
|
393
|
+
1800 // Cache for 30 minutes
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// First call: hits API
|
|
398
|
+
const weather1 = await getWeather('london');
|
|
399
|
+
|
|
400
|
+
// Second call: returns cached result (fast!)
|
|
401
|
+
const weather2 = await getWeather('london');
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### **Session Management**
|
|
405
|
+
|
|
406
|
+
```typescript
|
|
407
|
+
import { cacheClass } from '@bloomneo/appkit/cache';
|
|
408
|
+
|
|
409
|
+
const sessionCache = cacheClass.get('sessions');
|
|
410
|
+
|
|
411
|
+
// Store session
|
|
412
|
+
async function createSession(userId) {
|
|
413
|
+
const sessionId = crypto.randomUUID();
|
|
414
|
+
const sessionData = { userId, loginTime: Date.now() };
|
|
415
|
+
|
|
416
|
+
// Store for 2 hours
|
|
417
|
+
await sessionCache.set(`session:${sessionId}`, sessionData, 7200);
|
|
418
|
+
|
|
419
|
+
return sessionId;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Get session
|
|
423
|
+
async function getSession(sessionId) {
|
|
424
|
+
return await sessionCache.get(`session:${sessionId}`);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Remove session
|
|
428
|
+
async function logout(sessionId) {
|
|
429
|
+
await sessionCache.delete(`session:${sessionId}`);
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### **Shopping Cart**
|
|
434
|
+
|
|
435
|
+
```typescript
|
|
436
|
+
import { cacheClass } from '@bloomneo/appkit/cache';
|
|
437
|
+
|
|
438
|
+
const cartCache = cacheClass.get('shopping-carts');
|
|
439
|
+
|
|
440
|
+
// Add item to cart
|
|
441
|
+
async function addToCart(userId, item) {
|
|
442
|
+
const cart = (await cartCache.get(`cart:${userId}`)) || [];
|
|
443
|
+
cart.push(item);
|
|
444
|
+
|
|
445
|
+
// Cart expires in 24 hours
|
|
446
|
+
await cartCache.set(`cart:${userId}`, cart, 86400);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Get cart
|
|
450
|
+
async function getCart(userId) {
|
|
451
|
+
return (await cartCache.get(`cart:${userId}`)) || [];
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Clear cart
|
|
455
|
+
async function clearCart(userId) {
|
|
456
|
+
await cartCache.delete(`cart:${userId}`);
|
|
457
|
+
}
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
### **Rate Limiting Cache**
|
|
461
|
+
|
|
462
|
+
```typescript
|
|
463
|
+
import { cacheClass } from '@bloomneo/appkit/cache';
|
|
464
|
+
|
|
465
|
+
const rateLimitCache = cacheClass.get('rate-limits');
|
|
466
|
+
|
|
467
|
+
async function checkRateLimit(userId, maxRequests = 100, windowSeconds = 3600) {
|
|
468
|
+
const key = `rate:${userId}:${Math.floor(Date.now() / 1000 / windowSeconds)}`;
|
|
469
|
+
|
|
470
|
+
const current = (await rateLimitCache.get(key)) || 0;
|
|
471
|
+
|
|
472
|
+
if (current >= maxRequests) {
|
|
473
|
+
throw new Error('Rate limit exceeded');
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Increment counter
|
|
477
|
+
await rateLimitCache.set(key, current + 1, windowSeconds);
|
|
478
|
+
|
|
479
|
+
return {
|
|
480
|
+
remaining: maxRequests - current - 1,
|
|
481
|
+
resetTime: Math.ceil(Date.now() / 1000 / windowSeconds) * windowSeconds,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
## ๐ง Platform Setup
|
|
487
|
+
|
|
488
|
+
### **Local Development**
|
|
489
|
+
|
|
490
|
+
```bash
|
|
491
|
+
# No setup needed - uses memory automatically
|
|
492
|
+
npm start
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
### **Production with Docker**
|
|
496
|
+
|
|
497
|
+
```yaml
|
|
498
|
+
version: '3.8'
|
|
499
|
+
services:
|
|
500
|
+
app:
|
|
501
|
+
image: my-app
|
|
502
|
+
environment:
|
|
503
|
+
REDIS_URL: redis://redis:6379
|
|
504
|
+
redis:
|
|
505
|
+
image: redis:alpine
|
|
506
|
+
command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
### **Production with Redis Cloud**
|
|
510
|
+
|
|
511
|
+
```bash
|
|
512
|
+
# Redis Cloud / AWS ElastiCache / Azure Redis
|
|
513
|
+
REDIS_URL=redis://username:password@your-redis-host:6379
|
|
514
|
+
|
|
515
|
+
# Redis Cluster
|
|
516
|
+
REDIS_URL=redis://user:pass@cluster.cache.amazonaws.com:6379
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
### **Vercel/Railway/Heroku**
|
|
520
|
+
|
|
521
|
+
```bash
|
|
522
|
+
# Just add Redis URL in dashboard
|
|
523
|
+
REDIS_URL=redis://your-redis-provider.com:6379
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
## ๐ Development vs Production
|
|
527
|
+
|
|
528
|
+
### **Development Mode**
|
|
529
|
+
|
|
530
|
+
```bash
|
|
531
|
+
# No environment variables needed
|
|
532
|
+
NODE_ENV=development
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
```typescript
|
|
536
|
+
const cache = cacheClass.get('users');
|
|
537
|
+
// Strategy: Memory (in-process)
|
|
538
|
+
// Features: LRU eviction, TTL cleanup, memory limits
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
### **Production Mode**
|
|
542
|
+
|
|
543
|
+
```bash
|
|
544
|
+
# Redis required for scaling
|
|
545
|
+
NODE_ENV=production
|
|
546
|
+
REDIS_URL=redis://your-redis-host:6379
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
```typescript
|
|
550
|
+
const cache = cacheClass.get('users');
|
|
551
|
+
// Strategy: Redis (distributed)
|
|
552
|
+
// Features: Clustering, persistence, atomic operations
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
### **Scaling Pattern**
|
|
556
|
+
|
|
557
|
+
```typescript
|
|
558
|
+
// Week 1: Local development
|
|
559
|
+
// No Redis needed - works immediately
|
|
560
|
+
|
|
561
|
+
// Month 1: Add Redis
|
|
562
|
+
// Set REDIS_URL - zero code changes
|
|
563
|
+
|
|
564
|
+
// Year 1: Redis clustering
|
|
565
|
+
// Update REDIS_URL to cluster - automatic scaling
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
## ๐งช Testing
|
|
569
|
+
|
|
570
|
+
```typescript
|
|
571
|
+
import { cacheClass } from '@bloomneo/appkit/cache';
|
|
572
|
+
|
|
573
|
+
describe('Cache Tests', () => {
|
|
574
|
+
afterEach(async () => {
|
|
575
|
+
// flushAll() clears cached data. clear() disconnects all instances
|
|
576
|
+
// (use only for full teardown, not between individual tests).
|
|
577
|
+
await cacheClass.flushAll();
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
test('basic caching', async () => {
|
|
581
|
+
const cache = cacheClass.get('test');
|
|
582
|
+
|
|
583
|
+
await cache.set('key', 'value', 60);
|
|
584
|
+
const result = await cache.get('key');
|
|
585
|
+
|
|
586
|
+
expect(result).toBe('value');
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
test('cache miss returns null', async () => {
|
|
590
|
+
const cache = cacheClass.get('test');
|
|
591
|
+
const result = await cache.get('nonexistent');
|
|
592
|
+
expect(result).toBeNull();
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
test('namespace isolation', async () => {
|
|
596
|
+
const cache1 = cacheClass.get('namespace1');
|
|
597
|
+
const cache2 = cacheClass.get('namespace2');
|
|
598
|
+
|
|
599
|
+
await cache1.set('key', 'value1');
|
|
600
|
+
await cache2.set('key', 'value2');
|
|
601
|
+
|
|
602
|
+
expect(await cache1.get('key')).toBe('value1');
|
|
603
|
+
expect(await cache2.get('key')).toBe('value2');
|
|
604
|
+
});
|
|
605
|
+
});
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
### **Force Memory Strategy for Tests**
|
|
609
|
+
|
|
610
|
+
```typescript
|
|
611
|
+
describe('Cache with Memory Strategy', () => {
|
|
612
|
+
beforeEach(async () => {
|
|
613
|
+
// Force memory strategy โ no Redis required in CI
|
|
614
|
+
await cacheClass.reset({
|
|
615
|
+
strategy: 'memory',
|
|
616
|
+
memory: {
|
|
617
|
+
maxItems: 1000,
|
|
618
|
+
maxSizeBytes: 1048576, // 1MB
|
|
619
|
+
checkInterval: 60000,
|
|
620
|
+
},
|
|
621
|
+
});
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
afterEach(async () => {
|
|
625
|
+
await cacheClass.flushAll(); // clear data, keep instances alive
|
|
626
|
+
});
|
|
627
|
+
});
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
## ๐ Performance
|
|
631
|
+
|
|
632
|
+
- **Memory Strategy**: ~0.1ms per operation
|
|
633
|
+
- **Redis Strategy**: ~1-5ms per operation (network dependent)
|
|
634
|
+
- **Automatic Strategy**: Zero overhead detection
|
|
635
|
+
- **TTL Cleanup**: Background cleanup with minimal impact
|
|
636
|
+
- **Memory Usage**: Configurable limits with LRU eviction
|
|
637
|
+
- **Redis Clustering**: Horizontal scaling support
|
|
638
|
+
|
|
639
|
+
## ๐ฐ Cost Comparison
|
|
640
|
+
|
|
641
|
+
| Strategy | Speed | Persistence | Scaling | Best For |
|
|
642
|
+
| ---------- | ---------------- | ----------- | ------------- | ---------------------------- |
|
|
643
|
+
| **Memory** | Fastest (~0.1ms) | No | Single server | Development, testing |
|
|
644
|
+
| **Redis** | Fast (~1-5ms) | Yes | Multi-server | Production, distributed apps |
|
|
645
|
+
|
|
646
|
+
## ๐ TypeScript Support
|
|
647
|
+
|
|
648
|
+
```typescript
|
|
649
|
+
import { cacheClass, CacheError } from '@bloomneo/appkit/cache';
|
|
650
|
+
import type { Cache } from '@bloomneo/appkit/cache';
|
|
651
|
+
|
|
652
|
+
// Generics โ no casting needed
|
|
653
|
+
const cache: Cache = cacheClass.get('users');
|
|
654
|
+
const user = await cache.get<User>('user:123'); // User | null
|
|
655
|
+
const ok = await cache.set<User>('user:123', userData); // boolean
|
|
656
|
+
const list = await cache.getOrSet<User[]>('all', fetchUsers, 60); // User[]
|
|
657
|
+
|
|
658
|
+
// Type-narrow infrastructure errors
|
|
659
|
+
try {
|
|
660
|
+
await cache.set('key', value);
|
|
661
|
+
} catch (err) {
|
|
662
|
+
if (err instanceof CacheError) {
|
|
663
|
+
console.error(err.code); // 'CACHE_SET_FAILED' | 'CACHE_INVALID_KEY' | ...
|
|
664
|
+
console.error(err.message); // '[@bloomneo/appkit/cache] set failed for key ...'
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
### **CacheError codes**
|
|
670
|
+
|
|
671
|
+
| Code | When |
|
|
672
|
+
|---|---|
|
|
673
|
+
| `CACHE_CONNECT_FAILED` | Strategy failed to connect (Redis unreachable) |
|
|
674
|
+
| `CACHE_GET_FAILED` | Strategy threw during `get` |
|
|
675
|
+
| `CACHE_SET_FAILED` | Strategy threw during `set` |
|
|
676
|
+
| `CACHE_DELETE_FAILED` | Strategy threw during `delete` |
|
|
677
|
+
| `CACHE_CLEAR_FAILED` | Strategy threw during `clear` |
|
|
678
|
+
| `CACHE_INVALID_KEY` | Key is empty, too long, has colons or newlines |
|
|
679
|
+
| `CACHE_INVALID_VALUE` | Value is `undefined` or not JSON-serializable |
|
|
680
|
+
| `CACHE_ERROR` | Generic fallback (use when no specific code fits) |
|
|
681
|
+
|
|
682
|
+
## ๐ Why Not Redis directly?
|
|
683
|
+
|
|
684
|
+
**Other approaches:**
|
|
685
|
+
|
|
686
|
+
```javascript
|
|
687
|
+
// Redis directly: Complex setup, manual serialization
|
|
688
|
+
const redis = require('redis');
|
|
689
|
+
const client = redis.createClient(process.env.REDIS_URL);
|
|
690
|
+
|
|
691
|
+
await client.connect();
|
|
692
|
+
const user = JSON.parse(await client.get('user:123'));
|
|
693
|
+
await client.setEx('user:123', 3600, JSON.stringify(userData));
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
**This library:**
|
|
697
|
+
|
|
698
|
+
```typescript
|
|
699
|
+
// 3 lines, automatic Redis/Memory, built-in serialization
|
|
700
|
+
import { cacheClass } from '@bloomneo/appkit/cache';
|
|
701
|
+
const cache = cacheClass.get('users');
|
|
702
|
+
await cache.set('user:123', userData, 3600);
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
**Same features, 90% less code, automatic strategy selection.**
|
|
706
|
+
|
|
707
|
+
## Agent-Dev Friendliness Score
|
|
708
|
+
|
|
709
|
+
**Score: 75.3/100 โ ๐ก Solid** *(no cap)*
|
|
710
|
+
*Scored 2026-04-11 by Claude ยท Rubric [`AGENT_DEV_SCORING_ALGORITHM.md`](../../AGENT_DEV_SCORING_ALGORITHM.md) v1.1*
|
|
711
|
+
|
|
712
|
+
| # | Dimension | Score | Notes |
|
|
713
|
+
|---|---|---:|---|
|
|
714
|
+
| 1 | API correctness | **9** | `examples/cache.ts` had `cache.has()` (not public) and `cache.del()` (wrong name) โ both fixed. |
|
|
715
|
+
| 2 | Doc consistency | **9** | README now matches source after fixing test-cleanup pattern (`flushAll` vs `clear`). |
|
|
716
|
+
| 3 | Runtime verification | **9** | 49 vitest tests covering all public methods + drift-check section. |
|
|
717
|
+
| 4 | Type safety | **5** | `Cache` interface uses `any` for all values โ no generics like `cache.get<T>()`. |
|
|
718
|
+
| 5 | Discoverability | **8** | Quick Start is clear and concise. |
|
|
719
|
+
| 6 | Example completeness | **7** | `examples/cache.ts` covers main patterns. Utility methods (`getConfig`, `getActiveNamespaces`) not shown. |
|
|
720
|
+
| 7 | Composability | **8** | `examples/cache.ts` now compiles. Patterns compose naturally. |
|
|
721
|
+
| 8 | Educational errors | **5** | Errors are plain English but missing `[@bloomneo/appkit/cache]` prefix + DOCS_URL anchor format used by auth module. |
|
|
722
|
+
| 9 | Convention enforcement | **9** | One canonical entry point: `cacheClass.get(namespace)`. Consistent across all examples. |
|
|
723
|
+
| 10 | Drift prevention | **5** | Drift-check section in test catches runtime drift. No scripted doc-vs-source checker. |
|
|
724
|
+
| 11 | Reading order | **8** | Quick Start โ LLM ref โ errors โ API โ examples โ testing โ no dead ends. |
|
|
725
|
+
| **12** | **Simplicity** | **8** | 5 core ops on instance + 9 class utilities. Dual surface is minimal. |
|
|
726
|
+
| **13** | **Clarity** | **6** | `cacheClass.clear()` (disconnects all instances) vs `cache.clear()` (clears namespace data) โ same name, different effects. Confusing. |
|
|
727
|
+
| **14** | **Unambiguity** | **5** | The `clear()` collision is the dominant ambiguity โ easy to call the wrong one in teardown. Also: `getOrSet` miss-then-throw behavior (does not cache error path) could surprise users. |
|
|
728
|
+
| **15** | **Learning curve** | **9** | Zero config. `cacheClass.get(ns) โ cache.set/get/delete`. First call in < 2 minutes. |
|
|
729
|
+
|
|
730
|
+
### Weighted (v1.1)
|
|
731
|
+
|
|
732
|
+
```
|
|
733
|
+
(9ร.12)+(9ร.08)+(9ร.09)+(5ร.06)+(8ร.06)+(7ร.08)+(8ร.06)+(5ร.05)+(9ร.05)+(5ร.04)+(8ร.03)
|
|
734
|
+
+(8ร.09)+(6ร.09)+(5ร.05)+(9ร.05) = 7.53 โ 75.3/100
|
|
735
|
+
No anti-pattern cap (examples compile after fixes).
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
### Gaps to reach ๐ข 90+
|
|
739
|
+
|
|
740
|
+
1. **D13/D14 โ 9**: Rename `cacheClass.clear()` โ `cacheClass.disconnect()` (or `shutdown()` โ already exists) to eliminate the collision with `cache.clear()`. The two `clear()` callsites should behave consistently.
|
|
741
|
+
2. **D4 Type safety โ 9**: Add generic overload `get<T>(key: string): Promise<T | null>` and `set<T>(key, value: T, ttl?)`.
|
|
742
|
+
3. **D8 Educational errors โ 9**: Adopt `[@bloomneo/appkit/cache] message + DOCS_URL#anchor` format from auth module.
|
|
743
|
+
4. **D10 Drift prevention โ 8**: Scripted doc-vs-source drift checker.
|
|
744
|
+
|
|
745
|
+
**Realistic ceiling:** ~88/100 with fixes 1โ4.
|
|
746
|
+
|
|
747
|
+
## ๐ License
|
|
748
|
+
|
|
749
|
+
MIT ยฉ [Bloomneo](https://github.com/bloomneo)
|
|
750
|
+
|
|
751
|
+
---
|
|
752
|
+
|
|
753
|
+
<p align="center">
|
|
754
|
+
<strong>Built with โค๏ธ by the <a href="https://github.com/bloomneo">Bloomneo Team</a></strong><br>
|
|
755
|
+
Because caching should be simple, not a Redis nightmare.
|
|
756
|
+
</p>
|