@signaltree/events 7.3.1
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/angular.d.ts +1 -0
- package/angular.esm.js +547 -0
- package/factory.esm.js +178 -0
- package/idempotency.esm.js +701 -0
- package/index.d.ts +1 -0
- package/index.esm.js +167 -0
- package/nestjs.d.ts +1 -0
- package/nestjs.esm.js +944 -0
- package/package.json +110 -0
- package/src/angular/handlers.d.ts +132 -0
- package/src/angular/index.d.ts +12 -0
- package/src/angular/optimistic-updates.d.ts +117 -0
- package/src/angular/websocket.service.d.ts +158 -0
- package/src/angular.d.ts +7 -0
- package/src/core/error-classification.d.ts +100 -0
- package/src/core/factory.d.ts +114 -0
- package/src/core/idempotency.d.ts +209 -0
- package/src/core/registry.d.ts +147 -0
- package/src/core/types.d.ts +127 -0
- package/src/core/validation.d.ts +619 -0
- package/src/index.d.ts +56 -0
- package/src/nestjs/base.subscriber.d.ts +169 -0
- package/src/nestjs/decorators.d.ts +37 -0
- package/src/nestjs/dlq.service.d.ts +117 -0
- package/src/nestjs/event-bus.module.d.ts +117 -0
- package/src/nestjs/event-bus.service.d.ts +114 -0
- package/src/nestjs/index.d.ts +16 -0
- package/src/nestjs/tokens.d.ts +8 -0
- package/src/nestjs.d.ts +7 -0
- package/src/testing/assertions.d.ts +113 -0
- package/src/testing/factories.d.ts +106 -0
- package/src/testing/helpers.d.ts +104 -0
- package/src/testing/index.d.ts +13 -0
- package/src/testing/mock-event-bus.d.ts +144 -0
- package/src/testing.d.ts +7 -0
- package/testing.d.ts +1 -0
- package/testing.esm.js +743 -0
package/angular.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./src/angular";
|
package/angular.esm.js
ADDED
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
import { __decorate, __metadata } from 'tslib';
|
|
2
|
+
import { Injectable, inject, DestroyRef, signal, computed } from '@angular/core';
|
|
3
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
4
|
+
import { Subject } from 'rxjs';
|
|
5
|
+
import { filter, map } from 'rxjs/operators';
|
|
6
|
+
import { webSocket } from 'rxjs/webSocket';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Default configuration
|
|
10
|
+
*/
|
|
11
|
+
const DEFAULT_CONFIG = {
|
|
12
|
+
reconnect: {
|
|
13
|
+
enabled: true,
|
|
14
|
+
initialDelayMs: 1000,
|
|
15
|
+
maxDelayMs: 30000,
|
|
16
|
+
maxAttempts: 10
|
|
17
|
+
},
|
|
18
|
+
heartbeat: {
|
|
19
|
+
enabled: true,
|
|
20
|
+
intervalMs: 30000,
|
|
21
|
+
timeoutMs: 5000
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Base WebSocket service for real-time event synchronization
|
|
26
|
+
*
|
|
27
|
+
* Extend this class in your application and wire it to your SignalTree store.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* @Injectable({ providedIn: 'root' })
|
|
32
|
+
* export class AppWebSocketService extends WebSocketService {
|
|
33
|
+
* private readonly store = inject(AppStore);
|
|
34
|
+
*
|
|
35
|
+
* constructor() {
|
|
36
|
+
* super({
|
|
37
|
+
* url: environment.wsUrl,
|
|
38
|
+
* getAuthToken: () => this.store.$.session.token(),
|
|
39
|
+
* });
|
|
40
|
+
*
|
|
41
|
+
* // Subscribe to events
|
|
42
|
+
* this.onEvent<TradeProposalCreated>('TradeProposalCreated').subscribe(event => {
|
|
43
|
+
* this.store.$.trades.entities.upsertOne(event.data);
|
|
44
|
+
* });
|
|
45
|
+
* }
|
|
46
|
+
* }
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
let WebSocketService = class WebSocketService {
|
|
50
|
+
destroyRef = inject(DestroyRef);
|
|
51
|
+
// Signals for reactive state
|
|
52
|
+
_connectionState = signal('disconnected');
|
|
53
|
+
_lastError = signal(null);
|
|
54
|
+
_reconnectAttempts = signal(0);
|
|
55
|
+
_lastMessageTime = signal(null);
|
|
56
|
+
// Public readonly signals
|
|
57
|
+
connectionState = this._connectionState.asReadonly();
|
|
58
|
+
lastError = this._lastError.asReadonly();
|
|
59
|
+
isConnected = computed(() => this._connectionState() === 'connected');
|
|
60
|
+
isReconnecting = computed(() => this._connectionState() === 'reconnecting');
|
|
61
|
+
// WebSocket subject
|
|
62
|
+
socket$;
|
|
63
|
+
messageSubject = new Subject();
|
|
64
|
+
// Subscriptions tracking
|
|
65
|
+
subscribedEvents = new Set();
|
|
66
|
+
heartbeatInterval;
|
|
67
|
+
reconnectTimer;
|
|
68
|
+
// Config
|
|
69
|
+
config;
|
|
70
|
+
constructor(config) {
|
|
71
|
+
this.config = {
|
|
72
|
+
...config,
|
|
73
|
+
reconnect: {
|
|
74
|
+
...DEFAULT_CONFIG.reconnect,
|
|
75
|
+
...config.reconnect
|
|
76
|
+
},
|
|
77
|
+
heartbeat: {
|
|
78
|
+
...DEFAULT_CONFIG.heartbeat,
|
|
79
|
+
...config.heartbeat
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Connect to WebSocket server
|
|
85
|
+
*/
|
|
86
|
+
async connect() {
|
|
87
|
+
if (this._connectionState() === 'connected' || this._connectionState() === 'connecting') {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
this._connectionState.set('connecting');
|
|
91
|
+
this._lastError.set(null);
|
|
92
|
+
try {
|
|
93
|
+
// Get auth token if needed
|
|
94
|
+
let url = this.config.url;
|
|
95
|
+
if (this.config.getAuthToken) {
|
|
96
|
+
const token = await this.config.getAuthToken();
|
|
97
|
+
if (token) {
|
|
98
|
+
url = `${url}${url.includes('?') ? '&' : '?'}token=${encodeURIComponent(token)}`;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Create WebSocket
|
|
102
|
+
const wsConfig = {
|
|
103
|
+
url,
|
|
104
|
+
openObserver: {
|
|
105
|
+
next: () => this.handleOpen()
|
|
106
|
+
},
|
|
107
|
+
closeObserver: {
|
|
108
|
+
next: event => this.handleClose(event)
|
|
109
|
+
},
|
|
110
|
+
protocol: this.config.protocols
|
|
111
|
+
};
|
|
112
|
+
this.socket$ = webSocket(wsConfig);
|
|
113
|
+
// Subscribe to messages
|
|
114
|
+
this.socket$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
|
|
115
|
+
next: message => this.handleMessage(message),
|
|
116
|
+
error: error => this.handleError(error),
|
|
117
|
+
complete: () => this.handleComplete()
|
|
118
|
+
});
|
|
119
|
+
} catch (error) {
|
|
120
|
+
this._connectionState.set('error');
|
|
121
|
+
this._lastError.set(error instanceof Error ? error : new Error(String(error)));
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Disconnect from WebSocket server
|
|
127
|
+
*/
|
|
128
|
+
disconnect() {
|
|
129
|
+
this.stopHeartbeat();
|
|
130
|
+
this.clearReconnectTimer();
|
|
131
|
+
if (this.socket$) {
|
|
132
|
+
this.socket$.complete();
|
|
133
|
+
this.socket$ = undefined;
|
|
134
|
+
}
|
|
135
|
+
this._connectionState.set('disconnected');
|
|
136
|
+
this.subscribedEvents.clear();
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Send a message
|
|
140
|
+
*/
|
|
141
|
+
send(message) {
|
|
142
|
+
if (!this.socket$ || this._connectionState() !== 'connected') {
|
|
143
|
+
console.warn('WebSocket not connected, message not sent:', message);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
this.socket$.next(message);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Subscribe to a specific event type
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```typescript
|
|
153
|
+
* this.onEvent<TradeProposalCreated>('TradeProposalCreated').subscribe(event => {
|
|
154
|
+
* console.log('Trade created:', event);
|
|
155
|
+
* });
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
onEvent(eventType) {
|
|
159
|
+
// Track subscription
|
|
160
|
+
if (!this.subscribedEvents.has(eventType)) {
|
|
161
|
+
this.subscribedEvents.add(eventType);
|
|
162
|
+
// Send subscribe message if connected
|
|
163
|
+
if (this._connectionState() === 'connected') {
|
|
164
|
+
this.send({
|
|
165
|
+
type: 'subscribe',
|
|
166
|
+
eventType
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return this.messageSubject.pipe(filter(msg => msg.type === 'event' && msg.eventType === eventType), map(msg => msg.payload));
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Unsubscribe from an event type
|
|
174
|
+
*/
|
|
175
|
+
offEvent(eventType) {
|
|
176
|
+
if (this.subscribedEvents.has(eventType)) {
|
|
177
|
+
this.subscribedEvents.delete(eventType);
|
|
178
|
+
if (this._connectionState() === 'connected') {
|
|
179
|
+
this.send({
|
|
180
|
+
type: 'unsubscribe',
|
|
181
|
+
eventType
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Get all messages (for debugging/logging)
|
|
188
|
+
*/
|
|
189
|
+
get messages$() {
|
|
190
|
+
return this.messageSubject.asObservable();
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Wait for connection to be established
|
|
194
|
+
*/
|
|
195
|
+
async waitForConnection(timeoutMs = 10000) {
|
|
196
|
+
return new Promise((resolve, reject) => {
|
|
197
|
+
if (this._connectionState() === 'connected') {
|
|
198
|
+
resolve();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const startTime = Date.now();
|
|
202
|
+
const checkInterval = setInterval(() => {
|
|
203
|
+
if (this._connectionState() === 'connected') {
|
|
204
|
+
clearInterval(checkInterval);
|
|
205
|
+
resolve();
|
|
206
|
+
} else if (Date.now() - startTime > timeoutMs) {
|
|
207
|
+
clearInterval(checkInterval);
|
|
208
|
+
reject(new Error('Connection timeout'));
|
|
209
|
+
}
|
|
210
|
+
}, 100);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
ngOnDestroy() {
|
|
214
|
+
this.disconnect();
|
|
215
|
+
this.messageSubject.complete();
|
|
216
|
+
}
|
|
217
|
+
// =====================================================================
|
|
218
|
+
// Protected methods for subclass customization
|
|
219
|
+
// =====================================================================
|
|
220
|
+
/**
|
|
221
|
+
* Called when connection is established
|
|
222
|
+
* Override in subclass to perform initialization
|
|
223
|
+
*/
|
|
224
|
+
onConnected() {
|
|
225
|
+
// Override in subclass
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Called when connection is lost
|
|
229
|
+
* Override in subclass to handle cleanup
|
|
230
|
+
*/
|
|
231
|
+
onDisconnected() {
|
|
232
|
+
// Override in subclass
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Called when an event is received
|
|
236
|
+
* Override in subclass to dispatch to store
|
|
237
|
+
*/
|
|
238
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
239
|
+
onEventReceived(_event) {
|
|
240
|
+
// Override in subclass
|
|
241
|
+
}
|
|
242
|
+
// =====================================================================
|
|
243
|
+
// Private handlers
|
|
244
|
+
// =====================================================================
|
|
245
|
+
handleOpen() {
|
|
246
|
+
this._connectionState.set('connected');
|
|
247
|
+
this._reconnectAttempts.set(0);
|
|
248
|
+
this._lastError.set(null);
|
|
249
|
+
// Re-subscribe to events
|
|
250
|
+
for (const eventType of this.subscribedEvents) {
|
|
251
|
+
this.send({
|
|
252
|
+
type: 'subscribe',
|
|
253
|
+
eventType
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
// Start heartbeat
|
|
257
|
+
if (this.config.heartbeat.enabled) {
|
|
258
|
+
this.startHeartbeat();
|
|
259
|
+
}
|
|
260
|
+
this.onConnected();
|
|
261
|
+
}
|
|
262
|
+
handleClose(event) {
|
|
263
|
+
this.stopHeartbeat();
|
|
264
|
+
if (event.wasClean) {
|
|
265
|
+
this._connectionState.set('disconnected');
|
|
266
|
+
} else {
|
|
267
|
+
this._connectionState.set('error');
|
|
268
|
+
this._lastError.set(new Error(`Connection closed: ${event.reason || 'Unknown reason'}`));
|
|
269
|
+
}
|
|
270
|
+
this.onDisconnected();
|
|
271
|
+
// Attempt reconnection if enabled
|
|
272
|
+
if (this.config.reconnect.enabled && !event.wasClean) {
|
|
273
|
+
this.scheduleReconnect();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
handleMessage(message) {
|
|
277
|
+
this._lastMessageTime.set(new Date());
|
|
278
|
+
this.messageSubject.next(message);
|
|
279
|
+
switch (message.type) {
|
|
280
|
+
case 'event':
|
|
281
|
+
if (message.payload) {
|
|
282
|
+
this.onEventReceived(message.payload);
|
|
283
|
+
}
|
|
284
|
+
break;
|
|
285
|
+
case 'pong':
|
|
286
|
+
// Heartbeat response received
|
|
287
|
+
break;
|
|
288
|
+
case 'error':
|
|
289
|
+
console.error('Server error:', message.payload);
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
handleError(error) {
|
|
294
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
295
|
+
this._lastError.set(err);
|
|
296
|
+
this._connectionState.set('error');
|
|
297
|
+
console.error('WebSocket error:', err);
|
|
298
|
+
// Attempt reconnection
|
|
299
|
+
if (this.config.reconnect.enabled) {
|
|
300
|
+
this.scheduleReconnect();
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
handleComplete() {
|
|
304
|
+
this._connectionState.set('disconnected');
|
|
305
|
+
this.onDisconnected();
|
|
306
|
+
}
|
|
307
|
+
// =====================================================================
|
|
308
|
+
// Reconnection logic
|
|
309
|
+
// =====================================================================
|
|
310
|
+
scheduleReconnect() {
|
|
311
|
+
const attempts = this._reconnectAttempts();
|
|
312
|
+
const maxAttempts = this.config.reconnect.maxAttempts;
|
|
313
|
+
if (attempts >= maxAttempts) {
|
|
314
|
+
console.error(`Max reconnection attempts (${maxAttempts}) reached`);
|
|
315
|
+
this._connectionState.set('error');
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
this._connectionState.set('reconnecting');
|
|
319
|
+
this._reconnectAttempts.set(attempts + 1);
|
|
320
|
+
// Exponential backoff
|
|
321
|
+
const delayMs = Math.min(this.config.reconnect.initialDelayMs * Math.pow(2, attempts), this.config.reconnect.maxDelayMs);
|
|
322
|
+
console.log(`Reconnecting in ${delayMs}ms (attempt ${attempts + 1}/${maxAttempts})`);
|
|
323
|
+
this.reconnectTimer = setTimeout(() => {
|
|
324
|
+
this.connect().catch(error => {
|
|
325
|
+
console.error('Reconnection failed:', error);
|
|
326
|
+
});
|
|
327
|
+
}, delayMs);
|
|
328
|
+
}
|
|
329
|
+
clearReconnectTimer() {
|
|
330
|
+
if (this.reconnectTimer) {
|
|
331
|
+
clearTimeout(this.reconnectTimer);
|
|
332
|
+
this.reconnectTimer = undefined;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
// =====================================================================
|
|
336
|
+
// Heartbeat logic
|
|
337
|
+
// =====================================================================
|
|
338
|
+
startHeartbeat() {
|
|
339
|
+
this.stopHeartbeat();
|
|
340
|
+
this.heartbeatInterval = setInterval(() => {
|
|
341
|
+
if (this._connectionState() === 'connected') {
|
|
342
|
+
this.send({
|
|
343
|
+
type: 'ping'
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}, this.config.heartbeat.intervalMs);
|
|
347
|
+
}
|
|
348
|
+
stopHeartbeat() {
|
|
349
|
+
if (this.heartbeatInterval) {
|
|
350
|
+
clearInterval(this.heartbeatInterval);
|
|
351
|
+
this.heartbeatInterval = undefined;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
WebSocketService = __decorate([Injectable(), __metadata("design:paramtypes", [Object])], WebSocketService);
|
|
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 };
|