packet-events-js 1.0.0 → 1.0.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.
@@ -0,0 +1,486 @@
1
+ import { EventEmitter } from '../events/EventEmitter.js';
2
+
3
+ export const ReconnectStrategy = Object.freeze({
4
+ IMMEDIATE: 'immediate',
5
+ LINEAR: 'linear',
6
+ EXPONENTIAL: 'exponential',
7
+ FIBONACCI: 'fibonacci'
8
+ });
9
+
10
+ export const HealthStatus = Object.freeze({
11
+ HEALTHY: 'healthy',
12
+ DEGRADED: 'degraded',
13
+ UNHEALTHY: 'unhealthy',
14
+ UNKNOWN: 'unknown'
15
+ });
16
+
17
+ export class AutoReconnect extends EventEmitter {
18
+ constructor(options = {}) {
19
+ super();
20
+
21
+ this.enabled = options.enabled !== false;
22
+ this.strategy = options.strategy || ReconnectStrategy.EXPONENTIAL;
23
+ this.maxAttempts = options.maxAttempts || 10;
24
+ this.baseDelay = options.baseDelay || 1000;
25
+ this.maxDelay = options.maxDelay || 30000;
26
+ this.jitter = options.jitter !== false;
27
+ this.jitterFactor = options.jitterFactor || 0.1;
28
+
29
+ this.attempts = 0;
30
+ this.isReconnecting = false;
31
+ this.lastAttempt = null;
32
+ this.connectFn = null;
33
+ this.timeoutId = null;
34
+
35
+ this._fibCache = [1, 1];
36
+ }
37
+
38
+ _getFibonacci(n) {
39
+ while (this._fibCache.length <= n) {
40
+ this._fibCache.push(this._fibCache[this._fibCache.length - 1] + this._fibCache[this._fibCache.length - 2]);
41
+ }
42
+ return this._fibCache[n];
43
+ }
44
+
45
+ getDelay() {
46
+ let delay;
47
+
48
+ switch (this.strategy) {
49
+ case ReconnectStrategy.IMMEDIATE:
50
+ delay = 0;
51
+ break;
52
+ case ReconnectStrategy.LINEAR:
53
+ delay = this.baseDelay * this.attempts;
54
+ break;
55
+ case ReconnectStrategy.EXPONENTIAL:
56
+ delay = this.baseDelay * Math.pow(2, this.attempts - 1);
57
+ break;
58
+ case ReconnectStrategy.FIBONACCI:
59
+ delay = this.baseDelay * this._getFibonacci(this.attempts);
60
+ break;
61
+ default:
62
+ delay = this.baseDelay;
63
+ }
64
+
65
+ if (this.jitter && delay > 0) {
66
+ const jitterAmount = delay * this.jitterFactor;
67
+ delay += (Math.random() * jitterAmount * 2) - jitterAmount;
68
+ }
69
+
70
+ return Math.min(Math.max(0, delay), this.maxDelay);
71
+ }
72
+
73
+ async start(connectFn) {
74
+ if (!this.enabled) return false;
75
+ if (this.isReconnecting) return false;
76
+
77
+ this.connectFn = connectFn;
78
+ this.isReconnecting = true;
79
+ this.attempts = 0;
80
+
81
+ return this._attemptReconnect();
82
+ }
83
+
84
+ async _attemptReconnect() {
85
+ if (!this.isReconnecting) return false;
86
+ if (this.attempts >= this.maxAttempts) {
87
+ this.isReconnecting = false;
88
+ this.emit('failed', { attempts: this.attempts });
89
+ return false;
90
+ }
91
+
92
+ this.attempts++;
93
+ this.lastAttempt = Date.now();
94
+ const delay = this.getDelay();
95
+
96
+ this.emit('attempt', {
97
+ attempt: this.attempts,
98
+ maxAttempts: this.maxAttempts,
99
+ delay,
100
+ strategy: this.strategy
101
+ });
102
+
103
+ if (delay > 0) {
104
+ await new Promise(resolve => {
105
+ this.timeoutId = setTimeout(resolve, delay);
106
+ });
107
+ }
108
+
109
+ if (!this.isReconnecting) return false;
110
+
111
+ try {
112
+ await this.connectFn();
113
+ this.isReconnecting = false;
114
+ this.emit('success', { attempts: this.attempts });
115
+ return true;
116
+ } catch (e) {
117
+ this.emit('error', { attempt: this.attempts, error: e.message });
118
+ return this._attemptReconnect();
119
+ }
120
+ }
121
+
122
+ stop() {
123
+ this.isReconnecting = false;
124
+ if (this.timeoutId) {
125
+ clearTimeout(this.timeoutId);
126
+ this.timeoutId = null;
127
+ }
128
+ this.emit('stopped');
129
+ return this;
130
+ }
131
+
132
+ reset() {
133
+ this.attempts = 0;
134
+ this.lastAttempt = null;
135
+ return this;
136
+ }
137
+
138
+ getStatus() {
139
+ return {
140
+ enabled: this.enabled,
141
+ isReconnecting: this.isReconnecting,
142
+ attempts: this.attempts,
143
+ maxAttempts: this.maxAttempts,
144
+ strategy: this.strategy,
145
+ nextDelay: this.getDelay(),
146
+ lastAttempt: this.lastAttempt
147
+ };
148
+ }
149
+ }
150
+
151
+ export class HealthMonitor extends EventEmitter {
152
+ constructor(options = {}) {
153
+ super();
154
+
155
+ this.checkInterval = options.checkInterval || 5000;
156
+ this.latencyThreshold = options.latencyThreshold || 200;
157
+ this.packetLossThreshold = options.packetLossThreshold || 0.05;
158
+ this.healthyStreak = options.healthyStreak || 3;
159
+
160
+ this.status = HealthStatus.UNKNOWN;
161
+ this.latencyHistory = [];
162
+ this.packetsSent = 0;
163
+ this.packetsReceived = 0;
164
+ this.consecutiveHealthy = 0;
165
+ this.consecutiveUnhealthy = 0;
166
+ this.intervalId = null;
167
+ this.checkFn = null;
168
+ }
169
+
170
+ start(checkFn) {
171
+ this.checkFn = checkFn;
172
+ this.intervalId = setInterval(() => this._performCheck(), this.checkInterval);
173
+ this.emit('start');
174
+ return this;
175
+ }
176
+
177
+ stop() {
178
+ if (this.intervalId) {
179
+ clearInterval(this.intervalId);
180
+ this.intervalId = null;
181
+ }
182
+ this.emit('stop');
183
+ return this;
184
+ }
185
+
186
+ async _performCheck() {
187
+ if (!this.checkFn) return;
188
+
189
+ const start = Date.now();
190
+ this.packetsSent++;
191
+
192
+ try {
193
+ await this.checkFn();
194
+ this.packetsReceived++;
195
+
196
+ const latency = Date.now() - start;
197
+ this.recordLatency(latency);
198
+
199
+ this._evaluateHealth();
200
+ } catch (e) {
201
+ this._evaluateHealth();
202
+ this.emit('checkFailed', { error: e.message });
203
+ }
204
+ }
205
+
206
+ recordLatency(ms) {
207
+ this.latencyHistory.push({ time: Date.now(), latency: ms });
208
+ if (this.latencyHistory.length > 100) {
209
+ this.latencyHistory.shift();
210
+ }
211
+ }
212
+
213
+ _evaluateHealth() {
214
+ const avgLatency = this.getAverageLatency();
215
+ const packetLoss = this.getPacketLoss();
216
+
217
+ let newStatus;
218
+
219
+ if (avgLatency > this.latencyThreshold * 2 || packetLoss > this.packetLossThreshold * 2) {
220
+ newStatus = HealthStatus.UNHEALTHY;
221
+ this.consecutiveUnhealthy++;
222
+ this.consecutiveHealthy = 0;
223
+ } else if (avgLatency > this.latencyThreshold || packetLoss > this.packetLossThreshold) {
224
+ newStatus = HealthStatus.DEGRADED;
225
+ this.consecutiveHealthy = 0;
226
+ this.consecutiveUnhealthy = 0;
227
+ } else {
228
+ this.consecutiveHealthy++;
229
+ this.consecutiveUnhealthy = 0;
230
+
231
+ if (this.consecutiveHealthy >= this.healthyStreak) {
232
+ newStatus = HealthStatus.HEALTHY;
233
+ } else {
234
+ newStatus = this.status === HealthStatus.UNKNOWN ? HealthStatus.HEALTHY : this.status;
235
+ }
236
+ }
237
+
238
+ if (newStatus !== this.status) {
239
+ const oldStatus = this.status;
240
+ this.status = newStatus;
241
+ this.emit('statusChange', { from: oldStatus, to: newStatus });
242
+ }
243
+
244
+ this.emit('check', this.getReport());
245
+ }
246
+
247
+ getAverageLatency() {
248
+ if (this.latencyHistory.length === 0) return 0;
249
+ return this.latencyHistory.reduce((a, b) => a + b.latency, 0) / this.latencyHistory.length;
250
+ }
251
+
252
+ getPacketLoss() {
253
+ if (this.packetsSent === 0) return 0;
254
+ return 1 - (this.packetsReceived / this.packetsSent);
255
+ }
256
+
257
+ getReport() {
258
+ return {
259
+ status: this.status,
260
+ latency: {
261
+ current: this.latencyHistory.length > 0 ? this.latencyHistory[this.latencyHistory.length - 1].latency : 0,
262
+ average: this.getAverageLatency().toFixed(2),
263
+ min: this.latencyHistory.length > 0 ? Math.min(...this.latencyHistory.map(l => l.latency)) : 0,
264
+ max: this.latencyHistory.length > 0 ? Math.max(...this.latencyHistory.map(l => l.latency)) : 0
265
+ },
266
+ packets: {
267
+ sent: this.packetsSent,
268
+ received: this.packetsReceived,
269
+ loss: (this.getPacketLoss() * 100).toFixed(2) + '%'
270
+ },
271
+ consecutiveHealthy: this.consecutiveHealthy,
272
+ consecutiveUnhealthy: this.consecutiveUnhealthy
273
+ };
274
+ }
275
+
276
+ reset() {
277
+ this.latencyHistory = [];
278
+ this.packetsSent = 0;
279
+ this.packetsReceived = 0;
280
+ this.consecutiveHealthy = 0;
281
+ this.consecutiveUnhealthy = 0;
282
+ this.status = HealthStatus.UNKNOWN;
283
+ return this;
284
+ }
285
+ }
286
+
287
+ export class ConnectionPool extends EventEmitter {
288
+ constructor(options = {}) {
289
+ super();
290
+
291
+ this.maxConnections = options.maxConnections || 10;
292
+ this.connections = new Map();
293
+ this.roundRobinIndex = 0;
294
+ this.strategy = options.strategy || 'round-robin';
295
+ }
296
+
297
+ add(id, connection, metadata = {}) {
298
+ if (this.connections.size >= this.maxConnections) {
299
+ throw new Error('Connection pool is full');
300
+ }
301
+
302
+ this.connections.set(id, {
303
+ connection,
304
+ metadata,
305
+ activeRequests: 0,
306
+ totalRequests: 0,
307
+ createdAt: Date.now()
308
+ });
309
+
310
+ this.emit('add', { id, poolSize: this.connections.size });
311
+ return this;
312
+ }
313
+
314
+ remove(id) {
315
+ const entry = this.connections.get(id);
316
+ if (entry) {
317
+ this.connections.delete(id);
318
+ this.emit('remove', { id, poolSize: this.connections.size });
319
+ }
320
+ return this;
321
+ }
322
+
323
+ get() {
324
+ if (this.connections.size === 0) return null;
325
+
326
+ const entries = [...this.connections.entries()];
327
+ let selected;
328
+
329
+ switch (this.strategy) {
330
+ case 'round-robin':
331
+ this.roundRobinIndex = (this.roundRobinIndex + 1) % entries.length;
332
+ selected = entries[this.roundRobinIndex];
333
+ break;
334
+
335
+ case 'least-loaded':
336
+ selected = entries.reduce((min, entry) =>
337
+ entry[1].activeRequests < min[1].activeRequests ? entry : min
338
+ );
339
+ break;
340
+
341
+ case 'random':
342
+ selected = entries[Math.floor(Math.random() * entries.length)];
343
+ break;
344
+
345
+ default:
346
+ selected = entries[0];
347
+ }
348
+
349
+ if (selected) {
350
+ selected[1].activeRequests++;
351
+ selected[1].totalRequests++;
352
+ }
353
+
354
+ return selected ? { id: selected[0], ...selected[1] } : null;
355
+ }
356
+
357
+ release(id) {
358
+ const entry = this.connections.get(id);
359
+ if (entry && entry.activeRequests > 0) {
360
+ entry.activeRequests--;
361
+ }
362
+ return this;
363
+ }
364
+
365
+ getStats() {
366
+ const stats = {
367
+ size: this.connections.size,
368
+ maxConnections: this.maxConnections,
369
+ strategy: this.strategy,
370
+ connections: []
371
+ };
372
+
373
+ for (const [id, entry] of this.connections) {
374
+ stats.connections.push({
375
+ id,
376
+ activeRequests: entry.activeRequests,
377
+ totalRequests: entry.totalRequests,
378
+ uptime: Date.now() - entry.createdAt,
379
+ metadata: entry.metadata
380
+ });
381
+ }
382
+
383
+ return stats;
384
+ }
385
+
386
+ clear() {
387
+ this.connections.clear();
388
+ this.roundRobinIndex = 0;
389
+ this.emit('clear');
390
+ return this;
391
+ }
392
+ }
393
+
394
+ export class CircuitBreaker extends EventEmitter {
395
+ constructor(options = {}) {
396
+ super();
397
+
398
+ this.failureThreshold = options.failureThreshold || 5;
399
+ this.successThreshold = options.successThreshold || 3;
400
+ this.timeout = options.timeout || 30000;
401
+
402
+ this.state = 'closed';
403
+ this.failures = 0;
404
+ this.successes = 0;
405
+ this.lastFailure = null;
406
+ this.timeoutId = null;
407
+ }
408
+
409
+ async execute(fn) {
410
+ if (this.state === 'open') {
411
+ if (Date.now() - this.lastFailure > this.timeout) {
412
+ this._transition('half-open');
413
+ } else {
414
+ throw new Error('Circuit breaker is open');
415
+ }
416
+ }
417
+
418
+ try {
419
+ const result = await fn();
420
+ this._onSuccess();
421
+ return result;
422
+ } catch (e) {
423
+ this._onFailure();
424
+ throw e;
425
+ }
426
+ }
427
+
428
+ _onSuccess() {
429
+ this.failures = 0;
430
+
431
+ if (this.state === 'half-open') {
432
+ this.successes++;
433
+ if (this.successes >= this.successThreshold) {
434
+ this._transition('closed');
435
+ }
436
+ }
437
+ }
438
+
439
+ _onFailure() {
440
+ this.failures++;
441
+ this.lastFailure = Date.now();
442
+ this.successes = 0;
443
+
444
+ if (this.failures >= this.failureThreshold) {
445
+ this._transition('open');
446
+ }
447
+ }
448
+
449
+ _transition(newState) {
450
+ const oldState = this.state;
451
+ this.state = newState;
452
+
453
+ if (newState === 'closed') {
454
+ this.failures = 0;
455
+ this.successes = 0;
456
+ }
457
+
458
+ this.emit('stateChange', { from: oldState, to: newState });
459
+ }
460
+
461
+ force(state) {
462
+ this._transition(state);
463
+ return this;
464
+ }
465
+
466
+ getStatus() {
467
+ return {
468
+ state: this.state,
469
+ failures: this.failures,
470
+ successes: this.successes,
471
+ failureThreshold: this.failureThreshold,
472
+ successThreshold: this.successThreshold,
473
+ lastFailure: this.lastFailure,
474
+ timeout: this.timeout
475
+ };
476
+ }
477
+ }
478
+
479
+ export default {
480
+ AutoReconnect,
481
+ ReconnectStrategy,
482
+ HealthMonitor,
483
+ HealthStatus,
484
+ ConnectionPool,
485
+ CircuitBreaker
486
+ };