@donkeylabs/server 0.1.1 → 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/events.md ADDED
@@ -0,0 +1,460 @@
1
+ # Events Service
2
+
3
+ Asynchronous pub/sub event system with pattern matching, history tracking, and support for both sync and async handlers.
4
+
5
+ ## Quick Start
6
+
7
+ ```ts
8
+ // Subscribe to events
9
+ ctx.core.events.on("user.created", async (user) => {
10
+ console.log("New user:", user.email);
11
+ });
12
+
13
+ // Emit events
14
+ await ctx.core.events.emit("user.created", { id: 1, email: "alice@example.com" });
15
+ ```
16
+
17
+ ---
18
+
19
+ ## API Reference
20
+
21
+ ### Interface
22
+
23
+ ```ts
24
+ interface Events {
25
+ emit<T = any>(event: string, data: T): Promise<void>;
26
+ on<T = any>(event: string, handler: EventHandler<T>): Subscription;
27
+ once<T = any>(event: string, handler: EventHandler<T>): Subscription;
28
+ off(event: string, handler?: EventHandler): void;
29
+ getHistory(event: string, limit?: number): Promise<EventRecord[]>;
30
+ }
31
+
32
+ interface Subscription {
33
+ unsubscribe(): void;
34
+ }
35
+
36
+ interface EventRecord {
37
+ event: string;
38
+ data: any;
39
+ timestamp: Date;
40
+ }
41
+ ```
42
+
43
+ ### Methods
44
+
45
+ | Method | Description |
46
+ |--------|-------------|
47
+ | `emit(event, data)` | Emit event to all subscribers |
48
+ | `on(event, handler)` | Subscribe to event, returns subscription |
49
+ | `once(event, handler)` | Subscribe to single occurrence |
50
+ | `off(event, handler?)` | Unsubscribe handler or all handlers |
51
+ | `getHistory(event, limit?)` | Get past events for replay |
52
+
53
+ ---
54
+
55
+ ## Configuration
56
+
57
+ ```ts
58
+ const server = new AppServer({
59
+ db,
60
+ events: {
61
+ maxHistorySize: 1000, // Events to keep in history (default: 1000)
62
+ },
63
+ });
64
+ ```
65
+
66
+ ---
67
+
68
+ ## Usage Examples
69
+
70
+ ### Basic Pub/Sub
71
+
72
+ ```ts
73
+ // Subscribe
74
+ const subscription = ctx.core.events.on("order.created", (order) => {
75
+ console.log("Order received:", order.id);
76
+ });
77
+
78
+ // Emit
79
+ await ctx.core.events.emit("order.created", {
80
+ id: "order-123",
81
+ total: 99.99,
82
+ items: ["item-1", "item-2"],
83
+ });
84
+
85
+ // Unsubscribe
86
+ subscription.unsubscribe();
87
+ ```
88
+
89
+ ### Once Handler
90
+
91
+ Subscribe to a single event occurrence:
92
+
93
+ ```ts
94
+ // Only fires once
95
+ ctx.core.events.once("app.ready", () => {
96
+ console.log("Application started!");
97
+ });
98
+
99
+ await ctx.core.events.emit("app.ready", {});
100
+ await ctx.core.events.emit("app.ready", {}); // Handler not called
101
+ ```
102
+
103
+ ### Pattern Matching
104
+
105
+ Subscribe to events matching a pattern using wildcards:
106
+
107
+ ```ts
108
+ // Match all user events
109
+ ctx.core.events.on("user.*", (data) => {
110
+ console.log("User event:", data);
111
+ });
112
+
113
+ // These all match
114
+ await ctx.core.events.emit("user.created", { id: 1 });
115
+ await ctx.core.events.emit("user.updated", { id: 1 });
116
+ await ctx.core.events.emit("user.deleted", { id: 1 });
117
+
118
+ // Match all events in a namespace
119
+ ctx.core.events.on("analytics.*", (data) => {
120
+ // Handle all analytics events
121
+ });
122
+
123
+ // Multi-level patterns
124
+ ctx.core.events.on("shop.order.*", (data) => {
125
+ // Matches shop.order.created, shop.order.paid, etc.
126
+ });
127
+ ```
128
+
129
+ ### Async Handlers
130
+
131
+ Handlers can be async - emit() waits for all handlers:
132
+
133
+ ```ts
134
+ ctx.core.events.on("order.paid", async (order) => {
135
+ // These run in parallel
136
+ await sendConfirmationEmail(order);
137
+ });
138
+
139
+ ctx.core.events.on("order.paid", async (order) => {
140
+ await updateInventory(order);
141
+ });
142
+
143
+ ctx.core.events.on("order.paid", async (order) => {
144
+ await notifyWarehouse(order);
145
+ });
146
+
147
+ // emit() waits for all handlers to complete
148
+ await ctx.core.events.emit("order.paid", order);
149
+ console.log("All handlers finished");
150
+ ```
151
+
152
+ ### Event History
153
+
154
+ Retrieve past events for debugging or replay:
155
+
156
+ ```ts
157
+ // Get last 10 events of a type
158
+ const history = await ctx.core.events.getHistory("user.login", 10);
159
+
160
+ for (const record of history) {
161
+ console.log(`${record.timestamp}: ${record.event}`, record.data);
162
+ }
163
+
164
+ // Get all events (use "*" pattern)
165
+ const allEvents = await ctx.core.events.getHistory("*", 100);
166
+ ```
167
+
168
+ ---
169
+
170
+ ## Real-World Examples
171
+
172
+ ### Event-Driven Architecture
173
+
174
+ ```ts
175
+ // plugins/orders/index.ts
176
+ service: async (ctx) => ({
177
+ async createOrder(data: OrderData) {
178
+ const order = await ctx.db
179
+ .insertInto("orders")
180
+ .values(data)
181
+ .returningAll()
182
+ .executeTakeFirstOrThrow();
183
+
184
+ // Emit event - let other systems react
185
+ await ctx.core.events.emit("order.created", order);
186
+
187
+ return order;
188
+ },
189
+
190
+ async markAsPaid(orderId: string) {
191
+ const order = await ctx.db
192
+ .updateTable("orders")
193
+ .set({ status: "paid", paidAt: new Date().toISOString() })
194
+ .where("id", "=", orderId)
195
+ .returningAll()
196
+ .executeTakeFirstOrThrow();
197
+
198
+ await ctx.core.events.emit("order.paid", order);
199
+
200
+ return order;
201
+ },
202
+ });
203
+
204
+ // plugins/inventory/index.ts
205
+ service: async (ctx) => {
206
+ // React to order events
207
+ ctx.core.events.on("order.paid", async (order) => {
208
+ for (const item of order.items) {
209
+ await ctx.db
210
+ .updateTable("inventory")
211
+ .set((eb) => ({ quantity: eb("quantity", "-", item.quantity) }))
212
+ .where("productId", "=", item.productId)
213
+ .execute();
214
+ }
215
+ ctx.core.logger.info("Inventory updated", { orderId: order.id });
216
+ });
217
+
218
+ return { /* inventory methods */ };
219
+ };
220
+
221
+ // plugins/notifications/index.ts
222
+ service: async (ctx) => {
223
+ ctx.core.events.on("order.created", async (order) => {
224
+ await sendEmail(order.customerEmail, "Order Confirmation", { order });
225
+ });
226
+
227
+ ctx.core.events.on("order.paid", async (order) => {
228
+ await sendEmail(order.customerEmail, "Payment Received", { order });
229
+ await notifySlack(`New paid order: ${order.id} - $${order.total}`);
230
+ });
231
+
232
+ return { /* notification methods */ };
233
+ };
234
+ ```
235
+
236
+ ### Audit Logging
237
+
238
+ ```ts
239
+ // Log all events for audit trail
240
+ ctx.core.events.on("*", async (data) => {
241
+ // This catches ALL events
242
+ await ctx.db.insertInto("audit_log").values({
243
+ event: data._eventName, // Added automatically
244
+ data: JSON.stringify(data),
245
+ timestamp: new Date().toISOString(),
246
+ }).execute();
247
+ });
248
+ ```
249
+
250
+ ### Real-Time Updates with SSE
251
+
252
+ ```ts
253
+ // Bridge events to SSE for real-time UI updates
254
+ ctx.core.events.on("notification.*", async (data) => {
255
+ const { userId } = data;
256
+
257
+ // Broadcast to user's SSE channel
258
+ ctx.core.sse.broadcast(`user:${userId}`, "notification", data);
259
+ });
260
+
261
+ // When notification is created
262
+ await ctx.core.events.emit("notification.created", {
263
+ userId: 123,
264
+ message: "You have a new message",
265
+ });
266
+ // -> Automatically pushed to user's browser via SSE
267
+ ```
268
+
269
+ ### Deferred Processing with Jobs
270
+
271
+ ```ts
272
+ // Convert events to background jobs for heavy processing
273
+ ctx.core.events.on("video.uploaded", async (video) => {
274
+ // Queue for background processing instead of blocking
275
+ await ctx.core.jobs.enqueue("processVideo", {
276
+ videoId: video.id,
277
+ operations: ["transcode", "thumbnail", "analyze"],
278
+ });
279
+ });
280
+ ```
281
+
282
+ ---
283
+
284
+ ## Event Naming Conventions
285
+
286
+ Use consistent, hierarchical event names:
287
+
288
+ ```ts
289
+ // Resource lifecycle events
290
+ "user.created"
291
+ "user.updated"
292
+ "user.deleted"
293
+
294
+ // State transitions
295
+ "order.pending"
296
+ "order.paid"
297
+ "order.shipped"
298
+ "order.delivered"
299
+
300
+ // Actions
301
+ "email.sent"
302
+ "payment.processed"
303
+ "file.uploaded"
304
+
305
+ // Namespaced events
306
+ "auth.login.success"
307
+ "auth.login.failed"
308
+ "auth.logout"
309
+ ```
310
+
311
+ ---
312
+
313
+ ## Error Handling
314
+
315
+ Handler errors are caught and logged, but don't prevent other handlers:
316
+
317
+ ```ts
318
+ ctx.core.events.on("test", async () => {
319
+ throw new Error("Handler 1 failed");
320
+ });
321
+
322
+ ctx.core.events.on("test", async () => {
323
+ console.log("Handler 2 still runs");
324
+ });
325
+
326
+ // Both handlers are called, error is logged
327
+ await ctx.core.events.emit("test", {});
328
+ ```
329
+
330
+ To handle errors explicitly:
331
+
332
+ ```ts
333
+ ctx.core.events.on("critical.event", async (data) => {
334
+ try {
335
+ await riskyOperation(data);
336
+ } catch (error) {
337
+ ctx.core.logger.error("Handler failed", { error: error.message, data });
338
+ // Optionally emit error event
339
+ await ctx.core.events.emit("critical.event.failed", { error, data });
340
+ }
341
+ });
342
+ ```
343
+
344
+ ---
345
+
346
+ ## Custom Adapters
347
+
348
+ Implement `EventAdapter` for persistence or distribution:
349
+
350
+ ```ts
351
+ interface EventAdapter {
352
+ publish(event: string, data: any): Promise<void>;
353
+ getHistory(event: string, limit?: number): Promise<EventRecord[]>;
354
+ }
355
+ ```
356
+
357
+ ### Redis Pub/Sub Adapter
358
+
359
+ ```ts
360
+ import { createEvents, type EventAdapter } from "./core/events";
361
+ import Redis from "ioredis";
362
+
363
+ class RedisEventAdapter implements EventAdapter {
364
+ private history: EventRecord[] = [];
365
+
366
+ constructor(
367
+ private publisher: Redis,
368
+ private subscriber: Redis,
369
+ private onMessage: (event: string, data: any) => void
370
+ ) {
371
+ subscriber.on("pmessage", (pattern, channel, message) => {
372
+ const data = JSON.parse(message);
373
+ this.onMessage(channel, data);
374
+ });
375
+ subscriber.psubscribe("*");
376
+ }
377
+
378
+ async publish(event: string, data: any): Promise<void> {
379
+ this.history.push({ event, data, timestamp: new Date() });
380
+ await this.publisher.publish(event, JSON.stringify(data));
381
+ }
382
+
383
+ async getHistory(event: string, limit: number = 100): Promise<EventRecord[]> {
384
+ return this.history
385
+ .filter((r) => r.event === event || event === "*")
386
+ .slice(-limit);
387
+ }
388
+ }
389
+ ```
390
+
391
+ ---
392
+
393
+ ## Best Practices
394
+
395
+ ### 1. Keep Events Small
396
+
397
+ ```ts
398
+ // Good - include only what's needed
399
+ await events.emit("user.created", {
400
+ id: user.id,
401
+ email: user.email,
402
+ });
403
+
404
+ // Bad - including unnecessary data
405
+ await events.emit("user.created", {
406
+ ...user, // Full user object
407
+ password: user.password, // Sensitive!
408
+ internalData: {...}, // Unnecessary
409
+ });
410
+ ```
411
+
412
+ ### 2. Use Past Tense for Completed Actions
413
+
414
+ ```ts
415
+ // Good - indicates action completed
416
+ "order.created"
417
+ "payment.processed"
418
+ "email.sent"
419
+
420
+ // Avoid - ambiguous timing
421
+ "order.create"
422
+ "payment.process"
423
+ ```
424
+
425
+ ### 3. Include Identifiers for Correlation
426
+
427
+ ```ts
428
+ await events.emit("order.shipped", {
429
+ orderId: order.id,
430
+ userId: order.userId,
431
+ trackingNumber: shipment.trackingNumber,
432
+ // Correlate with requestId if available
433
+ requestId: ctx.requestId,
434
+ });
435
+ ```
436
+
437
+ ### 4. Document Event Schemas
438
+
439
+ ```ts
440
+ // events.ts - Central event type definitions
441
+ export interface UserCreatedEvent {
442
+ id: number;
443
+ email: string;
444
+ createdAt: string;
445
+ }
446
+
447
+ export interface OrderPaidEvent {
448
+ orderId: string;
449
+ amount: number;
450
+ currency: string;
451
+ paidAt: string;
452
+ }
453
+
454
+ // Type-safe emission
455
+ await ctx.core.events.emit<UserCreatedEvent>("user.created", {
456
+ id: 1,
457
+ email: "test@example.com",
458
+ createdAt: new Date().toISOString(),
459
+ });
460
+ ```