icom-wlan-node 0.2.2 → 0.2.4
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 +9 -2
- package/dist/core/Session.js +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +9 -1
- package/dist/rig/IcomControl.d.ts +7 -2
- package/dist/rig/IcomControl.js +99 -26
- package/dist/types.d.ts +32 -0
- package/dist/types.js +18 -1
- package/dist/utils/errorHandling.d.ts +55 -0
- package/dist/utils/errorHandling.js +147 -0
- package/dist/utils/errors.d.ts +33 -0
- package/dist/utils/errors.js +64 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ npm run build
|
|
|
29
29
|
## Quick Start
|
|
30
30
|
|
|
31
31
|
```ts
|
|
32
|
-
import { IcomControl, AUDIO_RATE } from 'icom-wlan-node';
|
|
32
|
+
import { IcomControl, AUDIO_RATE, DisconnectReason } from 'icom-wlan-node';
|
|
33
33
|
|
|
34
34
|
const rig = new IcomControl({
|
|
35
35
|
control: { ip: '192.168.1.50', port: 50001 },
|
|
@@ -111,7 +111,8 @@ await rig.setPtt(false);
|
|
|
111
111
|
- `reconnectAttempting(ReconnectAttemptInfo)` — reconnect attempt started
|
|
112
112
|
- `reconnectFailed(ReconnectFailedInfo)` — reconnect attempt failed
|
|
113
113
|
- Methods
|
|
114
|
-
- **Connection**: `connect()` / `disconnect()` — connects control + CIV + audio sub‑sessions; resolves when all ready
|
|
114
|
+
- **Connection**: `connect()` / `disconnect(options?)` — connects control + CIV + audio sub‑sessions; resolves when all ready
|
|
115
|
+
- `disconnect()` accepts optional `DisconnectOptions` or `DisconnectReason` for better error handling
|
|
115
116
|
- **Raw CI‑V**: `sendCiv(buf: Buffer)` — send a raw CI‑V frame
|
|
116
117
|
- **Audio TX**: `setPtt(on: boolean)`, `sendAudioFloat32()`, `sendAudioPcm16()`
|
|
117
118
|
- **Rig Control**: `setFrequency()`, `setMode()`, `setConnectorDataMode()`, `setConnectorWLanLevel()`
|
|
@@ -151,6 +152,12 @@ console.log(metrics.sessions); // Per-session states {control, civ, audio}
|
|
|
151
152
|
|
|
152
153
|
// Disconnect (also idempotent)
|
|
153
154
|
await rig.disconnect();
|
|
155
|
+
|
|
156
|
+
// Disconnect with reason (provides better error messages)
|
|
157
|
+
await rig.disconnect(DisconnectReason.TIMEOUT);
|
|
158
|
+
|
|
159
|
+
// Silent disconnect (cleanup mode - no error events)
|
|
160
|
+
await rig.disconnect({ reason: DisconnectReason.CLEANUP, silent: true });
|
|
154
161
|
```
|
|
155
162
|
|
|
156
163
|
#### Connection Monitoring Events
|
package/dist/core/Session.js
CHANGED
|
@@ -111,6 +111,8 @@ class Session {
|
|
|
111
111
|
* (especially important after radio restart)
|
|
112
112
|
*/
|
|
113
113
|
resetState() {
|
|
114
|
+
// Stop all timers to prevent leaks and interference with new connection
|
|
115
|
+
this.stopTimers();
|
|
114
116
|
// Reset destroyed flag to ensure session is usable after state reset
|
|
115
117
|
this.destroyed = false;
|
|
116
118
|
// Generate new IDs
|
package/dist/index.d.ts
CHANGED
|
@@ -4,3 +4,5 @@ export { MODE_MAP, CONNECTOR_MODE_MAP, DEFAULT_CONTROLLER_ADDR, METER_THRESHOLDS
|
|
|
4
4
|
export { parseTwoByteBcd, intToTwoByteBcd } from './utils/bcd';
|
|
5
5
|
export { IcomRigCommands } from './rig/IcomRigCommands';
|
|
6
6
|
export { AUDIO_RATE } from './rig/IcomAudio';
|
|
7
|
+
export { setupGlobalErrorHandlers, setupBasicErrorProtection, GlobalErrorHandlerOptions } from './utils/errorHandling';
|
|
8
|
+
export { ConnectionAbortedError, getDisconnectMessage } from './utils/errors';
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.AUDIO_RATE = exports.IcomRigCommands = exports.intToTwoByteBcd = exports.parseTwoByteBcd = exports.rawToCurrent = exports.rawToVoltage = exports.rawToPowerPercent = exports.getFilterString = exports.getConnectorModeString = exports.getModeString = exports.getConnectorModeCode = exports.getModeCode = exports.METER_CALIBRATION = exports.METER_THRESHOLDS = exports.DEFAULT_CONTROLLER_ADDR = exports.CONNECTOR_MODE_MAP = exports.MODE_MAP = exports.IcomControl = void 0;
|
|
17
|
+
exports.getDisconnectMessage = exports.ConnectionAbortedError = exports.setupBasicErrorProtection = exports.setupGlobalErrorHandlers = exports.AUDIO_RATE = exports.IcomRigCommands = exports.intToTwoByteBcd = exports.parseTwoByteBcd = exports.rawToCurrent = exports.rawToVoltage = exports.rawToPowerPercent = exports.getFilterString = exports.getConnectorModeString = exports.getModeString = exports.getConnectorModeCode = exports.getModeCode = exports.METER_CALIBRATION = exports.METER_THRESHOLDS = exports.DEFAULT_CONTROLLER_ADDR = exports.CONNECTOR_MODE_MAP = exports.MODE_MAP = exports.IcomControl = void 0;
|
|
18
18
|
// Export types (includes ConnectionPhase, ConnectionMetrics, etc.)
|
|
19
19
|
__exportStar(require("./types"), exports);
|
|
20
20
|
// Export main class
|
|
@@ -44,3 +44,11 @@ var IcomRigCommands_1 = require("./rig/IcomRigCommands");
|
|
|
44
44
|
Object.defineProperty(exports, "IcomRigCommands", { enumerable: true, get: function () { return IcomRigCommands_1.IcomRigCommands; } });
|
|
45
45
|
var IcomAudio_1 = require("./rig/IcomAudio");
|
|
46
46
|
Object.defineProperty(exports, "AUDIO_RATE", { enumerable: true, get: function () { return IcomAudio_1.AUDIO_RATE; } });
|
|
47
|
+
// Export error handling utilities (optional, for robustness)
|
|
48
|
+
var errorHandling_1 = require("./utils/errorHandling");
|
|
49
|
+
Object.defineProperty(exports, "setupGlobalErrorHandlers", { enumerable: true, get: function () { return errorHandling_1.setupGlobalErrorHandlers; } });
|
|
50
|
+
Object.defineProperty(exports, "setupBasicErrorProtection", { enumerable: true, get: function () { return errorHandling_1.setupBasicErrorProtection; } });
|
|
51
|
+
// Export disconnect error utilities
|
|
52
|
+
var errors_1 = require("./utils/errors");
|
|
53
|
+
Object.defineProperty(exports, "ConnectionAbortedError", { enumerable: true, get: function () { return errors_1.ConnectionAbortedError; } });
|
|
54
|
+
Object.defineProperty(exports, "getDisconnectMessage", { enumerable: true, get: function () { return errors_1.getDisconnectMessage; } });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IcomRigOptions, RigEventEmitter, IcomMode, ConnectorDataMode, SetModeOptions, QueryOptions, SwrReading, AlcReading, WlanLevelReading, LevelMeterReading, SquelchStatusReading, AudioSquelchReading, OvfStatusReading, PowerLevelReading, CompLevelReading, VoltageReading, CurrentReading, ConnectionState, ConnectionMonitorConfig, ConnectionPhase, ConnectionMetrics } from '../types';
|
|
1
|
+
import { IcomRigOptions, RigEventEmitter, IcomMode, ConnectorDataMode, SetModeOptions, QueryOptions, SwrReading, AlcReading, WlanLevelReading, LevelMeterReading, SquelchStatusReading, AudioSquelchReading, OvfStatusReading, PowerLevelReading, CompLevelReading, VoltageReading, CurrentReading, ConnectionState, ConnectionMonitorConfig, ConnectionPhase, ConnectionMetrics, DisconnectReason, DisconnectOptions } from '../types';
|
|
2
2
|
import { IcomCiv } from './IcomCiv';
|
|
3
3
|
import { IcomAudio } from './IcomAudio';
|
|
4
4
|
export declare class IcomControl {
|
|
@@ -66,7 +66,12 @@ export declare class IcomControl {
|
|
|
66
66
|
* @private
|
|
67
67
|
*/
|
|
68
68
|
private stopUnifiedMonitoring;
|
|
69
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Disconnect from the rig
|
|
71
|
+
* @param options - Optional disconnect options (reason, silent mode)
|
|
72
|
+
* @returns Promise that resolves when disconnect is complete
|
|
73
|
+
*/
|
|
74
|
+
disconnect(options?: DisconnectOptions | DisconnectReason): Promise<void>;
|
|
70
75
|
sendCiv(data: Buffer): void;
|
|
71
76
|
/**
|
|
72
77
|
* Set PTT (Push-To-Talk) state
|
package/dist/rig/IcomControl.js
CHANGED
|
@@ -44,6 +44,7 @@ const IcomAudio_1 = require("./IcomAudio");
|
|
|
44
44
|
const IcomRigCommands_1 = require("./IcomRigCommands");
|
|
45
45
|
const IcomConstants_1 = require("./IcomConstants");
|
|
46
46
|
const bcd_1 = require("../utils/bcd");
|
|
47
|
+
const errors_1 = require("../utils/errors");
|
|
47
48
|
class IcomControl {
|
|
48
49
|
constructor(options) {
|
|
49
50
|
this.ev = new events_1.EventEmitter();
|
|
@@ -134,11 +135,12 @@ class IcomControl {
|
|
|
134
135
|
* Abort an ongoing connection attempt by session ID
|
|
135
136
|
* @private
|
|
136
137
|
*/
|
|
137
|
-
abortConnectionAttempt(sessionId, reason) {
|
|
138
|
+
abortConnectionAttempt(sessionId, reason, silent = false) {
|
|
138
139
|
const abortHandler = this.abortHandlers.get(sessionId);
|
|
139
140
|
if (abortHandler) {
|
|
140
|
-
(0,
|
|
141
|
-
|
|
141
|
+
const message = (0, errors_1.getDisconnectMessage)(reason);
|
|
142
|
+
(0, debug_1.dbg)(`Aborting connection session ${sessionId}: ${message} (silent=${silent})`);
|
|
143
|
+
abortHandler(reason, silent);
|
|
142
144
|
this.abortHandlers.delete(sessionId);
|
|
143
145
|
}
|
|
144
146
|
}
|
|
@@ -272,27 +274,48 @@ class IcomControl {
|
|
|
272
274
|
});
|
|
273
275
|
// Store abort handler bound to this specific sessionId
|
|
274
276
|
// This prevents race conditions when multiple connection attempts overlap
|
|
275
|
-
const abortHandler = (reason) => {
|
|
276
|
-
(0,
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
277
|
+
const abortHandler = (reason, silent) => {
|
|
278
|
+
const message = (0, errors_1.getDisconnectMessage)(reason);
|
|
279
|
+
(0, debug_1.dbg)(`Aborting connection session ${sessionId}: ${message} (silent=${silent})`);
|
|
280
|
+
// Create a single error instance to be shared across all rejections
|
|
281
|
+
const error = new errors_1.ConnectionAbortedError(reason, sessionId, this.connectionSession.phase);
|
|
282
|
+
// IMPORTANT: Always reject promises to unblock waiting code
|
|
283
|
+
// The "silent" flag only affects whether errors propagate to user code,
|
|
284
|
+
// but internally we must settle the promise to prevent hanging
|
|
285
|
+
(0, debug_1.dbg)(`${silent ? 'Silent' : 'Normal'} abort - rejecting login promise for session ${sessionId}`);
|
|
286
|
+
// Defensive error handling: wrap reject calls in try-catch to prevent
|
|
287
|
+
// synchronous errors from propagating if promises are already settled
|
|
288
|
+
// Only reject one promise to avoid duplicate errors
|
|
289
|
+
try {
|
|
290
|
+
rejectLogin(error);
|
|
291
|
+
}
|
|
292
|
+
catch (err) {
|
|
293
|
+
(0, debug_1.dbg)(`Warning: Failed to reject loginReady promise: ${err}`);
|
|
294
|
+
}
|
|
295
|
+
// Don't reject civ and audio promises separately - they'll be cleaned up
|
|
296
|
+
// This prevents the "3x User disconnect()" log spam
|
|
281
297
|
};
|
|
282
298
|
this.abortHandlers.set(sessionId, abortHandler);
|
|
299
|
+
// Track promise states for defensive cleanup
|
|
300
|
+
let loginResolved = false;
|
|
301
|
+
let civResolved = false;
|
|
302
|
+
let audioResolved = false;
|
|
283
303
|
// Temporary event listeners (local scope)
|
|
284
304
|
const onLogin = (res) => {
|
|
285
305
|
if (res.ok) {
|
|
286
|
-
(0, debug_1.dbg)(
|
|
306
|
+
(0, debug_1.dbg)(`Login ready - resolving local loginReady promise (sessionId=${sessionId})`);
|
|
307
|
+
loginResolved = true;
|
|
287
308
|
resolveLogin();
|
|
288
309
|
}
|
|
289
310
|
};
|
|
290
311
|
const onCivReady = () => {
|
|
291
|
-
(0, debug_1.dbg)(
|
|
312
|
+
(0, debug_1.dbg)(`CIV ready - resolving local civReady promise (sessionId=${sessionId})`);
|
|
313
|
+
civResolved = true;
|
|
292
314
|
resolveCiv();
|
|
293
315
|
};
|
|
294
316
|
const onAudioReady = () => {
|
|
295
|
-
(0, debug_1.dbg)(
|
|
317
|
+
(0, debug_1.dbg)(`Audio ready - resolving local audioReady promise (sessionId=${sessionId})`);
|
|
318
|
+
audioResolved = true;
|
|
296
319
|
resolveAudio();
|
|
297
320
|
};
|
|
298
321
|
this.ev.once('login', onLogin);
|
|
@@ -303,13 +326,36 @@ class IcomControl {
|
|
|
303
326
|
civReady,
|
|
304
327
|
audioReady,
|
|
305
328
|
cleanup: () => {
|
|
329
|
+
// Defensive cleanup: wrap each cleanup step in try-catch
|
|
330
|
+
// to ensure all cleanup steps execute even if one fails
|
|
331
|
+
(0, debug_1.dbg)(`Cleaning up connection session ${sessionId} (login=${loginResolved}, civ=${civResolved}, audio=${audioResolved})`);
|
|
306
332
|
// Remove abort handler for this specific sessionId
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
333
|
+
try {
|
|
334
|
+
this.abortHandlers.delete(sessionId);
|
|
335
|
+
}
|
|
336
|
+
catch (err) {
|
|
337
|
+
(0, debug_1.dbg)(`Warning: Failed to delete abort handler: ${err}`);
|
|
338
|
+
}
|
|
339
|
+
// Remove event listeners - wrap each in try-catch
|
|
340
|
+
try {
|
|
341
|
+
this.ev.off('login', onLogin);
|
|
342
|
+
}
|
|
343
|
+
catch (err) {
|
|
344
|
+
(0, debug_1.dbg)(`Warning: Failed to remove login listener: ${err}`);
|
|
345
|
+
}
|
|
346
|
+
try {
|
|
347
|
+
this.ev.off('_civReady', onCivReady);
|
|
348
|
+
}
|
|
349
|
+
catch (err) {
|
|
350
|
+
(0, debug_1.dbg)(`Warning: Failed to remove civReady listener: ${err}`);
|
|
351
|
+
}
|
|
352
|
+
try {
|
|
353
|
+
this.ev.off('_audioReady', onAudioReady);
|
|
354
|
+
}
|
|
355
|
+
catch (err) {
|
|
356
|
+
(0, debug_1.dbg)(`Warning: Failed to remove audioReady listener: ${err}`);
|
|
357
|
+
}
|
|
358
|
+
(0, debug_1.dbg)(`Cleanup complete for session ${sessionId}`);
|
|
313
359
|
}
|
|
314
360
|
};
|
|
315
361
|
}
|
|
@@ -354,8 +400,20 @@ class IcomControl {
|
|
|
354
400
|
this.monitorTimer = undefined;
|
|
355
401
|
}
|
|
356
402
|
}
|
|
357
|
-
|
|
403
|
+
/**
|
|
404
|
+
* Disconnect from the rig
|
|
405
|
+
* @param options - Optional disconnect options (reason, silent mode)
|
|
406
|
+
* @returns Promise that resolves when disconnect is complete
|
|
407
|
+
*/
|
|
408
|
+
async disconnect(options) {
|
|
409
|
+
// Support both object and enum parameter for backward compatibility
|
|
410
|
+
const opts = typeof options === 'string'
|
|
411
|
+
? { reason: options, silent: false }
|
|
412
|
+
: { reason: types_1.DisconnectReason.USER_REQUEST, silent: false, ...options };
|
|
413
|
+
const reason = opts.reason ?? types_1.DisconnectReason.USER_REQUEST;
|
|
414
|
+
const silent = opts.silent ?? false;
|
|
358
415
|
const currentPhase = this.connectionSession.phase;
|
|
416
|
+
(0, debug_1.dbg)(`disconnect() called with reason=${reason}, silent=${silent}, currentPhase=${currentPhase}`);
|
|
359
417
|
// If already disconnecting or idle, avoid duplicate work
|
|
360
418
|
if (currentPhase === types_1.ConnectionPhase.DISCONNECTING) {
|
|
361
419
|
(0, debug_1.dbg)('disconnect() called but already DISCONNECTING - waiting for completion');
|
|
@@ -376,14 +434,22 @@ class IcomControl {
|
|
|
376
434
|
(0, debug_1.dbg)('disconnect() called but already IDLE - no-op');
|
|
377
435
|
return;
|
|
378
436
|
}
|
|
379
|
-
// Abort any ongoing connection attempts
|
|
437
|
+
// Abort any ongoing connection attempts - wrap in try-catch for defensive programming
|
|
380
438
|
const currentSessionId = this.connectionSession.sessionId;
|
|
381
439
|
if (currentPhase === types_1.ConnectionPhase.CONNECTING || currentPhase === types_1.ConnectionPhase.RECONNECTING) {
|
|
382
|
-
|
|
383
|
-
|
|
440
|
+
try {
|
|
441
|
+
(0, debug_1.dbg)(`Aborting ongoing connection attempt (sessionId=${currentSessionId})`);
|
|
442
|
+
this.abortConnectionAttempt(currentSessionId, reason, silent);
|
|
443
|
+
}
|
|
444
|
+
catch (abortErr) {
|
|
445
|
+
// Log but continue - abort failure shouldn't prevent disconnect
|
|
446
|
+
const errMsg = abortErr instanceof Error ? abortErr.message : String(abortErr);
|
|
447
|
+
(0, debug_1.dbg)(`Warning: Failed to abort connection attempt: ${errMsg} - continuing with disconnect`);
|
|
448
|
+
}
|
|
384
449
|
}
|
|
385
450
|
// Transition to DISCONNECTING state
|
|
386
|
-
|
|
451
|
+
const transitionReason = (0, errors_1.getDisconnectMessage)(reason);
|
|
452
|
+
this.transitionTo(types_1.ConnectionPhase.DISCONNECTING, transitionReason);
|
|
387
453
|
try {
|
|
388
454
|
// 1. Stop all timers first to prevent interference
|
|
389
455
|
this.stopUnifiedMonitoring(); // Stop unified monitoring
|
|
@@ -1078,7 +1144,7 @@ class IcomControl {
|
|
|
1078
1144
|
const currentPhase = this.connectionSession.phase;
|
|
1079
1145
|
if (currentPhase === types_1.ConnectionPhase.CONNECTING || currentPhase === types_1.ConnectionPhase.RECONNECTING) {
|
|
1080
1146
|
(0, debug_1.dbg)('Aborting ongoing connection attempt due to connected=false');
|
|
1081
|
-
this.abortConnectionAttempt(this.connectionSession.sessionId,
|
|
1147
|
+
this.abortConnectionAttempt(this.connectionSession.sessionId, types_1.DisconnectReason.ERROR, false);
|
|
1082
1148
|
}
|
|
1083
1149
|
else if (currentPhase === types_1.ConnectionPhase.CONNECTED) {
|
|
1084
1150
|
// Trigger reconnection for established connection
|
|
@@ -1544,9 +1610,16 @@ class IcomControl {
|
|
|
1544
1610
|
(0, debug_1.dbg)(`Full reconnect attempt #${attempt} (delay: ${delay}ms)`);
|
|
1545
1611
|
await this.sleep(delay);
|
|
1546
1612
|
try {
|
|
1547
|
-
// Disconnect all sessions
|
|
1613
|
+
// Disconnect all sessions - wrap in try-catch to prevent disconnect errors from aborting reconnect
|
|
1548
1614
|
(0, debug_1.dbg)('Full reconnect: disconnecting all sessions');
|
|
1549
|
-
|
|
1615
|
+
try {
|
|
1616
|
+
await this.disconnect();
|
|
1617
|
+
}
|
|
1618
|
+
catch (disconnectErr) {
|
|
1619
|
+
// Log but don't fail - we still want to attempt reconnect even if disconnect fails
|
|
1620
|
+
const errMsg = disconnectErr instanceof Error ? disconnectErr.message : String(disconnectErr);
|
|
1621
|
+
(0, debug_1.dbg)(`Warning: Disconnect failed during reconnect: ${errMsg} - continuing with reconnect anyway`);
|
|
1622
|
+
}
|
|
1550
1623
|
// Wait longer before reconnecting to allow rig to fully clean up old connection
|
|
1551
1624
|
// This is critical - rig may report CONNINFO busy=true if we reconnect too quickly
|
|
1552
1625
|
(0, debug_1.dbg)('Full reconnect: waiting 5s for rig to clean up old session...');
|
package/dist/types.d.ts
CHANGED
|
@@ -373,6 +373,38 @@ export interface ReconnectFailedInfo {
|
|
|
373
373
|
/** Next retry delay (ms), if retrying */
|
|
374
374
|
nextRetryDelay?: number;
|
|
375
375
|
}
|
|
376
|
+
/**
|
|
377
|
+
* Reason for disconnection
|
|
378
|
+
* Used to distinguish between different types of disconnections
|
|
379
|
+
*/
|
|
380
|
+
export declare enum DisconnectReason {
|
|
381
|
+
/** User explicitly called disconnect() */
|
|
382
|
+
USER_REQUEST = "user_request",
|
|
383
|
+
/** Connection timed out */
|
|
384
|
+
TIMEOUT = "timeout",
|
|
385
|
+
/** Cleanup after connection failure */
|
|
386
|
+
CLEANUP = "cleanup",
|
|
387
|
+
/** Error occurred during connection */
|
|
388
|
+
ERROR = "error",
|
|
389
|
+
/** Network connection was lost */
|
|
390
|
+
NETWORK_LOST = "network_lost"
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Options for disconnect() method
|
|
394
|
+
*/
|
|
395
|
+
export interface DisconnectOptions {
|
|
396
|
+
/**
|
|
397
|
+
* Reason for disconnection
|
|
398
|
+
* Used to generate appropriate error messages and logs
|
|
399
|
+
*/
|
|
400
|
+
reason?: DisconnectReason;
|
|
401
|
+
/**
|
|
402
|
+
* Silent mode - don't throw exceptions or emit error events
|
|
403
|
+
* Useful for cleanup operations where exceptions are not desired
|
|
404
|
+
* Default: false
|
|
405
|
+
*/
|
|
406
|
+
silent?: boolean;
|
|
407
|
+
}
|
|
376
408
|
/**
|
|
377
409
|
* Configuration for connection monitoring
|
|
378
410
|
*/
|
package/dist/types.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.SessionType = exports.ConnectionPhase = exports.ConnectionState = void 0;
|
|
3
|
+
exports.DisconnectReason = exports.SessionType = exports.ConnectionPhase = exports.ConnectionState = void 0;
|
|
4
4
|
// ============================================================================
|
|
5
5
|
// Connection Monitoring Types
|
|
6
6
|
// ============================================================================
|
|
@@ -46,3 +46,20 @@ var SessionType;
|
|
|
46
46
|
/** Audio streaming session */
|
|
47
47
|
SessionType["AUDIO"] = "AUDIO";
|
|
48
48
|
})(SessionType || (exports.SessionType = SessionType = {}));
|
|
49
|
+
/**
|
|
50
|
+
* Reason for disconnection
|
|
51
|
+
* Used to distinguish between different types of disconnections
|
|
52
|
+
*/
|
|
53
|
+
var DisconnectReason;
|
|
54
|
+
(function (DisconnectReason) {
|
|
55
|
+
/** User explicitly called disconnect() */
|
|
56
|
+
DisconnectReason["USER_REQUEST"] = "user_request";
|
|
57
|
+
/** Connection timed out */
|
|
58
|
+
DisconnectReason["TIMEOUT"] = "timeout";
|
|
59
|
+
/** Cleanup after connection failure */
|
|
60
|
+
DisconnectReason["CLEANUP"] = "cleanup";
|
|
61
|
+
/** Error occurred during connection */
|
|
62
|
+
DisconnectReason["ERROR"] = "error";
|
|
63
|
+
/** Network connection was lost */
|
|
64
|
+
DisconnectReason["NETWORK_LOST"] = "network_lost";
|
|
65
|
+
})(DisconnectReason || (exports.DisconnectReason = DisconnectReason = {}));
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 全局错误处理工具函数
|
|
3
|
+
*
|
|
4
|
+
* 这个模块提供可选的全局错误处理器,帮助用户防止未捕获的 Promise rejection 和异常导致进程崩溃。
|
|
5
|
+
*
|
|
6
|
+
* 使用方式:
|
|
7
|
+
* ```typescript
|
|
8
|
+
* import { setupGlobalErrorHandlers } from 'icom-wlan-node/utils/errorHandling';
|
|
9
|
+
*
|
|
10
|
+
* // 在应用启动时调用(可选)
|
|
11
|
+
* setupGlobalErrorHandlers({
|
|
12
|
+
* onUnhandledRejection: (reason, promise) => {
|
|
13
|
+
* console.error('未处理的 Promise rejection:', reason);
|
|
14
|
+
* // 自定义处理逻辑
|
|
15
|
+
* },
|
|
16
|
+
* onUncaughtException: (error, origin) => {
|
|
17
|
+
* console.error('未捕获的异常:', error);
|
|
18
|
+
* // 自定义处理逻辑
|
|
19
|
+
* }
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export interface GlobalErrorHandlerOptions {
|
|
24
|
+
/**
|
|
25
|
+
* 处理未捕获的 Promise rejection
|
|
26
|
+
* @param reason - rejection 的原因
|
|
27
|
+
* @param promise - 被 reject 的 Promise
|
|
28
|
+
*/
|
|
29
|
+
onUnhandledRejection?: (reason: any, promise: Promise<any>) => void;
|
|
30
|
+
/**
|
|
31
|
+
* 处理未捕获的异常
|
|
32
|
+
* @param error - 异常对象
|
|
33
|
+
* @param origin - 异常来源 ('uncaughtException' 或 'unhandledRejection')
|
|
34
|
+
*/
|
|
35
|
+
onUncaughtException?: (error: Error, origin: string) => void;
|
|
36
|
+
/**
|
|
37
|
+
* 是否在处理错误后阻止进程退出(默认 true)
|
|
38
|
+
* 设为 false 时,错误会被记录但进程仍可能退出
|
|
39
|
+
*/
|
|
40
|
+
preventExit?: boolean;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 设置全局错误处理器
|
|
44
|
+
*
|
|
45
|
+
* 注意:这是一个可选的工具函数,仅在需要时使用。
|
|
46
|
+
* 如果你的应用已经有全局错误处理逻辑,不需要调用此函数。
|
|
47
|
+
*
|
|
48
|
+
* @param options - 错误处理选项
|
|
49
|
+
* @returns cleanup 函数,用于移除错误处理器
|
|
50
|
+
*/
|
|
51
|
+
export declare function setupGlobalErrorHandlers(options?: GlobalErrorHandlerOptions): () => void;
|
|
52
|
+
/**
|
|
53
|
+
* 快速设置:仅防止进程崩溃,使用默认错误处理
|
|
54
|
+
*/
|
|
55
|
+
export declare function setupBasicErrorProtection(): () => void;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 全局错误处理工具函数
|
|
4
|
+
*
|
|
5
|
+
* 这个模块提供可选的全局错误处理器,帮助用户防止未捕获的 Promise rejection 和异常导致进程崩溃。
|
|
6
|
+
*
|
|
7
|
+
* 使用方式:
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { setupGlobalErrorHandlers } from 'icom-wlan-node/utils/errorHandling';
|
|
10
|
+
*
|
|
11
|
+
* // 在应用启动时调用(可选)
|
|
12
|
+
* setupGlobalErrorHandlers({
|
|
13
|
+
* onUnhandledRejection: (reason, promise) => {
|
|
14
|
+
* console.error('未处理的 Promise rejection:', reason);
|
|
15
|
+
* // 自定义处理逻辑
|
|
16
|
+
* },
|
|
17
|
+
* onUncaughtException: (error, origin) => {
|
|
18
|
+
* console.error('未捕获的异常:', error);
|
|
19
|
+
* // 自定义处理逻辑
|
|
20
|
+
* }
|
|
21
|
+
* });
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.setupGlobalErrorHandlers = setupGlobalErrorHandlers;
|
|
26
|
+
exports.setupBasicErrorProtection = setupBasicErrorProtection;
|
|
27
|
+
/**
|
|
28
|
+
* 设置全局错误处理器
|
|
29
|
+
*
|
|
30
|
+
* 注意:这是一个可选的工具函数,仅在需要时使用。
|
|
31
|
+
* 如果你的应用已经有全局错误处理逻辑,不需要调用此函数。
|
|
32
|
+
*
|
|
33
|
+
* @param options - 错误处理选项
|
|
34
|
+
* @returns cleanup 函数,用于移除错误处理器
|
|
35
|
+
*/
|
|
36
|
+
function setupGlobalErrorHandlers(options = {}) {
|
|
37
|
+
const { onUnhandledRejection = defaultUnhandledRejectionHandler, onUncaughtException = defaultUncaughtExceptionHandler, preventExit = true } = options;
|
|
38
|
+
// 未处理的 Promise rejection 处理器
|
|
39
|
+
const unhandledRejectionHandler = (reason, promise) => {
|
|
40
|
+
console.error('⚠️ [icom-wlan-node] 检测到未处理的 Promise rejection:');
|
|
41
|
+
console.error('原因:', reason);
|
|
42
|
+
console.error('Promise:', promise);
|
|
43
|
+
if (reason instanceof Error) {
|
|
44
|
+
console.error('错误堆栈:', reason.stack);
|
|
45
|
+
}
|
|
46
|
+
// 调用用户自定义处理器
|
|
47
|
+
onUnhandledRejection(reason, promise);
|
|
48
|
+
};
|
|
49
|
+
// 未捕获的异常处理器
|
|
50
|
+
const uncaughtExceptionHandler = (error, origin) => {
|
|
51
|
+
console.error('⚠️ [icom-wlan-node] 检测到未捕获的异常:');
|
|
52
|
+
console.error('错误:', error);
|
|
53
|
+
console.error('来源:', origin);
|
|
54
|
+
console.error('堆栈:', error.stack);
|
|
55
|
+
// 调用用户自定义处理器
|
|
56
|
+
onUncaughtException(error, origin);
|
|
57
|
+
// 根据配置决定是否退出
|
|
58
|
+
if (!preventExit) {
|
|
59
|
+
console.error('进程即将退出...');
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
// 注册处理器
|
|
64
|
+
process.on('unhandledRejection', unhandledRejectionHandler);
|
|
65
|
+
process.on('uncaughtException', uncaughtExceptionHandler);
|
|
66
|
+
console.log('✓ [icom-wlan-node] 全局错误处理器已设置');
|
|
67
|
+
// 返回 cleanup 函数
|
|
68
|
+
return () => {
|
|
69
|
+
process.off('unhandledRejection', unhandledRejectionHandler);
|
|
70
|
+
process.off('uncaughtException', uncaughtExceptionHandler);
|
|
71
|
+
console.log('✓ [icom-wlan-node] 全局错误处理器已移除');
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 默认的未处理 Promise rejection 处理器
|
|
76
|
+
*/
|
|
77
|
+
function defaultUnhandledRejectionHandler(reason, promise) {
|
|
78
|
+
// 检查是否是网络错误(可以优雅处理)
|
|
79
|
+
if (isNetworkError(reason)) {
|
|
80
|
+
console.warn('网络错误已被捕获,但未影响进程稳定性');
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
// 其他错误记录为警告
|
|
84
|
+
console.warn('检测到未处理的 Promise rejection,但进程继续运行');
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* 默认的未捕获异常处理器
|
|
88
|
+
*/
|
|
89
|
+
function defaultUncaughtExceptionHandler(error, origin) {
|
|
90
|
+
// 检查是否是可恢复的错误
|
|
91
|
+
if (isRecoverableError(error)) {
|
|
92
|
+
console.warn('可恢复的错误已被捕获,进程继续运行');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
console.error('检测到严重错误,建议检查应用逻辑');
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* 判断是否是网络错误
|
|
99
|
+
*/
|
|
100
|
+
function isNetworkError(error) {
|
|
101
|
+
if (!error)
|
|
102
|
+
return false;
|
|
103
|
+
const networkErrorCodes = [
|
|
104
|
+
'EHOSTDOWN',
|
|
105
|
+
'EHOSTUNREACH',
|
|
106
|
+
'ENETDOWN',
|
|
107
|
+
'ENETUNREACH',
|
|
108
|
+
'ECONNREFUSED',
|
|
109
|
+
'ECONNRESET',
|
|
110
|
+
'ETIMEDOUT',
|
|
111
|
+
'ENOTFOUND'
|
|
112
|
+
];
|
|
113
|
+
// 检查错误码
|
|
114
|
+
if (error.code && networkErrorCodes.includes(error.code)) {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
// 检查错误消息
|
|
118
|
+
if (error.message && typeof error.message === 'string') {
|
|
119
|
+
return networkErrorCodes.some(code => error.message.includes(code));
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* 判断是否是可恢复的错误
|
|
125
|
+
*/
|
|
126
|
+
function isRecoverableError(error) {
|
|
127
|
+
// 网络错误通常是可恢复的
|
|
128
|
+
if (isNetworkError(error)) {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
// 连接相关的错误也是可恢复的
|
|
132
|
+
const recoverableMessages = [
|
|
133
|
+
'Connection failed',
|
|
134
|
+
'User disconnect',
|
|
135
|
+
'Connection timeout',
|
|
136
|
+
'CIV/Audio sessions timeout'
|
|
137
|
+
];
|
|
138
|
+
return recoverableMessages.some(msg => error.message.includes(msg));
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* 快速设置:仅防止进程崩溃,使用默认错误处理
|
|
142
|
+
*/
|
|
143
|
+
function setupBasicErrorProtection() {
|
|
144
|
+
return setupGlobalErrorHandlers({
|
|
145
|
+
preventExit: true
|
|
146
|
+
});
|
|
147
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { DisconnectReason, ConnectionPhase } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Get human-readable message for a disconnect reason
|
|
4
|
+
*/
|
|
5
|
+
export declare function getDisconnectMessage(reason: DisconnectReason): string;
|
|
6
|
+
/**
|
|
7
|
+
* Error thrown when a connection attempt is aborted
|
|
8
|
+
* This is typically used when disconnect() is called during connect()
|
|
9
|
+
*/
|
|
10
|
+
export declare class ConnectionAbortedError extends Error {
|
|
11
|
+
readonly reason: DisconnectReason;
|
|
12
|
+
readonly sessionId: number;
|
|
13
|
+
readonly phase: ConnectionPhase;
|
|
14
|
+
readonly context?: Record<string, any> | undefined;
|
|
15
|
+
readonly name = "ConnectionAbortedError";
|
|
16
|
+
constructor(reason: DisconnectReason, sessionId: number, phase: ConnectionPhase, context?: Record<string, any> | undefined);
|
|
17
|
+
/**
|
|
18
|
+
* Check if this error should be silent (not thrown to user)
|
|
19
|
+
* Cleanup and timeout errors during connect are typically expected
|
|
20
|
+
*/
|
|
21
|
+
isSilent(): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Get detailed error information for debugging
|
|
24
|
+
*/
|
|
25
|
+
toJSON(): {
|
|
26
|
+
name: string;
|
|
27
|
+
message: string;
|
|
28
|
+
reason: DisconnectReason;
|
|
29
|
+
sessionId: number;
|
|
30
|
+
phase: ConnectionPhase;
|
|
31
|
+
context: Record<string, any> | undefined;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ConnectionAbortedError = void 0;
|
|
4
|
+
exports.getDisconnectMessage = getDisconnectMessage;
|
|
5
|
+
const types_1 = require("../types");
|
|
6
|
+
/**
|
|
7
|
+
* Get human-readable message for a disconnect reason
|
|
8
|
+
*/
|
|
9
|
+
function getDisconnectMessage(reason) {
|
|
10
|
+
switch (reason) {
|
|
11
|
+
case types_1.DisconnectReason.USER_REQUEST:
|
|
12
|
+
return 'Connection closed by user request';
|
|
13
|
+
case types_1.DisconnectReason.TIMEOUT:
|
|
14
|
+
return 'Connection timed out';
|
|
15
|
+
case types_1.DisconnectReason.CLEANUP:
|
|
16
|
+
return 'Connection cleanup';
|
|
17
|
+
case types_1.DisconnectReason.ERROR:
|
|
18
|
+
return 'Connection closed due to error';
|
|
19
|
+
case types_1.DisconnectReason.NETWORK_LOST:
|
|
20
|
+
return 'Network connection lost';
|
|
21
|
+
default:
|
|
22
|
+
return 'Connection closed';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Error thrown when a connection attempt is aborted
|
|
27
|
+
* This is typically used when disconnect() is called during connect()
|
|
28
|
+
*/
|
|
29
|
+
class ConnectionAbortedError extends Error {
|
|
30
|
+
constructor(reason, sessionId, phase, context) {
|
|
31
|
+
super(getDisconnectMessage(reason));
|
|
32
|
+
this.reason = reason;
|
|
33
|
+
this.sessionId = sessionId;
|
|
34
|
+
this.phase = phase;
|
|
35
|
+
this.context = context;
|
|
36
|
+
this.name = 'ConnectionAbortedError';
|
|
37
|
+
// Maintains proper stack trace for where error was thrown (V8 only)
|
|
38
|
+
if (Error.captureStackTrace) {
|
|
39
|
+
Error.captureStackTrace(this, ConnectionAbortedError);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Check if this error should be silent (not thrown to user)
|
|
44
|
+
* Cleanup and timeout errors during connect are typically expected
|
|
45
|
+
*/
|
|
46
|
+
isSilent() {
|
|
47
|
+
return this.reason === types_1.DisconnectReason.CLEANUP ||
|
|
48
|
+
this.reason === types_1.DisconnectReason.TIMEOUT;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Get detailed error information for debugging
|
|
52
|
+
*/
|
|
53
|
+
toJSON() {
|
|
54
|
+
return {
|
|
55
|
+
name: this.name,
|
|
56
|
+
message: this.message,
|
|
57
|
+
reason: this.reason,
|
|
58
|
+
sessionId: this.sessionId,
|
|
59
|
+
phase: this.phase,
|
|
60
|
+
context: this.context
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
exports.ConnectionAbortedError = ConnectionAbortedError;
|