@camstack/addon-pipeline 1.2.294 → 1.2.296

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.
Files changed (46) hide show
  1. package/THIRD_PARTY_MODELS.md +241 -0
  2. package/dist/audio-analyzer/index.js +2 -2
  3. package/dist/audio-analyzer/index.mjs +2 -2
  4. package/dist/{default-detection-model-0dPKRKUD.mjs → default-detection-model-Co578D8C.mjs} +181 -99
  5. package/dist/{default-detection-model-D24AJOTn.js → default-detection-model-D1daTtqT.js} +181 -99
  6. package/dist/detection-pipeline/index.js +1301 -531
  7. package/dist/detection-pipeline/index.mjs +1301 -531
  8. package/dist/{dist-CJR259Xf.js → dist-8up-f2TX.js} +3688 -2698
  9. package/dist/{dist-RXbmRAwP.mjs → dist-CCd0Q3nr.mjs} +3676 -2698
  10. package/dist/motion-wasm/index.js +1 -1
  11. package/dist/motion-wasm/index.mjs +1 -1
  12. package/dist/{node-atmRSHPk.mjs → node-DgMSXSWP.mjs} +1 -1
  13. package/dist/{node-DWg9zbY1.js → node-lpQgHes9.js} +1 -1
  14. package/dist/pipeline-runner/index.js +975 -270
  15. package/dist/pipeline-runner/index.mjs +975 -270
  16. package/dist/{process-memory-BJUXvTjd.js → process-memory-CX_92V_r.js} +1 -1
  17. package/dist/{process-memory-BgFOHFnx.mjs → process-memory-DFC_O5zE.mjs} +1 -1
  18. package/dist/recorder/index.js +14 -6
  19. package/dist/recorder/index.mjs +14 -6
  20. package/dist/{segment-demux-js-C_fPJub3.js → segment-demux-js-DzBx6NN2.js} +1 -1
  21. package/dist/{segment-demux-js-G7wFpHzn.mjs → segment-demux-js-FZbBuk3F.mjs} +1 -1
  22. package/dist/session-decode/{decode-worker-child.js → decode-worker-main.js} +481 -72
  23. package/dist/session-decode/{decode-worker-child.mjs → decode-worker-main.mjs} +482 -71
  24. package/dist/stream-broker/_stub.js +2 -2
  25. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-6IyM-BIn.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-C_i7oFBl.mjs} +2 -2
  26. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DUGQKsKL.mjs +26 -0
  27. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CkbplMHA.mjs +26 -0
  28. package/dist/stream-broker/demux-worker-child.js +1 -1
  29. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  30. package/dist/stream-broker/{hostInit-BBYHWS3M.mjs → hostInit-BPtppL3W.mjs} +2 -2
  31. package/dist/stream-broker/index.js +4 -4
  32. package/dist/stream-broker/index.mjs +4 -4
  33. package/dist/stream-broker/remoteEntry.js +1 -1
  34. package/dist/{worker-protocol-B2MfQLlu.js → worker-protocol-C-G8qmye.js} +3 -1
  35. package/dist/{worker-protocol-C_W-P_g-.mjs → worker-protocol-D_NzPcnh.mjs} +3 -1
  36. package/package.json +3 -2
  37. package/python/inference_pool.py +422 -64
  38. package/python/postprocessors/__init__.py +2 -0
  39. package/python/postprocessors/ssd.py +73 -17
  40. package/python/postprocessors/test_ssd.py +205 -0
  41. package/python/postprocessors/test_yunet.py +292 -0
  42. package/python/postprocessors/testdata/ssdlite_mobiledet_outputs.json +1 -0
  43. package/python/postprocessors/yunet.py +275 -0
  44. package/python/test_inference_pool_compile_off_loop.py +414 -0
  45. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-6IHzlLJ_.mjs +0 -26
  46. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DoyA71_q.mjs +0 -26
@@ -1,6 +1,73 @@
1
- Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_worker_protocol = require("../worker-protocol-B2MfQLlu.js");
1
+ const require_worker_protocol = require("../worker-protocol-C-G8qmye.js");
3
2
  const require_lazy_sharp = require("../lazy-sharp-BuwKlQLR.js");
3
+ var OutboundSendTracker = class {
4
+ inFlight = 0;
5
+ idleWaiters = [];
6
+ /** A send left for the channel; call the returned function from its callback. */
7
+ begin() {
8
+ this.inFlight += 1;
9
+ let done = false;
10
+ return () => {
11
+ if (done) return;
12
+ done = true;
13
+ this.inFlight = Math.max(0, this.inFlight - 1);
14
+ if (this.inFlight > 0) return;
15
+ const waiters = this.idleWaiters;
16
+ this.idleWaiters = [];
17
+ for (const resolve of waiters) resolve();
18
+ };
19
+ }
20
+ get pending() {
21
+ return this.inFlight;
22
+ }
23
+ /** Resolves when nothing is in flight, or after `boundMs` — whichever is first. */
24
+ whenIdle(boundMs) {
25
+ if (this.inFlight === 0) return Promise.resolve("idle");
26
+ return new Promise((resolve) => {
27
+ const timer = setTimeout(() => resolve("bound"), boundMs);
28
+ this.idleWaiters.push(() => {
29
+ clearTimeout(timer);
30
+ resolve("idle");
31
+ });
32
+ });
33
+ }
34
+ };
35
+ function createWorkerExit(deps) {
36
+ let exiting = false;
37
+ return (code) => {
38
+ if (exiting) return;
39
+ exiting = true;
40
+ let exitCode = code;
41
+ try {
42
+ deps.steps.quiesce();
43
+ } catch {
44
+ exitCode = 1;
45
+ } finally {
46
+ try {
47
+ deps.steps.flushFinalLines();
48
+ } catch {
49
+ exitCode = 1;
50
+ } finally {
51
+ deps.sends.whenIdle(deps.boundMs ?? 250).then(() => deps.exit(exitCode));
52
+ }
53
+ }
54
+ };
55
+ }
56
+ /**
57
+ * Route every event that ends the worker through the one bounded exit:
58
+ * SIGTERM (the parent's `kill()`) and a lost channel exit 0, an uncaught
59
+ * error is reported and exits 1. Before D652 there was no SIGTERM handler and
60
+ * a `kill()` ended the process with its last lines still queued.
61
+ */
62
+ function wireWorkerExit(deps) {
63
+ deps.on("SIGTERM", () => deps.exitWorker(0));
64
+ deps.on("disconnect", () => deps.exitWorker(0));
65
+ deps.on("uncaughtException", (err) => {
66
+ deps.reportUncaught(err);
67
+ deps.exitWorker(1);
68
+ });
69
+ }
70
+ //#endregion
4
71
  //#region src/session-decode/color-conversion.ts
5
72
  /**
6
73
  * Explicit YUV→RGB colorspace/range resolution for the session-decode worker.
@@ -302,6 +369,18 @@ var FrameSlot = class {
302
369
  if (!this.reserved || this.reserved.frameId !== frameId) return null;
303
370
  return this.reserved.frame;
304
371
  }
372
+ /**
373
+ * Free only the UNDELIVERED slot frame, keeping the reserved one (D652 drain):
374
+ * after a drain no `pull` will ever take the slot frame, while the reserved
375
+ * frame is one the parent may still read. Returns whether a frame was freed.
376
+ */
377
+ freeUndelivered() {
378
+ const held = this.slot;
379
+ if (!held) return false;
380
+ held.frame.free();
381
+ this.slot = null;
382
+ return true;
383
+ }
305
384
  /** Free both the slot and the reserved frame (teardown). */
306
385
  free() {
307
386
  this.slot?.frame.free();
@@ -615,6 +694,7 @@ var NativeLeaseStore = class {
615
694
  budgetBytes;
616
695
  maxFrames;
617
696
  now;
697
+ onEvict;
618
698
  bytes = 0;
619
699
  /**
620
700
  * Frames dropped because the hold bound or the byte ceiling was reached
@@ -627,6 +707,7 @@ var NativeLeaseStore = class {
627
707
  this.budgetBytes = options.budgetBytes;
628
708
  this.maxFrames = Math.max(1, options.maxFrames);
629
709
  this.now = options.now ?? Date.now;
710
+ this.onEvict = options.onEvict;
630
711
  }
631
712
  /** Frames dropped by a bound rather than released (tests / metrics). */
632
713
  get overflowCount() {
@@ -668,6 +749,7 @@ var NativeLeaseStore = class {
668
749
  if (oldest === frameId) break;
669
750
  this.overflowed += 1;
670
751
  this.drop(oldest);
752
+ this.onEvict?.(oldest);
671
753
  }
672
754
  return true;
673
755
  }
@@ -1351,6 +1433,40 @@ function planPreRollBurst(input) {
1351
1433
  };
1352
1434
  }
1353
1435
  /**
1436
+ * Fit a burst inside the hold its frames live in (D652), without shrinking the
1437
+ * history it covers.
1438
+ *
1439
+ * Every pre-roll frame the runner admits is a lazy ref: its pixels are a frame
1440
+ * the worker HOLDS until inference reads it, and the hold keeps
1441
+ * `holdFrames`. A burst of 12 into a hold of 8, read oldest-first by the lane,
1442
+ * evicts its own oldest frames before inference reaches them — 54 of 55 live
1443
+ * `toBuffer miss … released/hold-full` lines named a frameId ≤ 12. So the burst
1444
+ * sends at most `holdFrames - 1` frames (the ceiling D367 already gives the
1445
+ * runner's in-flight frames, leaving the newest slot to the first live frame).
1446
+ *
1447
+ * The REACH is kept: the window handed to the planner is still
1448
+ * `requestedFrames × cadence`, so the capped frames are spread across the same
1449
+ * span of media the operator asked for (D382 — a consumer is served the history
1450
+ * it asked for), sampled less densely. The alternative, raising the hold for the
1451
+ * burst, would pin up to `requestedFrames - holdFrames + 1` extra native rasters
1452
+ * (~24 MB each at 4K) on every session open.
1453
+ */
1454
+ function planPreRollWithinHold(input) {
1455
+ const requested = Number.isFinite(input.requestedFrames) && input.requestedFrames > 0 ? Math.floor(input.requestedFrames) : 0;
1456
+ const holdCeiling = Math.max(1, Math.floor(input.holdFrames) - 1);
1457
+ const budgetFrames = input.holdEnabled ? Math.min(requested, holdCeiling) : requested;
1458
+ const cadenceMs = Number.isFinite(input.cadenceMs) && input.cadenceMs > 0 ? input.cadenceMs : 1e3;
1459
+ return {
1460
+ budgetFrames,
1461
+ holdCapped: budgetFrames < requested,
1462
+ plan: planPreRollBurst({
1463
+ spanMs: requested * cadenceMs,
1464
+ detectionFps: 1e3 / cadenceMs,
1465
+ maxFrames: budgetFrames
1466
+ })
1467
+ };
1468
+ }
1469
+ /**
1354
1470
  * The live, per-session budget. Stateful by necessity — a stride is a decision
1355
1471
  * about the PREVIOUS admission — and deliberately free of any clock of its own:
1356
1472
  * both times are passed in, so a test can drive a catch-up burst without
@@ -1444,6 +1560,121 @@ var PreRollBurst = class {
1444
1560
  }
1445
1561
  };
1446
1562
  //#endregion
1563
+ //#region src/session-decode/pre-roll-hold-ledger.ts
1564
+ var PreRollSettleTracker = class {
1565
+ pending = /* @__PURE__ */ new Set();
1566
+ read = 0;
1567
+ released = 0;
1568
+ lost = {
1569
+ evicted: 0,
1570
+ cleared: 0,
1571
+ "download-failed": 0,
1572
+ miss: 0
1573
+ };
1574
+ burstEnded = false;
1575
+ emitted = false;
1576
+ /** A pre-roll frame was delivered to the runner; inference has not read it yet. */
1577
+ admit(frameId) {
1578
+ if (this.emitted) return;
1579
+ this.pending.add(frameId);
1580
+ }
1581
+ /**
1582
+ * One frame's fate. Only the FIRST outcome of a delivered pre-roll frame
1583
+ * counts: a miss that follows an eviction is the same loss, not two. Returns
1584
+ * the summary to send when this settle was the last one owed after the burst
1585
+ * ended, else `null`.
1586
+ */
1587
+ settle(frameId, outcome) {
1588
+ if (!this.pending.delete(frameId)) return null;
1589
+ if (outcome === "read") this.read += 1;
1590
+ else if (outcome === "released") this.released += 1;
1591
+ else this.lost[outcome] += 1;
1592
+ return this.summaryIfSettled();
1593
+ }
1594
+ /** Every frame still pending is lost the same way at once (a re-dial clearing the hold). */
1595
+ settleAll(outcome) {
1596
+ let summary = null;
1597
+ for (const frameId of [...this.pending]) summary = this.settle(frameId, outcome) ?? summary;
1598
+ return summary;
1599
+ }
1600
+ /** The burst is over: from now on the summary goes out when nothing is owed. */
1601
+ endBurst() {
1602
+ this.burstEnded = true;
1603
+ return this.summaryIfSettled();
1604
+ }
1605
+ /**
1606
+ * Teardown: send what is known now, `unsettled` included. `null` when there
1607
+ * was no burst or the summary already went out.
1608
+ */
1609
+ flush() {
1610
+ if (this.emitted || !this.burstEnded) return null;
1611
+ return this.emit();
1612
+ }
1613
+ summaryIfSettled() {
1614
+ if (this.emitted || !this.burstEnded || this.pending.size > 0) return null;
1615
+ return this.emit();
1616
+ }
1617
+ emit() {
1618
+ this.emitted = true;
1619
+ return {
1620
+ read: this.read,
1621
+ released: this.released,
1622
+ lostByReason: { ...this.lost },
1623
+ unsettled: this.pending.size
1624
+ };
1625
+ }
1626
+ };
1627
+ //#endregion
1628
+ //#region src/session-decode/native-disposer.ts
1629
+ var NativeDisposer = class {
1630
+ onRefused;
1631
+ refusing = false;
1632
+ refused = 0;
1633
+ constructor(onRefused) {
1634
+ this.onRefused = onRefused;
1635
+ }
1636
+ /** From now on nothing native is disposed (the worker is exiting). Idempotent. */
1637
+ refuseFromNowOn() {
1638
+ this.refusing = true;
1639
+ }
1640
+ get isRefusing() {
1641
+ return this.refusing;
1642
+ }
1643
+ /** Disposes refused since {@link refuseFromNowOn} — diagnostics and tests. */
1644
+ get refusedCount() {
1645
+ return this.refused;
1646
+ }
1647
+ /**
1648
+ * Run `dispose` unless the worker is exiting. `what` names the object for the
1649
+ * refusal line. Returns whether it ran — a caller that dropped its reference
1650
+ * either way loses nothing: an unrun dispose is reclaimed by process exit.
1651
+ */
1652
+ run(what, dispose) {
1653
+ if (this.refusing) {
1654
+ this.refused += 1;
1655
+ this.onRefused?.(what);
1656
+ return false;
1657
+ }
1658
+ dispose();
1659
+ return true;
1660
+ }
1661
+ };
1662
+ //#endregion
1663
+ //#region src/session-decode/decode-loop-policy.ts
1664
+ function inputCloseAfterLoop(state) {
1665
+ if (state.exiting) return "none";
1666
+ if (state.draining) return "demuxer-only";
1667
+ return "all";
1668
+ }
1669
+ /**
1670
+ * The frame decoded after a drain/stop is nobody's: free it — unless the
1671
+ * worker is exiting, where exit reclaims it and no native call is made.
1672
+ */
1673
+ function releaseFrameAfterStop(frame, state) {
1674
+ if (state.exiting || frame === null) return;
1675
+ frame.free();
1676
+ }
1677
+ //#endregion
1447
1678
  //#region src/session-decode/pull-demuxer-open.ts
1448
1679
  /**
1449
1680
  * Open the pull input, skipping `avformat_find_stream_info` when the opened
@@ -2014,10 +2245,11 @@ async function getConstants() {
2014
2245
  }
2015
2246
  }
2016
2247
  /** The HOLD store, sized from one session's knobs. */
2017
- function buildLeaseStore(lease) {
2248
+ function buildLeaseStore(lease, onEvict) {
2018
2249
  return new NativeLeaseStore({
2019
2250
  budgetBytes: lease.values.budgetMb * MB,
2020
- maxFrames: lease.values.holdFrames
2251
+ maxFrames: lease.values.holdFrames,
2252
+ ...onEvict ? { onEvict } : {}
2021
2253
  });
2022
2254
  }
2023
2255
  /** A TILE store (subject or scene) under `budgetMb`, on the shared tile TTL. */
@@ -2137,11 +2369,17 @@ function packSinglePlane(plane, linesize, width, height, channels) {
2137
2369
  else for (let row = 0; row < height; row++) plane.copy(out, row * rowBytes, row * stride, row * stride + rowBytes);
2138
2370
  return out;
2139
2371
  }
2140
- /**
2141
- * The forked child's whole runtime: decode-loop lifecycle, the latest-wins
2142
- * frame slot, and the ROI scaler for `toBuffer`. One instance per process.
2143
- */
2144
2372
  var DecodeWorkerChild = class {
2373
+ /**
2374
+ * Every native dispose / close goes through here, and it refuses once the
2375
+ * exit has begun (D652) — see `native-disposer.ts`.
2376
+ */
2377
+ native;
2378
+ requestExit;
2379
+ constructor(hooks = {}) {
2380
+ this.requestExit = hooks.requestExit ?? (() => void 0);
2381
+ this.native = hooks.disposer ?? new NativeDisposer((what) => this.emitStderr(`decode-worker-child: native dispose left to process exit — ${what}\n`, "debug"));
2382
+ }
2145
2383
  nav = null;
2146
2384
  consts = null;
2147
2385
  hwContext = null;
@@ -2152,7 +2390,7 @@ var DecodeWorkerChild = class {
2152
2390
  * ~10GB/h). Detection uses a stable ROI + resize target, so the scaler is
2153
2391
  * built once and reused, rebuilt only when the key changes — mirroring the
2154
2392
  * shared decoder session (`nodeav-decoder-session.ts`, which reuses one
2155
- * `this.scaler` for the same reason). Disposed in `teardown`.
2393
+ * `this.scaler` for the same reason). Reclaimed by process exit (D652).
2156
2394
  */
2157
2395
  scaler = null;
2158
2396
  scalerKey = "";
@@ -2177,7 +2415,7 @@ var DecodeWorkerChild = class {
2177
2415
  * for — the HW analogue of {@link scaler}/{@link scalerKey}. Rebuilt (old
2178
2416
  * CLOSED) only when the key changes. Its internal `scale_<be>` OUTPUT hw
2179
2417
  * frames pool is the leak-prone VAAPI surface pool, so it is closed on EVERY
2180
- * re-dial ({@link closeInput}) and on teardown — this discipline is what stops
2418
+ * re-dial ({@link closeInput}) and by process exit — this discipline is what stops
2181
2419
  * the prior "keeping decoded frames on the GPU leaked the VAAPI surface pool
2182
2420
  * on every re-dial" incident from recurring.
2183
2421
  */
@@ -2189,7 +2427,7 @@ var DecodeWorkerChild = class {
2189
2427
  * evicts (rebuilds) the hot-path detection filter cache — the crop geometry
2190
2428
  * differs from the stable detection geometry, so sharing one cache would
2191
2429
  * thrash it. Same dispose discipline: both are closed on every re-dial
2192
- * ({@link closeInput}) and on teardown. Rebuilt lazily per crop geometry.
2430
+ * ({@link closeInput}) and by process exit. Rebuilt lazily per crop geometry.
2193
2431
  */
2194
2432
  nativeCropHwFilter = null;
2195
2433
  nativeCropHwFilterKey = "";
@@ -2201,7 +2439,7 @@ var DecodeWorkerChild = class {
2201
2439
  * request that follows. Hard-capped at {@link NATIVE_RING_CAP}; single
2202
2440
  * ownership via the {@link FrameSlot} `retainSuperseded` hook (a frame enters
2203
2441
  * the ring exactly when the slot would otherwise free it). Cleared on every
2204
- * re-dial + teardown (see {@link closeInput}).
2442
+ * re-dial, reclaimed by process exit (see {@link closeInput}).
2205
2443
  */
2206
2444
  nativeRing = new NativeFrameRing(NATIVE_RING_CAP);
2207
2445
  /**
@@ -2210,7 +2448,7 @@ var DecodeWorkerChild = class {
2210
2448
  * eviction. Replaces the tiny GPU {@link nativeRing} as the default (the ring
2211
2449
  * is used only when the lease is disabled via a 0 budget). Because it holds
2212
2450
  * DOWNLOADED (system-memory) frames, retaining a large window has no VAAPI
2213
- * surface-pool starvation risk. Cleared on every re-dial + teardown.
2451
+ * surface-pool starvation risk. Cleared on every re-dial; reclaimed by process exit.
2214
2452
  */
2215
2453
  leaseStore = buildLeaseStore(UNSTARTED_LEASE_KNOBS);
2216
2454
  /**
@@ -2296,7 +2534,7 @@ var DecodeWorkerChild = class {
2296
2534
  * materialize a HW surface into a leasable software frame at NATIVE
2297
2535
  * resolution (no scale). Kept separate from the detection/native-crop filters
2298
2536
  * (different geometry). Same dispose discipline: closed on every re-dial
2299
- * ({@link closeInput}) + teardown, since it references the decoder's per-dial
2537
+ * ({@link closeInput}), since it references the decoder's per-dial
2300
2538
  * `hw_frames_ctx`.
2301
2539
  */
2302
2540
  nativeLeaseDownloadFilter = null;
@@ -2390,6 +2628,19 @@ var DecodeWorkerChild = class {
2390
2628
  started = false;
2391
2629
  stopped = false;
2392
2630
  /**
2631
+ * Set by `drain` (D652): decoding has stopped but every frame already handed
2632
+ * out is kept, so the parent can still read what it admitted. The decode
2633
+ * loop's per-dial `closeInput` is skipped while this is set — that is what
2634
+ * would free the hold — and `stop` ends the process, whose exit reclaims everything.
2635
+ */
2636
+ draining = false;
2637
+ /**
2638
+ * Set by the EXIT path (SIGTERM / `stop` / a lost channel / an uncaught
2639
+ * error): nothing native is disposed from here on — the decode loop may
2640
+ * still be inside libav, and process exit reclaims it all (D652).
2641
+ */
2642
+ exiting = false;
2643
+ /**
2393
2644
  * The numeric device id this worker decodes for — learned from the `start`
2394
2645
  * message's {@link VideoFrameSource}. The child's stderr is INHERITED by the
2395
2646
  * pipeline-runner (silent:false fork), so these lines land in the runner's
@@ -2423,6 +2674,16 @@ var DecodeWorkerChild = class {
2423
2674
  preRollBurst = null;
2424
2675
  /** Whether the burst's report has already been sent — it is sent exactly once. */
2425
2676
  preRollReported = false;
2677
+ /**
2678
+ * What became of the pre-roll frames the burst delivered — read, let go, or
2679
+ * lost after the burst's report went out (D652). Its summary is a second,
2680
+ * later line (`preRollSettled`), never a delay on the report itself.
2681
+ */
2682
+ preRollSettle = new PreRollSettleTracker();
2683
+ /** Every IPC send until its callback fires, so an exit can wait for them (D652). */
2684
+ outbound = new OutboundSendTracker();
2685
+ /** Frames this burst may send to inference, after the hold cap (D652). */
2686
+ preRollBudgetFrames = 0;
2426
2687
  /** Media wall clock of the OLDEST and NEWEST frames the burst admitted. */
2427
2688
  preRollFirstMediaWall = null;
2428
2689
  preRollLastMediaWall = null;
@@ -2435,7 +2696,7 @@ var DecodeWorkerChild = class {
2435
2696
  frames = new FrameSlot({ retainSuperseded: (frameId, frame) => this.captureNativeFrame(frameId, frame) });
2436
2697
  /**
2437
2698
  * Mirrors whether `this.frames` currently has a pull waiting, so
2438
- * `teardown` knows to resolve it with `{kind:'ended'}` — `FrameSlot`
2699
+ * `quiesceForExit` knows to resolve it with `{kind:'ended'}` — `FrameSlot`
2439
2700
  * intentionally exposes no pending-pull query, only `markPendingPull()`,
2440
2701
  * so the child tracks this in lockstep (set in `handlePull`, cleared
2441
2702
  * whenever `consumeDecodedFrame`'s `publish` resolves it).
@@ -2448,6 +2709,10 @@ var DecodeWorkerChild = class {
2448
2709
  this.sendError("decode-worker-child: malformed request");
2449
2710
  return;
2450
2711
  }
2712
+ if (this.exiting) {
2713
+ this.refuseWhileExiting(message);
2714
+ return;
2715
+ }
2451
2716
  switch (message.kind) {
2452
2717
  case "start":
2453
2718
  this.handleStart(message.source, message.opts, message.lease);
@@ -2470,13 +2735,73 @@ var DecodeWorkerChild = class {
2470
2735
  case "markInferred":
2471
2736
  this.admission.mark(message.frameId);
2472
2737
  return;
2738
+ case "drain":
2739
+ this.handleDrain();
2740
+ return;
2473
2741
  case "stop":
2474
2742
  this.handleStop();
2475
2743
  return;
2476
2744
  }
2477
2745
  }
2478
- /** Channel teardown (explicit `stop` or the parent disconnecting) — dispose, then exit. */
2479
- teardown() {
2746
+ /** The answer to a request that arrives after the exit began (D652). */
2747
+ refuseWhileExiting(message) {
2748
+ switch (message.kind) {
2749
+ case "toBuffer":
2750
+ this.send({
2751
+ kind: "error",
2752
+ message: "decode-worker-child: exiting — toBuffer refused",
2753
+ frameId: message.frameId
2754
+ });
2755
+ return;
2756
+ case "nativeCrop":
2757
+ this.send({
2758
+ kind: "nativeCropMiss",
2759
+ requestId: message.requestId,
2760
+ reason: "worker exiting"
2761
+ });
2762
+ return;
2763
+ case "pull":
2764
+ this.send({ kind: "ended" });
2765
+ return;
2766
+ case "stop":
2767
+ this.requestExit(0);
2768
+ return;
2769
+ default: return;
2770
+ }
2771
+ }
2772
+ /**
2773
+ * Stop decoding without giving up a single frame the parent holds (D652).
2774
+ *
2775
+ * The loop ends at its next frame and never re-dials, a dial still opening is
2776
+ * aborted, and a later `pull` answers `ended`. The reserved slot, the hold and
2777
+ * the tile/scene stores are left exactly as they are: those are the frames the
2778
+ * runner already admitted, and the whole point is that they stay readable
2779
+ * until the parent has read them and sends `stop`.
2780
+ */
2781
+ handleDrain() {
2782
+ if (this.stopped) return;
2783
+ this.draining = true;
2784
+ this.stopped = true;
2785
+ this.ended = true;
2786
+ if (this.redialTimer) {
2787
+ clearTimeout(this.redialTimer);
2788
+ this.redialTimer = null;
2789
+ }
2790
+ this.abortController?.abort();
2791
+ this.native.run("undeliveredSlotFrame", () => this.frames.freeUndelivered());
2792
+ this.endPreRollBurst();
2793
+ }
2794
+ /**
2795
+ * Exit step 1 (`createWorkerExit`): stop everything that runs, WITHOUT a
2796
+ * native call. Before D652 this also freed the frames and disposed the
2797
+ * demuxer, decoder, scalers and GPU context — while the decode loop could
2798
+ * still sit in native I/O, and the process now lives on up to
2799
+ * `WORKER_EXIT_FLUSH_BOUND_MS` after it. Process exit reclaims every native
2800
+ * resource, the GPU pool included, so the exit path disposes nothing.
2801
+ */
2802
+ quiesceForExit() {
2803
+ this.native.refuseFromNowOn();
2804
+ this.exiting = true;
2480
2805
  this.stopped = true;
2481
2806
  this.ended = true;
2482
2807
  if (this.redialTimer) {
@@ -2490,21 +2815,19 @@ var DecodeWorkerChild = class {
2490
2815
  if (this.started) this.logMetrics(true);
2491
2816
  this.endPreRollBurst();
2492
2817
  this.abortController?.abort();
2493
- this.frames.free();
2494
- this.closeInput();
2495
- this.scaler?.[Symbol.dispose]?.();
2496
- this.scaler = null;
2497
- this.scalerKey = "";
2498
- this.nativeCropScaler?.[Symbol.dispose]?.();
2499
- this.nativeCropScaler = null;
2500
- this.nativeCropScalerKey = "";
2501
- this.hwContext?.[Symbol.dispose]?.();
2502
- this.hwContext = null;
2503
2818
  if (this.pendingPull) {
2504
2819
  this.pendingPull = false;
2505
2820
  this.send({ kind: "ended" });
2506
2821
  }
2507
2822
  }
2823
+ /** Exit step 2: the last report lines — `unsettled` included. */
2824
+ flushFinalLines() {
2825
+ const settled = this.preRollSettle.flush();
2826
+ if (settled) this.send({
2827
+ kind: "preRollSettled",
2828
+ ...settled
2829
+ });
2830
+ }
2508
2831
  /**
2509
2832
  * Emit the compact throughput line (window `deliveredFps`); cheap,
2510
2833
  * integer-only.
@@ -2546,7 +2869,7 @@ var DecodeWorkerChild = class {
2546
2869
  this.started = true;
2547
2870
  this.deviceId = source.deviceId;
2548
2871
  this.lease = lease;
2549
- this.leaseStore = buildLeaseStore(lease);
2872
+ this.leaseStore = buildLeaseStore(lease, (frameId) => this.settlePreRoll(frameId, "evicted"));
2550
2873
  this.tileStore = buildTileStore(lease.values.tileBudgetMb);
2551
2874
  this.sceneStore = buildTileStore(lease.values.sceneBudgetMb);
2552
2875
  this.leaseGate = new LeaseActivityGate(lease.values.activityMs);
@@ -2582,6 +2905,7 @@ var DecodeWorkerChild = class {
2582
2905
  * supersede declines it (`LeaseAdmissionLedger.unmark`), and count it.
2583
2906
  */
2584
2907
  noteLeaseRelease(frameId, held, by) {
2908
+ this.settlePreRoll(frameId, "released");
2585
2909
  if (held) {
2586
2910
  this.leaseReleased += 1;
2587
2911
  return;
@@ -2599,6 +2923,7 @@ var DecodeWorkerChild = class {
2599
2923
  }
2600
2924
  async handleToBuffer(frameId, opts) {
2601
2925
  const frame = this.leaseStore.get(frameId)?.frame ?? this.frames.toBuffer(frameId);
2926
+ this.settlePreRoll(frameId, frame ? "read" : "miss");
2602
2927
  if (!frame) {
2603
2928
  this.sendError(describeToBufferMiss({
2604
2929
  frameId,
@@ -3038,13 +3363,25 @@ var DecodeWorkerChild = class {
3038
3363
  * `docs/design/decode-path.md` → "Lease admission".
3039
3364
  */
3040
3365
  captureNativeFrame(frameId, frame) {
3041
- if (!this.leaseStore.enabled) return this.nativeRing.retain(frameId, frame);
3042
- if (!this.leaseGate.active) return false;
3043
- if (!this.admission.admit(frameId)) return false;
3366
+ if (!this.leaseStore.enabled) {
3367
+ this.settlePreRoll(frameId, "released");
3368
+ return this.nativeRing.retain(frameId, frame);
3369
+ }
3370
+ if (!this.leaseGate.active) {
3371
+ this.settlePreRoll(frameId, "released");
3372
+ return false;
3373
+ }
3374
+ if (!this.admission.admit(frameId)) {
3375
+ this.settlePreRoll(frameId, "released");
3376
+ return false;
3377
+ }
3044
3378
  if (frame.isHwFrame()) {
3045
3379
  const software = this.downloadToSoftware(frame);
3046
3380
  frame.free();
3047
- if (!software) return true;
3381
+ if (!software) {
3382
+ this.settlePreRoll(frameId, "download-failed");
3383
+ return true;
3384
+ }
3048
3385
  this.leaseStore.put(frameId, PINNED_RGB_CROP ? toLeasedRgbNavFrame(software) : toLeasedNavFrame(software));
3049
3386
  return true;
3050
3387
  }
@@ -3070,7 +3407,7 @@ var DecodeWorkerChild = class {
3070
3407
  return first;
3071
3408
  } catch (err) {
3072
3409
  this.emitStderr(`decode-worker-child: native lease download failed — ${errMessage(err)} [forensics: frame=${frame.width}x${frame.height} fmt=${frame.format} hw=${frame.isHwFrame()} filterUses=${this.leaseDownloadFilterUses} filterAgeMs=${Date.now() - this.leaseDownloadFilterBuiltAt} dialAgeMs=${Date.now() - this.dialStartedAt} decoded=${this.framesDecoded}]\n`);
3073
- this.nativeLeaseDownloadFilter?.close();
3410
+ this.native.run("nativeLeaseDownloadFilter", () => this.nativeLeaseDownloadFilter?.close());
3074
3411
  this.nativeLeaseDownloadFilter = null;
3075
3412
  this.nativeLeaseDownloadFilterKey = "";
3076
3413
  this.leaseDownloadFailuresThisDial++;
@@ -3098,7 +3435,7 @@ var DecodeWorkerChild = class {
3098
3435
  this.leaseDownloadFilterUses++;
3099
3436
  return cached;
3100
3437
  }
3101
- this.nativeLeaseDownloadFilter?.close();
3438
+ this.native.run("nativeLeaseDownloadFilter", () => this.nativeLeaseDownloadFilter?.close());
3102
3439
  this.nativeLeaseDownloadFilter = null;
3103
3440
  this.nativeLeaseDownloadFilterKey = "";
3104
3441
  const description = buildLeaseDownloadDescription(scaleFilter, color);
@@ -3207,14 +3544,14 @@ var DecodeWorkerChild = class {
3207
3544
  const key = nativeCropScalerKey(srcW, srcH, dstW, dstH, srcFormat);
3208
3545
  const cached = this.nativeCropScaler;
3209
3546
  if (cached && key === this.nativeCropScalerKey) return cached;
3210
- this.nativeCropScaler?.[Symbol.dispose]?.();
3547
+ this.native.run("nativeCropScaler", () => this.nativeCropScaler?.[Symbol.dispose]?.());
3211
3548
  this.nativeCropScaler = null;
3212
3549
  this.nativeCropScalerKey = "";
3213
3550
  const scaler = new nav.SoftwareScaleContext();
3214
3551
  scaler.getContext(srcW, srcH, srcFmt, dstW, dstH, C.AV_PIX_FMT_RGB24, C.SWS_FAST_BILINEAR);
3215
3552
  const initRet = scaler.initContext();
3216
3553
  if (initRet < 0) {
3217
- scaler[Symbol.dispose]?.();
3554
+ this.native.run("nativeCropScaler", () => scaler[Symbol.dispose]?.());
3218
3555
  throw new Error(`native crop sws_init_context failed: ${initRet}`);
3219
3556
  }
3220
3557
  this.nativeCropScaler = scaler;
@@ -3229,7 +3566,7 @@ var DecodeWorkerChild = class {
3229
3566
  const key = `${region.left},${region.top},${region.width},${region.height}->${target.width}x${target.height}:${sourceColorKey(color)}`;
3230
3567
  const cached = this.nativeCropHwFilter;
3231
3568
  if (cached && key === this.nativeCropHwFilterKey) return cached;
3232
- this.nativeCropHwFilter?.close();
3569
+ this.native.run("nativeCropHwFilter", () => this.nativeCropHwFilter?.close());
3233
3570
  this.nativeCropHwFilter = null;
3234
3571
  this.nativeCropHwFilterKey = "";
3235
3572
  const description = buildGpuFilterDescription(scaleFilter, region, target, false, "rgb24", color);
@@ -3242,8 +3579,7 @@ var DecodeWorkerChild = class {
3242
3579
  return filter;
3243
3580
  }
3244
3581
  handleStop() {
3245
- this.teardown();
3246
- process.exit(0);
3582
+ this.requestExit(0);
3247
3583
  }
3248
3584
  async runDecodeLoop(url) {
3249
3585
  const nav = await getNodeAv();
@@ -3262,7 +3598,15 @@ var DecodeWorkerChild = class {
3262
3598
  if (this.stopped) break;
3263
3599
  this.emitStderr(`decode-worker-child: dial ended — re-dial scheduled: ${errMessage(err)}\n`);
3264
3600
  } finally {
3265
- this.closeInput();
3601
+ const close = inputCloseAfterLoop({
3602
+ draining: this.draining,
3603
+ exiting: this.exiting
3604
+ });
3605
+ if (close === "all") this.closeInput();
3606
+ else if (close === "demuxer-only") {
3607
+ this.native.run("demuxer", () => this.demuxer?.[Symbol.dispose]?.());
3608
+ this.demuxer = null;
3609
+ }
3266
3610
  }
3267
3611
  if (this.stopped) break;
3268
3612
  const skipBackoff = this.redialAsSoftware;
@@ -3306,7 +3650,7 @@ var DecodeWorkerChild = class {
3306
3650
  });
3307
3651
  const demuxer = opened.demuxer;
3308
3652
  if (this.stopped) {
3309
- demuxer[Symbol.dispose]?.();
3653
+ this.native.run("demuxer", () => demuxer[Symbol.dispose]?.());
3310
3654
  return;
3311
3655
  }
3312
3656
  this.demuxer = demuxer;
@@ -3322,13 +3666,19 @@ var DecodeWorkerChild = class {
3322
3666
  });
3323
3667
  const decoderCreateMs = Date.now() - decoderStartedAt;
3324
3668
  if (this.stopped) {
3325
- decoder[Symbol.dispose]?.();
3669
+ this.native.run("decoder", () => decoder[Symbol.dispose]?.());
3326
3670
  return;
3327
3671
  }
3328
3672
  this.decoder = decoder;
3329
3673
  const decodeStartedAt = Date.now();
3330
3674
  for await (const frame of decoder.frames(demuxer.packets(videoStream.index))) {
3331
- if (this.stopped) break;
3675
+ if (this.stopped) {
3676
+ releaseFrameAfterStop(frame, {
3677
+ draining: this.draining,
3678
+ exiting: this.exiting
3679
+ });
3680
+ break;
3681
+ }
3332
3682
  if (!frame) continue;
3333
3683
  this.reportDialTiming({
3334
3684
  hwContextMs: this.hwContextMs,
@@ -3458,16 +3808,19 @@ var DecodeWorkerChild = class {
3458
3808
  });
3459
3809
  if (this.preRollMs > 0 && this.preRollMaxFrames > 0) {
3460
3810
  const preRollCadenceMs = this.minIntervalMs > 0 ? this.minIntervalMs : 1e3;
3811
+ const withinHold = planPreRollWithinHold({
3812
+ requestedFrames: this.preRollMaxFrames,
3813
+ cadenceMs: preRollCadenceMs,
3814
+ holdFrames: this.lease?.values.holdFrames ?? UNSTARTED_LEASE_KNOBS.values.holdFrames,
3815
+ holdEnabled: this.leaseStore.enabled
3816
+ });
3817
+ this.preRollBudgetFrames = withinHold.budgetFrames;
3461
3818
  this.preRollBurst = new PreRollBurst({
3462
3819
  budget: {
3463
- maxFrames: this.preRollMaxFrames,
3820
+ maxFrames: withinHold.budgetFrames,
3464
3821
  maxWallMs: PRE_ROLL_BURST_MAX_WALL_MS
3465
3822
  },
3466
- plan: planPreRollBurst({
3467
- spanMs: this.preRollMaxFrames * preRollCadenceMs,
3468
- detectionFps: 1e3 / preRollCadenceMs,
3469
- maxFrames: this.preRollMaxFrames
3470
- }),
3823
+ plan: withinHold.plan,
3471
3824
  startedAtWall: now
3472
3825
  });
3473
3826
  }
@@ -3528,12 +3881,27 @@ var DecodeWorkerChild = class {
3528
3881
  droppedByReason: report.droppedByReason,
3529
3882
  strideMs: report.strideMs,
3530
3883
  subsampled: report.subsampled,
3531
- spanMs: first !== null && last !== null ? Math.round(last - first) : 0
3884
+ spanMs: first !== null && last !== null ? Math.round(last - first) : 0,
3885
+ budgetFrames: this.preRollBudgetFrames
3886
+ });
3887
+ const settled = this.preRollSettle.endBurst();
3888
+ if (settled) this.send({
3889
+ kind: "preRollSettled",
3890
+ ...settled
3891
+ });
3892
+ }
3893
+ /** One pre-roll frame's fate; sends the settle summary when it was the last owed. */
3894
+ settlePreRoll(frameId, outcome) {
3895
+ const settled = this.preRollSettle.settle(frameId, outcome);
3896
+ if (settled) this.send({
3897
+ kind: "preRollSettled",
3898
+ ...settled
3532
3899
  });
3533
3900
  }
3534
3901
  /** Send a `frame` reply to the parent and count it toward `deliveredFps`. */
3535
3902
  deliverFrame(reply) {
3536
3903
  this.framesDelivered++;
3904
+ if (reply.preRoll) this.preRollSettle.admit(reply.frameId);
3537
3905
  this.send(this.toFrameReply(reply));
3538
3906
  }
3539
3907
  /**
@@ -3563,7 +3931,7 @@ var DecodeWorkerChild = class {
3563
3931
  this.emitStderr(`decode-worker-child: GPU filter probe failed — using software decode: ${errMessage(err)}\n`);
3564
3932
  return false;
3565
3933
  } finally {
3566
- filter?.close();
3934
+ this.native.run("hwFilterProbe", () => filter?.close());
3567
3935
  }
3568
3936
  }
3569
3937
  /**
@@ -3578,7 +3946,7 @@ var DecodeWorkerChild = class {
3578
3946
  if (this.hwFilterDecision === "software") return;
3579
3947
  this.hwFilterDecision = "software";
3580
3948
  this.redialAsSoftware = true;
3581
- this.hwFilter?.close();
3949
+ this.native.run("hwFilter", () => this.hwFilter?.close());
3582
3950
  this.hwFilter = null;
3583
3951
  this.hwFilterKey = "";
3584
3952
  this.abortController?.abort();
@@ -3652,14 +4020,14 @@ var DecodeWorkerChild = class {
3652
4020
  const key = `${srcKind}:${srcW}x${srcH}->${dstW}x${dstH}:${dstFmt}`;
3653
4021
  const cached = this.scaler;
3654
4022
  if (cached && key === this.scalerKey) return cached;
3655
- this.scaler?.[Symbol.dispose]?.();
4023
+ this.native.run("scaler", () => this.scaler?.[Symbol.dispose]?.());
3656
4024
  this.scaler = null;
3657
4025
  this.scalerKey = "";
3658
4026
  const scaler = new nav.SoftwareScaleContext();
3659
4027
  scaler.getContext(srcW, srcH, navPixelFormatFor(srcKind, C), dstW, dstH, dstFmt, C.SWS_FAST_BILINEAR);
3660
4028
  const initRet = scaler.initContext();
3661
4029
  if (initRet < 0) {
3662
- scaler[Symbol.dispose]?.();
4030
+ this.native.run("scaler", () => scaler[Symbol.dispose]?.());
3663
4031
  throw new Error(`sws_init_context failed: ${initRet}`);
3664
4032
  }
3665
4033
  this.scaler = scaler;
@@ -3717,7 +4085,7 @@ var DecodeWorkerChild = class {
3717
4085
  const key = `${region.left},${region.top},${region.width},${region.height}->${target.width}x${target.height}:${pixel}:${sourceColorKey(color)}`;
3718
4086
  const cached = this.hwFilter;
3719
4087
  if (cached && key === this.hwFilterKey) return cached;
3720
- this.hwFilter?.close();
4088
+ this.native.run("hwFilter", () => this.hwFilter?.close());
3721
4089
  this.hwFilter = null;
3722
4090
  this.hwFilterKey = "";
3723
4091
  const description = buildGpuFilterDescription(scaleFilter, region, target, fullFrame, pixel, color);
@@ -3772,25 +4140,30 @@ var DecodeWorkerChild = class {
3772
4140
  * the next dial's first `toBuffer`.
3773
4141
  */
3774
4142
  closeInput() {
3775
- this.nativeRing.clear();
3776
- this.leaseStore.clear();
4143
+ this.native.run("nativeRing", () => this.nativeRing.clear());
4144
+ this.native.run("leaseStore", () => this.leaseStore.clear());
4145
+ const cleared = this.preRollSettle.settleAll("cleared");
4146
+ if (cleared) this.send({
4147
+ kind: "preRollSettled",
4148
+ ...cleared
4149
+ });
3777
4150
  this.tileStore.clear();
3778
4151
  this.sceneStore.clear();
3779
4152
  this.pendingCuts.clear();
3780
4153
  this.tileFrameWidth = 0;
3781
4154
  this.tileFrameHeight = 0;
3782
- this.nativeLeaseDownloadFilter?.close();
4155
+ this.native.run("nativeLeaseDownloadFilter", () => this.nativeLeaseDownloadFilter?.close());
3783
4156
  this.nativeLeaseDownloadFilter = null;
3784
4157
  this.nativeLeaseDownloadFilterKey = "";
3785
- this.nativeCropHwFilter?.close();
4158
+ this.native.run("nativeCropHwFilter", () => this.nativeCropHwFilter?.close());
3786
4159
  this.nativeCropHwFilter = null;
3787
4160
  this.nativeCropHwFilterKey = "";
3788
- this.hwFilter?.close();
4161
+ this.native.run("hwFilter", () => this.hwFilter?.close());
3789
4162
  this.hwFilter = null;
3790
4163
  this.hwFilterKey = "";
3791
- this.decoder?.[Symbol.dispose]?.();
4164
+ this.native.run("decoder", () => this.decoder?.[Symbol.dispose]?.());
3792
4165
  this.decoder = null;
3793
- this.demuxer?.[Symbol.dispose]?.();
4166
+ this.native.run("demuxer", () => this.demuxer?.[Symbol.dispose]?.());
3794
4167
  this.demuxer = null;
3795
4168
  }
3796
4169
  /**
@@ -3823,7 +4196,13 @@ var DecodeWorkerChild = class {
3823
4196
  }
3824
4197
  }
3825
4198
  send(reply) {
3826
- process.send?.(reply);
4199
+ if (!process.send) return;
4200
+ const done = this.outbound.begin();
4201
+ try {
4202
+ process.send(reply, void 0, void 0, () => done());
4203
+ } catch {
4204
+ done();
4205
+ }
3827
4206
  }
3828
4207
  /**
3829
4208
  * The `User-Agent` this worker's RTSP dial carries — its identity inside the
@@ -3872,7 +4251,20 @@ var DecodeWorkerChild = class {
3872
4251
  });
3873
4252
  }
3874
4253
  };
3875
- var worker = new DecodeWorkerChild();
4254
+ //#endregion
4255
+ //#region src/session-decode/decode-worker-main.ts
4256
+ /**
4257
+ * Decode-worker — the process ENTRY (its own vite build entry, the path
4258
+ * `fork-decode-worker.ts` forks). The worker itself is `DecodeWorkerChild` in
4259
+ * `decode-worker-child.ts`; this file only instantiates it and wires the process:
4260
+ * IPC messages, the native preload, the sharp warn sink, and the ONE bounded
4261
+ * exit every ending goes through (D652).
4262
+ *
4263
+ * ‼ SANCTIONED RAW IPC ‼ — `process.on('message')` / `process.send` are the
4264
+ * correct mechanism for this private, directly-forked worker only; see the
4265
+ * note at the top of `decode-worker-child.ts`.
4266
+ */
4267
+ var worker = new DecodeWorkerChild({ requestExit: (code) => exitWorker(code) });
3876
4268
  require_lazy_sharp.setSharpWarnSink((message) => {
3877
4269
  worker.emitStderr(`decode-worker-child: ${message}\n`, "warn");
3878
4270
  });
@@ -3880,17 +4272,34 @@ process.on("message", (message) => {
3880
4272
  worker.handleMessage(message);
3881
4273
  });
3882
4274
  worker.warm();
3883
- process.on("disconnect", () => {
3884
- worker.teardown();
3885
- process.exit(0);
4275
+ /**
4276
+ * The ONE way this worker exits: quiesce (the disposer starts refusing, so
4277
+ * nothing native is freed under a live read), queue the last lines, wait —
4278
+ * bounded — for them to leave, exit. `stop`, SIGTERM (the parent's `kill()`),
4279
+ * a lost channel and an uncaught error all land here, and a step that throws
4280
+ * still exits (with code 1). Before D652 there was no SIGTERM handler and a
4281
+ * `kill()` ended the process with those lines still queued.
4282
+ */
4283
+ var exitWorker = createWorkerExit({
4284
+ sends: worker.outbound,
4285
+ steps: {
4286
+ quiesce: () => worker.quiesceForExit(),
4287
+ flushFinalLines: () => worker.flushFinalLines()
4288
+ },
4289
+ exit: (code) => process.exit(code)
3886
4290
  });
3887
- process.on("uncaughtException", (err) => {
3888
- worker.emitStderr(`decode-worker-child: uncaught exception — ${errMessage(err)}\n`);
3889
- worker.teardown();
3890
- process.exit(1);
4291
+ wireWorkerExit({
4292
+ on: (event, listener) => {
4293
+ if (event === "SIGTERM") process.on("SIGTERM", () => listener(void 0));
4294
+ else if (event === "disconnect") process.on("disconnect", () => listener(void 0));
4295
+ else process.on("uncaughtException", (err) => listener(err));
4296
+ },
4297
+ exitWorker,
4298
+ reportUncaught: (err) => {
4299
+ worker.emitStderr(`decode-worker-child: uncaught exception — ${errMessage(err)}\n`);
4300
+ }
3891
4301
  });
3892
4302
  process.on("unhandledRejection", (reason) => {
3893
4303
  worker.emitStderr(`decode-worker-child: unhandled rejection — ${errMessage(reason)}\n`);
3894
4304
  });
3895
4305
  //#endregion
3896
- exports.DecodeWorkerChild = DecodeWorkerChild;