@donkeylabs/server 0.1.3 → 0.1.4
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/examples/starter/node_modules/@donkeylabs/server/README.md +15 -0
- package/examples/starter/node_modules/@donkeylabs/server/cli/commands/generate.ts +461 -0
- package/examples/starter/node_modules/@donkeylabs/server/cli/commands/init.ts +476 -0
- package/examples/starter/node_modules/@donkeylabs/server/cli/commands/interactive.ts +223 -0
- package/examples/starter/node_modules/@donkeylabs/server/cli/commands/plugin.ts +192 -0
- package/examples/starter/node_modules/@donkeylabs/server/cli/donkeylabs +106 -0
- package/examples/starter/node_modules/@donkeylabs/server/cli/index.ts +100 -0
- package/examples/starter/node_modules/@donkeylabs/server/context.d.ts +17 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/api-client.md +520 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/cache.md +437 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/cli.md +353 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/core-services.md +338 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/cron.md +465 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/errors.md +303 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/events.md +460 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/handlers.md +549 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/jobs.md +556 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/logger.md +316 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/middleware.md +682 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/plugins.md +524 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/project-structure.md +493 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/rate-limiter.md +525 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/router.md +566 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/sse.md +542 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/svelte-frontend.md +324 -0
- package/examples/starter/node_modules/@donkeylabs/server/mcp/donkeylabs-mcp +3238 -0
- package/examples/starter/node_modules/@donkeylabs/server/mcp/server.ts +3238 -0
- package/examples/starter/node_modules/@donkeylabs/server/package.json +77 -0
- package/examples/starter/node_modules/@donkeylabs/server/registry.d.ts +11 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/client/base.ts +481 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/client/index.ts +150 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/core/cache.ts +183 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/core/cron.ts +255 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/core/errors.ts +320 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/core/events.ts +163 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/core/index.ts +94 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/core/jobs.ts +334 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/core/logger.ts +131 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/core/rate-limiter.ts +193 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/core/sse.ts +210 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/core.ts +428 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/handlers.ts +87 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/harness.ts +70 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/index.ts +38 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/middleware.ts +34 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/registry.ts +13 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/router.ts +155 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/server.ts +234 -0
- package/examples/starter/node_modules/@donkeylabs/server/templates/init/donkeylabs.config.ts.template +14 -0
- package/examples/starter/node_modules/@donkeylabs/server/templates/init/index.ts.template +41 -0
- package/examples/starter/node_modules/@donkeylabs/server/templates/plugin/index.ts.template +25 -0
- package/examples/starter/src/routes/health/ping/models/model.ts +11 -7
- package/package.json +3 -3
- package/examples/starter/node_modules/.svelte2tsx-language-server-files/svelte-native-jsx.d.ts +0 -32
- package/examples/starter/node_modules/.svelte2tsx-language-server-files/svelte-shims-v4.d.ts +0 -290
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
# Cache Service
|
|
2
|
+
|
|
3
|
+
High-performance key-value store with TTL (time-to-live), LRU eviction, and pattern-based key listing.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
// Set and get values
|
|
9
|
+
await ctx.core.cache.set("user:123", { name: "Alice" }, 60000); // 1 minute TTL
|
|
10
|
+
const user = await ctx.core.cache.get("user:123");
|
|
11
|
+
|
|
12
|
+
// Cache-aside pattern
|
|
13
|
+
const data = await ctx.core.cache.getOrSet("expensive:query", async () => {
|
|
14
|
+
return await db.selectFrom("large_table").execute();
|
|
15
|
+
}, 300000); // 5 minutes TTL
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## API Reference
|
|
21
|
+
|
|
22
|
+
### Interface
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
interface Cache {
|
|
26
|
+
get<T>(key: string): Promise<T | null>;
|
|
27
|
+
set<T>(key: string, value: T, ttlMs?: number): Promise<void>;
|
|
28
|
+
delete(key: string): Promise<boolean>;
|
|
29
|
+
has(key: string): Promise<boolean>;
|
|
30
|
+
clear(): Promise<void>;
|
|
31
|
+
keys(pattern?: string): Promise<string[]>;
|
|
32
|
+
getOrSet<T>(key: string, factory: () => Promise<T>, ttlMs?: number): Promise<T>;
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Methods
|
|
37
|
+
|
|
38
|
+
| Method | Description |
|
|
39
|
+
|--------|-------------|
|
|
40
|
+
| `get(key)` | Retrieve value, returns `null` if not found or expired |
|
|
41
|
+
| `set(key, value, ttl?)` | Store value with optional TTL in milliseconds |
|
|
42
|
+
| `delete(key)` | Remove key, returns `true` if existed |
|
|
43
|
+
| `has(key)` | Check if key exists and is not expired |
|
|
44
|
+
| `clear()` | Remove all keys |
|
|
45
|
+
| `keys(pattern?)` | List keys matching glob pattern |
|
|
46
|
+
| `getOrSet(key, factory, ttl?)` | Get existing or compute and cache |
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Configuration
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
const server = new AppServer({
|
|
54
|
+
db,
|
|
55
|
+
cache: {
|
|
56
|
+
defaultTtlMs: 300000, // Default TTL: 5 minutes
|
|
57
|
+
maxSize: 1000, // Max items before LRU eviction
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Usage Examples
|
|
65
|
+
|
|
66
|
+
### Basic Operations
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
// Store various types
|
|
70
|
+
await cache.set("string", "hello");
|
|
71
|
+
await cache.set("number", 42);
|
|
72
|
+
await cache.set("object", { nested: { data: true } });
|
|
73
|
+
await cache.set("array", [1, 2, 3]);
|
|
74
|
+
|
|
75
|
+
// Retrieve with type
|
|
76
|
+
const str = await cache.get<string>("string");
|
|
77
|
+
const num = await cache.get<number>("number");
|
|
78
|
+
const obj = await cache.get<{ nested: { data: boolean } }>("object");
|
|
79
|
+
|
|
80
|
+
// Check existence
|
|
81
|
+
if (await cache.has("string")) {
|
|
82
|
+
console.log("Key exists");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Delete
|
|
86
|
+
const deleted = await cache.delete("string");
|
|
87
|
+
console.log(deleted); // true
|
|
88
|
+
|
|
89
|
+
// Clear all
|
|
90
|
+
await cache.clear();
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### TTL (Time-To-Live)
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
// Cache for 1 minute
|
|
97
|
+
await cache.set("short-lived", data, 60000);
|
|
98
|
+
|
|
99
|
+
// Cache for 1 hour
|
|
100
|
+
await cache.set("long-lived", data, 3600000);
|
|
101
|
+
|
|
102
|
+
// Use default TTL (from config)
|
|
103
|
+
await cache.set("default-ttl", data);
|
|
104
|
+
|
|
105
|
+
// No expiration (pass null or 0)
|
|
106
|
+
await cache.set("permanent", data, 0);
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Cache-Aside Pattern (getOrSet)
|
|
110
|
+
|
|
111
|
+
The most common caching pattern - return cached value or compute and cache:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
// Database query caching
|
|
115
|
+
const users = await ctx.core.cache.getOrSet(
|
|
116
|
+
"users:active",
|
|
117
|
+
async () => {
|
|
118
|
+
return ctx.db
|
|
119
|
+
.selectFrom("users")
|
|
120
|
+
.where("active", "=", true)
|
|
121
|
+
.execute();
|
|
122
|
+
},
|
|
123
|
+
60000 // 1 minute
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// API response caching
|
|
127
|
+
const weather = await ctx.core.cache.getOrSet(
|
|
128
|
+
`weather:${city}`,
|
|
129
|
+
async () => {
|
|
130
|
+
const res = await fetch(`https://api.weather.com/${city}`);
|
|
131
|
+
return res.json();
|
|
132
|
+
},
|
|
133
|
+
300000 // 5 minutes
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
// Computed value caching
|
|
137
|
+
const stats = await ctx.core.cache.getOrSet(
|
|
138
|
+
"dashboard:stats",
|
|
139
|
+
async () => ({
|
|
140
|
+
totalUsers: await countUsers(),
|
|
141
|
+
totalOrders: await countOrders(),
|
|
142
|
+
revenue: await calculateRevenue(),
|
|
143
|
+
}),
|
|
144
|
+
60000
|
|
145
|
+
);
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Key Patterns
|
|
149
|
+
|
|
150
|
+
List keys using glob patterns:
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
// Store user-related data
|
|
154
|
+
await cache.set("user:1:profile", { name: "Alice" });
|
|
155
|
+
await cache.set("user:1:preferences", { theme: "dark" });
|
|
156
|
+
await cache.set("user:2:profile", { name: "Bob" });
|
|
157
|
+
await cache.set("session:abc123", { userId: 1 });
|
|
158
|
+
|
|
159
|
+
// Find all user:1 keys
|
|
160
|
+
const user1Keys = await cache.keys("user:1:*");
|
|
161
|
+
// ["user:1:profile", "user:1:preferences"]
|
|
162
|
+
|
|
163
|
+
// Find all profile keys
|
|
164
|
+
const profileKeys = await cache.keys("user:*:profile");
|
|
165
|
+
// ["user:1:profile", "user:2:profile"]
|
|
166
|
+
|
|
167
|
+
// Find all keys
|
|
168
|
+
const allKeys = await cache.keys();
|
|
169
|
+
// ["user:1:profile", "user:1:preferences", "user:2:profile", "session:abc123"]
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Real-World Examples
|
|
175
|
+
|
|
176
|
+
### User Session Caching
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
// plugins/auth/index.ts
|
|
180
|
+
service: async (ctx) => {
|
|
181
|
+
const cache = ctx.core.cache;
|
|
182
|
+
const SESSION_TTL = 3600000; // 1 hour
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
async createSession(userId: number) {
|
|
186
|
+
const sessionId = crypto.randomUUID();
|
|
187
|
+
const session = {
|
|
188
|
+
userId,
|
|
189
|
+
createdAt: new Date().toISOString(),
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
await cache.set(`session:${sessionId}`, session, SESSION_TTL);
|
|
193
|
+
return sessionId;
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
async getSession(sessionId: string) {
|
|
197
|
+
return cache.get(`session:${sessionId}`);
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
async destroySession(sessionId: string) {
|
|
201
|
+
return cache.delete(`session:${sessionId}`);
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
async destroyUserSessions(userId: number) {
|
|
205
|
+
const keys = await cache.keys(`session:*`);
|
|
206
|
+
for (const key of keys) {
|
|
207
|
+
const session = await cache.get(key);
|
|
208
|
+
if (session?.userId === userId) {
|
|
209
|
+
await cache.delete(key);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
};
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Rate Limit State Caching
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
async function checkRateLimit(ctx: ServerContext, key: string, limit: number, windowMs: number) {
|
|
221
|
+
const cacheKey = `ratelimit:${key}`;
|
|
222
|
+
const current = await ctx.core.cache.get<number>(cacheKey) ?? 0;
|
|
223
|
+
|
|
224
|
+
if (current >= limit) {
|
|
225
|
+
return { allowed: false, remaining: 0 };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
await ctx.core.cache.set(cacheKey, current + 1, windowMs);
|
|
229
|
+
return { allowed: true, remaining: limit - current - 1 };
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Database Query Caching
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
// Cache expensive queries
|
|
237
|
+
router.route("dashboard").typed({
|
|
238
|
+
handle: async (input, ctx) => {
|
|
239
|
+
const cacheKey = `dashboard:${ctx.user.id}`;
|
|
240
|
+
|
|
241
|
+
return ctx.core.cache.getOrSet(cacheKey, async () => {
|
|
242
|
+
// These queries only run on cache miss
|
|
243
|
+
const [orders, notifications, stats] = await Promise.all([
|
|
244
|
+
ctx.db.selectFrom("orders")
|
|
245
|
+
.where("userId", "=", ctx.user.id)
|
|
246
|
+
.orderBy("createdAt", "desc")
|
|
247
|
+
.limit(10)
|
|
248
|
+
.execute(),
|
|
249
|
+
|
|
250
|
+
ctx.db.selectFrom("notifications")
|
|
251
|
+
.where("userId", "=", ctx.user.id)
|
|
252
|
+
.where("read", "=", false)
|
|
253
|
+
.execute(),
|
|
254
|
+
|
|
255
|
+
ctx.db.selectFrom("user_stats")
|
|
256
|
+
.where("userId", "=", ctx.user.id)
|
|
257
|
+
.executeTakeFirst(),
|
|
258
|
+
]);
|
|
259
|
+
|
|
260
|
+
return { orders, notifications, stats };
|
|
261
|
+
}, 30000); // 30 seconds
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Cache Invalidation
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
// Invalidate on data changes
|
|
270
|
+
service: async (ctx) => ({
|
|
271
|
+
async updateUserProfile(userId: number, data: ProfileUpdate) {
|
|
272
|
+
await ctx.db.updateTable("users")
|
|
273
|
+
.set(data)
|
|
274
|
+
.where("id", "=", userId)
|
|
275
|
+
.execute();
|
|
276
|
+
|
|
277
|
+
// Invalidate related caches
|
|
278
|
+
await ctx.core.cache.delete(`user:${userId}:profile`);
|
|
279
|
+
await ctx.core.cache.delete(`dashboard:${userId}`);
|
|
280
|
+
|
|
281
|
+
// Invalidate pattern-matched keys
|
|
282
|
+
const userKeys = await ctx.core.cache.keys(`user:${userId}:*`);
|
|
283
|
+
for (const key of userKeys) {
|
|
284
|
+
await ctx.core.cache.delete(key);
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## LRU Eviction
|
|
293
|
+
|
|
294
|
+
When the cache reaches `maxSize`, the least recently used items are evicted:
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
// With maxSize: 3
|
|
298
|
+
await cache.set("a", 1); // Cache: [a]
|
|
299
|
+
await cache.set("b", 2); // Cache: [a, b]
|
|
300
|
+
await cache.set("c", 3); // Cache: [a, b, c]
|
|
301
|
+
|
|
302
|
+
await cache.get("a"); // Access 'a', moves to end: [b, c, a]
|
|
303
|
+
|
|
304
|
+
await cache.set("d", 4); // Evicts 'b' (LRU): [c, a, d]
|
|
305
|
+
await cache.get("b"); // null - was evicted
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## Custom Adapters
|
|
311
|
+
|
|
312
|
+
Implement `CacheAdapter` for custom backends:
|
|
313
|
+
|
|
314
|
+
```ts
|
|
315
|
+
interface CacheAdapter {
|
|
316
|
+
get<T>(key: string): Promise<T | null>;
|
|
317
|
+
set<T>(key: string, value: T, ttlMs?: number): Promise<void>;
|
|
318
|
+
delete(key: string): Promise<boolean>;
|
|
319
|
+
has(key: string): Promise<boolean>;
|
|
320
|
+
clear(): Promise<void>;
|
|
321
|
+
keys(pattern?: string): Promise<string[]>;
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Redis Adapter Example
|
|
326
|
+
|
|
327
|
+
```ts
|
|
328
|
+
import { createCache, type CacheAdapter } from "./core/cache";
|
|
329
|
+
import Redis from "ioredis";
|
|
330
|
+
|
|
331
|
+
class RedisCacheAdapter implements CacheAdapter {
|
|
332
|
+
constructor(private redis: Redis) {}
|
|
333
|
+
|
|
334
|
+
async get<T>(key: string): Promise<T | null> {
|
|
335
|
+
const value = await this.redis.get(key);
|
|
336
|
+
return value ? JSON.parse(value) : null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async set<T>(key: string, value: T, ttlMs?: number): Promise<void> {
|
|
340
|
+
const serialized = JSON.stringify(value);
|
|
341
|
+
if (ttlMs) {
|
|
342
|
+
await this.redis.set(key, serialized, "PX", ttlMs);
|
|
343
|
+
} else {
|
|
344
|
+
await this.redis.set(key, serialized);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async delete(key: string): Promise<boolean> {
|
|
349
|
+
const result = await this.redis.del(key);
|
|
350
|
+
return result > 0;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async has(key: string): Promise<boolean> {
|
|
354
|
+
return (await this.redis.exists(key)) === 1;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async clear(): Promise<void> {
|
|
358
|
+
await this.redis.flushdb();
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async keys(pattern?: string): Promise<string[]> {
|
|
362
|
+
return this.redis.keys(pattern ?? "*");
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Use Redis adapter
|
|
367
|
+
const cache = createCache({
|
|
368
|
+
adapter: new RedisCacheAdapter(new Redis()),
|
|
369
|
+
});
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
## Best Practices
|
|
375
|
+
|
|
376
|
+
### 1. Use Meaningful Key Prefixes
|
|
377
|
+
|
|
378
|
+
```ts
|
|
379
|
+
// Good - organized and predictable
|
|
380
|
+
await cache.set("user:123:profile", data);
|
|
381
|
+
await cache.set("user:123:settings", data);
|
|
382
|
+
await cache.set("session:abc123", data);
|
|
383
|
+
await cache.set("api:weather:seattle", data);
|
|
384
|
+
|
|
385
|
+
// Bad - inconsistent and hard to manage
|
|
386
|
+
await cache.set("u123", data);
|
|
387
|
+
await cache.set("profile_123", data);
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### 2. Set Appropriate TTLs
|
|
391
|
+
|
|
392
|
+
```ts
|
|
393
|
+
// Frequently changing data - short TTL
|
|
394
|
+
await cache.set("stock:price", price, 5000); // 5 seconds
|
|
395
|
+
|
|
396
|
+
// Session data - medium TTL
|
|
397
|
+
await cache.set("session:abc", session, 3600000); // 1 hour
|
|
398
|
+
|
|
399
|
+
// Static reference data - long TTL
|
|
400
|
+
await cache.set("countries:list", countries, 86400000); // 24 hours
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### 3. Handle Cache Misses Gracefully
|
|
404
|
+
|
|
405
|
+
```ts
|
|
406
|
+
const user = await cache.get("user:123");
|
|
407
|
+
if (!user) {
|
|
408
|
+
// Fetch from database
|
|
409
|
+
const dbUser = await db.selectFrom("users").where("id", "=", 123).executeTakeFirst();
|
|
410
|
+
if (dbUser) {
|
|
411
|
+
await cache.set("user:123", dbUser);
|
|
412
|
+
}
|
|
413
|
+
return dbUser;
|
|
414
|
+
}
|
|
415
|
+
return user;
|
|
416
|
+
|
|
417
|
+
// Or use getOrSet for cleaner code
|
|
418
|
+
const user = await cache.getOrSet("user:123", () =>
|
|
419
|
+
db.selectFrom("users").where("id", "=", 123).executeTakeFirst()
|
|
420
|
+
);
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
### 4. Invalidate Proactively
|
|
424
|
+
|
|
425
|
+
```ts
|
|
426
|
+
// When updating data, invalidate related caches
|
|
427
|
+
async function updateUser(id: number, data: UserUpdate) {
|
|
428
|
+
await db.updateTable("users").set(data).where("id", "=", id).execute();
|
|
429
|
+
|
|
430
|
+
// Invalidate all related caches
|
|
431
|
+
await Promise.all([
|
|
432
|
+
cache.delete(`user:${id}:profile`),
|
|
433
|
+
cache.delete(`user:${id}:settings`),
|
|
434
|
+
cache.delete(`dashboard:${id}`),
|
|
435
|
+
]);
|
|
436
|
+
}
|
|
437
|
+
```
|