@camstack/addon-pipeline 1.2.27 → 1.2.28

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.
@@ -1336,7 +1336,24 @@ function forkDecodeWorker(_source) {
1336
1336
  //#endregion
1337
1337
  //#region src/session-decode/guarded-session-decode.ts
1338
1338
  /**
1339
- * Guards the in-flight restream-acquire against a detach/unsubscribe race.
1339
+ * 2s 5s 15s 30s, then 30s forever.
1340
+ *
1341
+ * Indefinite is deliberate. The failure this recovers from is "the broker has
1342
+ * no profile bound yet" or "the broker call failed right now" — both of which
1343
+ * clear on their own, often minutes later (a source re-dialing, a runner
1344
+ * restarting). A bounded retry would turn a slow recovery back into the
1345
+ * permanent outage this exists to prevent, and teardown already stops it: a
1346
+ * detach aborts the loop, so nothing retries for a camera nobody wants.
1347
+ */
1348
+ var DEFAULT_ACQUIRE_RETRY_DELAYS_MS = [
1349
+ 2e3,
1350
+ 5e3,
1351
+ 15e3,
1352
+ 3e4
1353
+ ];
1354
+ /**
1355
+ * Guards the in-flight restream-acquire against a detach/unsubscribe race,
1356
+ * and retries an acquire that fails.
1340
1357
  *
1341
1358
  * `acquireSessionDecodeRestream` + forking the decode worker both take real
1342
1359
  * time (a broker round-trip, then a process fork). If the caller's teardown
@@ -1348,29 +1365,71 @@ function forkDecodeWorker(_source) {
1348
1365
  *
1349
1366
  * This helper returns the teardown SYNCHRONOUSLY. The acquire + pump-start
1350
1367
  * run in the background; an `aborted` flag recorded by the (possibly
1351
- * already-called) teardown is checked right after the acquire resolves —
1352
- * if set, the restream is released immediately and the pump is NEVER
1368
+ * already-called) teardown is checked right after each acquire resolves —
1369
+ * if set, any restream is released immediately and the pump is NEVER
1353
1370
  * started (no orphan worker). Otherwise the pump starts and the teardown,
1354
1371
  * once called, stops the pump then releases the restream.
1372
+ *
1373
+ * **A failed acquire is retried, not swallowed.** The teardown handed back
1374
+ * here is stored by the caller (as `attachment.detectionUnsubscribe`) the
1375
+ * instant this returns, so a `return` on failure left that handle permanently
1376
+ * non-null with no pump behind it: `onEnded` could never fire because nothing
1377
+ * had ever started, the camera's phase stayed `active`, and not one frame
1378
+ * arrived again. The motion path was hardened against this shape by a frame
1379
+ * freshness window (`MOTION_FRESHNESS_WINDOW_MS`); this is the detection
1380
+ * path's half.
1355
1381
  */
1356
1382
  function startGuardedSessionDecode(deps) {
1357
1383
  let aborted = false;
1358
1384
  let stopPump = null;
1359
1385
  let release = null;
1386
+ let retryTimer = null;
1387
+ const delays = deps.acquireRetryDelaysMs && deps.acquireRetryDelaysMs.length > 0 ? deps.acquireRetryDelaysMs : DEFAULT_ACQUIRE_RETRY_DELAYS_MS;
1388
+ /** Resolves after the backoff for `attempt`, or immediately once aborted. */
1389
+ const waitBackoff = (attempt) => new Promise((resolve) => {
1390
+ const ms = delays[Math.min(attempt, delays.length - 1)] ?? delays[delays.length - 1] ?? 2e3;
1391
+ retryTimer = setTimeout(() => {
1392
+ retryTimer = null;
1393
+ resolve();
1394
+ }, ms);
1395
+ retryTimer.unref?.();
1396
+ });
1360
1397
  (async () => {
1361
- const acquired = await deps.acquire();
1362
- if (!acquired) return;
1363
- if (aborted) {
1364
- acquired.release();
1398
+ for (let attempt = 0; !aborted; attempt++) {
1399
+ let acquired = null;
1400
+ try {
1401
+ acquired = await deps.acquire();
1402
+ } catch (err) {
1403
+ (attempt === 0 ? deps.logger.warn : deps.logger.debug).call(deps.logger, "session-decode: acquire failed — retrying", { meta: {
1404
+ attempt: attempt + 1,
1405
+ error: require_dist.errMsg(err)
1406
+ } });
1407
+ await waitBackoff(attempt);
1408
+ continue;
1409
+ }
1410
+ if (aborted) {
1411
+ if (acquired) acquired.release();
1412
+ return;
1413
+ }
1414
+ if (!acquired) {
1415
+ (attempt === 0 ? deps.logger.warn : deps.logger.debug).call(deps.logger, "session-decode: no broker profile bound yet — retrying", { meta: { attempt: attempt + 1 } });
1416
+ await waitBackoff(attempt);
1417
+ continue;
1418
+ }
1419
+ if (attempt > 0) deps.logger.info("session-decode: acquire succeeded after retry", { meta: { attempts: attempt + 1 } });
1420
+ release = acquired.release;
1421
+ stopPump = deps.startPump(acquired.source);
1365
1422
  return;
1366
1423
  }
1367
- release = acquired.release;
1368
- stopPump = deps.startPump(acquired.source);
1369
1424
  })().catch((err) => {
1370
1425
  deps.logger.warn("session-decode: guarded start failed", { meta: { error: require_dist.errMsg(err) } });
1371
1426
  });
1372
1427
  return () => {
1373
1428
  aborted = true;
1429
+ if (retryTimer !== null) {
1430
+ clearTimeout(retryTimer);
1431
+ retryTimer = null;
1432
+ }
1374
1433
  stopPump?.();
1375
1434
  if (release) release();
1376
1435
  };
@@ -1330,7 +1330,24 @@ function forkDecodeWorker(_source) {
1330
1330
  //#endregion
1331
1331
  //#region src/session-decode/guarded-session-decode.ts
1332
1332
  /**
1333
- * Guards the in-flight restream-acquire against a detach/unsubscribe race.
1333
+ * 2s 5s 15s 30s, then 30s forever.
1334
+ *
1335
+ * Indefinite is deliberate. The failure this recovers from is "the broker has
1336
+ * no profile bound yet" or "the broker call failed right now" — both of which
1337
+ * clear on their own, often minutes later (a source re-dialing, a runner
1338
+ * restarting). A bounded retry would turn a slow recovery back into the
1339
+ * permanent outage this exists to prevent, and teardown already stops it: a
1340
+ * detach aborts the loop, so nothing retries for a camera nobody wants.
1341
+ */
1342
+ var DEFAULT_ACQUIRE_RETRY_DELAYS_MS = [
1343
+ 2e3,
1344
+ 5e3,
1345
+ 15e3,
1346
+ 3e4
1347
+ ];
1348
+ /**
1349
+ * Guards the in-flight restream-acquire against a detach/unsubscribe race,
1350
+ * and retries an acquire that fails.
1334
1351
  *
1335
1352
  * `acquireSessionDecodeRestream` + forking the decode worker both take real
1336
1353
  * time (a broker round-trip, then a process fork). If the caller's teardown
@@ -1342,29 +1359,71 @@ function forkDecodeWorker(_source) {
1342
1359
  *
1343
1360
  * This helper returns the teardown SYNCHRONOUSLY. The acquire + pump-start
1344
1361
  * run in the background; an `aborted` flag recorded by the (possibly
1345
- * already-called) teardown is checked right after the acquire resolves —
1346
- * if set, the restream is released immediately and the pump is NEVER
1362
+ * already-called) teardown is checked right after each acquire resolves —
1363
+ * if set, any restream is released immediately and the pump is NEVER
1347
1364
  * started (no orphan worker). Otherwise the pump starts and the teardown,
1348
1365
  * once called, stops the pump then releases the restream.
1366
+ *
1367
+ * **A failed acquire is retried, not swallowed.** The teardown handed back
1368
+ * here is stored by the caller (as `attachment.detectionUnsubscribe`) the
1369
+ * instant this returns, so a `return` on failure left that handle permanently
1370
+ * non-null with no pump behind it: `onEnded` could never fire because nothing
1371
+ * had ever started, the camera's phase stayed `active`, and not one frame
1372
+ * arrived again. The motion path was hardened against this shape by a frame
1373
+ * freshness window (`MOTION_FRESHNESS_WINDOW_MS`); this is the detection
1374
+ * path's half.
1349
1375
  */
1350
1376
  function startGuardedSessionDecode(deps) {
1351
1377
  let aborted = false;
1352
1378
  let stopPump = null;
1353
1379
  let release = null;
1380
+ let retryTimer = null;
1381
+ const delays = deps.acquireRetryDelaysMs && deps.acquireRetryDelaysMs.length > 0 ? deps.acquireRetryDelaysMs : DEFAULT_ACQUIRE_RETRY_DELAYS_MS;
1382
+ /** Resolves after the backoff for `attempt`, or immediately once aborted. */
1383
+ const waitBackoff = (attempt) => new Promise((resolve) => {
1384
+ const ms = delays[Math.min(attempt, delays.length - 1)] ?? delays[delays.length - 1] ?? 2e3;
1385
+ retryTimer = setTimeout(() => {
1386
+ retryTimer = null;
1387
+ resolve();
1388
+ }, ms);
1389
+ retryTimer.unref?.();
1390
+ });
1354
1391
  (async () => {
1355
- const acquired = await deps.acquire();
1356
- if (!acquired) return;
1357
- if (aborted) {
1358
- acquired.release();
1392
+ for (let attempt = 0; !aborted; attempt++) {
1393
+ let acquired = null;
1394
+ try {
1395
+ acquired = await deps.acquire();
1396
+ } catch (err) {
1397
+ (attempt === 0 ? deps.logger.warn : deps.logger.debug).call(deps.logger, "session-decode: acquire failed — retrying", { meta: {
1398
+ attempt: attempt + 1,
1399
+ error: errMsg(err)
1400
+ } });
1401
+ await waitBackoff(attempt);
1402
+ continue;
1403
+ }
1404
+ if (aborted) {
1405
+ if (acquired) acquired.release();
1406
+ return;
1407
+ }
1408
+ if (!acquired) {
1409
+ (attempt === 0 ? deps.logger.warn : deps.logger.debug).call(deps.logger, "session-decode: no broker profile bound yet — retrying", { meta: { attempt: attempt + 1 } });
1410
+ await waitBackoff(attempt);
1411
+ continue;
1412
+ }
1413
+ if (attempt > 0) deps.logger.info("session-decode: acquire succeeded after retry", { meta: { attempts: attempt + 1 } });
1414
+ release = acquired.release;
1415
+ stopPump = deps.startPump(acquired.source);
1359
1416
  return;
1360
1417
  }
1361
- release = acquired.release;
1362
- stopPump = deps.startPump(acquired.source);
1363
1418
  })().catch((err) => {
1364
1419
  deps.logger.warn("session-decode: guarded start failed", { meta: { error: errMsg(err) } });
1365
1420
  });
1366
1421
  return () => {
1367
1422
  aborted = true;
1423
+ if (retryTimer !== null) {
1424
+ clearTimeout(retryTimer);
1425
+ retryTimer = null;
1426
+ }
1368
1427
  stopPump?.();
1369
1428
  if (release) release();
1370
1429
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-pipeline",
3
- "version": "1.2.27",
3
+ "version": "1.2.28",
4
4
  "description": "CamStack Pipeline bundle — runner, detection, motion, audio + stream broker. Multi-entry npm package shipping pipeline addons under a single bundle.",
5
5
  "keywords": [
6
6
  "camstack",