@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.
Files changed (2) hide show
  1. package/README.md +370 -0
  2. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signaltree/events",
3
- "version": "7.3.1",
3
+ "version": "7.3.2",
4
4
  "description": "Event-driven architecture infrastructure for SignalTree - event bus, subscribers, validation, and real-time sync",
5
5
  "license": "MIT",
6
6
  "type": "module",