@signaltree/events 7.3.1 → 7.3.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/README.md +370 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
# @signaltree/events
|
|
2
|
+
|
|
3
|
+
Event-driven architecture infrastructure for SignalTree applications. Provides a complete event bus system with validation, subscribers, error classification, and real-time sync.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @signaltree/events zod
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Subpath Exports
|
|
12
|
+
|
|
13
|
+
The package provides four entry points for different use cases:
|
|
14
|
+
|
|
15
|
+
| Import Path | Description |
|
|
16
|
+
| ---------------------------- | ---------------------------------------------------- |
|
|
17
|
+
| `@signaltree/events` | Core types, schemas, validation (framework-agnostic) |
|
|
18
|
+
| `@signaltree/events/nestjs` | NestJS integration (EventBusModule, BaseSubscriber) |
|
|
19
|
+
| `@signaltree/events/angular` | Angular integration (WebSocketService) |
|
|
20
|
+
| `@signaltree/events/testing` | Test utilities (MockEventBus, factories, assertions) |
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
### 1. Define Events with Zod Schemas
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { createEventSchema, EventPriority, z } from '@signaltree/events';
|
|
28
|
+
|
|
29
|
+
// Define your event schema
|
|
30
|
+
export const TradeProposalCreatedSchema = createEventSchema('TradeProposalCreated', {
|
|
31
|
+
tradeId: z.string().uuid(),
|
|
32
|
+
initiatorId: z.string().uuid(),
|
|
33
|
+
recipientId: z.string().uuid(),
|
|
34
|
+
vehicleOfferedId: z.string().uuid(),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export type TradeProposalCreated = z.infer<typeof TradeProposalCreatedSchema>;
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 2. Create Events with the Factory
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import { createEventFactory } from '@signaltree/events';
|
|
44
|
+
|
|
45
|
+
const eventFactory = createEventFactory({
|
|
46
|
+
source: 'trade-service',
|
|
47
|
+
defaultPriority: 'normal',
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const event = eventFactory.create('TradeProposalCreated', {
|
|
51
|
+
tradeId: '550e8400-e29b-41d4-a716-446655440000',
|
|
52
|
+
initiatorId: 'user-123',
|
|
53
|
+
recipientId: 'user-456',
|
|
54
|
+
vehicleOfferedId: 'vehicle-789',
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 3. Validate Events
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import { validateEvent, isValidEvent, parseEvent } from '@signaltree/events';
|
|
62
|
+
|
|
63
|
+
// Throws on invalid
|
|
64
|
+
const validatedEvent = validateEvent(TradeProposalCreatedSchema, rawEvent);
|
|
65
|
+
|
|
66
|
+
// Returns boolean
|
|
67
|
+
if (isValidEvent(TradeProposalCreatedSchema, rawEvent)) {
|
|
68
|
+
// rawEvent is typed as TradeProposalCreated
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Returns { success, data, error }
|
|
72
|
+
const result = parseEvent(TradeProposalCreatedSchema, rawEvent);
|
|
73
|
+
if (result.success) {
|
|
74
|
+
console.log(result.data);
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## NestJS Integration
|
|
79
|
+
|
|
80
|
+
### Module Setup
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
import { Module } from '@nestjs/common';
|
|
84
|
+
import { EventBusModule } from '@signaltree/events/nestjs';
|
|
85
|
+
|
|
86
|
+
@Module({
|
|
87
|
+
imports: [
|
|
88
|
+
EventBusModule.forRoot({
|
|
89
|
+
redis: {
|
|
90
|
+
host: process.env.REDIS_HOST || 'localhost',
|
|
91
|
+
port: parseInt(process.env.REDIS_PORT || '6379'),
|
|
92
|
+
},
|
|
93
|
+
queues: ['critical', 'high', 'normal', 'low', 'bulk'],
|
|
94
|
+
defaultQueue: 'normal',
|
|
95
|
+
}),
|
|
96
|
+
],
|
|
97
|
+
})
|
|
98
|
+
export class AppModule {}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Publishing Events
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
import { Injectable } from '@nestjs/common';
|
|
105
|
+
import { EventBusService } from '@signaltree/events/nestjs';
|
|
106
|
+
|
|
107
|
+
@Injectable()
|
|
108
|
+
export class TradeService {
|
|
109
|
+
constructor(private eventBus: EventBusService) {}
|
|
110
|
+
|
|
111
|
+
async createTrade(data: CreateTradeDto) {
|
|
112
|
+
const trade = await this.tradeRepo.create(data);
|
|
113
|
+
|
|
114
|
+
await this.eventBus.publish({
|
|
115
|
+
type: 'TradeProposalCreated',
|
|
116
|
+
payload: {
|
|
117
|
+
tradeId: trade.id,
|
|
118
|
+
initiatorId: data.initiatorId,
|
|
119
|
+
recipientId: data.recipientId,
|
|
120
|
+
vehicleOfferedId: data.vehicleOfferedId,
|
|
121
|
+
},
|
|
122
|
+
metadata: {
|
|
123
|
+
source: 'trade-service',
|
|
124
|
+
priority: 'high',
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
return trade;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Creating Subscribers
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
import { Injectable } from '@nestjs/common';
|
|
137
|
+
import { BaseSubscriber, OnEvent } from '@signaltree/events/nestjs';
|
|
138
|
+
|
|
139
|
+
@Injectable()
|
|
140
|
+
export class NotificationSubscriber extends BaseSubscriber {
|
|
141
|
+
readonly subscriberName = 'notification-subscriber';
|
|
142
|
+
readonly subscribedEvents = ['TradeProposalCreated', 'TradeAccepted'];
|
|
143
|
+
|
|
144
|
+
@OnEvent('TradeProposalCreated')
|
|
145
|
+
async handleTradeCreated(event: TradeProposalCreated) {
|
|
146
|
+
await this.notificationService.send({
|
|
147
|
+
userId: event.payload.recipientId,
|
|
148
|
+
title: 'New Trade Proposal',
|
|
149
|
+
body: 'You have received a new trade proposal!',
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
return { success: true };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Angular Integration
|
|
158
|
+
|
|
159
|
+
### WebSocket Service
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
import { Injectable } from '@angular/core';
|
|
163
|
+
import { WebSocketService } from '@signaltree/events/angular';
|
|
164
|
+
|
|
165
|
+
@Injectable({ providedIn: 'root' })
|
|
166
|
+
export class TradeWebSocketService extends WebSocketService {
|
|
167
|
+
constructor() {
|
|
168
|
+
super({
|
|
169
|
+
url: 'ws://localhost:3000/events',
|
|
170
|
+
reconnect: true,
|
|
171
|
+
reconnectInterval: 5000,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Optimistic Updates
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
import { OptimisticUpdateManager } from '@signaltree/events/angular';
|
|
181
|
+
|
|
182
|
+
const manager = new OptimisticUpdateManager();
|
|
183
|
+
|
|
184
|
+
// Apply optimistic update
|
|
185
|
+
const updateId = manager.apply({
|
|
186
|
+
type: 'TradeAccepted',
|
|
187
|
+
rollback: () => this.store.$.trades.revert(),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
await this.tradeService.acceptTrade(tradeId);
|
|
192
|
+
manager.confirm(updateId);
|
|
193
|
+
} catch (error) {
|
|
194
|
+
manager.rollback(updateId);
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Testing Utilities
|
|
199
|
+
|
|
200
|
+
### MockEventBus
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
import { MockEventBus, createTestEvent } from '@signaltree/events/testing';
|
|
204
|
+
|
|
205
|
+
describe('TradeService', () => {
|
|
206
|
+
let mockEventBus: MockEventBus;
|
|
207
|
+
|
|
208
|
+
beforeEach(() => {
|
|
209
|
+
mockEventBus = new MockEventBus();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
afterEach(() => {
|
|
213
|
+
mockEventBus.reset();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should publish TradeProposalCreated event', async () => {
|
|
217
|
+
await service.createTrade(tradeData);
|
|
218
|
+
|
|
219
|
+
expect(mockEventBus.wasPublished('TradeProposalCreated')).toBe(true);
|
|
220
|
+
|
|
221
|
+
const events = mockEventBus.getPublishedEventsByType('TradeProposalCreated');
|
|
222
|
+
expect(events).toHaveLength(1);
|
|
223
|
+
expect(events[0].payload.tradeId).toBe(expectedTradeId);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Event Factories
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
import { createTestEvent, createTestEventBatch } from '@signaltree/events/testing';
|
|
232
|
+
|
|
233
|
+
const event = createTestEvent('TradeProposalCreated', {
|
|
234
|
+
tradeId: 'test-trade-123',
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const events = createTestEventBatch('UserLoggedIn', 5, (index) => ({
|
|
238
|
+
userId: `user-${index}`,
|
|
239
|
+
}));
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Assertions
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
import { assertEventMatches, assertEventSequence } from '@signaltree/events/testing';
|
|
246
|
+
|
|
247
|
+
assertEventMatches(event, {
|
|
248
|
+
type: 'TradeProposalCreated',
|
|
249
|
+
payload: { tradeId: expect.any(String) },
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
assertEventSequence(events, ['TradeProposalCreated', 'NotificationSent', 'AuditLogCreated']);
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## Error Classification
|
|
256
|
+
|
|
257
|
+
Automatic error classification for retry logic:
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
import { classifyError, isRetryableError } from '@signaltree/events';
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
await processEvent(event);
|
|
264
|
+
} catch (error) {
|
|
265
|
+
const classification = classifyError(error);
|
|
266
|
+
|
|
267
|
+
if (classification.classification === 'transient') {
|
|
268
|
+
// Retry with exponential backoff
|
|
269
|
+
await retryWithBackoff(() => processEvent(event), classification.retryConfig);
|
|
270
|
+
} else if (classification.classification === 'permanent') {
|
|
271
|
+
// Send to DLQ
|
|
272
|
+
await dlqService.add(event, error);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## Idempotency
|
|
278
|
+
|
|
279
|
+
Prevent duplicate event processing:
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
import { InMemoryIdempotencyStore, generateIdempotencyKey } from '@signaltree/events';
|
|
283
|
+
|
|
284
|
+
const store = new InMemoryIdempotencyStore({ ttlMs: 24 * 60 * 60 * 1000 });
|
|
285
|
+
|
|
286
|
+
const key = generateIdempotencyKey(event);
|
|
287
|
+
const check = await store.check(key);
|
|
288
|
+
|
|
289
|
+
if (check.status === 'new') {
|
|
290
|
+
await processEvent(event);
|
|
291
|
+
await store.markProcessed(key, { result: 'success' });
|
|
292
|
+
} else if (check.status === 'processed') {
|
|
293
|
+
// Already processed, return cached result
|
|
294
|
+
return check.record.result;
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## Event Registry
|
|
299
|
+
|
|
300
|
+
Register and discover events:
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
import { createEventRegistry } from '@signaltree/events';
|
|
304
|
+
|
|
305
|
+
const registry = createEventRegistry();
|
|
306
|
+
|
|
307
|
+
registry.register({
|
|
308
|
+
type: 'TradeProposalCreated',
|
|
309
|
+
schema: TradeProposalCreatedSchema,
|
|
310
|
+
description: 'Emitted when a new trade proposal is created',
|
|
311
|
+
category: 'trades',
|
|
312
|
+
priority: 'high',
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// Get catalog of all events
|
|
316
|
+
const catalog = registry.getCatalog();
|
|
317
|
+
|
|
318
|
+
// Validate against registry
|
|
319
|
+
const isValid = registry.validate(event);
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
## API Reference
|
|
323
|
+
|
|
324
|
+
### Core Exports (`@signaltree/events`)
|
|
325
|
+
|
|
326
|
+
- **Types**: `BaseEvent`, `EventMetadata`, `EventPriority`
|
|
327
|
+
- **Schemas**: `createEventSchema`, `BaseEventSchema`, `validateEvent`, `parseEvent`
|
|
328
|
+
- **Factory**: `createEventFactory`, `createEvent`, `generateEventId`
|
|
329
|
+
- **Registry**: `EventRegistry`, `createEventRegistry`
|
|
330
|
+
- **Errors**: `classifyError`, `isRetryableError`, `createErrorClassifier`
|
|
331
|
+
- **Idempotency**: `InMemoryIdempotencyStore`, `generateIdempotencyKey`
|
|
332
|
+
|
|
333
|
+
### NestJS Exports (`@signaltree/events/nestjs`)
|
|
334
|
+
|
|
335
|
+
- **Module**: `EventBusModule`
|
|
336
|
+
- **Services**: `EventBusService`, `DlqService`
|
|
337
|
+
- **Subscriber**: `BaseSubscriber`
|
|
338
|
+
- **Decorators**: `@OnEvent`
|
|
339
|
+
- **Tokens**: `EVENT_BUS_CONFIG`, `EVENT_REGISTRY`
|
|
340
|
+
|
|
341
|
+
### Angular Exports (`@signaltree/events/angular`)
|
|
342
|
+
|
|
343
|
+
- **Services**: `WebSocketService`
|
|
344
|
+
- **Utilities**: `OptimisticUpdateManager`, `createEventHandler`
|
|
345
|
+
|
|
346
|
+
### Testing Exports (`@signaltree/events/testing`)
|
|
347
|
+
|
|
348
|
+
- **Mocks**: `MockEventBus`
|
|
349
|
+
- **Factories**: `createTestEvent`, `createTestEventBatch`
|
|
350
|
+
- **Assertions**: `assertEventMatches`, `assertEventSequence`
|
|
351
|
+
- **Helpers**: `waitForEvent`, `createEventSpy`
|
|
352
|
+
|
|
353
|
+
## Peer Dependencies
|
|
354
|
+
|
|
355
|
+
```json
|
|
356
|
+
{
|
|
357
|
+
"zod": "^3.0.0",
|
|
358
|
+
"@angular/core": "^18.0.0 || ^19.0.0 || ^20.0.0",
|
|
359
|
+
"rxjs": "^7.0.0",
|
|
360
|
+
"@nestjs/common": "^10.0.0 || ^11.0.0",
|
|
361
|
+
"bullmq": "^5.0.0",
|
|
362
|
+
"reflect-metadata": "^0.1.13 || ^0.2.0"
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
All peer dependencies except `zod` are optional - only install what you need for your framework.
|
|
367
|
+
|
|
368
|
+
## License
|
|
369
|
+
|
|
370
|
+
MIT © [SignalTree](https://github.com/JBorgia/signaltree)
|
package/package.json
CHANGED