@signaltree/events 7.6.0 → 8.0.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.
package/README.md CHANGED
@@ -8,6 +8,18 @@ Event-driven architecture infrastructure for SignalTree applications. Provides a
8
8
  npm install @signaltree/events zod
9
9
  ```
10
10
 
11
+ ## Module Format (ESM-only)
12
+
13
+ `@signaltree/events` ships as **pure ESM**.
14
+
15
+ - ✅ Works in ESM environments (modern bundlers, Vite, Angular, Node ESM)
16
+ - ❌ **Does not support** CommonJS `require()` (you'll get `ERR_REQUIRE_ESM`)
17
+
18
+ If you're using the NestJS entry (`@signaltree/events/nestjs`) from Node, make sure your backend runs in ESM mode, for example:
19
+
20
+ - `package.json`: set `"type": "module"`
21
+ - `tsconfig.json`: set `"module": "NodeNext"` and `"moduleResolution": "NodeNext"`
22
+
11
23
  ## Subpath Exports
12
24
 
13
25
  The package provides four entry points for different use cases:
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@signaltree/events",
3
- "version": "7.6.0",
3
+ "version": "8.0.0",
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",
7
7
  "sideEffects": false,
8
- "main": "./dist/index.cjs",
9
- "module": "./index.esm.js",
8
+ "main": "./dist/index.js",
9
+ "module": "./dist/index.js",
10
10
  "types": "./src/index.d.ts",
11
11
  "typesVersions": {
12
12
  "*": {
@@ -25,25 +25,21 @@
25
25
  ".": {
26
26
  "types": "./src/index.d.ts",
27
27
  "import": "./dist/index.js",
28
- "require": "./dist/index.cjs",
29
28
  "default": "./dist/index.js"
30
29
  },
31
30
  "./nestjs": {
32
31
  "types": "./src/nestjs/index.d.ts",
33
32
  "import": "./dist/nestjs/index.js",
34
- "require": "./dist/nestjs/index.cjs",
35
33
  "default": "./dist/nestjs/index.js"
36
34
  },
37
35
  "./angular": {
38
36
  "types": "./src/angular/index.d.ts",
39
37
  "import": "./dist/angular/index.js",
40
- "require": "./dist/angular/index.cjs",
41
38
  "default": "./dist/angular/index.js"
42
39
  },
43
40
  "./testing": {
44
41
  "types": "./src/testing/index.d.ts",
45
42
  "import": "./dist/testing/index.js",
46
- "require": "./dist/testing/index.cjs",
47
43
  "default": "./dist/testing/index.js"
48
44
  },
49
45
  "./package.json": "./package.json"
@@ -103,7 +99,6 @@
103
99
  },
104
100
  "files": [
105
101
  "dist/**/*.js",
106
- "dist/**/*.cjs",
107
102
  "src/**/*.d.ts",
108
103
  "README.md"
109
104
  ],
@@ -128,4 +123,4 @@
128
123
  "url": "https://github.com/JBorgia/signaltree/issues"
129
124
  },
130
125
  "homepage": "https://github.com/JBorgia/signaltree/tree/main/packages/events#readme"
131
- }
126
+ }
@@ -1,38 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * Create a simple event handler
5
- *
6
- * @example
7
- * ```typescript
8
- * const handler = createEventHandler<TradeProposalCreated>(event => {
9
- * store.$.trades.entities.upsertOne(event.data);
10
- * });
11
- * ```
12
- */
13
- function createEventHandler(handler) {
14
- return handler;
15
- }
16
- /**
17
- * Create a typed event handler with metadata
18
- *
19
- * @example
20
- * ```typescript
21
- * const handler = createTypedHandler('TradeProposalCreated', {
22
- * handle: (event) => {
23
- * store.$.trades.entities.upsertOne(event.data);
24
- * },
25
- * priority: 1,
26
- * });
27
- * ```
28
- */
29
- function createTypedHandler(eventType, options) {
30
- return {
31
- eventType,
32
- handle: options.handle,
33
- priority: options.priority ?? 10
34
- };
35
- }
36
-
37
- exports.createEventHandler = createEventHandler;
38
- exports.createTypedHandler = createTypedHandler;
@@ -1,15 +0,0 @@
1
- 'use strict';
2
-
3
- var websocket_service = require('./websocket.service.cjs');
4
- var optimisticUpdates = require('./optimistic-updates.cjs');
5
- var handlers = require('./handlers.cjs');
6
-
7
-
8
-
9
- Object.defineProperty(exports, "WebSocketService", {
10
- enumerable: true,
11
- get: function () { return websocket_service.WebSocketService; }
12
- });
13
- exports.OptimisticUpdateManager = optimisticUpdates.OptimisticUpdateManager;
14
- exports.createEventHandler = handlers.createEventHandler;
15
- exports.createTypedHandler = handlers.createTypedHandler;
@@ -1,161 +0,0 @@
1
- 'use strict';
2
-
3
- var core = require('@angular/core');
4
-
5
- /**
6
- * Optimistic Update Manager
7
- *
8
- * Tracks optimistic updates and handles confirmation/rollback.
9
- *
10
- * @example
11
- * ```typescript
12
- * const manager = new OptimisticUpdateManager();
13
- *
14
- * // Apply optimistic update
15
- * manager.apply({
16
- * id: 'update-1',
17
- * correlationId: 'corr-123',
18
- * type: 'UpdateTradeStatus',
19
- * data: { status: 'accepted' },
20
- * previousData: { status: 'pending' },
21
- * timeoutMs: 5000,
22
- * rollback: () => store.$.trade.status.set('pending'),
23
- * });
24
- *
25
- * // When server confirms
26
- * manager.confirm('corr-123');
27
- *
28
- * // Or when server rejects
29
- * manager.rollback('corr-123', new Error('Server rejected'));
30
- * ```
31
- */
32
- class OptimisticUpdateManager {
33
- _updates = core.signal(new Map());
34
- timeouts = new Map();
35
- /**
36
- * Number of pending updates
37
- */
38
- pendingCount = core.computed(() => this._updates().size);
39
- /**
40
- * Whether there are any pending updates
41
- */
42
- hasPending = core.computed(() => this._updates().size > 0);
43
- /**
44
- * Get all pending updates
45
- */
46
- pending = core.computed(() => Array.from(this._updates().values()));
47
- /**
48
- * Apply an optimistic update
49
- */
50
- apply(update) {
51
- // Store the update
52
- this._updates.update(map => {
53
- const newMap = new Map(map);
54
- newMap.set(update.correlationId, update);
55
- return newMap;
56
- });
57
- // Set timeout for automatic rollback
58
- const timeout = setTimeout(() => {
59
- this.rollback(update.correlationId, new Error(`Optimistic update timeout after ${update.timeoutMs}ms`));
60
- }, update.timeoutMs);
61
- this.timeouts.set(update.correlationId, timeout);
62
- }
63
- /**
64
- * Confirm an optimistic update (server accepted)
65
- */
66
- confirm(correlationId) {
67
- const update = this._updates().get(correlationId);
68
- if (!update) {
69
- return false;
70
- }
71
- // Clear timeout
72
- const timeout = this.timeouts.get(correlationId);
73
- if (timeout) {
74
- clearTimeout(timeout);
75
- this.timeouts.delete(correlationId);
76
- }
77
- // Remove from pending
78
- this._updates.update(map => {
79
- const newMap = new Map(map);
80
- newMap.delete(correlationId);
81
- return newMap;
82
- });
83
- return true;
84
- }
85
- /**
86
- * Rollback an optimistic update (server rejected or timeout)
87
- */
88
- rollback(correlationId, error) {
89
- const update = this._updates().get(correlationId);
90
- if (!update) {
91
- return false;
92
- }
93
- // Clear timeout
94
- const timeout = this.timeouts.get(correlationId);
95
- if (timeout) {
96
- clearTimeout(timeout);
97
- this.timeouts.delete(correlationId);
98
- }
99
- // Execute rollback
100
- try {
101
- update.rollback();
102
- } catch (rollbackError) {
103
- console.error('Rollback failed:', rollbackError);
104
- }
105
- // Remove from pending
106
- this._updates.update(map => {
107
- const newMap = new Map(map);
108
- newMap.delete(correlationId);
109
- return newMap;
110
- });
111
- if (error) {
112
- console.warn(`Optimistic update rolled back: ${error.message}`);
113
- }
114
- return true;
115
- }
116
- /**
117
- * Rollback all pending updates
118
- */
119
- rollbackAll(error) {
120
- const updates = Array.from(this._updates().keys());
121
- let count = 0;
122
- for (const correlationId of updates) {
123
- if (this.rollback(correlationId, error)) {
124
- count++;
125
- }
126
- }
127
- return count;
128
- }
129
- /**
130
- * Get update by correlation ID
131
- */
132
- get(correlationId) {
133
- return this._updates().get(correlationId);
134
- }
135
- /**
136
- * Check if an update is pending
137
- */
138
- isPending(correlationId) {
139
- return this._updates().has(correlationId);
140
- }
141
- /**
142
- * Clear all updates without rollback (use with caution)
143
- */
144
- clear() {
145
- // Clear all timeouts
146
- for (const timeout of this.timeouts.values()) {
147
- clearTimeout(timeout);
148
- }
149
- this.timeouts.clear();
150
- // Clear updates
151
- this._updates.set(new Map());
152
- }
153
- /**
154
- * Dispose the manager
155
- */
156
- dispose() {
157
- this.clear();
158
- }
159
- }
160
-
161
- exports.OptimisticUpdateManager = OptimisticUpdateManager;
@@ -1,357 +0,0 @@
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);