@bloomneo/appkit 1.2.9 → 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 +29 -29
- 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 +11 -11
- package/dist/config/index.d.ts +3 -3
- package/dist/config/index.js +4 -4
- package/dist/database/adapters/mongoose.d.ts +4 -4
- package/dist/database/adapters/mongoose.js +7 -7
- package/dist/database/adapters/prisma.d.ts +4 -4
- package/dist/database/adapters/prisma.js +7 -7
- 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 +26 -26
- package/dist/email/index.js +7 -7
- package/dist/email/strategies/resend.js +1 -1
- package/dist/error/defaults.d.ts +1 -1
- package/dist/error/defaults.js +13 -13
- 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 +35 -35
- package/dist/event/index.js +7 -7
- 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 +2 -2
- package/dist/logger/transports/http.d.ts +1 -1
- package/dist/logger/transports/http.js +2 -2
- package/dist/logger/transports/webhook.d.ts +1 -1
- package/dist/logger/transports/webhook.js +3 -3
- 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 +30 -30
- 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 +26 -26
- package/dist/storage/index.js +3 -3
- package/dist/util/defaults.d.ts +1 -1
- package/dist/util/defaults.js +41 -41
- 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/dist/util/util.js +1 -1
- 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,729 @@
|
|
|
1
|
+
# @bloomneo/appkit - Event Module 🚀
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@bloomneo/appkit)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
> Ultra-simple event-driven architecture that just works with automatic
|
|
7
|
+
> Redis/Memory strategy
|
|
8
|
+
|
|
9
|
+
**One function** returns an event system with automatic Redis detection. Zero
|
|
10
|
+
configuration needed, production-ready distribution by default, with built-in
|
|
11
|
+
wildcard patterns and event history.
|
|
12
|
+
|
|
13
|
+
## 🚀 Why Choose This?
|
|
14
|
+
|
|
15
|
+
- **⚡ One Function** - Just `eventClass.get()`, everything else is automatic
|
|
16
|
+
- **🔄 Auto Strategy** - REDIS_URL → Distributed, No Redis → Memory
|
|
17
|
+
- **🔧 Zero Configuration** - Smart defaults for everything
|
|
18
|
+
- **🌟 Wildcard Events** - Listen to `user.*` patterns automatically
|
|
19
|
+
- **📚 Event History** - Built-in replay and debugging
|
|
20
|
+
- **⚖️ Scales Perfectly** - Development → Production with no code changes
|
|
21
|
+
- **🤖 AI-Ready** - Optimized for LLM code generation
|
|
22
|
+
|
|
23
|
+
## 📦 Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install @bloomneo/appkit
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## 🏃♂️ Quick Start (30 seconds)
|
|
30
|
+
|
|
31
|
+
### Basic Events (Memory Strategy)
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { eventClass } from '@bloomneo/appkit/event';
|
|
35
|
+
|
|
36
|
+
const events = eventClass.get();
|
|
37
|
+
|
|
38
|
+
// Listen to events
|
|
39
|
+
events.on('user.signup', (data) => {
|
|
40
|
+
console.log('New user:', data.email);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Emit events
|
|
44
|
+
await events.emit('user.signup', {
|
|
45
|
+
email: 'john@example.com',
|
|
46
|
+
userId: 123,
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Distributed Events (Redis Strategy)
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# Just set Redis URL - everything else is automatic
|
|
54
|
+
REDIS_URL=redis://localhost:6379
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { eventClass } from '@bloomneo/appkit/event';
|
|
59
|
+
|
|
60
|
+
const events = eventClass.get();
|
|
61
|
+
|
|
62
|
+
// Same code - now distributed across all servers!
|
|
63
|
+
events.on('order.completed', (data) => {
|
|
64
|
+
console.log('Order completed:', data.orderId);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
await events.emit('order.completed', {
|
|
68
|
+
orderId: 'order-123',
|
|
69
|
+
amount: 99.99,
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**That's it!** Events automatically work across all your servers.
|
|
74
|
+
|
|
75
|
+
## 🧠 Mental Model
|
|
76
|
+
|
|
77
|
+
### **Strategy Auto-Detection**
|
|
78
|
+
|
|
79
|
+
This is the core innovation. Environment variables determine strategy:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# Development/Single Server
|
|
83
|
+
# (no Redis URL)
|
|
84
|
+
→ Memory Strategy: Fast local events
|
|
85
|
+
|
|
86
|
+
# Production/Multi-Server
|
|
87
|
+
REDIS_URL=redis://localhost:6379
|
|
88
|
+
→ Redis Strategy: Distributed events across all servers
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### **Namespace Isolation**
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
// Different parts of your app can use separate event channels
|
|
95
|
+
const userEvents = eventClass.get('users'); // users:*
|
|
96
|
+
const orderEvents = eventClass.get('orders'); // orders:*
|
|
97
|
+
const emailEvents = eventClass.get('emails'); // emails:*
|
|
98
|
+
|
|
99
|
+
// Events don't interfere with each other
|
|
100
|
+
userEvents.emit('created', data); // → users:created
|
|
101
|
+
orderEvents.emit('created', data); // → orders:created
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## 📖 Complete API Reference
|
|
105
|
+
|
|
106
|
+
### Core Function
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
const events = eventClass.get(namespace?); // One function, everything you need
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Event Methods
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
// Listen to events
|
|
116
|
+
events.on('event.name', (data) => {
|
|
117
|
+
/* handler */
|
|
118
|
+
});
|
|
119
|
+
events.once('event.name', (data) => {
|
|
120
|
+
/* one-time handler */
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Emit events
|
|
124
|
+
await events.emit('event.name', { key: 'value' });
|
|
125
|
+
await events.emitBatch([
|
|
126
|
+
{ event: 'user.created', data: { userId: 1 } },
|
|
127
|
+
{ event: 'user.created', data: { userId: 2 } },
|
|
128
|
+
]);
|
|
129
|
+
|
|
130
|
+
// Remove listeners
|
|
131
|
+
events.off('event.name'); // Remove all listeners
|
|
132
|
+
events.off('event.name', handler); // Remove specific listener
|
|
133
|
+
|
|
134
|
+
// Wildcard patterns
|
|
135
|
+
events.on('user.*', (eventName, data) => {
|
|
136
|
+
console.log(`User event: ${eventName}`, data);
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Utility Methods
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
// Get event history for debugging
|
|
144
|
+
const history = await events.history('user.created', 10); // Last 10 events
|
|
145
|
+
|
|
146
|
+
// Get current listeners
|
|
147
|
+
const listeners = events.getListeners('user.*');
|
|
148
|
+
|
|
149
|
+
// Get strategy info
|
|
150
|
+
const strategy = events.getStrategy(); // 'redis' or 'memory'
|
|
151
|
+
const config = events.getConfig();
|
|
152
|
+
|
|
153
|
+
// Cleanup
|
|
154
|
+
await events.disconnect();
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Global Methods
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
// System-wide operations
|
|
161
|
+
eventClass.getStrategy(); // Current strategy
|
|
162
|
+
eventClass.getActiveNamespaces(); // All active namespaces
|
|
163
|
+
eventClass.hasRedis(); // Check if Redis available
|
|
164
|
+
|
|
165
|
+
// Broadcast to all namespaces (use sparingly)
|
|
166
|
+
await eventClass.broadcast('system.shutdown');
|
|
167
|
+
|
|
168
|
+
// Cleanup for testing
|
|
169
|
+
await eventClass.clear();
|
|
170
|
+
await eventClass.reset(newConfig);
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## 🎯 Usage Examples
|
|
174
|
+
|
|
175
|
+
### **Express API with Events**
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
import express from 'express';
|
|
179
|
+
import { eventClass } from '@bloomneo/appkit/event';
|
|
180
|
+
|
|
181
|
+
const app = express();
|
|
182
|
+
const events = eventClass.get('api');
|
|
183
|
+
|
|
184
|
+
// Listen to user events
|
|
185
|
+
events.on('user.created', async (user) => {
|
|
186
|
+
console.log('📧 Sending welcome email to:', user.email);
|
|
187
|
+
await sendWelcomeEmail(user);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
events.on('user.*', (eventName, data) => {
|
|
191
|
+
console.log(`👤 User event: ${eventName}`, data);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// User registration endpoint
|
|
195
|
+
app.post('/register', async (req, res) => {
|
|
196
|
+
const { email, password } = req.body;
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
// Create user
|
|
200
|
+
const user = await createUser({ email, password });
|
|
201
|
+
|
|
202
|
+
// Emit event - triggers welcome email automatically
|
|
203
|
+
await events.emit('user.created', {
|
|
204
|
+
userId: user.id,
|
|
205
|
+
email: user.email,
|
|
206
|
+
source: 'registration',
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
res.json({ success: true, userId: user.id });
|
|
210
|
+
} catch (error) {
|
|
211
|
+
await events.emit('user.registration.failed', {
|
|
212
|
+
email,
|
|
213
|
+
error: error.message,
|
|
214
|
+
timestamp: new Date().toISOString(),
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
res.status(400).json({ error: error.message });
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
app.listen(3000, () => {
|
|
222
|
+
console.log('🚀 Server started with events:', events.getStrategy());
|
|
223
|
+
});
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### **Microservices Communication**
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
// User Service
|
|
230
|
+
import { eventClass } from '@bloomneo/appkit/event';
|
|
231
|
+
|
|
232
|
+
const userEvents = eventClass.get('users');
|
|
233
|
+
|
|
234
|
+
export class UserService {
|
|
235
|
+
async createUser(userData) {
|
|
236
|
+
const user = await this.db.create(userData);
|
|
237
|
+
|
|
238
|
+
// Notify other services
|
|
239
|
+
await userEvents.emit('user.created', {
|
|
240
|
+
userId: user.id,
|
|
241
|
+
email: user.email,
|
|
242
|
+
plan: user.plan,
|
|
243
|
+
createdAt: user.createdAt,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
return user;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
// Email Service (separate server/container)
|
|
253
|
+
import { eventClass } from '@bloomneo/appkit/event';
|
|
254
|
+
|
|
255
|
+
const emailEvents = eventClass.get('emails');
|
|
256
|
+
const userEvents = eventClass.get('users'); // Same namespace, distributed
|
|
257
|
+
|
|
258
|
+
// Listen to user events from User Service
|
|
259
|
+
userEvents.on('user.created', async (userData) => {
|
|
260
|
+
await emailEvents.emit('send.welcome', {
|
|
261
|
+
to: userData.email,
|
|
262
|
+
userId: userData.userId,
|
|
263
|
+
plan: userData.plan,
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Process email queue
|
|
268
|
+
emailEvents.on('send.*', async (eventName, emailData) => {
|
|
269
|
+
const emailType = eventName.split('.')[1]; // 'welcome', 'reset', etc.
|
|
270
|
+
await sendEmail(emailType, emailData);
|
|
271
|
+
});
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### **Background Jobs & Queues**
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
import { eventClass } from '@bloomneo/appkit/event';
|
|
278
|
+
|
|
279
|
+
const jobEvents = eventClass.get('jobs');
|
|
280
|
+
|
|
281
|
+
// Job processor
|
|
282
|
+
class JobProcessor {
|
|
283
|
+
constructor() {
|
|
284
|
+
jobEvents.on('job.email.*', this.processEmailJob.bind(this));
|
|
285
|
+
jobEvents.on('job.image.*', this.processImageJob.bind(this));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async processEmailJob(eventName, jobData) {
|
|
289
|
+
const jobType = eventName.split('.')[2];
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
await this.sendEmail(jobType, jobData);
|
|
293
|
+
await jobEvents.emit('job.completed', {
|
|
294
|
+
jobId: jobData.jobId,
|
|
295
|
+
type: 'email',
|
|
296
|
+
completedAt: new Date().toISOString(),
|
|
297
|
+
});
|
|
298
|
+
} catch (error) {
|
|
299
|
+
await jobEvents.emit('job.failed', {
|
|
300
|
+
jobId: jobData.jobId,
|
|
301
|
+
error: error.message,
|
|
302
|
+
retryCount: jobData.retryCount || 0,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Usage
|
|
309
|
+
await jobEvents.emit('job.email.welcome', {
|
|
310
|
+
jobId: crypto.randomUUID(),
|
|
311
|
+
email: 'user@example.com',
|
|
312
|
+
name: 'John',
|
|
313
|
+
});
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
## 🧪 Testing
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
import { eventClass } from '@bloomneo/appkit/event';
|
|
320
|
+
|
|
321
|
+
describe('Events', () => {
|
|
322
|
+
afterEach(() => eventClass.clear()); // Essential cleanup
|
|
323
|
+
|
|
324
|
+
test('basic event flow', async () => {
|
|
325
|
+
const events = eventClass.get('test');
|
|
326
|
+
|
|
327
|
+
const received = [];
|
|
328
|
+
events.on('user.created', (data) => received.push(data));
|
|
329
|
+
|
|
330
|
+
await events.emit('user.created', { userId: 123 });
|
|
331
|
+
|
|
332
|
+
expect(received[0].userId).toBe(123);
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
## ⚠️ Common Mistakes
|
|
338
|
+
|
|
339
|
+
### **1. Wrong Event Names**
|
|
340
|
+
|
|
341
|
+
```typescript
|
|
342
|
+
// ❌ Bad patterns
|
|
343
|
+
events.on('userCreated', handler); // Use dots
|
|
344
|
+
events.on('USER_CREATED', handler); // Use lowercase
|
|
345
|
+
events.on('created', handler); // Too generic
|
|
346
|
+
|
|
347
|
+
// ✅ Good patterns
|
|
348
|
+
events.on('user.created', handler);
|
|
349
|
+
events.on('order.payment.failed', handler);
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### **2. Missing Cleanup**
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
// ❌ Memory leaks in tests
|
|
356
|
+
test('my test', () => {
|
|
357
|
+
const events = eventClass.get('test');
|
|
358
|
+
// Missing: await eventClass.clear();
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// ✅ Always clean up
|
|
362
|
+
afterEach(() => eventClass.clear());
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### **3. Memory Strategy in Production**
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
// ❌ Single server only
|
|
369
|
+
// Without REDIS_URL, events don't work across servers
|
|
370
|
+
|
|
371
|
+
// ✅ Set Redis for distributed events
|
|
372
|
+
REDIS_URL=redis://localhost:6379
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### **4. Ignoring Emit Failures**
|
|
376
|
+
|
|
377
|
+
```typescript
|
|
378
|
+
// ❌ Silent failures
|
|
379
|
+
await events.emit('user.created', data);
|
|
380
|
+
|
|
381
|
+
// ✅ Check results
|
|
382
|
+
const result = await events.emit('user.created', data);
|
|
383
|
+
if (!result) console.error('Event failed');
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
## 🚨 Error Handling
|
|
387
|
+
|
|
388
|
+
### **Basic Pattern**
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
events.on('payment.process', async (payment) => {
|
|
392
|
+
try {
|
|
393
|
+
await processPayment(payment);
|
|
394
|
+
await events.emit('payment.completed', payment);
|
|
395
|
+
} catch (error) {
|
|
396
|
+
await events.emit('payment.failed', {
|
|
397
|
+
...payment,
|
|
398
|
+
error: error.message,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### **Redis Connection Errors**
|
|
405
|
+
|
|
406
|
+
```typescript
|
|
407
|
+
async function emitSafely(event, data) {
|
|
408
|
+
const result = await events.emit(event, data);
|
|
409
|
+
|
|
410
|
+
if (!result) {
|
|
411
|
+
console.warn(`Event failed: ${event}`, data);
|
|
412
|
+
// Store for retry or use fallback
|
|
413
|
+
await storeFailedEvent(event, data);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return result;
|
|
417
|
+
}
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
## 🔧 Startup Validation
|
|
421
|
+
|
|
422
|
+
### **Basic Validation**
|
|
423
|
+
|
|
424
|
+
```typescript
|
|
425
|
+
import { eventClass } from '@bloomneo/appkit/event';
|
|
426
|
+
|
|
427
|
+
async function startApp() {
|
|
428
|
+
// Validate events at startup
|
|
429
|
+
eventClass.validateConfig();
|
|
430
|
+
|
|
431
|
+
const strategy = eventClass.getStrategy();
|
|
432
|
+
console.log(`🚀 Events: ${strategy} strategy`);
|
|
433
|
+
|
|
434
|
+
app.listen(3000);
|
|
435
|
+
}
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### **Production Checks**
|
|
439
|
+
|
|
440
|
+
```typescript
|
|
441
|
+
if (process.env.NODE_ENV === 'production' && !eventClass.hasRedis()) {
|
|
442
|
+
throw new Error('Redis required in production for distributed events');
|
|
443
|
+
}
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
### **Health Check**
|
|
447
|
+
|
|
448
|
+
```typescript
|
|
449
|
+
app.get('/health/events', (req, res) => {
|
|
450
|
+
const stats = eventClass.getStats();
|
|
451
|
+
|
|
452
|
+
res.json({
|
|
453
|
+
status: 'healthy',
|
|
454
|
+
strategy: eventClass.getStrategy(),
|
|
455
|
+
connected: stats.connected,
|
|
456
|
+
redis: eventClass.hasRedis(),
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
## 🌍 Environment Variables
|
|
462
|
+
|
|
463
|
+
### Basic Configuration
|
|
464
|
+
|
|
465
|
+
```bash
|
|
466
|
+
# Strategy selection (auto-detected)
|
|
467
|
+
REDIS_URL=redis://localhost:6379 # Enables Redis strategy
|
|
468
|
+
# No REDIS_URL = Memory strategy
|
|
469
|
+
|
|
470
|
+
# Service identification
|
|
471
|
+
BLOOM_SERVICE_NAME=my-app # Used in namespacing
|
|
472
|
+
BLOOM_EVENT_NAMESPACE=production # Custom namespace
|
|
473
|
+
|
|
474
|
+
# Event history
|
|
475
|
+
BLOOM_EVENT_HISTORY_ENABLED=true # Default: true
|
|
476
|
+
BLOOM_EVENT_HISTORY_SIZE=100 # Default: 100
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### Redis Configuration (Advanced)
|
|
480
|
+
|
|
481
|
+
```bash
|
|
482
|
+
# Redis connection
|
|
483
|
+
REDIS_URL=redis://localhost:6379
|
|
484
|
+
REDIS_PASSWORD=your-redis-password
|
|
485
|
+
|
|
486
|
+
# Redis event settings
|
|
487
|
+
BLOOM_EVENT_REDIS_RETRIES=3 # Default: 3
|
|
488
|
+
BLOOM_EVENT_REDIS_RETRY_DELAY=1000 # Default: 1000ms
|
|
489
|
+
BLOOM_EVENT_REDIS_CONNECT_TIMEOUT=10000 # Default: 10s
|
|
490
|
+
BLOOM_EVENT_REDIS_COMMAND_TIMEOUT=5000 # Default: 5s
|
|
491
|
+
BLOOM_EVENT_REDIS_PREFIX=events # Default: events
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
### Memory Configuration (Advanced)
|
|
495
|
+
|
|
496
|
+
```bash
|
|
497
|
+
# Memory strategy settings
|
|
498
|
+
BLOOM_EVENT_MEMORY_MAX_LISTENERS=1000 # Default: 1000
|
|
499
|
+
BLOOM_EVENT_MEMORY_HISTORY=100 # Default: 100
|
|
500
|
+
BLOOM_EVENT_MEMORY_CHECK_INTERVAL=30000 # Default: 30s
|
|
501
|
+
BLOOM_EVENT_MEMORY_GC=true # Default: true
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
## 🎨 Event Patterns
|
|
505
|
+
|
|
506
|
+
### **Naming Conventions**
|
|
507
|
+
|
|
508
|
+
```typescript
|
|
509
|
+
// ✅ Good event names
|
|
510
|
+
'user.created'; // entity.action
|
|
511
|
+
'order.payment.failed'; // entity.context.action
|
|
512
|
+
'email.sent'; // service.action
|
|
513
|
+
'notification.push.delivered'; // service.type.action
|
|
514
|
+
|
|
515
|
+
// ❌ Avoid these patterns
|
|
516
|
+
'userCreated'; // Use dots for hierarchy
|
|
517
|
+
'USER_CREATED'; // Use lowercase
|
|
518
|
+
'created'; // Too generic
|
|
519
|
+
'user-created'; // Use dots, not dashes
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
### **Wildcard Patterns**
|
|
523
|
+
|
|
524
|
+
```typescript
|
|
525
|
+
// Listen to patterns
|
|
526
|
+
events.on('user.*', handler); // All user events
|
|
527
|
+
events.on('*.created', handler); // All creation events
|
|
528
|
+
events.on('order.*.failed', handler); // All order failures
|
|
529
|
+
|
|
530
|
+
// Event hierarchy examples
|
|
531
|
+
'user.created' → Matches: user.*
|
|
532
|
+
'user.updated.profile' → Matches: user.*, user.updated.*
|
|
533
|
+
'order.payment.failed' → Matches: order.*, order.payment.*, *.failed
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
### **Data Structure Conventions**
|
|
537
|
+
|
|
538
|
+
```typescript
|
|
539
|
+
// ✅ Good event data structure
|
|
540
|
+
await events.emit('user.created', {
|
|
541
|
+
// Always include entity ID
|
|
542
|
+
userId: 123,
|
|
543
|
+
|
|
544
|
+
// Include relevant entity data
|
|
545
|
+
email: 'user@example.com',
|
|
546
|
+
plan: 'premium',
|
|
547
|
+
|
|
548
|
+
// Include context
|
|
549
|
+
source: 'registration',
|
|
550
|
+
ip: '192.168.1.1',
|
|
551
|
+
|
|
552
|
+
// Include timing
|
|
553
|
+
createdAt: new Date().toISOString(),
|
|
554
|
+
});
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
## 🔄 Development vs Production
|
|
558
|
+
|
|
559
|
+
### **Development Mode**
|
|
560
|
+
|
|
561
|
+
```bash
|
|
562
|
+
# No Redis URL = Memory strategy
|
|
563
|
+
NODE_ENV=development
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
```typescript
|
|
567
|
+
// Fast local events, detailed logging
|
|
568
|
+
const events = eventClass.get();
|
|
569
|
+
await events.emit('test.event', { data: 'value' });
|
|
570
|
+
// ✅ [AppKit] Event emitted: test.event { data: 'value' }
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
### **Production Mode**
|
|
574
|
+
|
|
575
|
+
```bash
|
|
576
|
+
# Redis URL = Distributed strategy
|
|
577
|
+
NODE_ENV=production
|
|
578
|
+
REDIS_URL=redis://production-redis:6379
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
```typescript
|
|
582
|
+
// Same code - now distributed across all servers
|
|
583
|
+
const events = eventClass.get();
|
|
584
|
+
await events.emit('test.event', { data: 'value' });
|
|
585
|
+
// Events work across all server instances automatically
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
## 🤖 LLM Guidelines
|
|
589
|
+
|
|
590
|
+
### **Essential Patterns**
|
|
591
|
+
|
|
592
|
+
```typescript
|
|
593
|
+
// ✅ ALWAYS use these patterns
|
|
594
|
+
import { eventClass } from '@bloomneo/appkit/event';
|
|
595
|
+
const events = eventClass.get('namespace');
|
|
596
|
+
|
|
597
|
+
// ✅ Event listening
|
|
598
|
+
events.on('entity.action', (data) => {
|
|
599
|
+
// Handle event
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
// ✅ Event emitting
|
|
603
|
+
await events.emit('entity.action', {
|
|
604
|
+
entityId: 123,
|
|
605
|
+
timestamp: new Date().toISOString(),
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
// ✅ Wildcard patterns
|
|
609
|
+
events.on('user.*', (eventName, data) => {
|
|
610
|
+
console.log(`User event: ${eventName}`, data);
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
// ✅ One-time listeners
|
|
614
|
+
events.once('app.ready', () => {
|
|
615
|
+
console.log('App is ready');
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
// ✅ Cleanup
|
|
619
|
+
events.off('event.name');
|
|
620
|
+
events.off('event.name', specificHandler);
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
### **Anti-Patterns to Avoid**
|
|
624
|
+
|
|
625
|
+
```typescript
|
|
626
|
+
// ❌ DON'T create EventClass directly
|
|
627
|
+
import { EventClass } from '@bloomneo/appkit/event';
|
|
628
|
+
const events = new EventClass(config, namespace); // Wrong!
|
|
629
|
+
|
|
630
|
+
// ❌ DON'T forget to handle async properly
|
|
631
|
+
events.emit('event', data); // Missing await!
|
|
632
|
+
|
|
633
|
+
// ❌ DON'T use bad event names
|
|
634
|
+
events.on('userCreated', handler); // Use dots: 'user.created'
|
|
635
|
+
events.on('USER_CREATED', handler); // Use lowercase
|
|
636
|
+
events.on('created', handler); // Too generic
|
|
637
|
+
|
|
638
|
+
// ❌ DON'T forget cleanup in tests
|
|
639
|
+
test('my test', () => {
|
|
640
|
+
// ... test code
|
|
641
|
+
// Missing: await eventClass.clear();
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
// ❌ DON'T emit events without data structure
|
|
645
|
+
await events.emit('user.created', userId); // Should be object with userId property
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
### **Common Patterns**
|
|
649
|
+
|
|
650
|
+
```typescript
|
|
651
|
+
// Emit with proper data structure
|
|
652
|
+
await events.emit('user.created', {
|
|
653
|
+
userId: user.id,
|
|
654
|
+
email: user.email,
|
|
655
|
+
source: 'registration',
|
|
656
|
+
createdAt: new Date().toISOString(),
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
// Handle errors in event listeners
|
|
660
|
+
events.on('payment.process', async (payment) => {
|
|
661
|
+
try {
|
|
662
|
+
await processPayment(payment);
|
|
663
|
+
await events.emit('payment.completed', payment);
|
|
664
|
+
} catch (error) {
|
|
665
|
+
await events.emit('payment.failed', {
|
|
666
|
+
...payment,
|
|
667
|
+
error: error.message,
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
// Use namespaces for organization
|
|
673
|
+
const userEvents = eventClass.get('users');
|
|
674
|
+
const orderEvents = eventClass.get('orders');
|
|
675
|
+
const emailEvents = eventClass.get('emails');
|
|
676
|
+
|
|
677
|
+
// Batch operations for efficiency
|
|
678
|
+
await events.emitBatch([
|
|
679
|
+
{ event: 'user.created', data: { userId: 1 } },
|
|
680
|
+
{ event: 'user.created', data: { userId: 2 } },
|
|
681
|
+
]);
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
## 📈 Performance
|
|
685
|
+
|
|
686
|
+
- **Memory Strategy**: ~0.01ms per event (local EventEmitter)
|
|
687
|
+
- **Redis Strategy**: ~2-5ms per event (network + serialization)
|
|
688
|
+
- **Wildcard Matching**: ~0.1ms per pattern check
|
|
689
|
+
- **Event History**: <1MB per 1000 events
|
|
690
|
+
- **Memory Usage**: <10MB baseline per namespace
|
|
691
|
+
|
|
692
|
+
## 🔍 TypeScript Support
|
|
693
|
+
|
|
694
|
+
Full TypeScript support with comprehensive interfaces:
|
|
695
|
+
|
|
696
|
+
```typescript
|
|
697
|
+
import type {
|
|
698
|
+
Event,
|
|
699
|
+
EventHandler,
|
|
700
|
+
WildcardHandler,
|
|
701
|
+
BatchEvent,
|
|
702
|
+
EventHistoryEntry,
|
|
703
|
+
} from '@bloomneo/appkit/event';
|
|
704
|
+
|
|
705
|
+
// Strongly typed event handling
|
|
706
|
+
const events: Event = eventClass.get('users');
|
|
707
|
+
|
|
708
|
+
const handler: EventHandler = (data: UserData) => {
|
|
709
|
+
console.log('User created:', data.email);
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
const wildcardHandler: WildcardHandler = (eventName: string, data: any) => {
|
|
713
|
+
console.log(`Event ${eventName}:`, data);
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
events.on('user.created', handler);
|
|
717
|
+
events.on('user.*', wildcardHandler);
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
## 📄 License
|
|
721
|
+
|
|
722
|
+
MIT © [Bloomneo](https://github.com/bloomneo)
|
|
723
|
+
|
|
724
|
+
---
|
|
725
|
+
|
|
726
|
+
<p align="center">
|
|
727
|
+
<strong>Built with ❤️ by the <a href="https://github.com/bloomneo">Bloomneo Team</a></strong><br>
|
|
728
|
+
Because event-driven architecture should be simple, not rocket science.
|
|
729
|
+
</p>
|