prismcast 1.12.1 → 1.13.0

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 (568) hide show
  1. package/dist/app.d.ts +0 -1
  2. package/dist/app.js +8 -7
  3. package/dist/app.js.map +1 -1
  4. package/dist/browser/blockedPage.d.ts +0 -1
  5. package/dist/browser/browserSupervisor.d.ts +4 -4
  6. package/dist/browser/browserSupervisor.js.map +1 -1
  7. package/dist/browser/cdp.d.ts +2 -3
  8. package/dist/browser/cdp.js +5 -4
  9. package/dist/browser/cdp.js.map +1 -1
  10. package/dist/browser/channelSelection.d.ts +16 -4
  11. package/dist/browser/channelSelection.js +42 -31
  12. package/dist/browser/channelSelection.js.map +1 -1
  13. package/dist/browser/consent.d.ts +17 -4
  14. package/dist/browser/consent.js +25 -40
  15. package/dist/browser/consent.js.map +1 -1
  16. package/dist/browser/display.d.ts +0 -1
  17. package/dist/browser/hlsPlaylistObserver.d.ts +3 -2
  18. package/dist/browser/hlsPlaylistObserver.js +9 -4
  19. package/dist/browser/hlsPlaylistObserver.js.map +1 -1
  20. package/dist/browser/index.d.ts +76 -26
  21. package/dist/browser/index.js +201 -101
  22. package/dist/browser/index.js.map +1 -1
  23. package/dist/browser/launchGovernor.d.ts +0 -1
  24. package/dist/browser/login.d.ts +8 -7
  25. package/dist/browser/login.js +25 -16
  26. package/dist/browser/login.js.map +1 -1
  27. package/dist/browser/manifestInterceptor.d.ts +14 -5
  28. package/dist/browser/manifestInterceptor.js +25 -12
  29. package/dist/browser/manifestInterceptor.js.map +1 -1
  30. package/dist/browser/navigation.d.ts +28 -0
  31. package/dist/browser/navigation.js +57 -0
  32. package/dist/browser/navigation.js.map +1 -0
  33. package/dist/browser/pageStaleness.d.ts +0 -1
  34. package/dist/browser/precaching.d.ts +4 -2
  35. package/dist/browser/precaching.js +39 -32
  36. package/dist/browser/precaching.js.map +1 -1
  37. package/dist/browser/tabCapture.d.ts +7 -10
  38. package/dist/browser/tabCapture.js +35 -41
  39. package/dist/browser/tabCapture.js.map +1 -1
  40. package/dist/browser/tabNetworkObserver.d.ts +0 -1
  41. package/dist/browser/tabSelection.d.ts +5 -5
  42. package/dist/browser/tabSelection.js +11 -10
  43. package/dist/browser/tabSelection.js.map +1 -1
  44. package/dist/browser/tuning/cache.d.ts +0 -1
  45. package/dist/browser/tuning/comcastPolymer.d.ts +0 -1
  46. package/dist/browser/tuning/comcastPolymer.js +16 -17
  47. package/dist/browser/tuning/comcastPolymer.js.map +1 -1
  48. package/dist/browser/tuning/cox.d.ts +0 -1
  49. package/dist/browser/tuning/directv.d.ts +0 -1
  50. package/dist/browser/tuning/directv.js +11 -12
  51. package/dist/browser/tuning/directv.js.map +1 -1
  52. package/dist/browser/tuning/fox.d.ts +0 -1
  53. package/dist/browser/tuning/fox.js +2 -2
  54. package/dist/browser/tuning/fox.js.map +1 -1
  55. package/dist/browser/tuning/gridSearch.d.ts +0 -1
  56. package/dist/browser/tuning/hbo.d.ts +0 -1
  57. package/dist/browser/tuning/hulu.d.ts +14 -2
  58. package/dist/browser/tuning/hulu.js +144 -21
  59. package/dist/browser/tuning/hulu.js.map +1 -1
  60. package/dist/browser/tuning/shared.d.ts +0 -1
  61. package/dist/browser/tuning/sling.d.ts +0 -1
  62. package/dist/browser/tuning/sling.js +17 -22
  63. package/dist/browser/tuning/sling.js.map +1 -1
  64. package/dist/browser/tuning/spectrum.d.ts +0 -1
  65. package/dist/browser/tuning/thumbnailRow.d.ts +0 -1
  66. package/dist/browser/tuning/tileClick.d.ts +0 -1
  67. package/dist/browser/tuning/xfinity.d.ts +0 -1
  68. package/dist/browser/tuning/youtubeTv.d.ts +0 -1
  69. package/dist/browser/video.d.ts +22 -20
  70. package/dist/browser/video.js +102 -119
  71. package/dist/browser/video.js.map +1 -1
  72. package/dist/browser/windowSync.d.ts +0 -1
  73. package/dist/channels/index.d.ts +0 -1
  74. package/dist/config/channelForm.d.ts +0 -1
  75. package/dist/config/channelIdentity.d.ts +27 -0
  76. package/dist/config/channelIdentity.js +72 -0
  77. package/dist/config/channelIdentity.js.map +1 -0
  78. package/dist/config/channelSort.d.ts +25 -0
  79. package/dist/config/channelSort.js +101 -0
  80. package/dist/config/channelSort.js.map +1 -0
  81. package/dist/config/consistencyProbe.d.ts +0 -1
  82. package/dist/config/health.d.ts +3 -2
  83. package/dist/config/health.js +41 -28
  84. package/dist/config/health.js.map +1 -1
  85. package/dist/config/index.d.ts +0 -1
  86. package/dist/config/paths.d.ts +0 -1
  87. package/dist/config/persistence.context.d.ts +0 -1
  88. package/dist/config/persistence.d.ts +0 -1
  89. package/dist/config/presets.d.ts +0 -1
  90. package/dist/config/profiles.d.ts +9 -4
  91. package/dist/config/profiles.js +37 -3
  92. package/dist/config/profiles.js.map +1 -1
  93. package/dist/config/providerLineups.d.ts +0 -1
  94. package/dist/config/reactivity.d.ts +0 -1
  95. package/dist/config/servicePacks.d.ts +0 -1
  96. package/dist/config/services.d.ts +1 -47
  97. package/dist/config/services.js +3 -134
  98. package/dist/config/services.js.map +1 -1
  99. package/dist/config/sites.d.ts +0 -9
  100. package/dist/config/sites.js +5 -40
  101. package/dist/config/sites.js.map +1 -1
  102. package/dist/config/userChannels.d.ts +22 -28
  103. package/dist/config/userChannels.js +46 -60
  104. package/dist/config/userChannels.js.map +1 -1
  105. package/dist/config/userConfig.d.ts +4 -1
  106. package/dist/config/userConfig.js +4 -0
  107. package/dist/config/userConfig.js.map +1 -1
  108. package/dist/config/userProfiles.d.ts +2 -3
  109. package/dist/config/userProfiles.js +6 -2
  110. package/dist/config/userProfiles.js.map +1 -1
  111. package/dist/hdhr/channelMap.d.ts +0 -1
  112. package/dist/hdhr/deviceId.d.ts +0 -1
  113. package/dist/hdhr/discover.d.ts +0 -1
  114. package/dist/hdhr/getHandlers.d.ts +0 -1
  115. package/dist/hdhr/identity.d.ts +0 -1
  116. package/dist/hdhr/index.d.ts +0 -1
  117. package/dist/hdhr/protocol.d.ts +0 -1
  118. package/dist/hdhr/tunerState.d.ts +0 -1
  119. package/dist/hdhr/udp.d.ts +0 -1
  120. package/dist/identity.d.ts +0 -1
  121. package/dist/index.d.ts +0 -1
  122. package/dist/native/codecInference.d.ts +5 -4
  123. package/dist/native/codecInference.js +12 -7
  124. package/dist/native/codecInference.js.map +1 -1
  125. package/dist/native/decrypt.d.ts +10 -3
  126. package/dist/native/decrypt.js +9 -5
  127. package/dist/native/decrypt.js.map +1 -1
  128. package/dist/native/index.d.ts +25 -23
  129. package/dist/native/index.js +23 -17
  130. package/dist/native/index.js.map +1 -1
  131. package/dist/native/probe.d.ts +7 -10
  132. package/dist/native/probe.js +40 -36
  133. package/dist/native/probe.js.map +1 -1
  134. package/dist/native/proxy.d.ts +2 -3
  135. package/dist/native/proxy.js +86 -64
  136. package/dist/native/proxy.js.map +1 -1
  137. package/dist/native/tokenExpiry.d.ts +0 -1
  138. package/dist/routes/assets.d.ts +0 -1
  139. package/dist/routes/auth.d.ts +0 -1
  140. package/dist/routes/auth.js +1 -1
  141. package/dist/routes/auth.js.map +1 -1
  142. package/dist/routes/cdp.d.ts +11 -5
  143. package/dist/routes/cdp.js +90 -62
  144. package/dist/routes/cdp.js.map +1 -1
  145. package/dist/routes/channels.d.ts +0 -1
  146. package/dist/routes/clientActions.d.ts +0 -1
  147. package/dist/routes/components.d.ts +0 -1
  148. package/dist/routes/config/channels/endpoints/browse.d.ts +0 -1
  149. package/dist/routes/config/channels/endpoints/bulk.d.ts +0 -1
  150. package/dist/routes/config/channels/endpoints/bulk.js +1 -1
  151. package/dist/routes/config/channels/endpoints/bulk.js.map +1 -1
  152. package/dist/routes/config/channels/endpoints/crud.d.ts +0 -1
  153. package/dist/routes/config/channels/endpoints/crud.js +3 -2
  154. package/dist/routes/config/channels/endpoints/crud.js.map +1 -1
  155. package/dist/routes/config/channels/endpoints/importExport.d.ts +0 -1
  156. package/dist/routes/config/channels/endpoints/importExport.js +7 -3
  157. package/dist/routes/config/channels/endpoints/importExport.js.map +1 -1
  158. package/dist/routes/config/channels/endpoints/predefined.d.ts +0 -1
  159. package/dist/routes/config/channels/endpoints/prefs.d.ts +0 -1
  160. package/dist/routes/config/channels/endpoints/prefs.js +1 -1
  161. package/dist/routes/config/channels/endpoints/prefs.js.map +1 -1
  162. package/dist/routes/config/channels/endpoints/service.d.ts +0 -1
  163. package/dist/routes/config/channels/endpoints/service.js +8 -5
  164. package/dist/routes/config/channels/endpoints/service.js.map +1 -1
  165. package/dist/routes/config/channels/endpoints/tags.d.ts +0 -1
  166. package/dist/routes/config/channels/healthBridge.d.ts +0 -1
  167. package/dist/routes/config/channels/http/handler.d.ts +0 -1
  168. package/dist/routes/config/channels/http/playlistHint.d.ts +0 -1
  169. package/dist/routes/config/channels/http/serviceWarning.d.ts +0 -1
  170. package/dist/routes/config/channels/http/serviceWarning.js +1 -1
  171. package/dist/routes/config/channels/http/serviceWarning.js.map +1 -1
  172. package/dist/routes/config/channels/index.d.ts +0 -1
  173. package/dist/routes/config/channels/setup.d.ts +0 -1
  174. package/dist/routes/config/channels/table.d.ts +0 -1
  175. package/dist/routes/config/channels/table.js +2 -1
  176. package/dist/routes/config/channels/table.js.map +1 -1
  177. package/dist/routes/config/http/envelope.d.ts +0 -1
  178. package/dist/routes/config/index.d.ts +0 -1
  179. package/dist/routes/config/index.js +2 -1
  180. package/dist/routes/config/index.js.map +1 -1
  181. package/dist/routes/config/services.d.ts +0 -1
  182. package/dist/routes/config/services.js +4 -3
  183. package/dist/routes/config/services.js.map +1 -1
  184. package/dist/routes/config/settings.d.ts +0 -1
  185. package/dist/routes/config/settings.js +2 -1
  186. package/dist/routes/config/settings.js.map +1 -1
  187. package/dist/routes/debug.d.ts +0 -1
  188. package/dist/routes/health.d.ts +1 -2
  189. package/dist/routes/health.js +5 -2
  190. package/dist/routes/health.js.map +1 -1
  191. package/dist/routes/hls.d.ts +0 -1
  192. package/dist/routes/icons.d.ts +0 -1
  193. package/dist/routes/index.d.ts +0 -1
  194. package/dist/routes/logs.d.ts +3 -2
  195. package/dist/routes/logs.js +4 -2
  196. package/dist/routes/logs.js.map +1 -1
  197. package/dist/routes/mpegts.d.ts +0 -1
  198. package/dist/routes/play.d.ts +0 -1
  199. package/dist/routes/playlist.d.ts +0 -1
  200. package/dist/routes/playlist.js +2 -1
  201. package/dist/routes/playlist.js.map +1 -1
  202. package/dist/routes/root/content.d.ts +0 -1
  203. package/dist/routes/root/index.d.ts +0 -1
  204. package/dist/routes/root/scripts/channels.d.ts +0 -1
  205. package/dist/routes/root/scripts/channels.js +2 -2
  206. package/dist/routes/root/scripts/channels.js.map +1 -1
  207. package/dist/routes/root/scripts/clientEscape.d.ts +0 -1
  208. package/dist/routes/root/scripts/clientUrl.d.ts +0 -1
  209. package/dist/routes/root/scripts/config.d.ts +0 -1
  210. package/dist/routes/root/scripts/config.js +5 -26
  211. package/dist/routes/root/scripts/config.js.map +1 -1
  212. package/dist/routes/root/scripts/index.d.ts +0 -1
  213. package/dist/routes/root/scripts/shared.d.ts +0 -1
  214. package/dist/routes/root/scripts/status.d.ts +0 -1
  215. package/dist/routes/root/scripts/status.handlers.d.ts +0 -1
  216. package/dist/routes/root/scripts/status.handlers.js +3 -2
  217. package/dist/routes/root/scripts/status.handlers.js.map +1 -1
  218. package/dist/routes/root/styles.d.ts +0 -1
  219. package/dist/routes/services.d.ts +0 -1
  220. package/dist/routes/services.js +1 -1
  221. package/dist/routes/services.js.map +1 -1
  222. package/dist/routes/sse.d.ts +3 -2
  223. package/dist/routes/sse.js +7 -3
  224. package/dist/routes/sse.js.map +1 -1
  225. package/dist/routes/streams.d.ts +4 -2
  226. package/dist/routes/streams.js +8 -5
  227. package/dist/routes/streams.js.map +1 -1
  228. package/dist/routes/theme.d.ts +0 -1
  229. package/dist/routes/ui.d.ts +0 -1
  230. package/dist/routes/upgrade.d.ts +0 -1
  231. package/dist/routes/upgrade.js +2 -1
  232. package/dist/routes/upgrade.js.map +1 -1
  233. package/dist/service/commands.context.d.ts +0 -1
  234. package/dist/service/commands.context.js +7 -1
  235. package/dist/service/commands.context.js.map +1 -1
  236. package/dist/service/commands.d.ts +0 -1
  237. package/dist/service/generators.context.d.ts +0 -1
  238. package/dist/service/generators.d.ts +0 -1
  239. package/dist/service/index.d.ts +0 -1
  240. package/dist/streaming/captureLock.d.ts +1 -2
  241. package/dist/streaming/captureLock.js +6 -5
  242. package/dist/streaming/captureLock.js.map +1 -1
  243. package/dist/streaming/captureSession.d.ts +0 -1
  244. package/dist/streaming/clients.d.ts +4 -3
  245. package/dist/streaming/clients.js +5 -4
  246. package/dist/streaming/clients.js.map +1 -1
  247. package/dist/streaming/codec.d.ts +0 -1
  248. package/dist/streaming/fmp4Segmenter.d.ts +2 -1
  249. package/dist/streaming/fmp4Segmenter.js +12 -11
  250. package/dist/streaming/fmp4Segmenter.js.map +1 -1
  251. package/dist/streaming/hls.d.ts +34 -1
  252. package/dist/streaming/hls.js +121 -54
  253. package/dist/streaming/hls.js.map +1 -1
  254. package/dist/streaming/hlsResume.d.ts +8 -5
  255. package/dist/streaming/hlsResume.js +10 -8
  256. package/dist/streaming/hlsResume.js.map +1 -1
  257. package/dist/streaming/hlsSegments.d.ts +9 -7
  258. package/dist/streaming/hlsSegments.js +19 -14
  259. package/dist/streaming/hlsSegments.js.map +1 -1
  260. package/dist/streaming/lifecycle.d.ts +5 -4
  261. package/dist/streaming/lifecycle.js +25 -13
  262. package/dist/streaming/lifecycle.js.map +1 -1
  263. package/dist/streaming/monitor.d.ts +3 -2
  264. package/dist/streaming/monitor.js +63 -57
  265. package/dist/streaming/monitor.js.map +1 -1
  266. package/dist/streaming/mp4Parser.d.ts +0 -1
  267. package/dist/streaming/mpegts.d.ts +0 -1
  268. package/dist/streaming/mpegts.js +23 -7
  269. package/dist/streaming/mpegts.js.map +1 -1
  270. package/dist/streaming/playlistBuilder.d.ts +0 -1
  271. package/dist/streaming/preroll.d.ts +34 -15
  272. package/dist/streaming/preroll.js +30 -28
  273. package/dist/streaming/preroll.js.map +1 -1
  274. package/dist/streaming/pretune.d.ts +2 -3
  275. package/dist/streaming/pretune.js +45 -45
  276. package/dist/streaming/pretune.js.map +1 -1
  277. package/dist/streaming/pretuneTimers.d.ts +16 -12
  278. package/dist/streaming/pretuneTimers.js +27 -37
  279. package/dist/streaming/pretuneTimers.js.map +1 -1
  280. package/dist/streaming/recovery.d.ts +7 -5
  281. package/dist/streaming/recovery.js +10 -7
  282. package/dist/streaming/recovery.js.map +1 -1
  283. package/dist/streaming/registry.d.ts +23 -5
  284. package/dist/streaming/registry.js +17 -3
  285. package/dist/streaming/registry.js.map +1 -1
  286. package/dist/streaming/setup.d.ts +39 -7
  287. package/dist/streaming/setup.js +130 -65
  288. package/dist/streaming/setup.js.map +1 -1
  289. package/dist/streaming/showInfo.d.ts +8 -4
  290. package/dist/streaming/showInfo.js +54 -45
  291. package/dist/streaming/showInfo.js.map +1 -1
  292. package/dist/streaming/statusEmitter.d.ts +1 -2
  293. package/dist/streaming/statusEmitter.js +1 -1
  294. package/dist/streaming/statusEmitter.js.map +1 -1
  295. package/dist/types/channels.d.ts +0 -1
  296. package/dist/types/config.d.ts +0 -1
  297. package/dist/types/index.d.ts +1 -2
  298. package/dist/types/index.js.map +1 -1
  299. package/dist/types/profiles.d.ts +1 -1
  300. package/dist/types/selection.d.ts +10 -1
  301. package/dist/types/shared.d.ts +0 -1
  302. package/dist/types/streaming.d.ts +0 -1
  303. package/dist/upgrade/commands.context.d.ts +0 -1
  304. package/dist/upgrade/commands.d.ts +0 -1
  305. package/dist/upgrade/detection.context.d.ts +0 -1
  306. package/dist/upgrade/detection.d.ts +0 -1
  307. package/dist/upgrade/index.d.ts +0 -1
  308. package/dist/upgrade/lifecycle.context.d.ts +2 -1
  309. package/dist/upgrade/lifecycle.context.js +9 -4
  310. package/dist/upgrade/lifecycle.context.js.map +1 -1
  311. package/dist/upgrade/lifecycle.d.ts +0 -1
  312. package/dist/upgrade/pathHandle.d.ts +0 -1
  313. package/dist/utils/bootSession.context.d.ts +0 -1
  314. package/dist/utils/bootSession.context.js +2 -1
  315. package/dist/utils/bootSession.context.js.map +1 -1
  316. package/dist/utils/bootSession.d.ts +0 -1
  317. package/dist/utils/chromeFetch.d.ts +0 -1
  318. package/dist/utils/cliOutput.d.ts +0 -1
  319. package/dist/utils/debugFilter.d.ts +0 -1
  320. package/dist/utils/delay.d.ts +47 -25
  321. package/dist/utils/delay.js +74 -37
  322. package/dist/utils/delay.js.map +1 -1
  323. package/dist/utils/errors.d.ts +0 -1
  324. package/dist/utils/evaluate.d.ts +11 -3
  325. package/dist/utils/evaluate.js +4 -3
  326. package/dist/utils/evaluate.js.map +1 -1
  327. package/dist/utils/ffmpeg.context.d.ts +0 -1
  328. package/dist/utils/ffmpeg.d.ts +0 -1
  329. package/dist/utils/fileLogger.d.ts +3 -2
  330. package/dist/utils/fileLogger.js +16 -8
  331. package/dist/utils/fileLogger.js.map +1 -1
  332. package/dist/utils/format.d.ts +12 -3
  333. package/dist/utils/format.js +29 -11
  334. package/dist/utils/format.js.map +1 -1
  335. package/dist/utils/index.d.ts +0 -2
  336. package/dist/utils/index.js +0 -1
  337. package/dist/utils/index.js.map +1 -1
  338. package/dist/utils/logEmitter.d.ts +0 -1
  339. package/dist/utils/logger.d.ts +0 -1
  340. package/dist/utils/m3u.d.ts +0 -1
  341. package/dist/utils/markup.d.ts +0 -1
  342. package/dist/utils/memo.d.ts +0 -1
  343. package/dist/utils/morganStream.d.ts +0 -1
  344. package/dist/utils/network.d.ts +0 -1
  345. package/dist/utils/never.d.ts +0 -1
  346. package/dist/utils/pid.d.ts +0 -1
  347. package/dist/utils/platform.d.ts +0 -1
  348. package/dist/utils/processInspector.context.d.ts +32 -2
  349. package/dist/utils/processInspector.context.js +117 -1
  350. package/dist/utils/processInspector.context.js.map +1 -1
  351. package/dist/utils/processInspector.d.ts +0 -32
  352. package/dist/utils/processInspector.js +10 -117
  353. package/dist/utils/processInspector.js.map +1 -1
  354. package/dist/utils/retry.d.ts +5 -4
  355. package/dist/utils/retry.js +43 -33
  356. package/dist/utils/retry.js.map +1 -1
  357. package/dist/utils/runtimeIdentity.context.d.ts +0 -1
  358. package/dist/utils/runtimeIdentity.context.js +3 -1
  359. package/dist/utils/runtimeIdentity.context.js.map +1 -1
  360. package/dist/utils/runtimeIdentity.d.ts +1 -1
  361. package/dist/utils/runtimeIdentity.js +1 -1
  362. package/dist/utils/runtimeIdentity.js.map +1 -1
  363. package/dist/utils/sanitize.d.ts +0 -1
  364. package/dist/utils/streamContext.d.ts +0 -1
  365. package/dist/utils/timing.d.ts +3 -4
  366. package/dist/utils/timing.js +4 -4
  367. package/dist/utils/timing.js.map +1 -1
  368. package/dist/utils/version.d.ts +5 -4
  369. package/dist/utils/version.js +35 -24
  370. package/dist/utils/version.js.map +1 -1
  371. package/package.json +7 -7
  372. package/dist/app.d.ts.map +0 -1
  373. package/dist/browser/blockedPage.d.ts.map +0 -1
  374. package/dist/browser/browserSupervisor.d.ts.map +0 -1
  375. package/dist/browser/cdp.d.ts.map +0 -1
  376. package/dist/browser/channelSelection.d.ts.map +0 -1
  377. package/dist/browser/consent.d.ts.map +0 -1
  378. package/dist/browser/display.d.ts.map +0 -1
  379. package/dist/browser/hlsPlaylistObserver.d.ts.map +0 -1
  380. package/dist/browser/index.d.ts.map +0 -1
  381. package/dist/browser/launchGovernor.d.ts.map +0 -1
  382. package/dist/browser/login.d.ts.map +0 -1
  383. package/dist/browser/manifestInterceptor.d.ts.map +0 -1
  384. package/dist/browser/pageStaleness.d.ts.map +0 -1
  385. package/dist/browser/precaching.d.ts.map +0 -1
  386. package/dist/browser/tabCapture.d.ts.map +0 -1
  387. package/dist/browser/tabNetworkObserver.d.ts.map +0 -1
  388. package/dist/browser/tabSelection.d.ts.map +0 -1
  389. package/dist/browser/tuning/cache.d.ts.map +0 -1
  390. package/dist/browser/tuning/comcastPolymer.d.ts.map +0 -1
  391. package/dist/browser/tuning/cox.d.ts.map +0 -1
  392. package/dist/browser/tuning/directv.d.ts.map +0 -1
  393. package/dist/browser/tuning/fox.d.ts.map +0 -1
  394. package/dist/browser/tuning/gridSearch.d.ts.map +0 -1
  395. package/dist/browser/tuning/hbo.d.ts.map +0 -1
  396. package/dist/browser/tuning/hulu.d.ts.map +0 -1
  397. package/dist/browser/tuning/shared.d.ts.map +0 -1
  398. package/dist/browser/tuning/sling.d.ts.map +0 -1
  399. package/dist/browser/tuning/spectrum.d.ts.map +0 -1
  400. package/dist/browser/tuning/thumbnailRow.d.ts.map +0 -1
  401. package/dist/browser/tuning/tileClick.d.ts.map +0 -1
  402. package/dist/browser/tuning/xfinity.d.ts.map +0 -1
  403. package/dist/browser/tuning/youtubeTv.d.ts.map +0 -1
  404. package/dist/browser/video.d.ts.map +0 -1
  405. package/dist/browser/windowSync.d.ts.map +0 -1
  406. package/dist/channels/index.d.ts.map +0 -1
  407. package/dist/config/channelForm.d.ts.map +0 -1
  408. package/dist/config/consistencyProbe.d.ts.map +0 -1
  409. package/dist/config/health.d.ts.map +0 -1
  410. package/dist/config/index.d.ts.map +0 -1
  411. package/dist/config/paths.d.ts.map +0 -1
  412. package/dist/config/persistence.context.d.ts.map +0 -1
  413. package/dist/config/persistence.d.ts.map +0 -1
  414. package/dist/config/presets.d.ts.map +0 -1
  415. package/dist/config/profiles.d.ts.map +0 -1
  416. package/dist/config/providerLineups.d.ts.map +0 -1
  417. package/dist/config/reactivity.d.ts.map +0 -1
  418. package/dist/config/servicePacks.d.ts.map +0 -1
  419. package/dist/config/services.d.ts.map +0 -1
  420. package/dist/config/sites.d.ts.map +0 -1
  421. package/dist/config/userChannels.d.ts.map +0 -1
  422. package/dist/config/userConfig.d.ts.map +0 -1
  423. package/dist/config/userProfiles.d.ts.map +0 -1
  424. package/dist/hdhr/channelMap.d.ts.map +0 -1
  425. package/dist/hdhr/deviceId.d.ts.map +0 -1
  426. package/dist/hdhr/discover.d.ts.map +0 -1
  427. package/dist/hdhr/getHandlers.d.ts.map +0 -1
  428. package/dist/hdhr/identity.d.ts.map +0 -1
  429. package/dist/hdhr/index.d.ts.map +0 -1
  430. package/dist/hdhr/protocol.d.ts.map +0 -1
  431. package/dist/hdhr/tunerState.d.ts.map +0 -1
  432. package/dist/hdhr/udp.d.ts.map +0 -1
  433. package/dist/identity.d.ts.map +0 -1
  434. package/dist/index.d.ts.map +0 -1
  435. package/dist/native/codecInference.d.ts.map +0 -1
  436. package/dist/native/decrypt.d.ts.map +0 -1
  437. package/dist/native/index.d.ts.map +0 -1
  438. package/dist/native/probe.d.ts.map +0 -1
  439. package/dist/native/proxy.d.ts.map +0 -1
  440. package/dist/native/tokenExpiry.d.ts.map +0 -1
  441. package/dist/routes/assets.d.ts.map +0 -1
  442. package/dist/routes/auth.d.ts.map +0 -1
  443. package/dist/routes/cdp.d.ts.map +0 -1
  444. package/dist/routes/channels.d.ts.map +0 -1
  445. package/dist/routes/clientActions.d.ts.map +0 -1
  446. package/dist/routes/components.d.ts.map +0 -1
  447. package/dist/routes/config/channels/endpoints/browse.d.ts.map +0 -1
  448. package/dist/routes/config/channels/endpoints/bulk.d.ts.map +0 -1
  449. package/dist/routes/config/channels/endpoints/crud.d.ts.map +0 -1
  450. package/dist/routes/config/channels/endpoints/importExport.d.ts.map +0 -1
  451. package/dist/routes/config/channels/endpoints/predefined.d.ts.map +0 -1
  452. package/dist/routes/config/channels/endpoints/prefs.d.ts.map +0 -1
  453. package/dist/routes/config/channels/endpoints/service.d.ts.map +0 -1
  454. package/dist/routes/config/channels/endpoints/tags.d.ts.map +0 -1
  455. package/dist/routes/config/channels/healthBridge.d.ts.map +0 -1
  456. package/dist/routes/config/channels/http/handler.d.ts.map +0 -1
  457. package/dist/routes/config/channels/http/playlistHint.d.ts.map +0 -1
  458. package/dist/routes/config/channels/http/serviceWarning.d.ts.map +0 -1
  459. package/dist/routes/config/channels/index.d.ts.map +0 -1
  460. package/dist/routes/config/channels/setup.d.ts.map +0 -1
  461. package/dist/routes/config/channels/table.d.ts.map +0 -1
  462. package/dist/routes/config/http/envelope.d.ts.map +0 -1
  463. package/dist/routes/config/index.d.ts.map +0 -1
  464. package/dist/routes/config/services.d.ts.map +0 -1
  465. package/dist/routes/config/settings.d.ts.map +0 -1
  466. package/dist/routes/debug.d.ts.map +0 -1
  467. package/dist/routes/health.d.ts.map +0 -1
  468. package/dist/routes/hls.d.ts.map +0 -1
  469. package/dist/routes/icons.d.ts.map +0 -1
  470. package/dist/routes/index.d.ts.map +0 -1
  471. package/dist/routes/logs.d.ts.map +0 -1
  472. package/dist/routes/mpegts.d.ts.map +0 -1
  473. package/dist/routes/play.d.ts.map +0 -1
  474. package/dist/routes/playlist.d.ts.map +0 -1
  475. package/dist/routes/root/content.d.ts.map +0 -1
  476. package/dist/routes/root/index.d.ts.map +0 -1
  477. package/dist/routes/root/scripts/channels.d.ts.map +0 -1
  478. package/dist/routes/root/scripts/clientEscape.d.ts.map +0 -1
  479. package/dist/routes/root/scripts/clientUrl.d.ts.map +0 -1
  480. package/dist/routes/root/scripts/config.d.ts.map +0 -1
  481. package/dist/routes/root/scripts/index.d.ts.map +0 -1
  482. package/dist/routes/root/scripts/shared.d.ts.map +0 -1
  483. package/dist/routes/root/scripts/status.d.ts.map +0 -1
  484. package/dist/routes/root/scripts/status.handlers.d.ts.map +0 -1
  485. package/dist/routes/root/styles.d.ts.map +0 -1
  486. package/dist/routes/services.d.ts.map +0 -1
  487. package/dist/routes/sse.d.ts.map +0 -1
  488. package/dist/routes/streams.d.ts.map +0 -1
  489. package/dist/routes/theme.d.ts.map +0 -1
  490. package/dist/routes/ui.d.ts.map +0 -1
  491. package/dist/routes/upgrade.d.ts.map +0 -1
  492. package/dist/service/commands.context.d.ts.map +0 -1
  493. package/dist/service/commands.d.ts.map +0 -1
  494. package/dist/service/generators.context.d.ts.map +0 -1
  495. package/dist/service/generators.d.ts.map +0 -1
  496. package/dist/service/index.d.ts.map +0 -1
  497. package/dist/streaming/captureLock.d.ts.map +0 -1
  498. package/dist/streaming/captureSession.d.ts.map +0 -1
  499. package/dist/streaming/clients.d.ts.map +0 -1
  500. package/dist/streaming/codec.d.ts.map +0 -1
  501. package/dist/streaming/fmp4Segmenter.d.ts.map +0 -1
  502. package/dist/streaming/hls.d.ts.map +0 -1
  503. package/dist/streaming/hlsResume.d.ts.map +0 -1
  504. package/dist/streaming/hlsSegments.d.ts.map +0 -1
  505. package/dist/streaming/lifecycle.d.ts.map +0 -1
  506. package/dist/streaming/monitor.d.ts.map +0 -1
  507. package/dist/streaming/mp4Parser.d.ts.map +0 -1
  508. package/dist/streaming/mpegts.d.ts.map +0 -1
  509. package/dist/streaming/playlistBuilder.d.ts.map +0 -1
  510. package/dist/streaming/preroll.d.ts.map +0 -1
  511. package/dist/streaming/pretune.d.ts.map +0 -1
  512. package/dist/streaming/pretuneTimers.d.ts.map +0 -1
  513. package/dist/streaming/recovery.d.ts.map +0 -1
  514. package/dist/streaming/registry.d.ts.map +0 -1
  515. package/dist/streaming/setup.d.ts.map +0 -1
  516. package/dist/streaming/showInfo.d.ts.map +0 -1
  517. package/dist/streaming/statusEmitter.d.ts.map +0 -1
  518. package/dist/types/channels.d.ts.map +0 -1
  519. package/dist/types/config.d.ts.map +0 -1
  520. package/dist/types/index.d.ts.map +0 -1
  521. package/dist/types/profiles.d.ts.map +0 -1
  522. package/dist/types/selection.d.ts.map +0 -1
  523. package/dist/types/shared.d.ts.map +0 -1
  524. package/dist/types/streaming.d.ts.map +0 -1
  525. package/dist/upgrade/commands.context.d.ts.map +0 -1
  526. package/dist/upgrade/commands.d.ts.map +0 -1
  527. package/dist/upgrade/detection.context.d.ts.map +0 -1
  528. package/dist/upgrade/detection.d.ts.map +0 -1
  529. package/dist/upgrade/index.d.ts.map +0 -1
  530. package/dist/upgrade/lifecycle.context.d.ts.map +0 -1
  531. package/dist/upgrade/lifecycle.d.ts.map +0 -1
  532. package/dist/upgrade/pathHandle.d.ts.map +0 -1
  533. package/dist/utils/bootSession.context.d.ts.map +0 -1
  534. package/dist/utils/bootSession.d.ts.map +0 -1
  535. package/dist/utils/chromeFetch.d.ts.map +0 -1
  536. package/dist/utils/cliOutput.d.ts.map +0 -1
  537. package/dist/utils/clock.d.ts +0 -15
  538. package/dist/utils/clock.d.ts.map +0 -1
  539. package/dist/utils/clock.js +0 -31
  540. package/dist/utils/clock.js.map +0 -1
  541. package/dist/utils/debugFilter.d.ts.map +0 -1
  542. package/dist/utils/delay.d.ts.map +0 -1
  543. package/dist/utils/errors.d.ts.map +0 -1
  544. package/dist/utils/evaluate.d.ts.map +0 -1
  545. package/dist/utils/ffmpeg.context.d.ts.map +0 -1
  546. package/dist/utils/ffmpeg.d.ts.map +0 -1
  547. package/dist/utils/fileLogger.d.ts.map +0 -1
  548. package/dist/utils/format.d.ts.map +0 -1
  549. package/dist/utils/index.d.ts.map +0 -1
  550. package/dist/utils/logEmitter.d.ts.map +0 -1
  551. package/dist/utils/logger.d.ts.map +0 -1
  552. package/dist/utils/m3u.d.ts.map +0 -1
  553. package/dist/utils/markup.d.ts.map +0 -1
  554. package/dist/utils/memo.d.ts.map +0 -1
  555. package/dist/utils/morganStream.d.ts.map +0 -1
  556. package/dist/utils/network.d.ts.map +0 -1
  557. package/dist/utils/never.d.ts.map +0 -1
  558. package/dist/utils/pid.d.ts.map +0 -1
  559. package/dist/utils/platform.d.ts.map +0 -1
  560. package/dist/utils/processInspector.context.d.ts.map +0 -1
  561. package/dist/utils/processInspector.d.ts.map +0 -1
  562. package/dist/utils/retry.d.ts.map +0 -1
  563. package/dist/utils/runtimeIdentity.context.d.ts.map +0 -1
  564. package/dist/utils/runtimeIdentity.d.ts.map +0 -1
  565. package/dist/utils/sanitize.d.ts.map +0 -1
  566. package/dist/utils/streamContext.d.ts.map +0 -1
  567. package/dist/utils/timing.d.ts.map +0 -1
  568. package/dist/utils/version.d.ts.map +0 -1
@@ -1,6 +1,7 @@
1
- import { LOG, boundedWait, evaluateWithAbort, formatError, isProcessRunning, listProcesses, realClock, setChromeUserAgent, startTimer } from "../utils/index.js";
2
- import { clearLoginState, isLoginModeActive, setBrowserAccessors } from "./login.js";
3
- import { getAllStreams, getStreamCount, hasActiveCaptureStreams, isCaptureIdentity } from "../streaming/registry.js";
1
+ import { LOG, boundedWait, evaluateWithAbort, formatError, isProcessRunning, listProcesses, setChromeUserAgent, startTimer } from "../utils/index.js";
2
+ import { TimerRegistry, systemClock } from "homebridge-plugin-utils";
3
+ import { clearLoginState, isLoginModeActive, setLoginDeps } from "./login.js";
4
+ import { getAllStreams, getStreamCount, hasActiveCaptureStreams, hasEstablishedStreams, isCaptureIdentity } from "../streaming/registry.js";
4
5
  import { getCachedTabId, installStrayOpenTabReaper, onTabActivation } from "./tabSelection.js";
5
6
  import { getChromeDataDir, getDataDir, getExtensionDir } from "../config/paths.js";
6
7
  import { getExtensionPage, launch } from "puppeteer-stream";
@@ -18,7 +19,6 @@ import { getPresetViewport } from "../config/presets.js";
18
19
  import path from "node:path";
19
20
  import { launch as puppeteerLaunch } from "puppeteer-core";
20
21
  import { startPrecaching } from "./precaching.js";
21
- import { terminateStream } from "../streaming/lifecycle.js";
22
22
  const { promises: fsPromises } = fs;
23
23
  /* Global variables maintain the application's runtime state across all operations. We minimize global state where possible, but some values must be shared across
24
24
  * the application lifecycle:
@@ -71,13 +71,13 @@ function buildRelaunchPolicy() {
71
71
  }
72
72
  /* The one browser capture-readiness supervisor for the process lifetime. It owns the lifecycle state (absent/launching/ready/degraded/trialing) that unifies the
73
73
  * browser reference, launch promise, and launch timestamp, and routes every relaunch through one loop-safe governor. The adapter injects the impure
74
- * ports: launchReadyBrowser (spawn Chrome and run the readiness gate), closeBrowserInstance (teardown), realClock.now (time), buildRelaunchPolicy (live config
74
+ * ports: launchReadyBrowser (spawn Chrome and run the readiness gate), closeBrowserInstance (teardown), systemClock.now (time), buildRelaunchPolicy (live config
75
75
  * bounds), and onSupervisorStateChange (the loud degraded alarm and the recovery notice). All browser access flows through it: getCurrentBrowser is acquire(); the
76
76
  * non-launching reads derive from current() and currentLaunchTime(). The injected ports are hoisted function declarations, so referencing them here is safe even
77
77
  * though they are defined further down the module.
78
78
  */
79
- const supervisor = createBrowserSupervisor({ close: closeBrowserInstance, launch: launchReadyBrowser, now: realClock.now, onStateChange: onSupervisorStateChange,
80
- policy: buildRelaunchPolicy });
79
+ const supervisor = createBrowserSupervisor({ close: closeBrowserInstance, launch: launchReadyBrowser, now: () => systemClock.now(),
80
+ onStateChange: onSupervisorStateChange, policy: buildRelaunchPolicy });
81
81
  /**
82
82
  * Observes supervisor lifecycle transitions purely for operator-visible signals; it never affects the transition (the supervisor treats it as best-effort, so a
83
83
  * throwing logger cannot corrupt the lifecycle). It raises the loud degraded alarm when the relaunch governor trips, the capture-impairment alarm when a published
@@ -91,7 +91,7 @@ function onSupervisorStateChange(next, previous) {
91
91
  // The governor just tripped: relaunches are paused while the browser's capture system cools down. We log loudly at ERROR with the cooldown horizon so the
92
92
  // condition is never invisible - the enforceable form of "it is impossible to be silently un-tunable."
93
93
  if ((next.kind === "degraded") && (previous.kind !== "degraded")) {
94
- const cooldownMinutes = Math.max(1, Math.round((next.until - realClock.now()) / 60000));
94
+ const cooldownMinutes = Math.max(1, Math.round((next.until - systemClock.now()) / 60000));
95
95
  LOG.error("The browser capture system has degraded and the relaunch governor has tripped: %s Relaunches are paused for approximately %d minute(s) while it " +
96
96
  "cools down; new stream requests will receive a 503 back-off until it recovers.", next.reason, cooldownMinutes);
97
97
  }
@@ -103,9 +103,9 @@ function onSupervisorStateChange(next, previous) {
103
103
  else if ((next.kind === "ready") && (next.impairment !== null) && ((previous.kind !== "ready") || (previous.impairment === null))) {
104
104
  // The published browser has been marked as unable to start captures. The transition carrying the mark happens once per instance, so the alarm fires once per
105
105
  // instance too, and it says what an operator watching a stalled tune needs: the running captures are unaffected, new requests back off, and the cure arrives
106
- // on its own once the registry empties.
106
+ // on its own as soon as nothing on this browser holds a page.
107
107
  LOG.error("The browser can no longer start captures (%s). Its running captures continue, new stream requests receive a 503 back-off, and it will relaunch as " +
108
- "soon as no stream is active.", next.impairment.reason);
108
+ "soon as nothing holds a page on it.", next.impairment.reason);
109
109
  }
110
110
  // The browser's connectivity is part of the SSE system status, so emit when a ready browser is published. Readiness-loss emits are owned by handleBrowserDisconnect
111
111
  // (genuine disconnect) and the shutdown path; emitSystemStatusChanged dedupes, so a redundant emit is a cheap no-op.
@@ -114,8 +114,9 @@ function onSupervisorStateChange(next, previous) {
114
114
  }
115
115
  }
116
116
  /* The capture-readiness probe (capability tier of the launch gate). Null until streaming/setup.ts injects the real capture probe at module load, which the import
117
- * order guarantees runs before any launch: app.ts imports the streaming layer, whose module bodies evaluate during import resolution, before startServer's warm-up.
118
- * launchReadyBrowser refuses to publish a browser if it is somehow still null (see the call site), rather than serving an unverified one.
117
+ * order places ahead of any launch: index.ts value-imports app.ts, app.ts value-imports streaming/hls.ts, and hls.ts value-imports streaming/setup.ts, so the
118
+ * injection has run by the time index.ts's own body calls startServer. launchReadyBrowser refuses to publish a browser if it is somehow still null (see the call
119
+ * site), rather than serving an unverified one.
119
120
  */
120
121
  let captureProbe = null;
121
122
  /**
@@ -126,9 +127,27 @@ let captureProbe = null;
126
127
  export function setCaptureProbe(probe) {
127
128
  captureProbe = probe;
128
129
  }
129
- // The stale page cleanup interval handle, stored so we can clear it during graceful shutdown. The interval periodically checks for browser pages that are not
130
- // associated with active streams and closes them to prevent resource exhaustion.
131
- let stalePageCleanupInterval = null;
130
+ /* The authoritative stream terminator. Null until streaming/lifecycle.ts injects terminateStream at module load, which the import order places ahead of any
131
+ * browser launch: index.ts value-imports app.ts, app.ts value-imports streaming/lifecycle.ts, and a module body finishes before the body of anything that
132
+ * imports it, so the injection has run by the time index.ts's own body calls startServer - the earliest moment a launch, and so a disconnect, can happen. A
133
+ * still-null terminator at readiness loss is reported at ERROR and the teardown carries on (see the call site), because losing the rest of the teardown on top
134
+ * of the stream cleanup would make a recoverable situation worse.
135
+ */
136
+ let streamTerminator = null;
137
+ /**
138
+ * Injects the authoritative stream terminator used when browser readiness is lost. Called once from streaming/lifecycle.ts at module load. Separating the wiring
139
+ * from the call keeps browser/index.ts free of a streaming-layer import.
140
+ * @param terminator - The function that tears down one stream and all of the resources it owns.
141
+ */
142
+ export function setStreamTerminator(terminator) {
143
+ streamTerminator = terminator;
144
+ }
145
+ // The identity of the periodic sweep on the stale-page owner's registry.
146
+ const STALE_PAGE_SWEEP_KEY = "sweep";
147
+ /* The stale-page owner's timers: the periodic sweep that closes browser pages no active stream is using, so a long session cannot exhaust resources. The registry
148
+ * is built on the clock the start receives and disposed by the stop, so the sweep can never outlive the owner. Null until started, and again once stopped.
149
+ */
150
+ let stalePageTimers = null;
132
151
  /* Opportunistic browser restart state. Chrome accumulates memory pressure, GPU process issues, and general flakiness over multi-hour sessions with continuous
133
152
  * media playback. We proactively restart Chrome after it has been running for BROWSER_MAX_AGE, waiting for a quiet period with zero active streams before
134
153
  * executing the restart. After the restart, a fresh browser is launched immediately so it is ready for the next stream request.
@@ -139,20 +158,23 @@ const BROWSER_MAX_AGE = 6 * 60 * 60 * 1000;
139
158
  const BROWSER_RESTART_QUIET_PERIOD = 5 * 60 * 1000;
140
159
  // How often to check whether the browser qualifies for a restart (30 seconds).
141
160
  const BROWSER_RESTART_CHECK_INTERVAL = 30000;
142
- // Timer handle for the quiet period countdown. When set, the browser has exceeded BROWSER_MAX_AGE and we are waiting for BROWSER_RESTART_QUIET_PERIOD to
143
- // elapse with zero active streams. Cancelled if a stream starts during the quiet period.
144
- let restartQuietTimer = null;
161
+ // The identity of the periodic eligibility check on the restart owner's registry.
162
+ const RESTART_CHECK_KEY = "check";
163
+ // The identity of the quiet-period countdown on the restart owner's registry.
164
+ const RESTART_QUIET_KEY = "quiet";
165
+ /* The restart owner's timers, keyed on one registry built on the clock its start receives: the periodic eligibility check, and - while the browser has exceeded
166
+ * BROWSER_MAX_AGE and we are waiting for BROWSER_RESTART_QUIET_PERIOD to elapse with zero active streams - the quiet-period countdown, which a stream starting
167
+ * during the wait cancels. Holding both on one registry is what makes the stop drain the countdown along with the check. Null until the check is started, and
168
+ * again once it is stopped.
169
+ */
170
+ let restartTimers = null;
145
171
  /**
146
- * Cancels the scheduled-restart quiet period if one is pending, so the countdown it was running cannot fire against a browser it no longer describes.
172
+ * Cancels the scheduled-restart quiet period if one is pending, so the countdown it was running cannot fire against a browser it no longer describes. Reads the
173
+ * module binding rather than a handed-in registry because executeBrowserRestart calls it as well, which also makes it a no-op once the owner has been stopped.
147
174
  */
148
175
  function cancelRestartQuietTimer() {
149
- if (restartQuietTimer) {
150
- clearTimeout(restartQuietTimer);
151
- restartQuietTimer = null;
152
- }
176
+ restartTimers?.clear(RESTART_QUIET_KEY);
153
177
  }
154
- // Interval handle for the periodic restart eligibility check.
155
- let restartCheckInterval = null;
156
178
  // Flag indicating that the browser is being closed intentionally via closeBrowser(). When true, the disconnect handler skips error logging and stream termination
157
179
  // since these are handled by the shutdown code path. This prevents false "unexpected disconnect" errors during graceful shutdown.
158
180
  let gracefulShutdownInProgress = false;
@@ -310,7 +332,7 @@ export async function confirmSharedWindowPlacement(page) {
310
332
  return (await readPageWindow(page)) === sharedWindowId;
311
333
  }
312
334
  // Login mode management. State and functions live in login.ts; re-exported here so existing consumers don't need import path changes. clearLoginState,
313
- // isLoginModeActive, and setBrowserAccessors are imported above; the first two for internal use, setBrowserAccessors for one-time initialization below.
335
+ // isLoginModeActive, and setLoginDeps are imported above; the first two for internal use, setLoginDeps for one-time initialization below.
314
336
  export { clearLoginState, isLoginModeActive };
315
337
  export { endLoginMode, getLoginPage, getLoginStatus, setLoginModeEndObserver, startLoginMode } from "./login.js";
316
338
  // Re-export the supervisor's acquire() rejection classes through the browser surface so the stream-setup layer can map them to a 503 back-off without reaching into
@@ -329,7 +351,7 @@ export { BrowserCaptureImpairedError, BrowserSupersededError, BrowserUnavailable
329
351
  * preference or as the first page the browser happened to report.
330
352
  *
331
353
  * The instance stays private to this module and every caller reaches it through the exported function just below. That split is what makes the symbol safe to
332
- * reference at module-evaluation time: a function declaration is hoisted, so the setBrowserAccessors call further down - and any sibling module whose own body
354
+ * reference at module-evaluation time: a function declaration is hoisted, so the setLoginDeps call further down - and any sibling module whose own body
333
355
  * evaluates while this module is still evaluating, precaching.ts among them - resolves it whatever the declaration order turns out to be. A bare exported const
334
356
  * would leave those readers in the temporal dead zone, which no placement inside this file can fix for a reader in another file.
335
357
  */
@@ -380,10 +402,10 @@ const windowVisibilitySync = createWindowVisibilitySync({
380
402
  export async function syncWindowVisibility(page) {
381
403
  return windowVisibilitySync(page);
382
404
  }
383
- // Inject browser accessors into the login module. This breaks the circular dependency (login needs getBrowserInstance and the window sync, index needs login
384
- // functions) using the same setter/getter pattern as setChromeUserAgent in chromeFetch.ts. Both accessors are hoisted function declarations, so this call reads
385
- // them at module evaluation time regardless of where they sit in the file.
386
- setBrowserAccessors({ getBrowserInstance, syncWindowVisibility });
405
+ // Inject login's dependency set. This breaks the circular dependency (login needs getBrowserInstance and the window sync, index needs login functions) using the
406
+ // same setter/getter pattern as setChromeUserAgent in chromeFetch.ts. Both accessors are hoisted function declarations, so this call reads them at module
407
+ // evaluation time regardless of where they sit in the file. No clock is supplied, so login runs on the system clock the port defaults to.
408
+ setLoginDeps({ getBrowserInstance, syncWindowVisibility });
387
409
  /**
388
410
  * Computes the current system status and emits it to SSE subscribers. Called when browser state changes significantly or when streams are added/removed.
389
411
  */
@@ -642,15 +664,16 @@ export function killStaleChrome() {
642
664
  }
643
665
  /**
644
666
  * Polls until the Chrome process with the given PID has exited, or the timeout expires. Uses process.kill(pid, 0) to check process existence - throws ESRCH
645
- * when the process is gone. Between polls, sleeps synchronously using Atomics.wait() for cross-platform compatibility.
667
+ * when the process is gone. The wait runs inside the synchronous exit handler, so it reads the port for its instants and sleeps synchronously between them
668
+ * using Atomics.wait() for cross-platform compatibility.
646
669
  * @param pid - The Chrome process ID to wait for.
647
670
  * @param timeoutMs - Maximum time to wait in milliseconds.
648
671
  * @param pollIntervalMs - Time between existence checks in milliseconds.
649
672
  * @returns True if the process exited within the timeout, false otherwise.
650
673
  */
651
674
  function waitForChromeExit(pid, timeoutMs, pollIntervalMs) {
652
- const deadline = Date.now() + timeoutMs;
653
- while (Date.now() < deadline) {
675
+ const deadline = systemClock.now() + timeoutMs;
676
+ while (systemClock.now() < deadline) {
654
677
  if (!isProcessRunning(pid)) {
655
678
  return true;
656
679
  }
@@ -973,14 +996,17 @@ export function mirrorPlacement(placement) {
973
996
  /**
974
997
  * Opens a page for a channel guide discovery walk in a browser window of its own, and marks it as belonging to that window.
975
998
  *
976
- * A document renders only while it is the active tab of a window that is not minimized, and the shared window rests minimized whenever nothing is capturing and
977
- * no sign-in holds it on screen, with its selected tab belonging to the user. A guide walk needs its page to render - an observer-driven channel rail fills its
978
- * tiles from rendering updates, and a virtualized grid re-renders as the walk scrolls it - yet it is never captured, so it has no claim on the shared window's
979
- * presentation and no business moving the user's selection. Its own window resolves both: the page is the active tab there from the moment it exists.
999
+ * A document renders only while Chrome presents it: the active tab of a window the desktop is showing, or a page Chrome counts as captured. The shared window
1000
+ * rests minimized whenever nothing is capturing and no sign-in holds it on screen, with its selected tab belonging to the user. A guide walk needs its page to
1001
+ * render - an observer-driven channel rail fills its tiles from rendering updates, a virtualized grid re-renders as the walk scrolls it, and every wait the walk
1002
+ * makes polls on the page's animation frames - yet it is never captured, so it has no claim on the shared window's presentation and no business moving the
1003
+ * user's selection. A window of its own resolves the tab half: the page is that window's active tab from the moment it exists. The window is created in the
1004
+ * background, so Chrome shows it inactive and moves no focus (measured 2026-08-31), and such a window's document is one Chrome does not present on its own: it
1005
+ * reports itself hidden and unfocused and delivers no animation frame beyond a document load's first ones (measured 2026-09-13). Focus emulation resolves the
1006
+ * presentation half, for the page's whole life - Puppeteer's own emulateFocusedPage carries the mechanism, on the page's own session.
980
1007
  *
981
- * The window is created in the background, so Chrome shows it inactive and moves no focus (measured 2026-08-31), and at the shared window's own placement, so
982
- * the window placement Chrome persists for the profile never changes - readWindowPlacement carries the reasoning. The caller declares the layout surface on
983
- * the page and owns its registration, and closing the page closes the window with it.
1008
+ * The window opens at the shared window's own placement, so the window placement Chrome persists for the profile never changes - readWindowPlacement carries
1009
+ * the reasoning. The caller declares the layout surface on the page and owns its registration, and closing the page closes the window with it.
984
1010
  * @param browser - The browser to open the window in.
985
1011
  * @returns The page, as the active tab of its own window.
986
1012
  */
@@ -990,6 +1016,24 @@ export async function createDiscoveryPage(browser) {
990
1016
  // The mark is set with nothing awaited between the creation and it, so no page read can observe this page as a candidate to carry a shared-window command.
991
1017
  const page = await browser.newPage({ background: true, type: "window", windowBounds: placement ? mirrorPlacement(placement) : undefined });
992
1018
  ownWindowPages.add(page);
1019
+ /* The page is presented from before its first load, so every wait a walk makes - a visible selector, a condition polled from inside the page - runs as it
1020
+ * would in a window the desktop is showing. Chrome counts a focus-emulated page as captured, which is what presents its document, and Puppeteer keeps the
1021
+ * state on the page's own session for as long as the page lives (measured 2026-09-13). The mark above stays first: nothing is awaited between the creation
1022
+ * and it. A page that refuses the emulation is one no walk can use, and nothing else holds it yet - the caller never receives it and the managed-page sweep
1023
+ * never sees it - so the creator closes it, which closes the window with it, before the failure propagates.
1024
+ */
1025
+ try {
1026
+ await page.emulateFocusedPage(true);
1027
+ }
1028
+ catch (error) {
1029
+ try {
1030
+ await page.close();
1031
+ }
1032
+ catch {
1033
+ // The page is already gone, which is the state the close was asking for.
1034
+ }
1035
+ throw error;
1036
+ }
993
1037
  LOG.debug("browser:lifecycle", "Opened the discovery page in a window of its own, %s.", placement ? "mirroring the shared window's placement" : "with no shared window to read a placement from");
994
1038
  return page;
995
1039
  }
@@ -1026,10 +1070,10 @@ const activationHeals = new WeakMap();
1026
1070
  * @param page - The capture page this callback re-affirms.
1027
1071
  * @param reaffirm - The re-issue to invoke. A parameter rather than a direct call so the callback's behavior can be driven on its own: what it does with the page
1028
1072
  * it was built for, what schedule it keeps, and what it does with a rejection, is the whole of its contract.
1029
- * @param clock - The time source the ladder's waits run on. Defaults to realClock.
1073
+ * @param clock - The time source the ladder's waits run on. Defaults to the system clock.
1030
1074
  * @returns The callback, which never rejects. Its promise settles once the ladder is spent, which the page-side listener does not wait on.
1031
1075
  */
1032
- export function makeFocusReaffirmCallback(page, reaffirm, clock = realClock) {
1076
+ export function makeFocusReaffirmCallback(page, reaffirm, clock = systemClock) {
1033
1077
  // The generation this closure hands out. It only ever increments: an invocation claims the next one and owns the rung schedule until a later invocation claims
1034
1078
  // a higher one.
1035
1079
  let generation = 0;
@@ -1048,7 +1092,7 @@ export function makeFocusReaffirmCallback(page, reaffirm, clock = realClock) {
1048
1092
  let previousOffsetMs = 0;
1049
1093
  for (const offsetMs of ACTIVATION_REAFFIRM_LADDER_MS) {
1050
1094
  // eslint-disable-next-line no-await-in-loop -- The pacing is the point: each rung waits out the distance left to its own offset before anything else runs.
1051
- await clock.sleep(offsetMs - previousOffsetMs);
1095
+ await clock.delay(offsetMs - previousOffsetMs);
1052
1096
  // A newer activation owns the schedule, and it carries its own rungs timed from its own moment, so this ladder has nothing left to contribute.
1053
1097
  if (generation !== mine) {
1054
1098
  return;
@@ -1060,7 +1104,7 @@ export function makeFocusReaffirmCallback(page, reaffirm, clock = realClock) {
1060
1104
  };
1061
1105
  }
1062
1106
  // The production collaborators.
1063
- const defaultActivationHealDeps = { clock: realClock, reaffirm: reaffirmCaptureSurface };
1107
+ const defaultActivationHealDeps = { clock: systemClock, reaffirm: reaffirmCaptureSurface };
1064
1108
  /**
1065
1109
  * Installs the tab-activation heal on a capture page. Chrome composes the capture of a selected tab from the window's fitted presentation, so the instant a
1066
1110
  * capture tab becomes the selected one that capture starts recording a clipped view of itself; the re-issue ladder this installs moves the composition back to
@@ -1298,10 +1342,21 @@ function relinquishBrowserReadiness(streamTerminationReason) {
1298
1342
  if (clearLoginState() && !gracefulShutdownInProgress) {
1299
1343
  LOG.info("Login mode ended due to browser readiness loss.");
1300
1344
  }
1301
- // Terminate every active stream using the authoritative terminateStream for consistent cleanup. Kept even during graceful shutdown as a defensive measure -
1302
- // terminateStream() is safe to call more than once, so if streams were already terminated by the caller, this harmlessly iterates an empty array.
1303
- for (const streamInfo of getAllStreams()) {
1304
- terminateStream(streamInfo.id, streamInfo.info.storeKey, streamTerminationReason);
1345
+ /* Terminate every active stream through the streaming layer's authoritative terminator, so a readiness loss cleans up exactly the way every other termination
1346
+ * path does. Kept even during graceful shutdown as a defensive measure - termination is safe to call more than once, so streams the caller already tore down
1347
+ * leave this a harmless pass over an empty array.
1348
+ */
1349
+ const activeStreams = getAllStreams();
1350
+ if (streamTerminator) {
1351
+ for (const streamInfo of activeStreams) {
1352
+ streamTerminator(streamInfo.id, streamInfo.info.storeKey, streamTerminationReason);
1353
+ }
1354
+ }
1355
+ else {
1356
+ /* An unwired terminator means the streaming layer never loaded, so nothing here can run the streams' cleanup sequence. Report it and keep going: the page
1357
+ * tracking and status emission below are still worth doing, and abandoning them would compound a wiring failure with a half-finished teardown.
1358
+ */
1359
+ LOG.error("Stream cleanup was skipped on browser readiness loss because no stream terminator is wired. Active streams left untouched: %s.", activeStreams.length);
1305
1360
  }
1306
1361
  // The session those streams captured on is over, so whatever page tracking survived their termination belongs to a browser that is gone.
1307
1362
  clearPageTracking();
@@ -1325,24 +1380,29 @@ function handleBrowserDisconnect() {
1325
1380
  relinquishBrowserReadiness("browser disconnect");
1326
1381
  }
1327
1382
  /**
1328
- * Records that a still-connected browser can no longer start captures - a mid-life capture death that no "disconnected" event would surface - and schedules the
1383
+ * Records that a still-connected browser can no longer start captures - a mid-life capture death that no "disconnected" event would surface - and runs the
1329
1384
  * relaunch that cures it. This is the single recovery action for a browser that is alive and still serving: the mark lives on the supervisor's ready state, so its
1330
1385
  * running captures continue untouched, new stream requests are refused at acquire() with a 503 back-off, the recovery ladder stops offering tab replacement, and
1331
- * the relaunch waits for the registry to empty. Exported for the streaming layer to call once its probe or its wedge has produced the verdict.
1386
+ * the relaunch waits until nothing depends on the browser. Exported for the streaming layer to call once its probe or its wedge has produced the verdict.
1387
+ *
1388
+ * The returned promise settles once the relaunch this mark triggered has settled, which is what a caller that means to use the fresh browser awaits: the teardown
1389
+ * holds the supervisor in its draining state, where acquire() rejects rather than joins, so acquiring any earlier draws that rejection instead of the new
1390
+ * instance. A caller with nothing waiting on the outcome voids it and the relaunch proceeds on its own.
1332
1391
  *
1333
1392
  * The restart trigger uses this function's return value rather than the supervisor's transition observer, deliberately. The observer runs inside transition(), so a
1334
1393
  * restart begun there would call noteReadinessLost re-entrantly while the marking transition's notification is still on the stack. The observer stays a reporter -
1335
1394
  * the alarm and the status emit - and the caller that holds the verdict acts on it once the transition has completed.
1336
1395
  * @param browser - The specific browser instance the caller verified as unable to start captures.
1337
1396
  * @param reason - A short description of the evidence behind the verdict, carried in the alarm log and the impairment record.
1397
+ * @returns A promise settling once the relaunch this mark triggered has settled, or at once when the verdict landed on nothing.
1338
1398
  */
1339
- export function noteBrowserCaptureImpaired(browser, reason) {
1399
+ export async function noteBrowserCaptureImpaired(browser, reason) {
1340
1400
  // A false answer means the verdict landed on nothing: the instance was superseded by a disconnect and relaunch while the caller was confirming it, or the browser
1341
1401
  // already carries a mark whose alarm and status emit have already fired. Either way there is no new state to act on.
1342
1402
  if (!supervisor.noteCaptureImpaired(browser, reason)) {
1343
1403
  return;
1344
1404
  }
1345
- restartBrowserIfImpairedAndIdle();
1405
+ await restartBrowserIfImpairedAndIdle();
1346
1406
  }
1347
1407
  /**
1348
1408
  * Provides access to the capture-ready browser, launching one if needed. This is the single gated entry point for all browser access: it delegates to the
@@ -1430,8 +1490,9 @@ async function launchReadyBrowser() {
1430
1490
  const gpuSuffix = gpu ? formatGpuSuffix(gpu) : "";
1431
1491
  LOG.info("Chrome ready: %s%s.", chromeVersion, gpuSuffix);
1432
1492
  LOG.debug("timing:browser", "Browser ready. Total: %sms.", browserElapsed());
1433
- // Start background precaching of selected service channel lineups. Fire-and-forget - the setTimeout inside startPrecaching() defers the work until after this
1434
- // launch settles and the supervisor has published the ready browser, so its getCurrentBrowser() resolves immediately rather than re-entering this launch.
1493
+ // Start background precaching of selected service channel lineups. Fire-and-forget - the delay startPrecaching() arms on its own clock defers the work until
1494
+ // after this launch settles and the supervisor has published the ready browser, so its getCurrentBrowser() resolves immediately rather than re-entering this
1495
+ // launch.
1435
1496
  startPrecaching();
1436
1497
  /* The reaper belongs to the browser instance the supervisor is about to publish: puppeteer drops a closed browser's listeners along with the instance, and
1437
1498
  * a relaunch comes back through this same path and installs its own. It goes in ahead of the disconnect handler so that handler stays the last step before
@@ -1621,8 +1682,10 @@ export async function closeBrowser() {
1621
1682
  * 2. Exclude pages associated with active streams, and pages an operation still holds in flight
1622
1683
  * 3. Apply a grace period before closing (to handle race conditions)
1623
1684
  * 4. Preserve at least one page to keep the browser alive
1685
+ * @param now - The instant the sweep judges staleness against, read from the interval's own clock so the staleness clocks this sweep starts and the ones it
1686
+ * later reads are stamped on one time source.
1624
1687
  */
1625
- export async function cleanupStalePages() {
1688
+ export async function cleanupStalePages(now) {
1626
1689
  // Guard against calling this when no ready browser is running.
1627
1690
  const browser = supervisor.current();
1628
1691
  if (!browser?.connected) {
@@ -1644,7 +1707,6 @@ export async function cleanupStalePages() {
1644
1707
  }
1645
1708
  }
1646
1709
  }
1647
- const now = Date.now();
1648
1710
  // Project the browser's pages into the shape the decision core reads: the managed ids in the browser's own order, with undefined standing in for pages we
1649
1711
  // did not create, plus a lookup back to the Page objects so the ids it returns can be resolved to something closable.
1650
1712
  const idToPage = new Map();
@@ -1699,63 +1761,96 @@ export async function cleanupStalePages() {
1699
1761
  }
1700
1762
  }
1701
1763
  /**
1702
- * Starts the periodic stale page cleanup interval. This should be called once during server startup, after the browser is initialized. The interval runs
1703
- * indefinitely until stopStalePageCleanup() is called (typically during graceful shutdown).
1764
+ * Starts the periodic stale page cleanup. This should be called once during server startup, after the browser is initialized. The sweep runs indefinitely until
1765
+ * stopStalePageCleanup() is called (typically during graceful shutdown). A second start while one is running changes nothing.
1766
+ * @param clock - The clock the sweep's interval arms on and whose reading each sweep judges staleness against, so the cadence and the judgment share one time
1767
+ * source. Defaults to the system clock.
1704
1768
  */
1705
- export function startStalePageCleanup() {
1706
- stalePageCleanupInterval = setInterval(() => { void cleanupStalePages(); }, CONFIG.recovery.stalePageCleanupInterval);
1769
+ export function startStalePageCleanup(clock = systemClock) {
1770
+ if (stalePageTimers) {
1771
+ return;
1772
+ }
1773
+ const timers = new TimerRegistry({ clock });
1774
+ timers.setInterval(STALE_PAGE_SWEEP_KEY, () => { void cleanupStalePages(clock.now()); }, CONFIG.recovery.stalePageCleanupInterval);
1775
+ stalePageTimers = timers;
1707
1776
  }
1708
1777
  /**
1709
- * Stops the stale page cleanup interval. This should be called during graceful shutdown to prevent the cleanup from running after we've started shutting down
1710
- * the browser and streams.
1778
+ * Stops the periodic stale page cleanup. This should be called during graceful shutdown to prevent the sweep from running after we've started shutting down the
1779
+ * browser and streams. Disposing the registry drains the interval, and a stop with nothing running is a no-op.
1711
1780
  */
1712
1781
  export function stopStalePageCleanup() {
1713
- if (stalePageCleanupInterval) {
1714
- clearInterval(stalePageCleanupInterval);
1715
- stalePageCleanupInterval = null;
1716
- }
1782
+ stalePageTimers?.dispose();
1783
+ stalePageTimers = null;
1717
1784
  }
1718
- /* Browser restart functions. One routine performs the restart; what differs is the cause that reaches it.
1719
- *
1720
- * The maintenance cause is opportunistic and age-driven: the check runs on a 30-second interval and, when the browser exceeds BROWSER_MAX_AGE with zero active
1721
- * streams, starts a quiet period timer. The quiet timer is cancelled if a stream starts, ensuring active viewers are never disrupted. When the timer expires, the
1722
- * browser is closed and immediately re-launched.
1785
+ /**
1786
+ * Decides whether the browser may be torn down for the given cause, from facts read at the call. Each cause asks a different question of the same registry, so
1787
+ * the decision is stated once here rather than spelled out at each guard.
1723
1788
  *
1724
- * The impairment cause is a repair rather than hygiene: a browser that can no longer start captures is unusable for new tunes no matter how young it is, so age and
1725
- * the quiet period do not apply to it. It relaunches the moment the registry empties, which the mark itself, every stream end, and the periodic tick each check
1726
- * for.
1789
+ * Maintenance is opportunistic housekeeping, so it waits for an empty registry outright: a pending entry is a tune in progress, and replacing the browser under
1790
+ * one would fail it for nothing better than a fresher instance. Impairment is a repair the tunes themselves are waiting on, so it asks the narrower question of
1791
+ * what a teardown would actually destroy - a stream established on this browser, whose entry holds its page, or a page an operation still holds in flight. A tune
1792
+ * refused a capture start holds neither while it waits for the relaunch, so the very pending entry that maintenance would defer to is not a reason to leave an
1793
+ * unusable browser in place. A tune that has acquired the browser but not yet opened its page falls outside every dependency source for the same reason and
1794
+ * deliberately so: it holds nothing a relaunch would destroy, neither a page nor a capture, and its own capture start would meet the impaired browser in any
1795
+ * case, so the relaunch may run under it.
1796
+ * @param cause - Why the restart wants to run.
1797
+ * @param facts - The registry facts read at the call.
1798
+ * @returns True when the browser may be torn down for this cause.
1799
+ */
1800
+ export function isBrowserIdleForRestart(cause, facts) {
1801
+ switch (cause) {
1802
+ case "impairment": {
1803
+ return !facts.establishedStreams && (facts.inFlightPages === 0);
1804
+ }
1805
+ case "maintenance": {
1806
+ return facts.streamCount === 0;
1807
+ }
1808
+ }
1809
+ }
1810
+ /**
1811
+ * Reads the registry facts the idleness decision is made from, at the moment of the call.
1812
+ * @returns The facts.
1727
1813
  */
1814
+ function readRestartFacts() {
1815
+ return { establishedStreams: hasEstablishedStreams(), inFlightPages: inFlightPageIds.size, streamCount: getStreamCount() };
1816
+ }
1728
1817
  /**
1729
1818
  * Relaunches a browser that can no longer start captures, as soon as nothing depends on it. Every trigger routes here - the mark itself, each stream termination,
1730
1819
  * and the periodic restart check as the backstop for a moment when the other two could not act (login mode above all) - so the decision lives in one place rather
1731
1820
  * than being re-derived by each. Idleness rather than age is the condition, because the mark makes the browser useless for new tunes immediately while its running
1732
- * captures are still worth finishing, so the earliest safe moment is exactly the moment the last of them ends.
1821
+ * captures are still worth finishing, so the earliest safe moment is exactly the moment the last thing depending on this browser lets go of it.
1822
+ *
1823
+ * The returned promise settles once the relaunch has settled, for the caller that goes on to acquire the fresh browser; a trigger with nothing waiting on the
1824
+ * outcome voids it. An early return resolves at once, because there is nothing for such a caller to wait for.
1825
+ * @returns A promise settling once the relaunch has settled, or at once when no relaunch runs.
1733
1826
  */
1734
- export function restartBrowserIfImpairedAndIdle() {
1827
+ export async function restartBrowserIfImpairedAndIdle() {
1735
1828
  if (supervisor.captureImpairment() === null) {
1736
1829
  return;
1737
1830
  }
1738
1831
  // A marked browser's restart belongs to this path, so a maintenance quiet period pending from before the mark is retired the first time any trigger observes the
1739
- // mark - whether or not the registry is idle yet - rather than being left to fire against the browser the relaunch will have replaced.
1832
+ // mark - whether or not the browser is idle yet - rather than being left to fire against the browser the relaunch will have replaced.
1740
1833
  cancelRestartQuietTimer();
1741
- if (getStreamCount() > 0) {
1834
+ if (!isBrowserIdleForRestart("impairment", readRestartFacts())) {
1742
1835
  return;
1743
1836
  }
1744
- void executeBrowserRestart("impairment");
1837
+ await executeBrowserRestart("impairment");
1745
1838
  }
1746
1839
  /**
1747
1840
  * Checks whether the browser qualifies for a restart. Called periodically by the restart check interval. The check skips when any of these conditions hold:
1748
1841
  * graceful shutdown in progress, login mode active, browser not ready. A marked browser is handed to the impairment path and the tick ends there. Otherwise the
1749
1842
  * tick drives the supervisor's health-gated governor reset and applies the maintenance rules: skip below the age threshold, cancel any pending quiet timer while
1750
1843
  * active streams exist (streams started during the quiet period reset the countdown), and otherwise start a quiet timer if one is not already running.
1844
+ * @param timers - The restart owner's registry, whose quiet period this check arms and reads. Handed in by the interval closure, so the check never reads the
1845
+ * nullable module binding and needs no null branch of its own.
1751
1846
  */
1752
- function checkBrowserRestart() {
1847
+ function checkBrowserRestart(timers) {
1753
1848
  // Skip if the server is shutting down or login mode is active.
1754
1849
  if (gracefulShutdownInProgress || isLoginModeActive()) {
1755
1850
  return;
1756
1851
  }
1757
1852
  // Read the ready browser and its launch time from the supervisor. Both are non-null only in the ready state, so a single guard covers "no ready browser." The
1758
- // launch time is on the supervisor's clock (realClock.now), so age must be measured against the same clock - not Date.now() - or the units would not match.
1853
+ // launch time is read from the supervisor's clock (systemClock.now), so age is measured against that same clock rather than against any other time source.
1759
1854
  const browser = supervisor.current();
1760
1855
  const launchTime = supervisor.currentLaunchTime();
1761
1856
  if (!browser?.connected || (launchTime === null)) {
@@ -1767,7 +1862,7 @@ function checkBrowserRestart() {
1767
1862
  * all. The quiet period this tick would otherwise cancel below is retired inside the call, so the early return leaves no timer unobserved.
1768
1863
  */
1769
1864
  if (supervisor.captureImpairment() !== null) {
1770
- restartBrowserIfImpairedAndIdle();
1865
+ void restartBrowserIfImpairedAndIdle();
1771
1866
  return;
1772
1867
  }
1773
1868
  // Health-gated governor reset. On every eligible tick, tell the supervisor the browser is still ready; once it has been continuously ready for the policy's
@@ -1776,22 +1871,22 @@ function checkBrowserRestart() {
1776
1871
  LOG.info("Browser capture readiness has been sustained; the relaunch governor has reset to its normal state.");
1777
1872
  }
1778
1873
  // Skip if the browser has not exceeded the maximum age.
1779
- const age = realClock.now() - launchTime;
1874
+ const age = systemClock.now() - launchTime;
1780
1875
  if (age < BROWSER_MAX_AGE) {
1781
1876
  return;
1782
1877
  }
1783
1878
  // If there are active streams, cancel any pending quiet timer and return. Streams that start during the quiet period reset the countdown.
1784
1879
  if (getStreamCount() > 0) {
1785
- if (restartQuietTimer) {
1880
+ if (timers.has(RESTART_QUIET_KEY)) {
1786
1881
  LOG.debug("browser:lifecycle", "Browser restart quiet period cancelled - streams are active.");
1787
1882
  }
1788
1883
  cancelRestartQuietTimer();
1789
1884
  return;
1790
1885
  }
1791
1886
  // No active streams and the browser is old enough. Start the quiet timer if one is not already running.
1792
- if (!restartQuietTimer) {
1887
+ if (!timers.has(RESTART_QUIET_KEY)) {
1793
1888
  LOG.debug("browser:lifecycle", "Browser uptime exceeds threshold. Quiet period started - restart will proceed if no streams start within %s minutes.", Math.round(BROWSER_RESTART_QUIET_PERIOD / 60000));
1794
- restartQuietTimer = setTimeout(() => {
1889
+ timers.setTimeout(RESTART_QUIET_KEY, () => {
1795
1890
  void executeBrowserRestart("maintenance");
1796
1891
  }, BROWSER_RESTART_QUIET_PERIOD);
1797
1892
  }
@@ -1807,14 +1902,16 @@ async function executeBrowserRestart(cause) {
1807
1902
  // restart the fresh instance a second time.
1808
1903
  cancelRestartQuietTimer();
1809
1904
  // Final guard: re-check all preconditions. Conditions may have changed during the quiet period (e.g., a stream started just before the timer fired, login mode
1810
- // was activated, or the browser disconnected on its own). Reading current()/currentLaunchTime() together keeps the ready-state check and the age source consistent.
1905
+ // was activated, or the browser disconnected on its own). Reading current()/currentLaunchTime() together keeps the ready-state check and the age source
1906
+ // consistent. The idleness question is asked for this restart's own cause, because what a maintenance sweep must defer to and what a repair must defer to are
1907
+ // not the same set of streams.
1811
1908
  const browser = supervisor.current();
1812
1909
  const launchTime = supervisor.currentLaunchTime();
1813
- if (gracefulShutdownInProgress || isLoginModeActive() || (getStreamCount() > 0) || !browser?.connected || (launchTime === null)) {
1910
+ if (gracefulShutdownInProgress || isLoginModeActive() || !isBrowserIdleForRestart(cause, readRestartFacts()) || !browser?.connected || (launchTime === null)) {
1814
1911
  LOG.debug("browser:lifecycle", "Browser restart aborted - preconditions no longer met.");
1815
1912
  return;
1816
1913
  }
1817
- const age = realClock.now() - launchTime;
1914
+ const age = systemClock.now() - launchTime;
1818
1915
  const hours = Math.floor(age / 3600000);
1819
1916
  const minutes = Math.floor((age % 3600000) / 60000);
1820
1917
  if (cause === "impairment") {
@@ -1861,22 +1958,25 @@ async function executeBrowserRestart(cause) {
1861
1958
  }
1862
1959
  }
1863
1960
  /**
1864
- * Starts the periodic browser restart eligibility check. This should be called once during server startup, after the browser is initialized. The interval runs
1865
- * indefinitely until stopBrowserRestartChecking() is called (typically during graceful shutdown).
1961
+ * Starts the periodic browser restart eligibility check. This should be called once during server startup, after the browser is initialized. The check runs
1962
+ * indefinitely until stopBrowserRestartChecking() is called (typically during graceful shutdown). A second start while one is running changes nothing.
1963
+ * @param clock - The clock the check's interval and the quiet period it arms both run on. Defaults to the system clock.
1866
1964
  */
1867
- export function startBrowserRestartChecking() {
1868
- restartCheckInterval = setInterval(checkBrowserRestart, BROWSER_RESTART_CHECK_INTERVAL);
1965
+ export function startBrowserRestartChecking(clock = systemClock) {
1966
+ if (restartTimers) {
1967
+ return;
1968
+ }
1969
+ const timers = new TimerRegistry({ clock });
1970
+ timers.setInterval(RESTART_CHECK_KEY, () => { checkBrowserRestart(timers); }, BROWSER_RESTART_CHECK_INTERVAL);
1971
+ restartTimers = timers;
1869
1972
  }
1870
1973
  /**
1871
- * Stops the browser restart checking interval and cancels any pending quiet timer. This should be called during graceful shutdown to prevent a restart from
1872
- * racing with server shutdown.
1974
+ * Stops the periodic browser restart eligibility check. This should be called during graceful shutdown to prevent a restart from racing with server shutdown.
1975
+ * Disposing the registry drains the check and any pending quiet period together, and a stop with nothing running is a no-op.
1873
1976
  */
1874
1977
  export function stopBrowserRestartChecking() {
1875
- if (restartCheckInterval) {
1876
- clearInterval(restartCheckInterval);
1877
- restartCheckInterval = null;
1878
- }
1879
- cancelRestartQuietTimer();
1978
+ restartTimers?.dispose();
1979
+ restartTimers = null;
1880
1980
  }
1881
1981
  /* When running as a packaged executable (created by the `pkg` tool), the application is bundled into a single binary. Node modules like puppeteer-stream are
1882
1982
  * included in the bundle, but Chrome cannot load extensions from within the packaged binary - it needs actual files on the filesystem.