@apocaliss92/nodelink-js 0.6.0 → 0.6.2

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.
@@ -12,7 +12,7 @@ import {
12
12
  sampleStreams,
13
13
  sanitizeFixtureData,
14
14
  testChannelStreams
15
- } from "./chunk-XDVBNZGR.js";
15
+ } from "./chunk-IQVVVSXO.js";
16
16
  import "./chunk-MZUSWKF3.js";
17
17
  export {
18
18
  captureModelFixtures,
@@ -29,4 +29,4 @@ export {
29
29
  sanitizeFixtureData,
30
30
  testChannelStreams
31
31
  };
32
- //# sourceMappingURL=DiagnosticsTools-K4MF2VXZ.js.map
32
+ //# sourceMappingURL=DiagnosticsTools-QJ3CRYGA.js.map
@@ -30,7 +30,7 @@ import {
30
30
  runAllDiagnosticsConsecutively,
31
31
  runMultifocalDiagnosticsConsecutively,
32
32
  xmlEscape
33
- } from "./chunk-XDVBNZGR.js";
33
+ } from "./chunk-IQVVVSXO.js";
34
34
  import {
35
35
  BC_CLASS_FILE_DOWNLOAD,
36
36
  BC_CLASS_LEGACY,
@@ -232,35 +232,88 @@ function decodeHeader(buf) {
232
232
  return { header, headerLen, messageKey };
233
233
  }
234
234
  var BaichuanFrameParser = class {
235
+ /** Retained-but-unconsumed contiguous bytes from previous push() calls. */
235
236
  buffer = Buffer.alloc(0);
237
+ /** Chunks received since the last materialization, not yet concatenated. */
238
+ pending = [];
239
+ /** Total bytes held in `pending` (kept in sync to avoid re-summing). */
240
+ pendingLen = 0;
241
+ /**
242
+ * Total contiguous bytes (`buffer` + `pending`) required before the next
243
+ * parse attempt can make progress. While buffered bytes stay below this,
244
+ * incoming chunks are merely stashed in `pending` with no copy. This is
245
+ * the mechanism that turns the worst case (a large frame fragmented over
246
+ * many small TCP chunks) from O(n²) into O(n): we concatenate once, when
247
+ * enough bytes have arrived, instead of on every chunk.
248
+ *
249
+ * Starts at 4 — the minimum needed to inspect the magic header.
250
+ */
251
+ needed = 4;
252
+ /**
253
+ * Collapse `this.buffer` + all `pending` chunks into a single contiguous
254
+ * buffer. The retained leftover is copied at most once per materialize(),
255
+ * and materialize() only runs when `needed` bytes are available — so a
256
+ * fragmented frame is assembled with a single concat, not one per chunk.
257
+ */
258
+ materialize() {
259
+ if (this.pendingLen === 0) return;
260
+ if (this.buffer.length === 0 && this.pending.length === 1) {
261
+ this.buffer = this.pending[0];
262
+ } else {
263
+ const parts = this.buffer.length === 0 ? this.pending : [this.buffer, ...this.pending];
264
+ this.buffer = Buffer.concat(parts);
265
+ }
266
+ this.pending = [];
267
+ this.pendingLen = 0;
268
+ }
269
+ /** Total buffered bytes, whether materialized or still pending. */
270
+ get available() {
271
+ return this.buffer.length + this.pendingLen;
272
+ }
236
273
  push(chunk) {
237
274
  if (chunk.length === 0) return [];
238
- const c = chunk;
239
- this.buffer = this.buffer.length === 0 ? c : Buffer.concat([this.buffer, c]);
275
+ this.pending.push(chunk);
276
+ this.pendingLen += chunk.length;
277
+ if (this.available < this.needed) return [];
278
+ this.materialize();
240
279
  const out = [];
241
280
  while (true) {
242
- if (this.buffer.length < 4) break;
281
+ if (this.buffer.length < 4) {
282
+ this.needed = 4;
283
+ break;
284
+ }
243
285
  if (!this.buffer.subarray(0, 4).equals(BC_MAGIC) && !this.buffer.subarray(0, 4).equals(BC_MAGIC_REV)) {
244
286
  const idx = this.buffer.indexOf(BC_MAGIC);
245
287
  const idxRev = this.buffer.indexOf(BC_MAGIC_REV);
246
288
  const next = idx === -1 ? idxRev : idxRev === -1 ? idx : Math.min(idx, idxRev);
247
289
  if (next === -1) {
248
290
  this.buffer = this.buffer.subarray(Math.max(0, this.buffer.length - 3));
291
+ this.needed = 4;
249
292
  break;
250
293
  }
251
294
  this.buffer = this.buffer.subarray(next);
252
- if (this.buffer.length < 20) break;
295
+ if (this.buffer.length < 20) {
296
+ this.needed = 20;
297
+ break;
298
+ }
299
+ }
300
+ if (this.buffer.length < 20) {
301
+ this.needed = 20;
302
+ break;
253
303
  }
254
- if (this.buffer.length < 20) break;
255
304
  let headerInfo;
256
305
  try {
257
306
  headerInfo = decodeHeader(this.buffer);
258
307
  } catch {
308
+ this.needed = 24;
259
309
  break;
260
310
  }
261
311
  const { header, headerLen, messageKey } = headerInfo;
262
312
  const frameLen = headerLen + header.bodyLen;
263
- if (this.buffer.length < frameLen) break;
313
+ if (this.buffer.length < frameLen) {
314
+ this.needed = frameLen;
315
+ break;
316
+ }
264
317
  const raw = this.buffer.subarray(0, frameLen);
265
318
  const body = raw.subarray(headerLen);
266
319
  let extLen = 0;
@@ -272,6 +325,7 @@ var BaichuanFrameParser = class {
272
325
  const payload = body.subarray(extLen);
273
326
  out.push({ header, body, extension, payload, messageKey, raw });
274
327
  this.buffer = this.buffer.subarray(frameLen);
328
+ this.needed = 4;
275
329
  }
276
330
  return out;
277
331
  }
@@ -645,12 +699,28 @@ async function getServerBinding(uid, options = {}) {
645
699
  const fetchImpl = options.fetchImpl ?? globalThis.fetch;
646
700
  const logger = options.logger;
647
701
  if (typeof fetchImpl !== "function") {
648
- logger?.debug?.(
702
+ logger?.log?.(
649
703
  `[server-binding] global fetch unavailable; skipping cloud lookup`
650
704
  );
651
705
  cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
652
706
  return void 0;
653
707
  }
708
+ try {
709
+ const apiHostname = new URL(baseUrl).hostname;
710
+ const dns2 = await import("dns/promises");
711
+ const answers = await dns2.lookup(apiHostname, { family: 4, all: true });
712
+ const sinkholed = answers.find(
713
+ (a) => a.address?.startsWith("127.") || a.address === "0.0.0.0" || a.address?.startsWith("10.") || a.address?.startsWith("192.168.") || /^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(a.address ?? "")
714
+ );
715
+ if (sinkholed) {
716
+ logger?.log?.(
717
+ `[server-binding] ${uid}: DNS for ${apiHostname} resolves to ${sinkholed.address} (sinkhole / /etc/hosts override). Cloud directory unreachable \u2014 falling back to the 22-hostname P2P sweep. Whitelist ${apiHostname} to enable.`
718
+ );
719
+ cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
720
+ return void 0;
721
+ }
722
+ } catch {
723
+ }
654
724
  const url = `${baseUrl}/devices/${encodeURIComponent(uid)}/server-binding?language=${encodeURIComponent(language)}`;
655
725
  const controller = new AbortController();
656
726
  const timer = setTimeout(() => controller.abort(), timeoutMs);
@@ -661,8 +731,8 @@ async function getServerBinding(uid, options = {}) {
661
731
  headers: { Accept: "application/json" }
662
732
  });
663
733
  if (!res.ok) {
664
- logger?.debug?.(
665
- `[server-binding] ${uid}: HTTP ${res.status} ${res.statusText}`
734
+ logger?.log?.(
735
+ `[server-binding] ${uid}: HTTP ${res.status} ${res.statusText} from ${url}`
666
736
  );
667
737
  cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
668
738
  return void 0;
@@ -670,8 +740,15 @@ async function getServerBinding(uid, options = {}) {
670
740
  const json = await res.json();
671
741
  const parsed = parseServerBindingResponse(json);
672
742
  if (!parsed) {
673
- logger?.debug?.(
674
- `[server-binding] ${uid}: response shape did not match expectations`
743
+ logger?.log?.(
744
+ `[server-binding] ${uid}: response shape did not match expectations (Reolink schema change?)`
745
+ );
746
+ cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
747
+ return void 0;
748
+ }
749
+ if (parsed.availableZones.length === 0) {
750
+ logger?.log?.(
751
+ `[server-binding] ${uid}: cloud returned 0 zones \u2014 UID not registered with Reolink cloud (or wrong region)`
675
752
  );
676
753
  cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
677
754
  return void 0;
@@ -690,9 +767,23 @@ async function getServerBinding(uid, options = {}) {
690
767
  );
691
768
  return parsed;
692
769
  } catch (e) {
693
- logger?.debug?.(
694
- `[server-binding] ${uid}: ${e?.message ?? String(e)}`
695
- );
770
+ const msg = e?.message ?? String(e);
771
+ const errName = e?.name;
772
+ if (errName === "AbortError" || msg.includes("aborted")) {
773
+ logger?.log?.(
774
+ `[server-binding] ${uid}: timed out after ${timeoutMs}ms (cloud unreachable)`
775
+ );
776
+ } else if (msg.includes("ENOTFOUND") || msg.includes("EAI_AGAIN")) {
777
+ logger?.log?.(
778
+ `[server-binding] ${uid}: DNS failed (${msg}) \u2014 apis.reolink.com may be blocked at resolver`
779
+ );
780
+ } else if (msg.includes("ECONNREFUSED") || msg.includes("EHOSTUNREACH") || msg.includes("ENETUNREACH")) {
781
+ logger?.log?.(
782
+ `[server-binding] ${uid}: network unreachable (${msg}) \u2014 cloud port blocked`
783
+ );
784
+ } else {
785
+ logger?.log?.(`[server-binding] ${uid}: fetch failed \u2014 ${msg}`);
786
+ }
696
787
  cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
697
788
  return void 0;
698
789
  } finally {
@@ -1128,12 +1219,19 @@ var BcUdpStream = class extends EventEmitter {
1128
1219
  const tid = (Math.floor(Math.random() * 2147483647) | 0) >>> 0;
1129
1220
  const xml = buildC2mQ({ uid });
1130
1221
  const pkt = encodeDiscoveryPacket(tid, xml);
1222
+ const counters = { sentBytes: 0, rxBytes: 0 };
1131
1223
  return await new Promise((resolve, reject) => {
1132
1224
  const deadline = setTimeout(() => {
1133
1225
  cleanup();
1134
- reject(new Error(`P2P UID lookup timeout (${dest.host}:${dest.port})`));
1226
+ const err = new Error(
1227
+ `P2P UID lookup timeout (${dest.host}:${dest.port}) \u2014 sent=${counters.sentBytes}B rx=${counters.rxBytes}B`
1228
+ );
1229
+ err.sentBytes = counters.sentBytes;
1230
+ err.rxBytes = counters.rxBytes;
1231
+ reject(err);
1135
1232
  }, timeoutMs);
1136
1233
  const onMsg = (msg) => {
1234
+ counters.rxBytes += msg.length;
1137
1235
  try {
1138
1236
  const p = decodeBcUdpPacket(msg);
1139
1237
  if (p.kind !== "discovery") return;
@@ -1141,13 +1239,19 @@ var BcUdpStream = class extends EventEmitter {
1141
1239
  const qr = parseM2cQr(p.xml);
1142
1240
  if (!qr?.reg || !qr?.relay) return;
1143
1241
  cleanup();
1144
- resolve({ reg: qr.reg, relay: qr.relay });
1242
+ resolve({
1243
+ reg: qr.reg,
1244
+ relay: qr.relay,
1245
+ sentBytes: counters.sentBytes,
1246
+ rxBytes: counters.rxBytes
1247
+ });
1145
1248
  } catch {
1146
1249
  }
1147
1250
  };
1148
1251
  const send = () => {
1149
1252
  try {
1150
1253
  sock.send(pkt, dest.port, dest.host);
1254
+ counters.sentBytes += pkt.length;
1151
1255
  } catch {
1152
1256
  }
1153
1257
  };
@@ -20467,7 +20571,7 @@ ${xml}`
20467
20571
  * @returns Test results for all stream types and profiles
20468
20572
  */
20469
20573
  async testChannelStreams(channel, logger) {
20470
- const { testChannelStreams } = await import("./DiagnosticsTools-K4MF2VXZ.js");
20574
+ const { testChannelStreams } = await import("./DiagnosticsTools-QJ3CRYGA.js");
20471
20575
  return await testChannelStreams({
20472
20576
  api: this,
20473
20577
  channel: this.normalizeChannel(channel),
@@ -20483,7 +20587,7 @@ ${xml}`
20483
20587
  * @returns Complete diagnostics for all channels and streams
20484
20588
  */
20485
20589
  async collectMultifocalDiagnostics(logger) {
20486
- const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-K4MF2VXZ.js");
20590
+ const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-QJ3CRYGA.js");
20487
20591
  return await collectMultifocalDiagnostics({
20488
20592
  api: this,
20489
20593
  logger
@@ -25268,4 +25372,4 @@ export {
25268
25372
  tcpReachabilityProbe,
25269
25373
  autoDetectDeviceType
25270
25374
  };
25271
- //# sourceMappingURL=chunk-7HSTETZR.js.map
25375
+ //# sourceMappingURL=chunk-D4TKRGUP.js.map