@apocaliss92/nodelink-js 0.4.6 → 0.4.7

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.
@@ -17495,6 +17495,13 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
17495
17495
  * - "replay:XXX" - dedicated per replay session
17496
17496
  */
17497
17497
  socketPool = /* @__PURE__ */ new Map();
17498
+ /**
17499
+ * Consecutive stream-start (cmdId=3) timeout counter per socket tag.
17500
+ * When a streaming socket has N consecutive timeouts, the socket is force-closed
17501
+ * so the next attempt creates a fresh connection. Resets on success.
17502
+ */
17503
+ consecutiveStreamTimeouts = /* @__PURE__ */ new Map();
17504
+ static MAX_CONSECUTIVE_STREAM_TIMEOUTS = 3;
17498
17505
  /** BaichuanClientOptions to use when creating new sockets */
17499
17506
  clientOptions;
17500
17507
  /**
@@ -17649,14 +17656,20 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
17649
17656
  if (!xml) return;
17650
17657
  const channel = frame.header.channelId;
17651
17658
  const battery = this.parseBatteryInfoXml(xml, channel);
17652
- if (battery.batteryPercent !== void 0 || battery.chargeStatus !== void 0 || battery.adapterStatus !== void 0) {
17653
- this.dispatchSimpleEvent({
17654
- type: "battery",
17655
- channel,
17656
- timestamp: Date.now(),
17657
- battery
17658
- });
17659
+ if (battery.batteryPercent === void 0 && battery.chargeStatus === void 0 && battery.adapterStatus === void 0) {
17660
+ return;
17661
+ }
17662
+ const key = `${battery.batteryPercent ?? ""}|${battery.chargeStatus ?? ""}|${battery.adapterStatus ?? ""}`;
17663
+ if (this.lastBatteryPushKey.get(channel) === key) {
17664
+ return;
17659
17665
  }
17666
+ this.lastBatteryPushKey.set(channel, key);
17667
+ this.dispatchSimpleEvent({
17668
+ type: "battery",
17669
+ channel,
17670
+ timestamp: Date.now(),
17671
+ battery
17672
+ });
17660
17673
  } catch (e) {
17661
17674
  this.logger.debug?.(
17662
17675
  "[ReolinkBaichuanApi] Error parsing battery push",
@@ -17822,6 +17835,14 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
17822
17835
  deviceCapabilitiesCache = /* @__PURE__ */ new Map();
17823
17836
  static CAPABILITIES_CACHE_TTL_MS = 5 * 60 * 1e3;
17824
17837
  // 5 minutes
17838
+ /**
17839
+ * Dedupe key for battery push events (cmd_id 252), per channel.
17840
+ * Cameras emit BatteryInfoList frequently while streaming (every few
17841
+ * seconds). We only forward an event when the meaningful fields change
17842
+ * (percent, chargeStatus, adapterStatus) to avoid flooding SSE/MQTT
17843
+ * consumers and the UI event log.
17844
+ */
17845
+ lastBatteryPushKey = /* @__PURE__ */ new Map();
17825
17846
  // ─────────────────────────────────────────────────────────────────────────────
17826
17847
  // SOCKET POOL CONSTANTS
17827
17848
  // ─────────────────────────────────────────────────────────────────────────────
@@ -23294,6 +23315,7 @@ ${stderr}`)
23294
23315
  `${ch}:${profile}:${variant}`,
23295
23316
  frame.header.msgNum
23296
23317
  );
23318
+ this.resetStreamTimeoutCounter(targetClient);
23297
23319
  return;
23298
23320
  } catch (error) {
23299
23321
  lastError = error;
@@ -23308,6 +23330,10 @@ ${stderr}`)
23308
23330
  }
23309
23331
  }
23310
23332
  }
23333
+ const isTimeout = lastError instanceof Error && lastError.message?.includes("timeout");
23334
+ if (isTimeout) {
23335
+ this.trackStreamTimeout(targetClient);
23336
+ }
23311
23337
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
23312
23338
  }
23313
23339
  /**
@@ -23767,6 +23793,18 @@ ${stderr}`)
23767
23793
  notifyD2cDisc() {
23768
23794
  const now = Date.now();
23769
23795
  this.lastD2cDiscAtMs = now;
23796
+ const streamingTags = Array.from(this.socketPool.keys()).filter(
23797
+ (tag) => tag.startsWith("streaming:")
23798
+ );
23799
+ if (streamingTags.length > 0) {
23800
+ this.logger?.log?.(
23801
+ `[D2C_DISC] Force-closing ${streamingTags.length} streaming socket(s): ${streamingTags.join(", ")}`
23802
+ );
23803
+ for (const tag of streamingTags) {
23804
+ this.forceClosePooledSocket(tag, this.logger).catch(() => {
23805
+ });
23806
+ }
23807
+ }
23770
23808
  const immediateCooldownUntil = now + _ReolinkBaichuanApi.D2C_DISC_IMMEDIATE_COOLDOWN_MS;
23771
23809
  const existing = this.socketPoolCooldowns.get(this.host);
23772
23810
  if (!existing || existing.cooldownUntil < immediateCooldownUntil) {
@@ -23799,6 +23837,43 @@ ${stderr}`)
23799
23837
  }
23800
23838
  }
23801
23839
  }
23840
+ /**
23841
+ * Find the socket pool tag for a given BaichuanClient instance.
23842
+ * Returns undefined if the client is not in the pool (e.g. it's the general socket used directly).
23843
+ */
23844
+ findSocketTagForClient(client) {
23845
+ for (const [tag, entry] of this.socketPool) {
23846
+ if (entry.client === client) return tag;
23847
+ }
23848
+ return void 0;
23849
+ }
23850
+ /**
23851
+ * Reset the consecutive stream-start timeout counter for a streaming socket.
23852
+ * Called on successful stream start.
23853
+ */
23854
+ resetStreamTimeoutCounter(client) {
23855
+ const tag = this.findSocketTagForClient(client);
23856
+ if (tag) this.consecutiveStreamTimeouts.delete(tag);
23857
+ }
23858
+ /**
23859
+ * Track a stream-start timeout on a streaming socket.
23860
+ * After MAX_CONSECUTIVE_STREAM_TIMEOUTS consecutive timeouts, force-close the
23861
+ * socket so the next attempt creates a fresh connection.
23862
+ */
23863
+ trackStreamTimeout(client) {
23864
+ const tag = this.findSocketTagForClient(client);
23865
+ if (!tag || !tag.startsWith("streaming:")) return;
23866
+ const count = (this.consecutiveStreamTimeouts.get(tag) ?? 0) + 1;
23867
+ this.consecutiveStreamTimeouts.set(tag, count);
23868
+ if (count >= _ReolinkBaichuanApi.MAX_CONSECUTIVE_STREAM_TIMEOUTS) {
23869
+ this.logger?.warn?.(
23870
+ `[SocketPool] ${count} consecutive stream timeouts on tag=${tag}, force-closing socket`
23871
+ );
23872
+ this.consecutiveStreamTimeouts.delete(tag);
23873
+ this.forceClosePooledSocket(tag, this.logger).catch(() => {
23874
+ });
23875
+ }
23876
+ }
23802
23877
  /**
23803
23878
  * Best-effort sleeping inference for battery/BCUDP cameras.
23804
23879
  *
@@ -28036,16 +28111,16 @@ function isTcpFailureThatShouldFallbackToUdp(e) {
28036
28111
  return message.includes("ECONNREFUSED") || message.includes("ETIMEDOUT") || message.includes("EHOSTUNREACH") || message.includes("ENETUNREACH") || message.includes("socket hang up") || message.includes("TCP connection timeout") || message.includes("Baichuan socket closed") || message.includes("timeout waiting for nonce") || message.includes("expected encryption info") || message.includes("ECONNRESET") || message.includes("EPIPE");
28037
28112
  }
28038
28113
  async function pingHost(host, timeoutMs = 3e3) {
28114
+ const { exec } = await import("child_process");
28115
+ const platform2 = process.platform;
28116
+ const pingCmd = platform2 === "win32" ? `ping -n 1 -w ${timeoutMs} ${host}` : platform2 === "darwin" ? (
28117
+ // macOS: -W is in milliseconds (Linux: seconds)
28118
+ `ping -c 1 -W ${timeoutMs} ${host}`
28119
+ ) : (
28120
+ // Linux/BSD-ish: -W is in seconds on most distros
28121
+ `ping -c 1 -W ${Math.max(1, Math.floor(timeoutMs / 1e3))} ${host}`
28122
+ );
28039
28123
  return new Promise((resolve) => {
28040
- const { exec } = require("child_process");
28041
- const platform2 = process.platform;
28042
- const pingCmd = platform2 === "win32" ? `ping -n 1 -w ${timeoutMs} ${host}` : platform2 === "darwin" ? (
28043
- // macOS: -W is in milliseconds (Linux: seconds)
28044
- `ping -c 1 -W ${timeoutMs} ${host}`
28045
- ) : (
28046
- // Linux/BSD-ish: -W is in seconds on most distros
28047
- `ping -c 1 -W ${Math.max(1, Math.floor(timeoutMs / 1e3))} ${host}`
28048
- );
28049
28124
  exec(pingCmd, (error) => {
28050
28125
  resolve(!error);
28051
28126
  });