@camstack/addon-provider-reolink 1.2.161 → 1.2.162

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/dist/addon.js CHANGED
@@ -25360,6 +25360,39 @@ _enum([
25360
25360
  _enum(["deferred", "final"]);
25361
25361
  /** The camera's / provider's own words. Kept verbatim, never parsed. */
25362
25362
  var CLIP_STILL_REASON_HEADER = "x-camstack-reason";
25363
+ /**
25364
+ * Make a refusal sentence safe to put in an HTTP header.
25365
+ *
25366
+ * **An HTTP header value is Latin-1.** Node throws `Invalid character in
25367
+ * header content` on anything above U+00FF, the handler dies mid-response,
25368
+ * and the client gets a socket hang up — no status, no reason, nothing. The
25369
+ * mechanism that exists to EXPLAIN a refusal is then the thing that silences
25370
+ * it, and the surface shows "nothing happened".
25371
+ *
25372
+ * That is not hypothetical: measured 2026-09-23 on 1436, a Hikvision replay
25373
+ * refusal read `SETUP of the video track answered 500 — the camera refused
25374
+ * this replay`, and the EM DASH in it threw on every clip the operator
25375
+ * opened. This repository writes its refusals in prose, and its prose uses
25376
+ * `—`, `’` and `…` everywhere, so every reason string is a candidate.
25377
+ *
25378
+ * Substitutes rather than strips: an operator reading a log or a response
25379
+ * should get a readable sentence, not one with holes in it. The typographic
25380
+ * characters this repo actually writes have exact ASCII equivalents; anything
25381
+ * else non-Latin-1 becomes `?`, which is visible rather than silent. Control
25382
+ * characters go too — a newline in a header value is a response-splitting
25383
+ * bug, not a formatting one.
25384
+ */
25385
+ function clipReasonHeaderValue(reason) {
25386
+ const folded = reason.replace(/[\u2013\u2014\u2212]/g, "-").replace(/[\u2018\u2019\u201b]/g, "'").replace(/[\u201c\u201d]/g, "\"").replace(/\u2026/g, "...").replace(/[\u00a0\u2007\u202f]/g, " ");
25387
+ let out = "";
25388
+ for (const ch of folded) {
25389
+ const code = ch.codePointAt(0) ?? 0;
25390
+ if (code < 32 || code === 127) out += " ";
25391
+ else if (code > 255) out += "?";
25392
+ else out += ch;
25393
+ }
25394
+ return out.slice(0, 500).trim();
25395
+ }
25363
25396
  /** One {@link ClipStillRefusalCodeSchema} token, alone. */
25364
25397
  var CLIP_STILL_REASON_CODE_HEADER = "x-camstack-reason-code";
25365
25398
  /** One {@link ClipStillDispositionSchema} token, alone. */
@@ -151601,7 +151634,7 @@ function writeRefusal(res, refusal, deviceId, logger, clipKey) {
151601
151634
  else logger.debug(line, extras);
151602
151635
  const retryAfterMs = refusal.disposition === "deferred" ? refusal.retryAfterMs : void 0;
151603
151636
  res.writeHead(204, {
151604
- [CLIP_STILL_REASON_HEADER]: refusal.reason.slice(0, 500),
151637
+ [CLIP_STILL_REASON_HEADER]: clipReasonHeaderValue(refusal.reason),
151605
151638
  [CLIP_STILL_REASON_CODE_HEADER]: refusal.code,
151606
151639
  [CLIP_STILL_DISPOSITION_HEADER]: refusal.disposition,
151607
151640
  ...retryAfterMs === void 0 ? {} : { "retry-after": String(Math.max(1, Math.ceil(retryAfterMs / 1e3))) }
@@ -151798,7 +151831,7 @@ function createClipStreamHandler(deps) {
151798
151831
  status: resolved.status
151799
151832
  } });
151800
151833
  res.writeHead(resolved.status, {
151801
- [CLIP_STILL_REASON_HEADER]: resolved.detail.slice(0, 500),
151834
+ [CLIP_STILL_REASON_HEADER]: clipReasonHeaderValue(resolved.detail),
151802
151835
  [CLIP_STILL_REASON_CODE_HEADER]: resolved.code
151803
151836
  });
151804
151837
  res.end();
@@ -151871,7 +151904,7 @@ function createClipStreamHandler(deps) {
151871
151904
  }
151872
151905
  });
151873
151906
  res.writeHead(streamRefusalStatus(outcome.code), {
151874
- [CLIP_STILL_REASON_HEADER]: outcome.detail.slice(0, 500),
151907
+ [CLIP_STILL_REASON_HEADER]: clipReasonHeaderValue(outcome.detail),
151875
151908
  [CLIP_STILL_REASON_CODE_HEADER]: outcome.code
151876
151909
  });
151877
151910
  res.end();
@@ -152313,7 +152346,33 @@ function createClipStreamProducer(deps) {
152313
152346
  }
152314
152347
  });
152315
152348
  activeByHost.set(hostKey, ahead + 1);
152316
- const run = (lanes.get(hostKey) ?? Promise.resolve()).then(() => runAttempts(input)).finally(() => {
152349
+ let abandonedAt = null;
152350
+ input.sink.onceClose(() => {
152351
+ abandonedAt ??= Date.now();
152352
+ });
152353
+ const queuedAt = Date.now();
152354
+ const run = (lanes.get(hostKey) ?? Promise.resolve()).then(async () => {
152355
+ if (abandonedAt !== null) {
152356
+ const waitedMs = abandonedAt - queuedAt;
152357
+ deps.logger.info("videoclips: a queued clip stream was dropped, its peer had gone", {
152358
+ tags: { deviceId },
152359
+ meta: {
152360
+ clipKey: input.row.clipKey,
152361
+ twin: input.twin,
152362
+ branch: "queued-peer-gone",
152363
+ waitedMs,
152364
+ aheadWhenQueued: ahead
152365
+ }
152366
+ });
152367
+ return {
152368
+ kind: "refused",
152369
+ code: "fetch-failed",
152370
+ detail: `the peer left after ${String(waitedMs)} ms in the queue, so this clip was never fetched`,
152371
+ attempts: 0
152372
+ };
152373
+ }
152374
+ return await runAttempts(input);
152375
+ }).finally(() => {
152317
152376
  const left = (activeByHost.get(hostKey) ?? 1) - 1;
152318
152377
  if (left <= 0) activeByHost.delete(hostKey);
152319
152378
  else activeByHost.set(hostKey, left);
package/dist/addon.mjs CHANGED
@@ -25355,6 +25355,39 @@ _enum([
25355
25355
  _enum(["deferred", "final"]);
25356
25356
  /** The camera's / provider's own words. Kept verbatim, never parsed. */
25357
25357
  var CLIP_STILL_REASON_HEADER = "x-camstack-reason";
25358
+ /**
25359
+ * Make a refusal sentence safe to put in an HTTP header.
25360
+ *
25361
+ * **An HTTP header value is Latin-1.** Node throws `Invalid character in
25362
+ * header content` on anything above U+00FF, the handler dies mid-response,
25363
+ * and the client gets a socket hang up — no status, no reason, nothing. The
25364
+ * mechanism that exists to EXPLAIN a refusal is then the thing that silences
25365
+ * it, and the surface shows "nothing happened".
25366
+ *
25367
+ * That is not hypothetical: measured 2026-09-23 on 1436, a Hikvision replay
25368
+ * refusal read `SETUP of the video track answered 500 — the camera refused
25369
+ * this replay`, and the EM DASH in it threw on every clip the operator
25370
+ * opened. This repository writes its refusals in prose, and its prose uses
25371
+ * `—`, `’` and `…` everywhere, so every reason string is a candidate.
25372
+ *
25373
+ * Substitutes rather than strips: an operator reading a log or a response
25374
+ * should get a readable sentence, not one with holes in it. The typographic
25375
+ * characters this repo actually writes have exact ASCII equivalents; anything
25376
+ * else non-Latin-1 becomes `?`, which is visible rather than silent. Control
25377
+ * characters go too — a newline in a header value is a response-splitting
25378
+ * bug, not a formatting one.
25379
+ */
25380
+ function clipReasonHeaderValue(reason) {
25381
+ const folded = reason.replace(/[\u2013\u2014\u2212]/g, "-").replace(/[\u2018\u2019\u201b]/g, "'").replace(/[\u201c\u201d]/g, "\"").replace(/\u2026/g, "...").replace(/[\u00a0\u2007\u202f]/g, " ");
25382
+ let out = "";
25383
+ for (const ch of folded) {
25384
+ const code = ch.codePointAt(0) ?? 0;
25385
+ if (code < 32 || code === 127) out += " ";
25386
+ else if (code > 255) out += "?";
25387
+ else out += ch;
25388
+ }
25389
+ return out.slice(0, 500).trim();
25390
+ }
25358
25391
  /** One {@link ClipStillRefusalCodeSchema} token, alone. */
25359
25392
  var CLIP_STILL_REASON_CODE_HEADER = "x-camstack-reason-code";
25360
25393
  /** One {@link ClipStillDispositionSchema} token, alone. */
@@ -151596,7 +151629,7 @@ function writeRefusal(res, refusal, deviceId, logger, clipKey) {
151596
151629
  else logger.debug(line, extras);
151597
151630
  const retryAfterMs = refusal.disposition === "deferred" ? refusal.retryAfterMs : void 0;
151598
151631
  res.writeHead(204, {
151599
- [CLIP_STILL_REASON_HEADER]: refusal.reason.slice(0, 500),
151632
+ [CLIP_STILL_REASON_HEADER]: clipReasonHeaderValue(refusal.reason),
151600
151633
  [CLIP_STILL_REASON_CODE_HEADER]: refusal.code,
151601
151634
  [CLIP_STILL_DISPOSITION_HEADER]: refusal.disposition,
151602
151635
  ...retryAfterMs === void 0 ? {} : { "retry-after": String(Math.max(1, Math.ceil(retryAfterMs / 1e3))) }
@@ -151793,7 +151826,7 @@ function createClipStreamHandler(deps) {
151793
151826
  status: resolved.status
151794
151827
  } });
151795
151828
  res.writeHead(resolved.status, {
151796
- [CLIP_STILL_REASON_HEADER]: resolved.detail.slice(0, 500),
151829
+ [CLIP_STILL_REASON_HEADER]: clipReasonHeaderValue(resolved.detail),
151797
151830
  [CLIP_STILL_REASON_CODE_HEADER]: resolved.code
151798
151831
  });
151799
151832
  res.end();
@@ -151866,7 +151899,7 @@ function createClipStreamHandler(deps) {
151866
151899
  }
151867
151900
  });
151868
151901
  res.writeHead(streamRefusalStatus(outcome.code), {
151869
- [CLIP_STILL_REASON_HEADER]: outcome.detail.slice(0, 500),
151902
+ [CLIP_STILL_REASON_HEADER]: clipReasonHeaderValue(outcome.detail),
151870
151903
  [CLIP_STILL_REASON_CODE_HEADER]: outcome.code
151871
151904
  });
151872
151905
  res.end();
@@ -152308,7 +152341,33 @@ function createClipStreamProducer(deps) {
152308
152341
  }
152309
152342
  });
152310
152343
  activeByHost.set(hostKey, ahead + 1);
152311
- const run = (lanes.get(hostKey) ?? Promise.resolve()).then(() => runAttempts(input)).finally(() => {
152344
+ let abandonedAt = null;
152345
+ input.sink.onceClose(() => {
152346
+ abandonedAt ??= Date.now();
152347
+ });
152348
+ const queuedAt = Date.now();
152349
+ const run = (lanes.get(hostKey) ?? Promise.resolve()).then(async () => {
152350
+ if (abandonedAt !== null) {
152351
+ const waitedMs = abandonedAt - queuedAt;
152352
+ deps.logger.info("videoclips: a queued clip stream was dropped, its peer had gone", {
152353
+ tags: { deviceId },
152354
+ meta: {
152355
+ clipKey: input.row.clipKey,
152356
+ twin: input.twin,
152357
+ branch: "queued-peer-gone",
152358
+ waitedMs,
152359
+ aheadWhenQueued: ahead
152360
+ }
152361
+ });
152362
+ return {
152363
+ kind: "refused",
152364
+ code: "fetch-failed",
152365
+ detail: `the peer left after ${String(waitedMs)} ms in the queue, so this clip was never fetched`,
152366
+ attempts: 0
152367
+ };
152368
+ }
152369
+ return await runAttempts(input);
152370
+ }).finally(() => {
152312
152371
  const left = (activeByHost.get(hostKey) ?? 1) - 1;
152313
152372
  if (left <= 0) activeByHost.delete(hostKey);
152314
152373
  else activeByHost.set(hostKey, left);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-reolink",
3
- "version": "1.2.161",
3
+ "version": "1.2.162",
4
4
  "description": "Reolink camera device provider addon for CamStack — native Baichuan protocol",
5
5
  "keywords": [
6
6
  "camstack",