@signaltree/events 7.3.5 → 7.4.0

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 (54) hide show
  1. package/dist/angular/handlers.cjs +38 -0
  2. package/dist/angular/handlers.js +35 -0
  3. package/dist/angular/index.cjs +15 -0
  4. package/dist/angular/index.js +3 -0
  5. package/dist/angular/optimistic-updates.cjs +161 -0
  6. package/dist/angular/optimistic-updates.js +159 -0
  7. package/dist/angular/websocket.service.cjs +357 -0
  8. package/{angular.esm.js → dist/angular/websocket.service.js} +1 -191
  9. package/dist/core/error-classification.cjs +282 -0
  10. package/dist/core/error-classification.js +276 -0
  11. package/dist/core/factory.cjs +148 -0
  12. package/{factory.esm.js → dist/core/factory.js} +2 -37
  13. package/dist/core/idempotency.cjs +252 -0
  14. package/dist/core/idempotency.js +247 -0
  15. package/dist/core/registry.cjs +183 -0
  16. package/dist/core/registry.js +180 -0
  17. package/dist/core/types.cjs +41 -0
  18. package/dist/core/types.js +38 -0
  19. package/dist/core/validation.cjs +185 -0
  20. package/{index.esm.js → dist/core/validation.js} +1 -4
  21. package/dist/index.cjs +43 -0
  22. package/dist/index.js +7 -0
  23. package/dist/nestjs/base.subscriber.cjs +287 -0
  24. package/dist/nestjs/base.subscriber.js +287 -0
  25. package/dist/nestjs/decorators.cjs +35 -0
  26. package/dist/nestjs/decorators.js +32 -0
  27. package/dist/nestjs/dlq.service.cjs +249 -0
  28. package/dist/nestjs/dlq.service.js +249 -0
  29. package/dist/nestjs/event-bus.module.cjs +152 -0
  30. package/dist/nestjs/event-bus.module.js +152 -0
  31. package/dist/nestjs/event-bus.service.cjs +243 -0
  32. package/dist/nestjs/event-bus.service.js +243 -0
  33. package/dist/nestjs/index.cjs +33 -0
  34. package/dist/nestjs/index.js +6 -0
  35. package/dist/nestjs/tokens.cjs +14 -0
  36. package/dist/nestjs/tokens.js +9 -0
  37. package/dist/testing/assertions.cjs +172 -0
  38. package/dist/testing/assertions.js +169 -0
  39. package/dist/testing/factories.cjs +122 -0
  40. package/dist/testing/factories.js +119 -0
  41. package/dist/testing/helpers.cjs +233 -0
  42. package/dist/testing/helpers.js +227 -0
  43. package/dist/testing/index.cjs +20 -0
  44. package/dist/testing/index.js +4 -0
  45. package/dist/testing/mock-event-bus.cjs +237 -0
  46. package/dist/testing/mock-event-bus.js +234 -0
  47. package/package.json +22 -22
  48. package/angular.d.ts +0 -1
  49. package/idempotency.esm.js +0 -701
  50. package/index.d.ts +0 -1
  51. package/nestjs.d.ts +0 -1
  52. package/nestjs.esm.js +0 -944
  53. package/testing.d.ts +0 -1
  54. package/testing.esm.js +0 -743
@@ -0,0 +1,357 @@
1
+ 'use strict';
2
+
3
+ var tslib = require('tslib');
4
+ var core = require('@angular/core');
5
+ var rxjsInterop = require('@angular/core/rxjs-interop');
6
+ var rxjs = require('rxjs');
7
+ var operators = require('rxjs/operators');
8
+ var webSocket = require('rxjs/webSocket');
9
+
10
+ /**
11
+ * Default configuration
12
+ */
13
+ const DEFAULT_CONFIG = {
14
+ reconnect: {
15
+ enabled: true,
16
+ initialDelayMs: 1000,
17
+ maxDelayMs: 30000,
18
+ maxAttempts: 10
19
+ },
20
+ heartbeat: {
21
+ enabled: true,
22
+ intervalMs: 30000,
23
+ timeoutMs: 5000
24
+ }
25
+ };
26
+ /**
27
+ * Base WebSocket service for real-time event synchronization
28
+ *
29
+ * Extend this class in your application and wire it to your SignalTree store.
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * @Injectable({ providedIn: 'root' })
34
+ * export class AppWebSocketService extends WebSocketService {
35
+ * private readonly store = inject(AppStore);
36
+ *
37
+ * constructor() {
38
+ * super({
39
+ * url: environment.wsUrl,
40
+ * getAuthToken: () => this.store.$.session.token(),
41
+ * });
42
+ *
43
+ * // Subscribe to events
44
+ * this.onEvent<TradeProposalCreated>('TradeProposalCreated').subscribe(event => {
45
+ * this.store.$.trades.entities.upsertOne(event.data);
46
+ * });
47
+ * }
48
+ * }
49
+ * ```
50
+ */
51
+ exports.WebSocketService = class WebSocketService {
52
+ destroyRef = core.inject(core.DestroyRef);
53
+ // Signals for reactive state
54
+ _connectionState = core.signal('disconnected');
55
+ _lastError = core.signal(null);
56
+ _reconnectAttempts = core.signal(0);
57
+ _lastMessageTime = core.signal(null);
58
+ // Public readonly signals
59
+ connectionState = this._connectionState.asReadonly();
60
+ lastError = this._lastError.asReadonly();
61
+ isConnected = core.computed(() => this._connectionState() === 'connected');
62
+ isReconnecting = core.computed(() => this._connectionState() === 'reconnecting');
63
+ // WebSocket subject
64
+ socket$;
65
+ messageSubject = new rxjs.Subject();
66
+ // Subscriptions tracking
67
+ subscribedEvents = new Set();
68
+ heartbeatInterval;
69
+ reconnectTimer;
70
+ // Config
71
+ config;
72
+ constructor(config) {
73
+ this.config = {
74
+ ...config,
75
+ reconnect: {
76
+ ...DEFAULT_CONFIG.reconnect,
77
+ ...config.reconnect
78
+ },
79
+ heartbeat: {
80
+ ...DEFAULT_CONFIG.heartbeat,
81
+ ...config.heartbeat
82
+ }
83
+ };
84
+ }
85
+ /**
86
+ * Connect to WebSocket server
87
+ */
88
+ async connect() {
89
+ if (this._connectionState() === 'connected' || this._connectionState() === 'connecting') {
90
+ return;
91
+ }
92
+ this._connectionState.set('connecting');
93
+ this._lastError.set(null);
94
+ try {
95
+ // Get auth token if needed
96
+ let url = this.config.url;
97
+ if (this.config.getAuthToken) {
98
+ const token = await this.config.getAuthToken();
99
+ if (token) {
100
+ url = `${url}${url.includes('?') ? '&' : '?'}token=${encodeURIComponent(token)}`;
101
+ }
102
+ }
103
+ // Create WebSocket
104
+ const wsConfig = {
105
+ url,
106
+ openObserver: {
107
+ next: () => this.handleOpen()
108
+ },
109
+ closeObserver: {
110
+ next: event => this.handleClose(event)
111
+ },
112
+ protocol: this.config.protocols
113
+ };
114
+ this.socket$ = webSocket.webSocket(wsConfig);
115
+ // Subscribe to messages
116
+ this.socket$.pipe(rxjsInterop.takeUntilDestroyed(this.destroyRef)).subscribe({
117
+ next: message => this.handleMessage(message),
118
+ error: error => this.handleError(error),
119
+ complete: () => this.handleComplete()
120
+ });
121
+ } catch (error) {
122
+ this._connectionState.set('error');
123
+ this._lastError.set(error instanceof Error ? error : new Error(String(error)));
124
+ throw error;
125
+ }
126
+ }
127
+ /**
128
+ * Disconnect from WebSocket server
129
+ */
130
+ disconnect() {
131
+ this.stopHeartbeat();
132
+ this.clearReconnectTimer();
133
+ if (this.socket$) {
134
+ this.socket$.complete();
135
+ this.socket$ = undefined;
136
+ }
137
+ this._connectionState.set('disconnected');
138
+ this.subscribedEvents.clear();
139
+ }
140
+ /**
141
+ * Send a message
142
+ */
143
+ send(message) {
144
+ if (!this.socket$ || this._connectionState() !== 'connected') {
145
+ console.warn('WebSocket not connected, message not sent:', message);
146
+ return;
147
+ }
148
+ this.socket$.next(message);
149
+ }
150
+ /**
151
+ * Subscribe to a specific event type
152
+ *
153
+ * @example
154
+ * ```typescript
155
+ * this.onEvent<TradeProposalCreated>('TradeProposalCreated').subscribe(event => {
156
+ * console.log('Trade created:', event);
157
+ * });
158
+ * ```
159
+ */
160
+ onEvent(eventType) {
161
+ // Track subscription
162
+ if (!this.subscribedEvents.has(eventType)) {
163
+ this.subscribedEvents.add(eventType);
164
+ // Send subscribe message if connected
165
+ if (this._connectionState() === 'connected') {
166
+ this.send({
167
+ type: 'subscribe',
168
+ eventType
169
+ });
170
+ }
171
+ }
172
+ return this.messageSubject.pipe(operators.filter(msg => msg.type === 'event' && msg.eventType === eventType), operators.map(msg => msg.payload));
173
+ }
174
+ /**
175
+ * Unsubscribe from an event type
176
+ */
177
+ offEvent(eventType) {
178
+ if (this.subscribedEvents.has(eventType)) {
179
+ this.subscribedEvents.delete(eventType);
180
+ if (this._connectionState() === 'connected') {
181
+ this.send({
182
+ type: 'unsubscribe',
183
+ eventType
184
+ });
185
+ }
186
+ }
187
+ }
188
+ /**
189
+ * Get all messages (for debugging/logging)
190
+ */
191
+ get messages$() {
192
+ return this.messageSubject.asObservable();
193
+ }
194
+ /**
195
+ * Wait for connection to be established
196
+ */
197
+ async waitForConnection(timeoutMs = 10000) {
198
+ return new Promise((resolve, reject) => {
199
+ if (this._connectionState() === 'connected') {
200
+ resolve();
201
+ return;
202
+ }
203
+ const startTime = Date.now();
204
+ const checkInterval = setInterval(() => {
205
+ if (this._connectionState() === 'connected') {
206
+ clearInterval(checkInterval);
207
+ resolve();
208
+ } else if (Date.now() - startTime > timeoutMs) {
209
+ clearInterval(checkInterval);
210
+ reject(new Error('Connection timeout'));
211
+ }
212
+ }, 100);
213
+ });
214
+ }
215
+ ngOnDestroy() {
216
+ this.disconnect();
217
+ this.messageSubject.complete();
218
+ }
219
+ // =====================================================================
220
+ // Protected methods for subclass customization
221
+ // =====================================================================
222
+ /**
223
+ * Called when connection is established
224
+ * Override in subclass to perform initialization
225
+ */
226
+ onConnected() {
227
+ // Override in subclass
228
+ }
229
+ /**
230
+ * Called when connection is lost
231
+ * Override in subclass to handle cleanup
232
+ */
233
+ onDisconnected() {
234
+ // Override in subclass
235
+ }
236
+ /**
237
+ * Called when an event is received
238
+ * Override in subclass to dispatch to store
239
+ */
240
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
241
+ onEventReceived(_event) {
242
+ // Override in subclass
243
+ }
244
+ // =====================================================================
245
+ // Private handlers
246
+ // =====================================================================
247
+ handleOpen() {
248
+ this._connectionState.set('connected');
249
+ this._reconnectAttempts.set(0);
250
+ this._lastError.set(null);
251
+ // Re-subscribe to events
252
+ for (const eventType of this.subscribedEvents) {
253
+ this.send({
254
+ type: 'subscribe',
255
+ eventType
256
+ });
257
+ }
258
+ // Start heartbeat
259
+ if (this.config.heartbeat.enabled) {
260
+ this.startHeartbeat();
261
+ }
262
+ this.onConnected();
263
+ }
264
+ handleClose(event) {
265
+ this.stopHeartbeat();
266
+ if (event.wasClean) {
267
+ this._connectionState.set('disconnected');
268
+ } else {
269
+ this._connectionState.set('error');
270
+ this._lastError.set(new Error(`Connection closed: ${event.reason || 'Unknown reason'}`));
271
+ }
272
+ this.onDisconnected();
273
+ // Attempt reconnection if enabled
274
+ if (this.config.reconnect.enabled && !event.wasClean) {
275
+ this.scheduleReconnect();
276
+ }
277
+ }
278
+ handleMessage(message) {
279
+ this._lastMessageTime.set(new Date());
280
+ this.messageSubject.next(message);
281
+ switch (message.type) {
282
+ case 'event':
283
+ if (message.payload) {
284
+ this.onEventReceived(message.payload);
285
+ }
286
+ break;
287
+ case 'pong':
288
+ // Heartbeat response received
289
+ break;
290
+ case 'error':
291
+ console.error('Server error:', message.payload);
292
+ break;
293
+ }
294
+ }
295
+ handleError(error) {
296
+ const err = error instanceof Error ? error : new Error(String(error));
297
+ this._lastError.set(err);
298
+ this._connectionState.set('error');
299
+ console.error('WebSocket error:', err);
300
+ // Attempt reconnection
301
+ if (this.config.reconnect.enabled) {
302
+ this.scheduleReconnect();
303
+ }
304
+ }
305
+ handleComplete() {
306
+ this._connectionState.set('disconnected');
307
+ this.onDisconnected();
308
+ }
309
+ // =====================================================================
310
+ // Reconnection logic
311
+ // =====================================================================
312
+ scheduleReconnect() {
313
+ const attempts = this._reconnectAttempts();
314
+ const maxAttempts = this.config.reconnect.maxAttempts;
315
+ if (attempts >= maxAttempts) {
316
+ console.error(`Max reconnection attempts (${maxAttempts}) reached`);
317
+ this._connectionState.set('error');
318
+ return;
319
+ }
320
+ this._connectionState.set('reconnecting');
321
+ this._reconnectAttempts.set(attempts + 1);
322
+ // Exponential backoff
323
+ const delayMs = Math.min(this.config.reconnect.initialDelayMs * Math.pow(2, attempts), this.config.reconnect.maxDelayMs);
324
+ console.log(`Reconnecting in ${delayMs}ms (attempt ${attempts + 1}/${maxAttempts})`);
325
+ this.reconnectTimer = setTimeout(() => {
326
+ this.connect().catch(error => {
327
+ console.error('Reconnection failed:', error);
328
+ });
329
+ }, delayMs);
330
+ }
331
+ clearReconnectTimer() {
332
+ if (this.reconnectTimer) {
333
+ clearTimeout(this.reconnectTimer);
334
+ this.reconnectTimer = undefined;
335
+ }
336
+ }
337
+ // =====================================================================
338
+ // Heartbeat logic
339
+ // =====================================================================
340
+ startHeartbeat() {
341
+ this.stopHeartbeat();
342
+ this.heartbeatInterval = setInterval(() => {
343
+ if (this._connectionState() === 'connected') {
344
+ this.send({
345
+ type: 'ping'
346
+ });
347
+ }
348
+ }, this.config.heartbeat.intervalMs);
349
+ }
350
+ stopHeartbeat() {
351
+ if (this.heartbeatInterval) {
352
+ clearInterval(this.heartbeatInterval);
353
+ this.heartbeatInterval = undefined;
354
+ }
355
+ }
356
+ };
357
+ exports.WebSocketService = tslib.__decorate([core.Injectable(), tslib.__metadata("design:paramtypes", [Object])], exports.WebSocketService);
@@ -354,194 +354,4 @@ let WebSocketService = class WebSocketService {
354
354
  };
355
355
  WebSocketService = __decorate([Injectable(), __metadata("design:paramtypes", [Object])], WebSocketService);
356
356
 
357
- /**
358
- * Optimistic Update Manager
359
- *
360
- * Tracks optimistic updates and handles confirmation/rollback.
361
- *
362
- * @example
363
- * ```typescript
364
- * const manager = new OptimisticUpdateManager();
365
- *
366
- * // Apply optimistic update
367
- * manager.apply({
368
- * id: 'update-1',
369
- * correlationId: 'corr-123',
370
- * type: 'UpdateTradeStatus',
371
- * data: { status: 'accepted' },
372
- * previousData: { status: 'pending' },
373
- * timeoutMs: 5000,
374
- * rollback: () => store.$.trade.status.set('pending'),
375
- * });
376
- *
377
- * // When server confirms
378
- * manager.confirm('corr-123');
379
- *
380
- * // Or when server rejects
381
- * manager.rollback('corr-123', new Error('Server rejected'));
382
- * ```
383
- */
384
- class OptimisticUpdateManager {
385
- _updates = signal(new Map());
386
- timeouts = new Map();
387
- /**
388
- * Number of pending updates
389
- */
390
- pendingCount = computed(() => this._updates().size);
391
- /**
392
- * Whether there are any pending updates
393
- */
394
- hasPending = computed(() => this._updates().size > 0);
395
- /**
396
- * Get all pending updates
397
- */
398
- pending = computed(() => Array.from(this._updates().values()));
399
- /**
400
- * Apply an optimistic update
401
- */
402
- apply(update) {
403
- // Store the update
404
- this._updates.update(map => {
405
- const newMap = new Map(map);
406
- newMap.set(update.correlationId, update);
407
- return newMap;
408
- });
409
- // Set timeout for automatic rollback
410
- const timeout = setTimeout(() => {
411
- this.rollback(update.correlationId, new Error(`Optimistic update timeout after ${update.timeoutMs}ms`));
412
- }, update.timeoutMs);
413
- this.timeouts.set(update.correlationId, timeout);
414
- }
415
- /**
416
- * Confirm an optimistic update (server accepted)
417
- */
418
- confirm(correlationId) {
419
- const update = this._updates().get(correlationId);
420
- if (!update) {
421
- return false;
422
- }
423
- // Clear timeout
424
- const timeout = this.timeouts.get(correlationId);
425
- if (timeout) {
426
- clearTimeout(timeout);
427
- this.timeouts.delete(correlationId);
428
- }
429
- // Remove from pending
430
- this._updates.update(map => {
431
- const newMap = new Map(map);
432
- newMap.delete(correlationId);
433
- return newMap;
434
- });
435
- return true;
436
- }
437
- /**
438
- * Rollback an optimistic update (server rejected or timeout)
439
- */
440
- rollback(correlationId, error) {
441
- const update = this._updates().get(correlationId);
442
- if (!update) {
443
- return false;
444
- }
445
- // Clear timeout
446
- const timeout = this.timeouts.get(correlationId);
447
- if (timeout) {
448
- clearTimeout(timeout);
449
- this.timeouts.delete(correlationId);
450
- }
451
- // Execute rollback
452
- try {
453
- update.rollback();
454
- } catch (rollbackError) {
455
- console.error('Rollback failed:', rollbackError);
456
- }
457
- // Remove from pending
458
- this._updates.update(map => {
459
- const newMap = new Map(map);
460
- newMap.delete(correlationId);
461
- return newMap;
462
- });
463
- if (error) {
464
- console.warn(`Optimistic update rolled back: ${error.message}`);
465
- }
466
- return true;
467
- }
468
- /**
469
- * Rollback all pending updates
470
- */
471
- rollbackAll(error) {
472
- const updates = Array.from(this._updates().keys());
473
- let count = 0;
474
- for (const correlationId of updates) {
475
- if (this.rollback(correlationId, error)) {
476
- count++;
477
- }
478
- }
479
- return count;
480
- }
481
- /**
482
- * Get update by correlation ID
483
- */
484
- get(correlationId) {
485
- return this._updates().get(correlationId);
486
- }
487
- /**
488
- * Check if an update is pending
489
- */
490
- isPending(correlationId) {
491
- return this._updates().has(correlationId);
492
- }
493
- /**
494
- * Clear all updates without rollback (use with caution)
495
- */
496
- clear() {
497
- // Clear all timeouts
498
- for (const timeout of this.timeouts.values()) {
499
- clearTimeout(timeout);
500
- }
501
- this.timeouts.clear();
502
- // Clear updates
503
- this._updates.set(new Map());
504
- }
505
- /**
506
- * Dispose the manager
507
- */
508
- dispose() {
509
- this.clear();
510
- }
511
- }
512
-
513
- /**
514
- * Create a simple event handler
515
- *
516
- * @example
517
- * ```typescript
518
- * const handler = createEventHandler<TradeProposalCreated>(event => {
519
- * store.$.trades.entities.upsertOne(event.data);
520
- * });
521
- * ```
522
- */
523
- function createEventHandler(handler) {
524
- return handler;
525
- }
526
- /**
527
- * Create a typed event handler with metadata
528
- *
529
- * @example
530
- * ```typescript
531
- * const handler = createTypedHandler('TradeProposalCreated', {
532
- * handle: (event) => {
533
- * store.$.trades.entities.upsertOne(event.data);
534
- * },
535
- * priority: 1,
536
- * });
537
- * ```
538
- */
539
- function createTypedHandler(eventType, options) {
540
- return {
541
- eventType,
542
- handle: options.handle,
543
- priority: options.priority ?? 10
544
- };
545
- }
546
-
547
- export { OptimisticUpdateManager, WebSocketService, createEventHandler, createTypedHandler };
357
+ export { WebSocketService };