@camstack/addon-pipeline 1.2.295 → 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 (40) hide show
  1. package/THIRD_PARTY_MODELS.md +8 -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-CAUBVbgK.mjs → default-detection-model-Co578D8C.mjs} +75 -46
  5. package/dist/{default-detection-model-wNEISVA9.js → default-detection-model-D1daTtqT.js} +75 -46
  6. package/dist/detection-pipeline/index.js +1023 -80
  7. package/dist/detection-pipeline/index.mjs +1023 -80
  8. package/dist/{dist-BsVcf5wO.js → dist-8up-f2TX.js} +877 -365
  9. package/dist/{dist-CuxSNLKW.mjs → dist-CCd0Q3nr.mjs} +872 -366
  10. package/dist/motion-wasm/index.js +1 -1
  11. package/dist/motion-wasm/index.mjs +1 -1
  12. package/dist/{node-Cqb1QiXk.mjs → node-DgMSXSWP.mjs} +1 -1
  13. package/dist/{node-UFk6I2f6.js → node-lpQgHes9.js} +1 -1
  14. package/dist/pipeline-runner/index.js +789 -203
  15. package/dist/pipeline-runner/index.mjs +789 -203
  16. package/dist/{process-memory-CJV29sPf.js → process-memory-CX_92V_r.js} +1 -1
  17. package/dist/{process-memory-D0Nvs9rr.mjs → process-memory-DFC_O5zE.mjs} +1 -1
  18. package/dist/recorder/index.js +4 -6
  19. package/dist/recorder/index.mjs +4 -6
  20. package/dist/{segment-demux-js-DGwmf5hu.js → segment-demux-js-DzBx6NN2.js} +1 -1
  21. package/dist/{segment-demux-js-DIDWw1iE.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-B7nBFqva.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-BKlb3qac.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 +1 -1
  37. package/python/inference_pool.py +422 -64
  38. package/python/test_inference_pool_compile_off_loop.py +414 -0
  39. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CCIyBvRa.mjs +0 -26
  40. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DeD_UFdb.mjs +0 -26
@@ -1,5 +1,73 @@
1
- import { i as formatNativeLeaseKnobs, n as isWorkerRequest, r as logLevelForLine } from "../worker-protocol-C_W-P_g-.mjs";
1
+ import { i as formatNativeLeaseKnobs, n as isWorkerRequest, r as logLevelForLine } from "../worker-protocol-D_NzPcnh.mjs";
2
2
  import { n as setSharpWarnSink, r as hostExternalEntryUrls, t as getSharp } from "../lazy-sharp-DXsqwpph.mjs";
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
3
71
  //#region src/session-decode/color-conversion.ts
4
72
  /**
5
73
  * Explicit YUV→RGB colorspace/range resolution for the session-decode worker.
@@ -301,6 +369,18 @@ var FrameSlot = class {
301
369
  if (!this.reserved || this.reserved.frameId !== frameId) return null;
302
370
  return this.reserved.frame;
303
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
+ }
304
384
  /** Free both the slot and the reserved frame (teardown). */
305
385
  free() {
306
386
  this.slot?.frame.free();
@@ -614,6 +694,7 @@ var NativeLeaseStore = class {
614
694
  budgetBytes;
615
695
  maxFrames;
616
696
  now;
697
+ onEvict;
617
698
  bytes = 0;
618
699
  /**
619
700
  * Frames dropped because the hold bound or the byte ceiling was reached
@@ -626,6 +707,7 @@ var NativeLeaseStore = class {
626
707
  this.budgetBytes = options.budgetBytes;
627
708
  this.maxFrames = Math.max(1, options.maxFrames);
628
709
  this.now = options.now ?? Date.now;
710
+ this.onEvict = options.onEvict;
629
711
  }
630
712
  /** Frames dropped by a bound rather than released (tests / metrics). */
631
713
  get overflowCount() {
@@ -667,6 +749,7 @@ var NativeLeaseStore = class {
667
749
  if (oldest === frameId) break;
668
750
  this.overflowed += 1;
669
751
  this.drop(oldest);
752
+ this.onEvict?.(oldest);
670
753
  }
671
754
  return true;
672
755
  }
@@ -1350,6 +1433,40 @@ function planPreRollBurst(input) {
1350
1433
  };
1351
1434
  }
1352
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
+ /**
1353
1470
  * The live, per-session budget. Stateful by necessity — a stride is a decision
1354
1471
  * about the PREVIOUS admission — and deliberately free of any clock of its own:
1355
1472
  * both times are passed in, so a test can drive a catch-up burst without
@@ -1443,6 +1560,121 @@ var PreRollBurst = class {
1443
1560
  }
1444
1561
  };
1445
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
1446
1678
  //#region src/session-decode/pull-demuxer-open.ts
1447
1679
  /**
1448
1680
  * Open the pull input, skipping `avformat_find_stream_info` when the opened
@@ -2013,10 +2245,11 @@ async function getConstants() {
2013
2245
  }
2014
2246
  }
2015
2247
  /** The HOLD store, sized from one session's knobs. */
2016
- function buildLeaseStore(lease) {
2248
+ function buildLeaseStore(lease, onEvict) {
2017
2249
  return new NativeLeaseStore({
2018
2250
  budgetBytes: lease.values.budgetMb * MB,
2019
- maxFrames: lease.values.holdFrames
2251
+ maxFrames: lease.values.holdFrames,
2252
+ ...onEvict ? { onEvict } : {}
2020
2253
  });
2021
2254
  }
2022
2255
  /** A TILE store (subject or scene) under `budgetMb`, on the shared tile TTL. */
@@ -2136,11 +2369,17 @@ function packSinglePlane(plane, linesize, width, height, channels) {
2136
2369
  else for (let row = 0; row < height; row++) plane.copy(out, row * rowBytes, row * stride, row * stride + rowBytes);
2137
2370
  return out;
2138
2371
  }
2139
- /**
2140
- * The forked child's whole runtime: decode-loop lifecycle, the latest-wins
2141
- * frame slot, and the ROI scaler for `toBuffer`. One instance per process.
2142
- */
2143
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
+ }
2144
2383
  nav = null;
2145
2384
  consts = null;
2146
2385
  hwContext = null;
@@ -2151,7 +2390,7 @@ var DecodeWorkerChild = class {
2151
2390
  * ~10GB/h). Detection uses a stable ROI + resize target, so the scaler is
2152
2391
  * built once and reused, rebuilt only when the key changes — mirroring the
2153
2392
  * shared decoder session (`nodeav-decoder-session.ts`, which reuses one
2154
- * `this.scaler` for the same reason). Disposed in `teardown`.
2393
+ * `this.scaler` for the same reason). Reclaimed by process exit (D652).
2155
2394
  */
2156
2395
  scaler = null;
2157
2396
  scalerKey = "";
@@ -2176,7 +2415,7 @@ var DecodeWorkerChild = class {
2176
2415
  * for — the HW analogue of {@link scaler}/{@link scalerKey}. Rebuilt (old
2177
2416
  * CLOSED) only when the key changes. Its internal `scale_<be>` OUTPUT hw
2178
2417
  * frames pool is the leak-prone VAAPI surface pool, so it is closed on EVERY
2179
- * 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
2180
2419
  * the prior "keeping decoded frames on the GPU leaked the VAAPI surface pool
2181
2420
  * on every re-dial" incident from recurring.
2182
2421
  */
@@ -2188,7 +2427,7 @@ var DecodeWorkerChild = class {
2188
2427
  * evicts (rebuilds) the hot-path detection filter cache — the crop geometry
2189
2428
  * differs from the stable detection geometry, so sharing one cache would
2190
2429
  * thrash it. Same dispose discipline: both are closed on every re-dial
2191
- * ({@link closeInput}) and on teardown. Rebuilt lazily per crop geometry.
2430
+ * ({@link closeInput}) and by process exit. Rebuilt lazily per crop geometry.
2192
2431
  */
2193
2432
  nativeCropHwFilter = null;
2194
2433
  nativeCropHwFilterKey = "";
@@ -2200,7 +2439,7 @@ var DecodeWorkerChild = class {
2200
2439
  * request that follows. Hard-capped at {@link NATIVE_RING_CAP}; single
2201
2440
  * ownership via the {@link FrameSlot} `retainSuperseded` hook (a frame enters
2202
2441
  * the ring exactly when the slot would otherwise free it). Cleared on every
2203
- * re-dial + teardown (see {@link closeInput}).
2442
+ * re-dial, reclaimed by process exit (see {@link closeInput}).
2204
2443
  */
2205
2444
  nativeRing = new NativeFrameRing(NATIVE_RING_CAP);
2206
2445
  /**
@@ -2209,7 +2448,7 @@ var DecodeWorkerChild = class {
2209
2448
  * eviction. Replaces the tiny GPU {@link nativeRing} as the default (the ring
2210
2449
  * is used only when the lease is disabled via a 0 budget). Because it holds
2211
2450
  * DOWNLOADED (system-memory) frames, retaining a large window has no VAAPI
2212
- * surface-pool starvation risk. Cleared on every re-dial + teardown.
2451
+ * surface-pool starvation risk. Cleared on every re-dial; reclaimed by process exit.
2213
2452
  */
2214
2453
  leaseStore = buildLeaseStore(UNSTARTED_LEASE_KNOBS);
2215
2454
  /**
@@ -2295,7 +2534,7 @@ var DecodeWorkerChild = class {
2295
2534
  * materialize a HW surface into a leasable software frame at NATIVE
2296
2535
  * resolution (no scale). Kept separate from the detection/native-crop filters
2297
2536
  * (different geometry). Same dispose discipline: closed on every re-dial
2298
- * ({@link closeInput}) + teardown, since it references the decoder's per-dial
2537
+ * ({@link closeInput}), since it references the decoder's per-dial
2299
2538
  * `hw_frames_ctx`.
2300
2539
  */
2301
2540
  nativeLeaseDownloadFilter = null;
@@ -2389,6 +2628,19 @@ var DecodeWorkerChild = class {
2389
2628
  started = false;
2390
2629
  stopped = false;
2391
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
+ /**
2392
2644
  * The numeric device id this worker decodes for — learned from the `start`
2393
2645
  * message's {@link VideoFrameSource}. The child's stderr is INHERITED by the
2394
2646
  * pipeline-runner (silent:false fork), so these lines land in the runner's
@@ -2422,6 +2674,16 @@ var DecodeWorkerChild = class {
2422
2674
  preRollBurst = null;
2423
2675
  /** Whether the burst's report has already been sent — it is sent exactly once. */
2424
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;
2425
2687
  /** Media wall clock of the OLDEST and NEWEST frames the burst admitted. */
2426
2688
  preRollFirstMediaWall = null;
2427
2689
  preRollLastMediaWall = null;
@@ -2434,7 +2696,7 @@ var DecodeWorkerChild = class {
2434
2696
  frames = new FrameSlot({ retainSuperseded: (frameId, frame) => this.captureNativeFrame(frameId, frame) });
2435
2697
  /**
2436
2698
  * Mirrors whether `this.frames` currently has a pull waiting, so
2437
- * `teardown` knows to resolve it with `{kind:'ended'}` — `FrameSlot`
2699
+ * `quiesceForExit` knows to resolve it with `{kind:'ended'}` — `FrameSlot`
2438
2700
  * intentionally exposes no pending-pull query, only `markPendingPull()`,
2439
2701
  * so the child tracks this in lockstep (set in `handlePull`, cleared
2440
2702
  * whenever `consumeDecodedFrame`'s `publish` resolves it).
@@ -2447,6 +2709,10 @@ var DecodeWorkerChild = class {
2447
2709
  this.sendError("decode-worker-child: malformed request");
2448
2710
  return;
2449
2711
  }
2712
+ if (this.exiting) {
2713
+ this.refuseWhileExiting(message);
2714
+ return;
2715
+ }
2450
2716
  switch (message.kind) {
2451
2717
  case "start":
2452
2718
  this.handleStart(message.source, message.opts, message.lease);
@@ -2469,13 +2735,73 @@ var DecodeWorkerChild = class {
2469
2735
  case "markInferred":
2470
2736
  this.admission.mark(message.frameId);
2471
2737
  return;
2738
+ case "drain":
2739
+ this.handleDrain();
2740
+ return;
2472
2741
  case "stop":
2473
2742
  this.handleStop();
2474
2743
  return;
2475
2744
  }
2476
2745
  }
2477
- /** Channel teardown (explicit `stop` or the parent disconnecting) — dispose, then exit. */
2478
- 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;
2479
2805
  this.stopped = true;
2480
2806
  this.ended = true;
2481
2807
  if (this.redialTimer) {
@@ -2489,21 +2815,19 @@ var DecodeWorkerChild = class {
2489
2815
  if (this.started) this.logMetrics(true);
2490
2816
  this.endPreRollBurst();
2491
2817
  this.abortController?.abort();
2492
- this.frames.free();
2493
- this.closeInput();
2494
- this.scaler?.[Symbol.dispose]?.();
2495
- this.scaler = null;
2496
- this.scalerKey = "";
2497
- this.nativeCropScaler?.[Symbol.dispose]?.();
2498
- this.nativeCropScaler = null;
2499
- this.nativeCropScalerKey = "";
2500
- this.hwContext?.[Symbol.dispose]?.();
2501
- this.hwContext = null;
2502
2818
  if (this.pendingPull) {
2503
2819
  this.pendingPull = false;
2504
2820
  this.send({ kind: "ended" });
2505
2821
  }
2506
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
+ }
2507
2831
  /**
2508
2832
  * Emit the compact throughput line (window `deliveredFps`); cheap,
2509
2833
  * integer-only.
@@ -2545,7 +2869,7 @@ var DecodeWorkerChild = class {
2545
2869
  this.started = true;
2546
2870
  this.deviceId = source.deviceId;
2547
2871
  this.lease = lease;
2548
- this.leaseStore = buildLeaseStore(lease);
2872
+ this.leaseStore = buildLeaseStore(lease, (frameId) => this.settlePreRoll(frameId, "evicted"));
2549
2873
  this.tileStore = buildTileStore(lease.values.tileBudgetMb);
2550
2874
  this.sceneStore = buildTileStore(lease.values.sceneBudgetMb);
2551
2875
  this.leaseGate = new LeaseActivityGate(lease.values.activityMs);
@@ -2581,6 +2905,7 @@ var DecodeWorkerChild = class {
2581
2905
  * supersede declines it (`LeaseAdmissionLedger.unmark`), and count it.
2582
2906
  */
2583
2907
  noteLeaseRelease(frameId, held, by) {
2908
+ this.settlePreRoll(frameId, "released");
2584
2909
  if (held) {
2585
2910
  this.leaseReleased += 1;
2586
2911
  return;
@@ -2598,6 +2923,7 @@ var DecodeWorkerChild = class {
2598
2923
  }
2599
2924
  async handleToBuffer(frameId, opts) {
2600
2925
  const frame = this.leaseStore.get(frameId)?.frame ?? this.frames.toBuffer(frameId);
2926
+ this.settlePreRoll(frameId, frame ? "read" : "miss");
2601
2927
  if (!frame) {
2602
2928
  this.sendError(describeToBufferMiss({
2603
2929
  frameId,
@@ -3037,13 +3363,25 @@ var DecodeWorkerChild = class {
3037
3363
  * `docs/design/decode-path.md` → "Lease admission".
3038
3364
  */
3039
3365
  captureNativeFrame(frameId, frame) {
3040
- if (!this.leaseStore.enabled) return this.nativeRing.retain(frameId, frame);
3041
- if (!this.leaseGate.active) return false;
3042
- 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
+ }
3043
3378
  if (frame.isHwFrame()) {
3044
3379
  const software = this.downloadToSoftware(frame);
3045
3380
  frame.free();
3046
- if (!software) return true;
3381
+ if (!software) {
3382
+ this.settlePreRoll(frameId, "download-failed");
3383
+ return true;
3384
+ }
3047
3385
  this.leaseStore.put(frameId, PINNED_RGB_CROP ? toLeasedRgbNavFrame(software) : toLeasedNavFrame(software));
3048
3386
  return true;
3049
3387
  }
@@ -3069,7 +3407,7 @@ var DecodeWorkerChild = class {
3069
3407
  return first;
3070
3408
  } catch (err) {
3071
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`);
3072
- this.nativeLeaseDownloadFilter?.close();
3410
+ this.native.run("nativeLeaseDownloadFilter", () => this.nativeLeaseDownloadFilter?.close());
3073
3411
  this.nativeLeaseDownloadFilter = null;
3074
3412
  this.nativeLeaseDownloadFilterKey = "";
3075
3413
  this.leaseDownloadFailuresThisDial++;
@@ -3097,7 +3435,7 @@ var DecodeWorkerChild = class {
3097
3435
  this.leaseDownloadFilterUses++;
3098
3436
  return cached;
3099
3437
  }
3100
- this.nativeLeaseDownloadFilter?.close();
3438
+ this.native.run("nativeLeaseDownloadFilter", () => this.nativeLeaseDownloadFilter?.close());
3101
3439
  this.nativeLeaseDownloadFilter = null;
3102
3440
  this.nativeLeaseDownloadFilterKey = "";
3103
3441
  const description = buildLeaseDownloadDescription(scaleFilter, color);
@@ -3206,14 +3544,14 @@ var DecodeWorkerChild = class {
3206
3544
  const key = nativeCropScalerKey(srcW, srcH, dstW, dstH, srcFormat);
3207
3545
  const cached = this.nativeCropScaler;
3208
3546
  if (cached && key === this.nativeCropScalerKey) return cached;
3209
- this.nativeCropScaler?.[Symbol.dispose]?.();
3547
+ this.native.run("nativeCropScaler", () => this.nativeCropScaler?.[Symbol.dispose]?.());
3210
3548
  this.nativeCropScaler = null;
3211
3549
  this.nativeCropScalerKey = "";
3212
3550
  const scaler = new nav.SoftwareScaleContext();
3213
3551
  scaler.getContext(srcW, srcH, srcFmt, dstW, dstH, C.AV_PIX_FMT_RGB24, C.SWS_FAST_BILINEAR);
3214
3552
  const initRet = scaler.initContext();
3215
3553
  if (initRet < 0) {
3216
- scaler[Symbol.dispose]?.();
3554
+ this.native.run("nativeCropScaler", () => scaler[Symbol.dispose]?.());
3217
3555
  throw new Error(`native crop sws_init_context failed: ${initRet}`);
3218
3556
  }
3219
3557
  this.nativeCropScaler = scaler;
@@ -3228,7 +3566,7 @@ var DecodeWorkerChild = class {
3228
3566
  const key = `${region.left},${region.top},${region.width},${region.height}->${target.width}x${target.height}:${sourceColorKey(color)}`;
3229
3567
  const cached = this.nativeCropHwFilter;
3230
3568
  if (cached && key === this.nativeCropHwFilterKey) return cached;
3231
- this.nativeCropHwFilter?.close();
3569
+ this.native.run("nativeCropHwFilter", () => this.nativeCropHwFilter?.close());
3232
3570
  this.nativeCropHwFilter = null;
3233
3571
  this.nativeCropHwFilterKey = "";
3234
3572
  const description = buildGpuFilterDescription(scaleFilter, region, target, false, "rgb24", color);
@@ -3241,8 +3579,7 @@ var DecodeWorkerChild = class {
3241
3579
  return filter;
3242
3580
  }
3243
3581
  handleStop() {
3244
- this.teardown();
3245
- process.exit(0);
3582
+ this.requestExit(0);
3246
3583
  }
3247
3584
  async runDecodeLoop(url) {
3248
3585
  const nav = await getNodeAv();
@@ -3261,7 +3598,15 @@ var DecodeWorkerChild = class {
3261
3598
  if (this.stopped) break;
3262
3599
  this.emitStderr(`decode-worker-child: dial ended — re-dial scheduled: ${errMessage(err)}\n`);
3263
3600
  } finally {
3264
- 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
+ }
3265
3610
  }
3266
3611
  if (this.stopped) break;
3267
3612
  const skipBackoff = this.redialAsSoftware;
@@ -3305,7 +3650,7 @@ var DecodeWorkerChild = class {
3305
3650
  });
3306
3651
  const demuxer = opened.demuxer;
3307
3652
  if (this.stopped) {
3308
- demuxer[Symbol.dispose]?.();
3653
+ this.native.run("demuxer", () => demuxer[Symbol.dispose]?.());
3309
3654
  return;
3310
3655
  }
3311
3656
  this.demuxer = demuxer;
@@ -3321,13 +3666,19 @@ var DecodeWorkerChild = class {
3321
3666
  });
3322
3667
  const decoderCreateMs = Date.now() - decoderStartedAt;
3323
3668
  if (this.stopped) {
3324
- decoder[Symbol.dispose]?.();
3669
+ this.native.run("decoder", () => decoder[Symbol.dispose]?.());
3325
3670
  return;
3326
3671
  }
3327
3672
  this.decoder = decoder;
3328
3673
  const decodeStartedAt = Date.now();
3329
3674
  for await (const frame of decoder.frames(demuxer.packets(videoStream.index))) {
3330
- if (this.stopped) break;
3675
+ if (this.stopped) {
3676
+ releaseFrameAfterStop(frame, {
3677
+ draining: this.draining,
3678
+ exiting: this.exiting
3679
+ });
3680
+ break;
3681
+ }
3331
3682
  if (!frame) continue;
3332
3683
  this.reportDialTiming({
3333
3684
  hwContextMs: this.hwContextMs,
@@ -3457,16 +3808,19 @@ var DecodeWorkerChild = class {
3457
3808
  });
3458
3809
  if (this.preRollMs > 0 && this.preRollMaxFrames > 0) {
3459
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;
3460
3818
  this.preRollBurst = new PreRollBurst({
3461
3819
  budget: {
3462
- maxFrames: this.preRollMaxFrames,
3820
+ maxFrames: withinHold.budgetFrames,
3463
3821
  maxWallMs: PRE_ROLL_BURST_MAX_WALL_MS
3464
3822
  },
3465
- plan: planPreRollBurst({
3466
- spanMs: this.preRollMaxFrames * preRollCadenceMs,
3467
- detectionFps: 1e3 / preRollCadenceMs,
3468
- maxFrames: this.preRollMaxFrames
3469
- }),
3823
+ plan: withinHold.plan,
3470
3824
  startedAtWall: now
3471
3825
  });
3472
3826
  }
@@ -3527,12 +3881,27 @@ var DecodeWorkerChild = class {
3527
3881
  droppedByReason: report.droppedByReason,
3528
3882
  strideMs: report.strideMs,
3529
3883
  subsampled: report.subsampled,
3530
- 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
3531
3899
  });
3532
3900
  }
3533
3901
  /** Send a `frame` reply to the parent and count it toward `deliveredFps`. */
3534
3902
  deliverFrame(reply) {
3535
3903
  this.framesDelivered++;
3904
+ if (reply.preRoll) this.preRollSettle.admit(reply.frameId);
3536
3905
  this.send(this.toFrameReply(reply));
3537
3906
  }
3538
3907
  /**
@@ -3562,7 +3931,7 @@ var DecodeWorkerChild = class {
3562
3931
  this.emitStderr(`decode-worker-child: GPU filter probe failed — using software decode: ${errMessage(err)}\n`);
3563
3932
  return false;
3564
3933
  } finally {
3565
- filter?.close();
3934
+ this.native.run("hwFilterProbe", () => filter?.close());
3566
3935
  }
3567
3936
  }
3568
3937
  /**
@@ -3577,7 +3946,7 @@ var DecodeWorkerChild = class {
3577
3946
  if (this.hwFilterDecision === "software") return;
3578
3947
  this.hwFilterDecision = "software";
3579
3948
  this.redialAsSoftware = true;
3580
- this.hwFilter?.close();
3949
+ this.native.run("hwFilter", () => this.hwFilter?.close());
3581
3950
  this.hwFilter = null;
3582
3951
  this.hwFilterKey = "";
3583
3952
  this.abortController?.abort();
@@ -3651,14 +4020,14 @@ var DecodeWorkerChild = class {
3651
4020
  const key = `${srcKind}:${srcW}x${srcH}->${dstW}x${dstH}:${dstFmt}`;
3652
4021
  const cached = this.scaler;
3653
4022
  if (cached && key === this.scalerKey) return cached;
3654
- this.scaler?.[Symbol.dispose]?.();
4023
+ this.native.run("scaler", () => this.scaler?.[Symbol.dispose]?.());
3655
4024
  this.scaler = null;
3656
4025
  this.scalerKey = "";
3657
4026
  const scaler = new nav.SoftwareScaleContext();
3658
4027
  scaler.getContext(srcW, srcH, navPixelFormatFor(srcKind, C), dstW, dstH, dstFmt, C.SWS_FAST_BILINEAR);
3659
4028
  const initRet = scaler.initContext();
3660
4029
  if (initRet < 0) {
3661
- scaler[Symbol.dispose]?.();
4030
+ this.native.run("scaler", () => scaler[Symbol.dispose]?.());
3662
4031
  throw new Error(`sws_init_context failed: ${initRet}`);
3663
4032
  }
3664
4033
  this.scaler = scaler;
@@ -3716,7 +4085,7 @@ var DecodeWorkerChild = class {
3716
4085
  const key = `${region.left},${region.top},${region.width},${region.height}->${target.width}x${target.height}:${pixel}:${sourceColorKey(color)}`;
3717
4086
  const cached = this.hwFilter;
3718
4087
  if (cached && key === this.hwFilterKey) return cached;
3719
- this.hwFilter?.close();
4088
+ this.native.run("hwFilter", () => this.hwFilter?.close());
3720
4089
  this.hwFilter = null;
3721
4090
  this.hwFilterKey = "";
3722
4091
  const description = buildGpuFilterDescription(scaleFilter, region, target, fullFrame, pixel, color);
@@ -3771,25 +4140,30 @@ var DecodeWorkerChild = class {
3771
4140
  * the next dial's first `toBuffer`.
3772
4141
  */
3773
4142
  closeInput() {
3774
- this.nativeRing.clear();
3775
- 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
+ });
3776
4150
  this.tileStore.clear();
3777
4151
  this.sceneStore.clear();
3778
4152
  this.pendingCuts.clear();
3779
4153
  this.tileFrameWidth = 0;
3780
4154
  this.tileFrameHeight = 0;
3781
- this.nativeLeaseDownloadFilter?.close();
4155
+ this.native.run("nativeLeaseDownloadFilter", () => this.nativeLeaseDownloadFilter?.close());
3782
4156
  this.nativeLeaseDownloadFilter = null;
3783
4157
  this.nativeLeaseDownloadFilterKey = "";
3784
- this.nativeCropHwFilter?.close();
4158
+ this.native.run("nativeCropHwFilter", () => this.nativeCropHwFilter?.close());
3785
4159
  this.nativeCropHwFilter = null;
3786
4160
  this.nativeCropHwFilterKey = "";
3787
- this.hwFilter?.close();
4161
+ this.native.run("hwFilter", () => this.hwFilter?.close());
3788
4162
  this.hwFilter = null;
3789
4163
  this.hwFilterKey = "";
3790
- this.decoder?.[Symbol.dispose]?.();
4164
+ this.native.run("decoder", () => this.decoder?.[Symbol.dispose]?.());
3791
4165
  this.decoder = null;
3792
- this.demuxer?.[Symbol.dispose]?.();
4166
+ this.native.run("demuxer", () => this.demuxer?.[Symbol.dispose]?.());
3793
4167
  this.demuxer = null;
3794
4168
  }
3795
4169
  /**
@@ -3822,7 +4196,13 @@ var DecodeWorkerChild = class {
3822
4196
  }
3823
4197
  }
3824
4198
  send(reply) {
3825
- 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
+ }
3826
4206
  }
3827
4207
  /**
3828
4208
  * The `User-Agent` this worker's RTSP dial carries — its identity inside the
@@ -3871,7 +4251,20 @@ var DecodeWorkerChild = class {
3871
4251
  });
3872
4252
  }
3873
4253
  };
3874
- 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) });
3875
4268
  setSharpWarnSink((message) => {
3876
4269
  worker.emitStderr(`decode-worker-child: ${message}\n`, "warn");
3877
4270
  });
@@ -3879,17 +4272,35 @@ process.on("message", (message) => {
3879
4272
  worker.handleMessage(message);
3880
4273
  });
3881
4274
  worker.warm();
3882
- process.on("disconnect", () => {
3883
- worker.teardown();
3884
- 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)
3885
4290
  });
3886
- process.on("uncaughtException", (err) => {
3887
- worker.emitStderr(`decode-worker-child: uncaught exception — ${errMessage(err)}\n`);
3888
- worker.teardown();
3889
- 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
+ }
3890
4301
  });
3891
4302
  process.on("unhandledRejection", (reason) => {
3892
4303
  worker.emitStderr(`decode-worker-child: unhandled rejection — ${errMessage(reason)}\n`);
3893
4304
  });
3894
4305
  //#endregion
3895
- export { DecodeWorkerChild };
4306
+ export {};