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 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
@@ -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
- disconnect(): Promise<void>;
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
@@ -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, debug_1.dbg)(`Aborting connection session ${sessionId}: ${reason}`);
141
- abortHandler(reason);
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, debug_1.dbg)(`Aborting connection session ${sessionId}: ${reason}`);
277
- const error = new Error(reason);
278
- rejectLogin(error);
279
- rejectCiv(error);
280
- rejectAudio(error);
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)('Login ready - resolving local loginReady promise');
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)('CIV ready - resolving local civReady promise');
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)('Audio ready - resolving local audioReady promise');
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
- // This is safe because the connection attempt is complete (success or failure)
308
- this.abortHandlers.delete(sessionId);
309
- // Remove event listeners
310
- this.ev.off('login', onLogin);
311
- this.ev.off('_civReady', onCivReady);
312
- this.ev.off('_audioReady', onAudioReady);
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
- async disconnect() {
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
- (0, debug_1.dbg)(`Aborting ongoing connection attempt (sessionId=${currentSessionId})`);
383
- this.abortConnectionAttempt(currentSessionId, 'User disconnect()');
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
- this.transitionTo(types_1.ConnectionPhase.DISCONNECTING, 'User disconnect()');
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, 'Radio reported connected=false');
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
- await this.disconnect();
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icom-wlan-node",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Icom WLAN (CI‑V, audio) protocol implementation for Node.js/TypeScript.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",