@signaltree/events 7.6.1 → 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 +12 -0
- package/package.json +4 -9
- package/dist/angular/handlers.cjs +0 -38
- package/dist/angular/index.cjs +0 -15
- package/dist/angular/optimistic-updates.cjs +0 -161
- package/dist/angular/websocket.service.cjs +0 -357
- package/dist/core/error-classification.cjs +0 -282
- package/dist/core/factory.cjs +0 -148
- package/dist/core/idempotency.cjs +0 -252
- package/dist/core/registry.cjs +0 -183
- package/dist/core/types.cjs +0 -41
- package/dist/core/validation.cjs +0 -185
- package/dist/index.cjs +0 -43
- package/dist/nestjs/base.subscriber.cjs +0 -287
- package/dist/nestjs/decorators.cjs +0 -35
- package/dist/nestjs/dlq.service.cjs +0 -249
- package/dist/nestjs/event-bus.module.cjs +0 -218
- package/dist/nestjs/event-bus.service.cjs +0 -318
- package/dist/nestjs/index.cjs +0 -34
- package/dist/nestjs/tokens.cjs +0 -14
- package/dist/testing/assertions.cjs +0 -172
- package/dist/testing/factories.cjs +0 -122
- package/dist/testing/helpers.cjs +0 -233
- package/dist/testing/index.cjs +0 -20
- package/dist/testing/mock-event-bus.cjs +0 -301
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": "
|
|
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.
|
|
9
|
-
"module": "./index.
|
|
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;
|
package/dist/angular/index.cjs
DELETED
|
@@ -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);
|