@donkeylabs/server 0.1.0 → 0.1.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/docs/cache.md ADDED
@@ -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
+ ```