@gloom-sh/terminal 0.19.1 → 0.19.3

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 (554) hide show
  1. package/README.md +7 -1
  2. package/package.json +2 -1
  3. package/patches/electrobun@1.18.1.patch +108 -1
  4. package/src/account/upgrade-dialog.tsx +4 -23
  5. package/src/api-client/coalesce.ts +118 -0
  6. package/src/api-client/credit-boards.ts +13 -0
  7. package/src/api-client/crypto-etf-flows.ts +6 -0
  8. package/src/api-client/crypto-markets.ts +40 -0
  9. package/src/api-client/data.ts +201 -14
  10. package/src/api-client/equity-screener.ts +10 -0
  11. package/src/api-client/errors.ts +58 -0
  12. package/src/api-client/gpu.ts +29 -0
  13. package/src/api-client/index.ts +5 -2
  14. package/src/api-client/plan-access.ts +10 -1
  15. package/src/api-client/plan-rules.ts +35 -0
  16. package/src/api-client/research-activity.ts +1 -32
  17. package/src/api-client/types.ts +21 -1
  18. package/src/app/attention-notice.tsx +94 -0
  19. package/src/app/runtime/startup.ts +3 -1
  20. package/src/app.tsx +23 -1
  21. package/src/architecture/core-builtin-allowlist.json +2 -1
  22. package/src/capabilities/chart-series.ts +2 -1
  23. package/src/capabilities/factories.ts +1 -0
  24. package/src/cli/ascii-output.ts +42 -0
  25. package/src/cli/base-converter.ts +10 -2
  26. package/src/cli/command-options.ts +16 -1
  27. package/src/cli/commands/crypto-hints.ts +23 -4
  28. package/src/cli/commands/market.ts +101 -27
  29. package/src/cli/commands/news-search.ts +24 -5
  30. package/src/cli/commands/overview.ts +14 -10
  31. package/src/cli/commands/search.ts +84 -25
  32. package/src/cli/commands/system.ts +22 -30
  33. package/src/cli/commands/ticker.ts +49 -9
  34. package/src/cli/commands/version.ts +33 -0
  35. package/src/cli/context.ts +6 -9
  36. package/src/cli/data-dir.ts +7 -0
  37. package/src/cli/desktop-pane-shot-api-policy.ts +85 -0
  38. package/src/cli/desktop-pane-shot-browser.ts +320 -0
  39. package/src/cli/desktop-pane-shot-routes.ts +26 -1
  40. package/src/cli/desktop-pane-shot.ts +196 -256
  41. package/src/cli/early-command.ts +42 -0
  42. package/src/cli/earnings-calendar.ts +9 -0
  43. package/src/cli/entry.ts +19 -13
  44. package/src/cli/errors.ts +12 -2
  45. package/src/cli/glossary.ts +2 -1
  46. package/src/cli/help.ts +1 -1
  47. package/src/cli/history-export.ts +22 -2
  48. package/src/cli/history-rows.ts +189 -11
  49. package/src/cli/index.ts +18 -1
  50. package/src/cli/known-gaps.ts +51 -5
  51. package/src/cli/listing-arg.ts +63 -10
  52. package/src/cli/options.ts +1 -1
  53. package/src/cli/output-file.ts +60 -13
  54. package/src/cli/pane-functions/catalog.ts +14 -3
  55. package/src/cli/pane-functions/data.ts +1 -1
  56. package/src/cli/pane-functions/dom.ts +45 -0
  57. package/src/cli/pane-functions/freshness.ts +46 -9
  58. package/src/cli/pane-functions/function-help.ts +17 -9
  59. package/src/cli/pane-functions/headless.ts +22 -14
  60. package/src/cli/pane-functions/index.ts +6 -2
  61. package/src/cli/pane-functions/options.ts +6 -2
  62. package/src/cli/pane-functions/resolver.ts +27 -5
  63. package/src/cli/pane-functions/screenshot-assessment.ts +33 -0
  64. package/src/cli/pane-functions/screenshot-bridge.ts +94 -13
  65. package/src/cli/pane-functions/screenshot-http.ts +70 -0
  66. package/src/cli/pane-functions/screenshot-render.ts +19 -5
  67. package/src/cli/quote-answer.ts +49 -6
  68. package/src/cli/registry.ts +41 -13
  69. package/src/cli/result.ts +12 -6
  70. package/src/cli/ticker-resolution.ts +2 -1
  71. package/src/cli/types.ts +2 -0
  72. package/src/components/chart/composite/format.ts +3 -0
  73. package/src/components/chart-table/header.tsx +1 -1
  74. package/src/components/chart-table/layout.ts +28 -11
  75. package/src/components/command-bar/action-runtime.ts +4 -0
  76. package/src/components/command-bar/commands/direct/index.ts +18 -33
  77. package/src/components/command-bar/commands/direct/runtime.ts +5 -0
  78. package/src/components/command-bar/commands/registry.ts +10 -1
  79. package/src/components/command-bar/routes/root/command-items.ts +9 -4
  80. package/src/components/command-bar/routes/root/runtime.ts +51 -6
  81. package/src/components/command-bar/selection-runtime.ts +12 -29
  82. package/src/components/command-bar/surface/index.tsx +9 -3
  83. package/src/components/company-logo.tsx +2 -1
  84. package/src/components/layout/detached-pane-shell.tsx +2 -2
  85. package/src/components/layout/header.tsx +10 -5
  86. package/src/components/layout/market-summary.ts +25 -7
  87. package/src/components/layout/pane/footer/index.tsx +8 -2
  88. package/src/components/layout/pane/footer/model.ts +73 -6
  89. package/src/components/layout/pane/header.tsx +2 -2
  90. package/src/components/layout/pane/status-footer.ts +2 -1
  91. package/src/components/market-board/index.tsx +3 -2
  92. package/src/components/onboarding/desks-step.tsx +12 -4
  93. package/src/components/onboarding/first-run-workspace.ts +81 -4
  94. package/src/components/onboarding/onboarding-frame.tsx +4 -2
  95. package/src/components/onboarding/onboarding-keyboard.ts +8 -5
  96. package/src/components/onboarding/onboarding-wizard.tsx +60 -26
  97. package/src/components/onboarding/portfolio-step/positions-panel.tsx +7 -2
  98. package/src/components/onboarding/run-setup-again.ts +24 -0
  99. package/src/components/onboarding/use-onboarding-progress.ts +33 -15
  100. package/src/components/onboarding/use-onboarding-upgrade.ts +2 -6
  101. package/src/components/onboarding/wizard-model.ts +57 -3
  102. package/src/components/onboarding/wizard-positions.ts +1 -1
  103. package/src/components/ui/delayed-mark.tsx +56 -0
  104. package/src/components/ui/display.tsx +16 -5
  105. package/src/components/ui/external-link.tsx +3 -6
  106. package/src/components/ui/icon.tsx +16 -3
  107. package/src/components/ui/logo-mark.tsx +8 -5
  108. package/src/components/ui/stat-grid.tsx +263 -19
  109. package/src/core/app-services.ts +11 -2
  110. package/src/data/app-persistence.ts +9 -7
  111. package/src/data/config/store/node.ts +11 -5
  112. package/src/data/config/store/normalize.ts +16 -3
  113. package/src/data/plugin-cache.ts +15 -0
  114. package/src/data/plugin-state-store.ts +81 -24
  115. package/src/data/profile-permissions.ts +47 -0
  116. package/src/data/resource-store.ts +165 -90
  117. package/src/data/session-store.ts +31 -8
  118. package/src/data/sqlite/access.ts +324 -0
  119. package/src/data/sqlite/checkpointer.ts +285 -0
  120. package/src/data/sqlite/database.ts +80 -4
  121. package/src/data/sqlite/retry.ts +50 -9
  122. package/src/data/ticker-store.ts +57 -23
  123. package/src/docs/snapshot.json +191 -78
  124. package/src/i18n/es.ts +18 -2
  125. package/src/i18n/ja.ts +18 -2
  126. package/src/i18n/ko.ts +18 -2
  127. package/src/i18n/zh-cn.ts +18 -2
  128. package/src/i18n/zh-tw.ts +18 -2
  129. package/src/layout/desk-keys.ts +4 -1
  130. package/src/layout/desks.ts +38 -5
  131. package/src/layout-marketplace/gallery-desktop.tsx +45 -16
  132. package/src/market-data/batched-reads.ts +16 -0
  133. package/src/market-data/coordinator/auxiliary.ts +4 -2
  134. package/src/market-data/coordinator/index.ts +11 -2
  135. package/src/market-data/futures-board.ts +1 -1
  136. package/src/market-data/futures-contract.ts +3 -1
  137. package/src/market-data/market/index-level.ts +6 -4
  138. package/src/market-data/published-apac-sessions.ts +6 -0
  139. package/src/market-data/realized-volatility.ts +4 -2
  140. package/src/news/aggregator.ts +7 -4
  141. package/src/news/news-model.ts +11 -0
  142. package/src/news/story-id.ts +22 -0
  143. package/src/news/ticker-symbols.ts +17 -1
  144. package/src/notifications/app-notifier.ts +35 -14
  145. package/src/plugins/auto-update-node.ts +10 -6
  146. package/src/plugins/builtin/account-management/form-components.tsx +1 -1
  147. package/src/plugins/builtin/account-management/index.tsx +2 -3
  148. package/src/plugins/builtin/account-management/model.ts +1 -1
  149. package/src/plugins/builtin/alerts/cli-forms.ts +15 -1
  150. package/src/plugins/builtin/alerts/events-pane.tsx +7 -5
  151. package/src/plugins/builtin/alerts/events.ts +10 -2
  152. package/src/plugins/builtin/alerts/index.tsx +5 -5
  153. package/src/plugins/builtin/alerts/research-builder.ts +5 -1
  154. package/src/plugins/builtin/alerts/research-rules.ts +18 -0
  155. package/src/plugins/builtin/alerts/targets.ts +4 -1
  156. package/src/plugins/builtin/attention/headless.ts +1 -1
  157. package/src/plugins/builtin/attention/index.tsx +2 -4
  158. package/src/plugins/builtin/attention/listing.ts +1 -1
  159. package/src/plugins/builtin/attention/series.ts +1 -1
  160. package/src/plugins/builtin/awards/index.tsx +1 -2
  161. package/src/plugins/builtin/backtest/client.ts +1 -0
  162. package/src/plugins/builtin/backtest/index.tsx +1 -2
  163. package/src/plugins/builtin/backtest/rules.ts +3 -1
  164. package/src/plugins/builtin/backtest/window.ts +8 -7
  165. package/src/plugins/builtin/broker-manager/detail.tsx +16 -3
  166. package/src/plugins/builtin/broker-manager/index.tsx +1 -1
  167. package/src/plugins/builtin/buildout/cells.tsx +5 -0
  168. package/src/plugins/builtin/buildout/company/flow.tsx +367 -0
  169. package/src/plugins/builtin/buildout/company/id.ts +2 -0
  170. package/src/plugins/builtin/buildout/company/model.ts +247 -0
  171. package/src/plugins/builtin/buildout/company/pane.tsx +317 -0
  172. package/src/plugins/builtin/buildout/data-runtime.ts +31 -10
  173. package/src/plugins/builtin/buildout/facilities.ts +309 -0
  174. package/src/plugins/builtin/buildout/format.ts +9 -0
  175. package/src/plugins/builtin/buildout/index.tsx +51 -5
  176. package/src/plugins/builtin/buildout/model/index.ts +62 -10
  177. package/src/plugins/builtin/buildout/model/normalizers.ts +5 -0
  178. package/src/plugins/builtin/buildout/model/types.ts +11 -2
  179. package/src/plugins/builtin/buildout/pane/index.tsx +91 -20
  180. package/src/plugins/builtin/buildout/pane/status.tsx +1 -1
  181. package/src/plugins/builtin/buildout/pane/themes.tsx +110 -0
  182. package/src/plugins/builtin/buildout/table-model.ts +26 -2
  183. package/src/plugins/builtin/builtin-plugin-meta.ts +16 -0
  184. package/src/plugins/builtin/cds/headless.ts +10 -2
  185. package/src/plugins/builtin/cds/model.ts +7 -3
  186. package/src/plugins/builtin/central-bank-rates/index.tsx +1 -2
  187. package/src/plugins/builtin/central-bank-rates/pane.tsx +2 -1
  188. package/src/plugins/builtin/central-bank-rates/series.ts +2 -3
  189. package/src/plugins/builtin/changelog/index.tsx +3 -2
  190. package/src/plugins/builtin/chart-composer/cli-options.ts +16 -0
  191. package/src/plugins/builtin/chart-composer/headless-schema.ts +10 -0
  192. package/src/plugins/builtin/chart-composer/headless.ts +88 -21
  193. package/src/plugins/builtin/chart-composer/index.tsx +2 -3
  194. package/src/plugins/builtin/chart-composer/pane.tsx +30 -2
  195. package/src/plugins/builtin/chart-composer/series-expression.ts +3 -2
  196. package/src/plugins/builtin/chart-composer/session-range.ts +5 -2
  197. package/src/plugins/builtin/chart-composer/studies.ts +1 -1
  198. package/src/plugins/builtin/chat/controller/index.ts +1 -1
  199. package/src/plugins/builtin/chat/controller/messages.ts +15 -4
  200. package/src/plugins/builtin/chat/ghost-user.ts +8 -0
  201. package/src/plugins/builtin/chat/index.tsx +5 -5
  202. package/src/plugins/builtin/clinical-trials/index.tsx +1 -2
  203. package/src/plugins/builtin/cloud/company-picker.tsx +1 -1
  204. package/src/plugins/builtin/cloud/index.tsx +1 -2
  205. package/src/plugins/builtin/cloud/plugin.tsx +6 -4
  206. package/src/plugins/builtin/cloud/team/module.tsx +1 -1
  207. package/src/plugins/builtin/cloud/thesis/module.tsx +1 -1
  208. package/src/plugins/builtin/cloud/trial-offer-status-widget.tsx +24 -1
  209. package/src/plugins/builtin/cloud/upgrade-status-widget.tsx +39 -15
  210. package/src/plugins/builtin/comment-letters/index.tsx +1 -2
  211. package/src/plugins/builtin/commodity-balances/client.ts +4 -1
  212. package/src/plugins/builtin/commodity-balances/index.tsx +3 -3
  213. package/src/plugins/builtin/commodity-balances/pane.tsx +2 -2
  214. package/src/plugins/builtin/congress-trades/aggregates.ts +13 -2
  215. package/src/plugins/builtin/congress-trades/detail.tsx +20 -4
  216. package/src/plugins/builtin/congress-trades/headless.ts +2 -0
  217. package/src/plugins/builtin/congress-trades/model.ts +92 -64
  218. package/src/plugins/builtin/congress-trades/table.tsx +14 -2
  219. package/src/plugins/builtin/connections/index.ts +2 -3
  220. package/src/plugins/builtin/cot/client.ts +28 -2
  221. package/src/plugins/builtin/cot/headless.ts +4 -1
  222. package/src/plugins/builtin/cot/index.tsx +5 -4
  223. package/src/plugins/builtin/cot/model.ts +58 -7
  224. package/src/plugins/builtin/cot/pane.tsx +6 -2
  225. package/src/plugins/builtin/cpi/client.ts +6 -5
  226. package/src/plugins/builtin/cpi/index.tsx +1 -2
  227. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  228. package/src/plugins/builtin/credit-boards/client.ts +7 -2
  229. package/src/plugins/builtin/credit-boards/headless.ts +25 -12
  230. package/src/plugins/builtin/credit-boards/model.ts +29 -0
  231. package/src/plugins/builtin/credit-boards/series.ts +3 -4
  232. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +9 -4
  233. package/src/plugins/builtin/credit-conditions/client.ts +22 -14
  234. package/src/plugins/builtin/crypto-board/bitcoin-network.ts +143 -0
  235. package/src/plugins/builtin/crypto-board/client.ts +5 -5
  236. package/src/plugins/builtin/crypto-board/headless.ts +4 -3
  237. package/src/plugins/builtin/crypto-board/index.tsx +4 -2
  238. package/src/plugins/builtin/crypto-board/series.ts +6 -4
  239. package/src/plugins/builtin/crypto-etf-flows/client.ts +9 -2
  240. package/src/plugins/builtin/crypto-etf-flows/headless.ts +61 -6
  241. package/src/plugins/builtin/crypto-etf-flows/index.tsx +1 -2
  242. package/src/plugins/builtin/crypto-etf-flows/model.ts +14 -1
  243. package/src/plugins/builtin/crypto-etf-flows/pane.tsx +8 -5
  244. package/src/plugins/builtin/debt-maturities/headless.ts +4 -3
  245. package/src/plugins/builtin/debt-maturities/model.ts +17 -1
  246. package/src/plugins/builtin/debt-maturities/pane.tsx +18 -4
  247. package/src/plugins/builtin/debt-maturities/series.ts +3 -3
  248. package/src/plugins/builtin/debug/index.tsx +1 -1
  249. package/src/plugins/builtin/dividend-yield/client.ts +32 -2
  250. package/src/plugins/builtin/dividend-yield/headless.ts +58 -33
  251. package/src/plugins/builtin/dividend-yield/index.tsx +37 -2
  252. package/src/plugins/builtin/dividend-yield/pane.tsx +32 -13
  253. package/src/plugins/builtin/dividend-yield/ranking-reads.ts +73 -0
  254. package/src/plugins/builtin/dividend-yield/ranking-view.tsx +163 -0
  255. package/src/plugins/builtin/dividend-yield/ranking.ts +1 -1
  256. package/src/plugins/builtin/dividend-yield/view.ts +21 -0
  257. package/src/plugins/builtin/docs/index.tsx +2 -3
  258. package/src/plugins/builtin/doe/client.ts +6 -5
  259. package/src/plugins/builtin/doe/headless.ts +16 -3
  260. package/src/plugins/builtin/doe/index.tsx +5 -4
  261. package/src/plugins/builtin/doe/prices.ts +2 -3
  262. package/src/plugins/builtin/doe/series.ts +4 -4
  263. package/src/plugins/builtin/earnings/board.tsx +2 -1
  264. package/src/plugins/builtin/earnings/history.tsx +2 -1
  265. package/src/plugins/builtin/earnings-calls/calendar.tsx +10 -2
  266. package/src/plugins/builtin/earnings-calls/data.ts +19 -11
  267. package/src/plugins/builtin/earnings-calls/pane.tsx +27 -14
  268. package/src/plugins/builtin/earnings-ripple/client.ts +32 -7
  269. package/src/plugins/builtin/earnings-ripple/index.tsx +1 -2
  270. package/src/plugins/builtin/econ/calendar-model.ts +29 -6
  271. package/src/plugins/builtin/econ/detail-view.tsx +16 -2
  272. package/src/plugins/builtin/econ-statistics/index.tsx +1 -2
  273. package/src/plugins/builtin/equity-screener/client.ts +1 -1
  274. package/src/plugins/builtin/equity-screener/headless.ts +1 -0
  275. package/src/plugins/builtin/equity-screener/ideas.ts +30 -1
  276. package/src/plugins/builtin/equity-screener/index.tsx +1 -2
  277. package/src/plugins/builtin/equity-screener/model.ts +28 -4
  278. package/src/plugins/builtin/equity-screener/pane.tsx +5 -0
  279. package/src/plugins/builtin/equity-screener/screen-check.ts +23 -1
  280. package/src/plugins/builtin/estimate-revisions/index.tsx +1 -2
  281. package/src/plugins/builtin/executives/data.ts +6 -1
  282. package/src/plugins/builtin/executives/pane.tsx +9 -21
  283. package/src/plugins/builtin/exposure/holdings.ts +35 -13
  284. package/src/plugins/builtin/exposure/index.tsx +1 -2
  285. package/src/plugins/builtin/fear-greed/cache.ts +4 -0
  286. package/src/plugins/builtin/fear-greed/cli.ts +44 -0
  287. package/src/plugins/builtin/fear-greed/index.tsx +5 -53
  288. package/src/plugins/builtin/fear-greed/pane.tsx +2 -1
  289. package/src/plugins/builtin/fear-greed/series.ts +6 -4
  290. package/src/plugins/builtin/fixed-income/index.tsx +1 -2
  291. package/src/plugins/builtin/futures/board-reads.ts +25 -0
  292. package/src/plugins/builtin/futures/coalesced-bump.ts +44 -0
  293. package/src/plugins/builtin/futures/index.tsx +15 -0
  294. package/src/plugins/builtin/futures/model.ts +14 -0
  295. package/src/plugins/builtin/futures/use-board-terms.ts +22 -15
  296. package/src/plugins/builtin/futures/use-front-returns.ts +20 -14
  297. package/src/plugins/builtin/futures-curve/headless.ts +6 -1
  298. package/src/plugins/builtin/futures-curve/index.tsx +1 -2
  299. package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
  300. package/src/plugins/builtin/gpu/board.tsx +51 -12
  301. package/src/plugins/builtin/gpu/client.ts +19 -3
  302. package/src/plugins/builtin/gpu/equities.tsx +5 -2
  303. package/src/plugins/builtin/gpu/headless.ts +14 -2
  304. package/src/plugins/builtin/gpu/index.tsx +1 -2
  305. package/src/plugins/builtin/gpu/model.ts +20 -2
  306. package/src/plugins/builtin/gpu/pane.tsx +1 -1
  307. package/src/plugins/builtin/gpu/series.ts +2 -3
  308. package/src/plugins/builtin/help/index.tsx +1 -1
  309. package/src/plugins/builtin/holders/beneficial-view.tsx +1 -2
  310. package/src/plugins/builtin/holders/index-holders.ts +24 -12
  311. package/src/plugins/builtin/holders/index.tsx +2 -3
  312. package/src/plugins/builtin/ipo-calendar/index.tsx +1 -2
  313. package/src/plugins/builtin/iv-history/index.tsx +1 -2
  314. package/src/plugins/builtin/jodi/client.ts +4 -1
  315. package/src/plugins/builtin/jodi/index.tsx +1 -2
  316. package/src/plugins/builtin/kelly-sizer/index.tsx +1 -2
  317. package/src/plugins/builtin/layout-manager/index.tsx +9 -8
  318. package/src/plugins/builtin/macro-day/pane.tsx +4 -2
  319. package/src/plugins/builtin/market-halts/index.tsx +1 -2
  320. package/src/plugins/builtin/market-heatmap/index.tsx +11 -2
  321. package/src/plugins/builtin/market-movers/client.ts +1 -0
  322. package/src/plugins/builtin/market-movers/index.tsx +16 -5
  323. package/src/plugins/builtin/market-movers/screener.ts +23 -6
  324. package/src/plugins/builtin/market-movers/session-body.tsx +6 -2
  325. package/src/plugins/builtin/market-valuation/index.tsx +1 -2
  326. package/src/plugins/builtin/members/index.tsx +1 -2
  327. package/src/plugins/builtin/money-markets/index.tsx +1 -2
  328. package/src/plugins/builtin/money-markets/pane.tsx +2 -1
  329. package/src/plugins/builtin/money-markets/series.ts +3 -5
  330. package/src/plugins/builtin/news/headless.ts +7 -0
  331. package/src/plugins/builtin/news/wire/index.tsx +1 -1
  332. package/src/plugins/builtin/news/wire/news/pop-out.tsx +9 -3
  333. package/src/plugins/builtin/notes/index.tsx +3 -6
  334. package/src/plugins/builtin/openfda/index.tsx +1 -2
  335. package/src/plugins/builtin/options/view.tsx +6 -5
  336. package/src/plugins/builtin/options-calculator/index.tsx +1 -2
  337. package/src/plugins/builtin/options-positioning/index.tsx +1 -2
  338. package/src/plugins/builtin/options-scenario/index.tsx +2 -4
  339. package/src/plugins/builtin/perps/index.tsx +1 -2
  340. package/src/plugins/builtin/plugin-marketplace/index.tsx +2 -3
  341. package/src/plugins/builtin/plugin-marketplace/pane.tsx +2 -1
  342. package/src/plugins/builtin/plugin-module.ts +16 -2
  343. package/src/plugins/builtin/portfolio-list/cli/render.ts +53 -3
  344. package/src/plugins/builtin/portfolio-list/index.tsx +1 -2
  345. package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -0
  346. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -2
  347. package/src/plugins/builtin/power/gas-client.ts +2 -1
  348. package/src/plugins/builtin/power/index.tsx +1 -2
  349. package/src/plugins/builtin/power/prices-client.ts +3 -2
  350. package/src/plugins/builtin/rate-path/index.tsx +1 -2
  351. package/src/plugins/builtin/realized-vol/headless.ts +2 -0
  352. package/src/plugins/builtin/realized-vol/index.tsx +1 -2
  353. package/src/plugins/builtin/realized-vol/model.ts +16 -2
  354. package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
  355. package/src/plugins/builtin/relative-rotation/index.tsx +1 -2
  356. package/src/plugins/builtin/research/analyst-headless.ts +33 -6
  357. package/src/plugins/builtin/research/analyst-model.ts +37 -2
  358. package/src/plugins/builtin/research/analyst-pane.tsx +25 -5
  359. package/src/plugins/builtin/research/index.tsx +5 -8
  360. package/src/plugins/builtin/research-search/index.tsx +1 -2
  361. package/src/plugins/builtin/revenue-breakdown/headless.ts +22 -8
  362. package/src/plugins/builtin/revenue-breakdown/model.ts +23 -0
  363. package/src/plugins/builtin/reverse-dcf/pane.tsx +4 -2
  364. package/src/plugins/builtin/risk-factors/data.ts +8 -4
  365. package/src/plugins/builtin/risk-factors/index.tsx +3 -1
  366. package/src/plugins/builtin/risk-factors/pane.tsx +5 -8
  367. package/src/plugins/builtin/roadmap/index.tsx +2 -3
  368. package/src/plugins/builtin/roadmap/pane.tsx +49 -2
  369. package/src/plugins/builtin/scanner/hilo-model.ts +18 -1
  370. package/src/plugins/builtin/scanner/hilo-pane.tsx +12 -2
  371. package/src/plugins/builtin/scanner/live-pane.tsx +7 -4
  372. package/src/plugins/builtin/scanner/lock-note.ts +26 -3
  373. package/src/plugins/builtin/sec/filing-content.ts +38 -6
  374. package/src/plugins/builtin/sec/headless.ts +5 -1
  375. package/src/plugins/builtin/sec/index.tsx +4 -2
  376. package/src/plugins/builtin/sec/model.ts +2 -0
  377. package/src/plugins/builtin/sectors/index.tsx +11 -5
  378. package/src/plugins/builtin/series-inventory.ts +2 -0
  379. package/src/plugins/builtin/shared/cloud-resource.ts +73 -8
  380. package/src/plugins/builtin/shared/cloud-upgrade.ts +24 -1
  381. package/src/plugins/builtin/shared/forming-bars.ts +29 -0
  382. package/src/plugins/builtin/shared/fred-points.ts +6 -1
  383. package/src/plugins/builtin/shared/fx-rate-freshness.ts +17 -0
  384. package/src/plugins/builtin/shared/pro-lock-note.ts +23 -8
  385. package/src/plugins/builtin/shared/series-detail-chart.tsx +4 -2
  386. package/src/plugins/builtin/shared/series-warning.ts +13 -4
  387. package/src/plugins/builtin/shared/use-quote-board.ts +5 -0
  388. package/src/plugins/builtin/short-interest/publications-pane.tsx +2 -1
  389. package/src/plugins/builtin/short-volume/pane.tsx +2 -1
  390. package/src/plugins/builtin/short-volume/series.ts +2 -3
  391. package/src/plugins/builtin/social-mentions/client.ts +15 -10
  392. package/src/plugins/builtin/social-mentions/index.tsx +1 -2
  393. package/src/plugins/builtin/social-mentions/pane.tsx +2 -1
  394. package/src/plugins/builtin/social-mentions/series.ts +2 -3
  395. package/src/plugins/builtin/supply-chain/client.ts +22 -1
  396. package/src/plugins/builtin/supply-chain/graph-client.ts +22 -1
  397. package/src/plugins/builtin/supply-chain/index.tsx +1 -2
  398. package/src/plugins/builtin/thirteenf/api.ts +127 -29
  399. package/src/plugins/builtin/thirteenf/data.ts +165 -35
  400. package/src/plugins/builtin/thirteenf/index.tsx +2 -4
  401. package/src/plugins/builtin/thirteenf/model.ts +2 -0
  402. package/src/plugins/builtin/thirteenf/overlap-pane.tsx +5 -1
  403. package/src/plugins/builtin/thirteenf/pane.tsx +41 -16
  404. package/src/plugins/builtin/thirteenf/types.ts +14 -0
  405. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +22 -6
  406. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +81 -11
  407. package/src/plugins/builtin/ticker-detail/financials/model.ts +35 -2
  408. package/src/plugins/builtin/ticker-detail/financials/ratios.ts +53 -18
  409. package/src/plugins/builtin/ticker-detail/headless.ts +70 -11
  410. package/src/plugins/builtin/ticker-detail/index.tsx +5 -10
  411. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +16 -6
  412. package/src/plugins/builtin/time-sales/index.tsx +1 -2
  413. package/src/plugins/builtin/time-sales/pane.tsx +3 -1
  414. package/src/plugins/builtin/trace-bonds/headless.ts +11 -2
  415. package/src/plugins/builtin/trace-bonds/model.ts +44 -10
  416. package/src/plugins/builtin/trace-bonds/pane.tsx +12 -7
  417. package/src/plugins/builtin/treasury-auctions/cache.ts +20 -8
  418. package/src/plugins/builtin/treasury-auctions/client.ts +3 -3
  419. package/src/plugins/builtin/treasury-auctions/headless.ts +3 -1
  420. package/src/plugins/builtin/treasury-auctions/pane.tsx +10 -4
  421. package/src/plugins/builtin/vol-surface/client.ts +18 -3
  422. package/src/plugins/builtin/vol-surface/index.tsx +1 -2
  423. package/src/plugins/builtin/volatility/client.ts +43 -16
  424. package/src/plugins/builtin/volatility/headless.ts +5 -2
  425. package/src/plugins/builtin/volatility/index.tsx +1 -2
  426. package/src/plugins/builtin/volatility/model.ts +17 -6
  427. package/src/plugins/builtin/volatility/pane.tsx +6 -2
  428. package/src/plugins/builtin/volatility/series.ts +2 -1
  429. package/src/plugins/builtin/world-indices/footer.ts +6 -1
  430. package/src/plugins/builtin/world-venue-map/client.ts +10 -1
  431. package/src/plugins/builtin/world-venue-map/index.ts +6 -2
  432. package/src/plugins/builtin/world-venue-map/layered-pane.tsx +48 -7
  433. package/src/plugins/builtin/world-venue-map/layers.ts +17 -0
  434. package/src/plugins/builtin/world-venue-map/local-layers.ts +141 -0
  435. package/src/plugins/builtin/world-venue-map/map-symbols.ts +18 -0
  436. package/src/plugins/builtin/world-venue-map/map.tsx +4 -2
  437. package/src/plugins/builtin/world-venue-map/scope.ts +63 -0
  438. package/src/plugins/builtin/yield-curve/client.ts +4 -6
  439. package/src/plugins/builtin/yield-curve/curves.ts +16 -2
  440. package/src/plugins/builtin/yield-curve/headless.ts +3 -3
  441. package/src/plugins/bundle.ts +52 -2
  442. package/src/plugins/catalog-browser.ts +2 -2
  443. package/src/plugins/catalog-cli.ts +20 -0
  444. package/src/plugins/host-link.ts +13 -1
  445. package/src/plugins/installer.ts +64 -42
  446. package/src/plugins/loader.ts +43 -11
  447. package/src/plugins/peer-imports.ts +201 -0
  448. package/src/plugins/peer-retry.ts +26 -29
  449. package/src/plugins/plugin-process.ts +128 -0
  450. package/src/plugins/redirection-guard.ts +45 -0
  451. package/src/remote/endpoint.ts +16 -3
  452. package/src/renderers/browser/main.tsx +3 -2
  453. package/src/renderers/cloudflare/http-proxy.ts +7 -123
  454. package/src/renderers/dom/desktop/icons.tsx +5 -2
  455. package/src/renderers/dom/host/image-surface.tsx +46 -21
  456. package/src/renderers/dom/styles.css +7 -0
  457. package/src/renderers/dom/toast-host.tsx +2 -0
  458. package/src/renderers/electrobun/bun/desktop/backend.ts +7 -1
  459. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +23 -1
  460. package/src/renderers/electrobun/bun/desktop/initialization.ts +12 -3
  461. package/src/renderers/electrobun/bun/desktop/rpc-timing.ts +88 -0
  462. package/src/renderers/electrobun/bun/desktop/update.ts +20 -4
  463. package/src/renderers/electrobun/bun/desktop/view-ids.ts +8 -4
  464. package/src/renderers/electrobun/bun/external-plugins.ts +4 -0
  465. package/src/renderers/electrobun/bun/window/attention.ts +54 -0
  466. package/src/renderers/electrobun/bun/window/create.ts +6 -2
  467. package/src/renderers/electrobun/bun/window/native-timing.ts +48 -0
  468. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +82 -4
  469. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +13 -5
  470. package/src/renderers/electrobun/view/desktop/window/snapshot.ts +3 -0
  471. package/src/renderers/electrobun/view/main.tsx +7 -2
  472. package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -0
  473. package/src/renderers/electrobun/view/ui-host.tsx +9 -2
  474. package/src/renderers/opentui/browser-open.ts +10 -4
  475. package/src/renderers/opentui/image/surface.tsx +1 -1
  476. package/src/renderers/opentui/start.tsx +12 -6
  477. package/src/renderers/opentui/toast-host.tsx +2 -1
  478. package/src/series/registry.ts +18 -0
  479. package/src/series/resolve.ts +2 -1
  480. package/src/series/standalone.ts +10 -1
  481. package/src/series/types.ts +12 -0
  482. package/src/series/validate.ts +11 -2
  483. package/src/sources/gloomberb-cloud/fred-series.ts +5 -3
  484. package/src/sources/gloomberb-cloud/index.ts +28 -5
  485. package/src/sources/gloomberb-cloud/news.ts +24 -3
  486. package/src/sources/gloomberb-cloud/normalizers.ts +17 -2
  487. package/src/sources/provider-errors.ts +26 -3
  488. package/src/sources/provider-router/history.ts +5 -0
  489. package/src/sources/provider-router/index.ts +13 -0
  490. package/src/sync/core-contributors.ts +32 -0
  491. package/src/telemetry/attention-counts.ts +64 -6
  492. package/src/telemetry/attention-setting.ts +33 -0
  493. package/src/telemetry/crash-reports-node.ts +3 -1
  494. package/src/telemetry/stall-clocks.ts +93 -0
  495. package/src/telemetry/stall-reports-node.ts +25 -9
  496. package/src/telemetry/stall-reports.ts +218 -13
  497. package/src/test-support/outage.ts +8 -0
  498. package/src/test-support/preload.ts +9 -0
  499. package/src/tickers/company-name-listing.ts +59 -29
  500. package/src/tickers/company-name.ts +84 -1
  501. package/src/tickers/listing-fate.ts +14 -3
  502. package/src/tickers/saved-instrument.ts +17 -0
  503. package/src/tickers/search/home-listing.ts +104 -8
  504. package/src/tickers/search/index.ts +5 -2
  505. package/src/tickers/search/types.ts +6 -1
  506. package/src/time-series/bar-aggregation.ts +7 -1
  507. package/src/time-series/base-series.ts +3 -3
  508. package/src/time-series/chart-bounds.ts +1 -1
  509. package/src/time-series/chart-data.ts +5 -2
  510. package/src/time-series/chart-seed.ts +1 -1
  511. package/src/time-series/coin-session-close.ts +211 -0
  512. package/src/time-series/live-bars.ts +13 -2
  513. package/src/time-series/live-quotes.ts +1 -1
  514. package/src/time-series/market.ts +5 -3
  515. package/src/time-series/official-close.ts +159 -0
  516. package/src/time-series/price-comparison.ts +75 -8
  517. package/src/time-series/resolve.ts +32 -4
  518. package/src/time-series/series-presentation.ts +1 -1
  519. package/src/time-series/studies/formula.ts +205 -0
  520. package/src/time-series/studies/indicators.ts +299 -0
  521. package/src/time-series/studies/leverage.ts +75 -0
  522. package/src/time-series/studies/pair.ts +138 -0
  523. package/src/time-series/studies/resolve.ts +333 -0
  524. package/src/time-series/studies/shared.ts +141 -0
  525. package/src/time-series/studies/units.ts +243 -0
  526. package/src/time-series/studies/vwap.ts +274 -0
  527. package/src/time-series/types.ts +7 -0
  528. package/src/types/config.ts +18 -2
  529. package/src/types/data-provider.ts +23 -0
  530. package/src/types/financials.ts +40 -3
  531. package/src/types/headless.ts +42 -3
  532. package/src/types/plugin.ts +32 -3
  533. package/src/ui/host.tsx +17 -1
  534. package/src/ui/toast.tsx +26 -1
  535. package/src/utils/ascii-text.ts +140 -0
  536. package/src/utils/cli-output.ts +91 -18
  537. package/src/utils/concurrency.ts +24 -0
  538. package/src/utils/exchanges.ts +6 -0
  539. package/src/utils/external-url.ts +20 -7
  540. package/src/utils/financial-statements.ts +73 -16
  541. package/src/utils/fundamentals.ts +17 -5
  542. package/src/utils/futures-generic.ts +4 -4
  543. package/src/utils/http-proxy-upstream.ts +124 -0
  544. package/src/utils/lease-liabilities.ts +54 -0
  545. package/src/utils/main-thread-stall.ts +122 -5
  546. package/src/utils/perf-marks.ts +58 -12
  547. package/src/utils/price-history-integrity.ts +47 -1
  548. package/src/utils/private-files.ts +141 -0
  549. package/src/utils/public-address.ts +99 -0
  550. package/src/account/upgrade-personalization.ts +0 -57
  551. package/src/account/upgrade-tickers.ts +0 -124
  552. package/src/api-client/upgrade-experiment.ts +0 -91
  553. package/src/plugins/builtin/cloud/browser.tsx +0 -3
  554. package/src/time-series/studies.ts +0 -1628
@@ -1,4 +1,4 @@
1
- import { createPluginCache } from "../../data/plugin-cache";
1
+ import { createPluginCache, savedCopyForTransientFailure } from "../../data/plugin-cache";
2
2
  import { getPublishedUsEquityCalendarDay } from "../../market-data/published-us-sessions";
3
3
  import { zonedDateKey } from "../../utils/zoned-date-time";
4
4
  import type {
@@ -198,7 +198,9 @@ export async function loadCachedFredSeries(
198
198
  const cached = stored ? withFredSourceFreshness(stored) : null;
199
199
  if (!options?.force && !pending && cached && !cached.stale) return { ...cached, source: "cache" };
200
200
  const result = await cache.load(cacheKey(request), loader, pending ? { ...options, force: true } : options);
201
- // An early re-read is opportunistic: a failure keeps the still-fresh copy as it was.
201
+ // An early re-read is opportunistic: a failure keeps the still-fresh copy as it was, as not asking would have.
202
202
  if (pending && result.source === "stale-fallback" && cached && !cached.stale && !options?.force) return { ...cached, source: "cache" };
203
- return withFredSourceFreshness(result);
203
+ // A refusal (a series no longer served, a signed-out session) or a reply that fails validation is thrown,
204
+ // whatever copy is saved: a saved copy answers only for a failure asking again may get past.
205
+ return withFredSourceFreshness(savedCopyForTransientFailure(result));
204
206
  }
@@ -18,6 +18,7 @@ import type {
18
18
  EarningsEvent,
19
19
  SecFilingDocument,
20
20
  SecFilingItem,
21
+ BatchedReadMethod,
21
22
  MarketDataRequestContext,
22
23
  QuoteBatchOptions,
23
24
  QuoteBatchResult,
@@ -48,7 +49,7 @@ import { canonicalExchange, canonicalTickerKey, parsePublicTickerKey } from "../
48
49
  import { normalizePriceHistory, priceHistoryIntervalMs, reachesLatestSettledSession } from "../../utils/price-history";
49
50
  import { createProviderMiss, ProviderBusyError, providerMissReason, StaleHistoryError } from "../provider-errors";
50
51
  import { parseListingDelisting } from "../../market-data/quotes/delisting";
51
- import { isRetryableApiError } from "../../api-client/errors";
52
+ import { isRetryableApiError, markAskedTwice, sourceDidNotAnswer, wasAskedTwice } from "../../api-client/errors";
52
53
  import { assertSecRegistrantMatches } from "../sec-registrant";
53
54
  import { nonUsSecListingVenue } from "../../utils/sec";
54
55
  import { publicListingTarget } from "../listing-target";
@@ -66,6 +67,7 @@ import {
66
67
  GLOOMBERB_CLOUD_PROVIDER_ID,
67
68
  formatCloudDateTime,
68
69
  formatCloudHistoryEnd,
70
+ isCloudSourceFailure,
69
71
  isEmptyCloudStatus,
70
72
  mapBatchError,
71
73
  NOT_FOUND_REASON,
@@ -86,6 +88,7 @@ import {
86
88
  boardSearchesFrom,
87
89
  } from "./perpetuals";
88
90
  import { predictionMarketListing } from "../../tickers/prediction-listing";
91
+ import { cleanListingName } from "../../tickers/company-name";
89
92
  import { settleWithin } from "../../utils/async-deadline";
90
93
  import { cloudSearchBatch, cloudTickerChangesBatch } from "./batched-lookups";
91
94
 
@@ -127,6 +130,7 @@ function mapCloudSecFiling(item: {
127
130
  primaryDocumentUrl?: string;
128
131
  fileNumber?: string;
129
132
  fundSeries?: boolean;
133
+ pressReleaseUrl?: string;
130
134
  }): SecFilingItem {
131
135
  return {
132
136
  accessionNumber: item.accessionNumber,
@@ -144,9 +148,20 @@ function mapCloudSecFiling(item: {
144
148
  // Absent from an older server: no file number is not the same as an empty one.
145
149
  ...(typeof item.fileNumber === "string" ? { fileNumber: item.fileNumber } : {}),
146
150
  ...(typeof item.fundSeries === "boolean" ? { fundSeries: item.fundSeries } : {}),
151
+ ...(typeof item.pressReleaseUrl === "string" && /^https:\/\/www\.sec\.gov\//.test(item.pressReleaseUrl)
152
+ ? { pressReleaseUrl: item.pressReleaseUrl } : {}),
147
153
  };
148
154
  }
149
155
 
156
+ /** Search rows with a venue's fixed-width name cleaned of the stray code after its spaces ("Americold Realty Trust Inc. R"). */
157
+ function withCleanNames<T extends { name: string }>(rows: T[]): T[] {
158
+ return rows.map((row) => {
159
+ if (typeof row.name !== "string") return row;
160
+ const name = cleanListingName(row.name);
161
+ return name === row.name ? row : { ...row, name };
162
+ });
163
+ }
164
+
150
165
  function isCloudProviderMiss(error: unknown): boolean {
151
166
  const message = error instanceof Error ? error.message : String(error);
152
167
  return CLOUD_PROVIDER_MISS_PATTERNS.some((pattern) => pattern.test(message));
@@ -420,6 +435,8 @@ function unwrapRequiredCloudResponse<T>(response: CloudMarketResponse<T>, messag
420
435
  if (isEmptyCloudStatus(response.status)) {
421
436
  throw createProviderMiss(response.reasonCode ?? message, response.message, { notFound: response.reasonCode === NOT_FOUND_REASON });
422
437
  }
438
+ // The service's own source failed on a listing read asked twice (`readListing`): not an answer that the listing has nothing.
439
+ if (isCloudSourceFailure(response.status) && wasAskedTwice(response)) throw markAskedTwice(sourceDidNotAnswer(response.reasonCode ?? message));
423
440
  throw new Error(response.reasonCode ?? message);
424
441
  }
425
442
 
@@ -647,7 +664,7 @@ export class GloomberbCloudProvider implements AssetDataProvider {
647
664
  const pending = searchPerpetualMarkets(query).catch(logFailure("sources.gloomberb-cloud.index", "Searching perpetual markets failed", [] as InstrumentSearchResult[]));
648
665
  // A crypto exchange's line of a coin pair, which no source quotes, is offered as the crypto venue's listing.
649
666
  const listings = await withCloudFallback(
650
- async () => withExchangeLinesAsCoinListings(await apiClient.searchInstruments(query, context?.limit ?? 10)),
667
+ async () => withExchangeLinesAsCoinListings(withCleanNames(await apiClient.searchInstruments(query, context?.limit ?? 10))),
651
668
  "Cloud search is unavailable",
652
669
  ).catch((error: unknown) => {
653
670
  // Rate limited or overloaded: the search was not run, which is not a search that found nothing.
@@ -764,7 +781,7 @@ export class GloomberbCloudProvider implements AssetDataProvider {
764
781
  async searchBatch(queries: string[], options: { limit?: number; signal?: AbortSignal } = {}): Promise<Map<string, InstrumentSearchResult[]> | null> {
765
782
  const answers = await cloudSearchBatch(queries, options);
766
783
  if (!answers) return null;
767
- for (const [query, rows] of answers) answers.set(query, withExchangeLinesAsCoinListings(rows));
784
+ for (const [query, rows] of answers) answers.set(query, withExchangeLinesAsCoinListings(withCleanNames(rows)));
768
785
  // Perpetual markets lead each answer, as `search` gives them, so a symbol in a list resolves as it does alone:
769
786
  // one read of the board answers every query's search of it. A query that found no listing is searched again
770
787
  // on its own, which finds them then; a board that does not answer leaves them out, as a search does.
@@ -818,7 +835,8 @@ export class GloomberbCloudProvider implements AssetDataProvider {
818
835
  exchange = target.exchange ?? "";
819
836
  const request = toHistoryRequest(range);
820
837
  const { response, extendedHours } = await withCloudFallback(
821
- () => loadCloudHistory(request.interval, (session) => apiClient.getCloudHistory(target.symbol, exchange, { ...request, ...(session ? { session } : {}) }), _context),
838
+ () => loadCloudHistory(request.interval, (session) => apiClient.getCloudHistory(target.symbol, exchange, { ...request, ...(session ? { session } : {}) },
839
+ { batch: _context?.batch }), _context),
822
840
  `Cloud chart data is unavailable for ${ticker}`,
823
841
  );
824
842
  return mapCloudPriceHistory(response, target.symbol, exchange, request.interval, subtractTimeRange(new Date(), range), undefined, extendedHours);
@@ -905,11 +923,16 @@ export class GloomberbCloudProvider implements AssetDataProvider {
905
923
  return mapCloudPriceHistory(response, target.symbol, exchange, interval, startDate, endDate, extendedHours);
906
924
  }
907
925
 
926
+ /** Trailing history and dated expiries asked with others (`batch`) go out as batch requests while the service reads them so. */
927
+ batchesReads(method: BatchedReadMethod): boolean {
928
+ return apiClient.batchesReads(method === "getPriceHistory" ? "history" : "options");
929
+ }
930
+
908
931
  async getOptionsChain(ticker: string, exchange?: string, expirationDate?: number, _context?: MarketDataRequestContext): Promise<OptionsChain> {
909
932
  rejectPerpetual(ticker, exchange, "options chains");
910
933
  const target = cloudInstrumentTarget(ticker, exchange);
911
934
  return withCloudFallback(async () => {
912
- const response = await apiClient.getCloudOptionsChain(target.symbol, target.exchange, expirationDate);
935
+ const response = await apiClient.getCloudOptionsChain(target.symbol, target.exchange, expirationDate, { batch: _context?.batch });
913
936
  const chain = unwrapRequiredCloudResponse(
914
937
  response,
915
938
  `Cloud options chains are unavailable for ${ticker}`,
@@ -28,13 +28,34 @@ function uniqueStrings(values: string[]): string[] {
28
28
  return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
29
29
  }
30
30
 
31
+ type CloudNewsTickerLink = CloudNewsPayload["tickerLinks"][number];
32
+
33
+ const statesMarketCap = (link: CloudNewsTickerLink) => typeof link.marketCap === "number" && link.marketCap > 0;
34
+
35
+ /**
36
+ * One link a company: a symbol a story tags on several venues (ASML on
37
+ * Euronext Amsterdam and on NASDAQ) keeps its home listing, the line Gloom
38
+ * Cloud states the company's market cap on, else the venue tagged first. The
39
+ * tag stays where the company was first tagged.
40
+ */
41
+ function homeListingLinks(links: readonly CloudNewsTickerLink[]): CloudNewsTickerLink[] {
42
+ const companies = new Map<string, CloudNewsTickerLink>();
43
+ links.forEach((link, index) => {
44
+ const symbol = link.symbol?.trim().toUpperCase() || `#${index}`;
45
+ const kept = companies.get(symbol);
46
+ if (!kept || (!statesMarketCap(kept) && statesMarketCap(link))) companies.set(symbol, link);
47
+ });
48
+ return [...companies.values()];
49
+ }
50
+
31
51
  function mapCloudNewsTickers(
32
52
  item: CloudNewsPayload,
33
53
  fallbackTicker?: string,
34
54
  ): string[] {
35
- const tickers = collectNewsDisplayTickers(
36
- item.tickerLinks.flatMap((link) => [link.symbol, link.canonicalTicker]),
37
- );
55
+ // Each company's listing: its canonical key, or its symbol on its venue.
56
+ const tickers = collectNewsDisplayTickers(homeListingLinks(item.tickerLinks).map((link) => (
57
+ link.canonicalTicker || (link.symbol && link.exchange ? `${link.symbol}:${link.exchange}` : link.symbol)
58
+ )));
38
59
  const fallbackTickers = collectNewsDisplayTickers([fallbackTicker]);
39
60
  if (tickers.length === 0) tickers.push(...fallbackTickers);
40
61
  return tickers;
@@ -86,8 +86,8 @@ export function mapQuote(
86
86
  const changePercent = !impossibleClose && typeof quote.changePercent === "number" && Number.isFinite(quote.changePercent)
87
87
  ? quote.changePercent
88
88
  : Number.NaN;
89
- // A generic future (VX1, TY1) reads in points or 32nds, and an index future's
90
- // continuous symbol or contract (ES=F, SXOZ26.CME) in points, not dollars.
89
+ // Generics, continuous symbols and listed contracts share their root's
90
+ // price basis: index futures in points and Treasury futures in 32nds.
91
91
  const futuresBasis = isNonFutureType(quote.instrumentType) ? null
92
92
  : futuresListingPriceBasis(quote.symbol, quote.listingExchangeName ?? quote.exchangeName);
93
93
  const priceObservation = quote.priceBasis === "nav" ? "nav" : undefined;
@@ -234,6 +234,11 @@ function venueVolume(value: unknown): { venueVolume: { venue: string; volume: nu
234
234
  : null;
235
235
  }
236
236
 
237
+ /** What a served bar's volume counts, when it is a short plain phrase (`aggregate`, `3 spot venues`). */
238
+ function volumeBasis(value: unknown): { volumeBasis: string } | null {
239
+ return typeof value === "string" && /^[a-z0-9][a-z0-9 ]{0,31}$/i.test(value.trim()) ? { volumeBasis: value.trim() } : null;
240
+ }
241
+
237
242
  /**
238
243
  * One served bar. `coin`: the bars are a coin's, which trades every hour of every day. Its intraday volume is the
239
244
  * value traded in the bar on major spot venues, and a bar no venue reported has none rather than 0, so a 0 (from an
@@ -254,12 +259,17 @@ export function mapPricePoint(
254
259
  close: normalizePriceValueByDivisor(point.close, divisor) ?? point.close,
255
260
  volume: coin && point.volume === 0 ? undefined : point.volume,
256
261
  ...(venueVolume(point.venueVolume) ?? {}),
262
+ ...(volumeBasis(point.volumeBasis) ?? {}),
257
263
  ...(point.partial === true ? { partial: true } : {}),
258
264
  ...(point.filled === true ? { filled: true } : {}),
259
265
  ...(point.unsettled === true ? { unsettled: true } : {}),
266
+ ...(typeof point.contract === "string" && FUTURES_CONTRACT_CODE.test(point.contract) ? { contract: point.contract } : {}),
260
267
  };
261
268
  }
262
269
 
270
+ /** A listed contract as a continuous future's bar names it: root, month letter and year (NGZ26, ZQF27). */
271
+ const FUTURES_CONTRACT_CODE = /^[A-Z0-9]{1,4}[FGHJKMNQUVXZ]\d{2}$/;
272
+
263
273
  /** Per-share bases the statistics block serves beside its multiples. */
264
274
  const CLOUD_PER_SHARE_BASES = ["forwardEps", "dividendRate"] as const;
265
275
 
@@ -363,6 +373,11 @@ export function isEmptyCloudStatus(
363
373
  return status === "empty" || status === "unsupported";
364
374
  }
365
375
 
376
+ /** The service answered that its own source failed: `retryable_error`, or `fatal_error`, its catch-all for an upstream error. */
377
+ export function isCloudSourceFailure(status: CloudMarketResponse<unknown>["status"]): boolean {
378
+ return status === "retryable_error" || status === "fatal_error";
379
+ }
380
+
366
381
  export function mapBatchError<T>(
367
382
  item: CloudMarketBatchItem<T>,
368
383
  fallbackMessage: string,
@@ -1,4 +1,5 @@
1
1
  import { DataServiceUnreachableError, isNetworkFailure } from "../utils/network-failure";
2
+ import { wasAskedTwice } from "../api-client/errors";
2
3
 
3
4
  // Registered, so a miss raised by another copy of this module (a plugin that
4
5
  // resolved `gloomberb/market-data` from the source tree) is still recognized.
@@ -129,6 +130,16 @@ export function isNoProviderMessage(message: string): boolean {
129
130
  return /\bprovider available for\b/i.test(message) || notATickerSymbol(message) !== null;
130
131
  }
131
132
 
133
+ /**
134
+ * What a read says when Gloom's data service answered that its source failed,
135
+ * having been asked twice (`readListing`): the listing is not said to have no
136
+ * data, since asked again in a moment it may. "for SYMBOL" closes the first
137
+ * sentence, so a table's note can name several symbols in front of it.
138
+ */
139
+ export function serviceDidNotAnswerMessage(symbol?: string): string {
140
+ return `Gloom's data service did not answer${symbol ? ` for ${symbol}` : ""}. Try again in a moment.`;
141
+ }
142
+
132
143
  /** The router's generic answer that no provider served a request, holding the failure behind it when a provider failed rather than missed. */
133
144
  class NoProviderError extends Error {
134
145
  constructor(message: string, readonly failure: unknown) {
@@ -139,9 +150,11 @@ class NoProviderError extends Error {
139
150
  /**
140
151
  * The router's final error when no provider answered. A reason a provider gave
141
152
  * replaces the generic wording, and so does the service's answer that no
142
- * listing exists for `symbol` ("Not a ticker"), and a request that never
143
- * reached the data service, which says that rather than that no source has the
144
- * symbol; a timeout or any other failure keeps the generic message.
153
+ * listing exists for `symbol` ("Not a ticker"), a request that never reached
154
+ * the data service, and a listing read the data service failed to answer twice
155
+ * (`wasAskedTwice`, `serviceDidNotAnswerMessage`), each of which says that
156
+ * rather than that no source has the symbol; any other failure, a 5xx on a read
157
+ * that is not asked again among them, keeps the generic message.
145
158
  */
146
159
  export function noProviderError(message: string, note: ProviderMissNote | undefined, symbol?: string): Error {
147
160
  if (note?.reason) return createProviderMiss(note.reason, note.reason);
@@ -149,9 +162,19 @@ export function noProviderError(message: string, note: ProviderMissNote | undefi
149
162
  if (isNetworkFailure(note?.failure)) {
150
163
  return note!.failure instanceof DataServiceUnreachableError ? note!.failure : new DataServiceUnreachableError(note!.failure);
151
164
  }
165
+ if (wasAskedTwice(note?.failure)) return new NoProviderError(serviceDidNotAnswerMessage(symbol), note!.failure);
152
166
  return new NoProviderError(message, note?.failure);
153
167
  }
154
168
 
169
+ /**
170
+ * The router's answer that no source served a read because one failed (a
171
+ * timeout, a rate limit, a refused session), not because none has it: asked
172
+ * again shortly it may answer.
173
+ */
174
+ export function isSourceFailure(error: unknown): boolean {
175
+ return error instanceof NoProviderError && error.failure !== undefined;
176
+ }
177
+
155
178
  /**
156
179
  * Whether a failed read means no source has what was asked for: a source's
157
180
  * miss, or the router's answer that none served it when none failed another
@@ -781,6 +781,11 @@ export class ProviderRouterHistoryRoutes {
781
781
  throw noProviderError(coin ? `No source could load history for ${request.target.symbol} just now` : request.missingProviderError,
782
782
  attempts.misses, request.target.symbol);
783
783
  }
784
+ // A window of dates no source could answer, because one failed rather than had nothing, is that failure:
785
+ // an empty answer would read as no bars in the window (`history --from` printing "0 bars").
786
+ if (!providerResult && attempts.misses.failure !== undefined && !reportedGaps.some((value) => value.points.length > 0)) {
787
+ throw noProviderError(`No source could load history for ${request.target.symbol} just now`, attempts.misses, request.target.symbol);
788
+ }
784
789
  return withReportedGaps(providerResult?.value ?? { points: [], resolution: historyResolutionForInterval(request.interval) });
785
790
  }
786
791
 
@@ -9,6 +9,7 @@ import { createDefaultConfig } from "../../types/config";
9
9
  import type {
10
10
  CachedAssetArgs,
11
11
  CachedAssetMethod,
12
+ BatchedReadMethod,
12
13
  CachedFinancialsTarget,
13
14
  DataProvider,
14
15
  MarketDataRequestContext,
@@ -631,6 +632,18 @@ export class AssetDataRouter implements DataProvider {
631
632
  return this.providersInPriorityOrder()[0]?.name ?? null;
632
633
  }
633
634
 
635
+ /**
636
+ * Whether reads of `method` asked together go out as batch requests: only
637
+ * when every source the router could ask for them does, each broker that
638
+ * reads them and each provider. A caller asking many at once then never
639
+ * sends them one by one to a source that takes them one by one.
640
+ */
641
+ batchesReads(method: BatchedReadMethod): boolean {
642
+ if (this.getBrokerCandidatesForContext(undefined, false).some(({ broker }) => typeof broker[method] === "function")) return false;
643
+ const providers = this.providersInPriorityOrder().filter((provider) => typeof provider[method] === "function");
644
+ return providers.length > 0 && providers.every((provider) => provider.batchesReads?.(method) === true);
645
+ }
646
+
634
647
  private providersInPriorityOrder(): DataProvider[] {
635
648
  const providers = [...this.sortedProviders()];
636
649
  const fallbackProvider = this.fallbackSource?.market ?? null;
@@ -39,6 +39,7 @@ import { getBrokerInstance } from "../utils/broker-instances";
39
39
  import { withoutMissingWatchlists } from "../portfolio/mutations";
40
40
  import { mergeSuffixedListings, moveTargetWeights } from "../portfolio/suffixed-listings";
41
41
  import { positionContractMultiplier } from "../portfolio/position-metrics";
42
+ import { reportTelemetryConfig } from "../telemetry/live-config";
42
43
  import {
43
44
  clearLeftoverBrokerHoldings,
44
45
  knownBrokerHoldings,
@@ -421,9 +422,36 @@ function collectCoreConfigPayload(config: AppConfig) {
421
422
  // Completion is monotonic in synced state. A device only advertises the
422
423
  // completed state, while resumable progress and incomplete state stay local.
423
424
  onboardingComplete: config.onboardingComplete === true ? true : undefined,
425
+ // Only whether Attention Counts is off: see mergedAttentionTelemetry.
426
+ attentionOff: config.telemetry?.attention === false,
424
427
  });
425
428
  }
426
429
 
430
+ /**
431
+ * Attention Counts off belongs to the signed-in account, not to one device:
432
+ * an off made on another device turns it off here, and an explicit on made
433
+ * there afterwards turns it back on. The default never overrides either. An
434
+ * edit made here since the last sync wins, like any setting. No synced copy,
435
+ * or one from before `attentionOff` existed, reads as not off: a device's
436
+ * first sync takes an off already on the account, and keeps its own off
437
+ * instead of taking an on.
438
+ */
439
+ function mergedAttentionTelemetry(
440
+ config: AppConfig,
441
+ baselineConfig: AppConfig,
442
+ payload: Record<string, unknown>,
443
+ lastSynced: Record<string, unknown> | null,
444
+ ): AppConfig["telemetry"] {
445
+ // A copy pushed by an older build says nothing either way.
446
+ if (typeof payload.attentionOff !== "boolean") return config.telemetry;
447
+ const localOff = config.telemetry?.attention === false;
448
+ if (payload.attentionOff === localOff) return config.telemetry;
449
+ const editedHere = localOff !== (baselineConfig.telemetry?.attention === false)
450
+ || localOff !== (lastSynced?.attentionOff === true);
451
+ if (editedHere) return config.telemetry;
452
+ return { ...config.telemetry, attention: !payload.attentionOff };
453
+ }
454
+
427
455
  function positionValueBase(
428
456
  ticker: TickerRecord,
429
457
  position: TickerPosition,
@@ -957,6 +985,8 @@ function mergeConfigPayload(
957
985
  assign("valueFlashingEnabled");
958
986
  assign("recentTickers");
959
987
  assign("recentCommands");
988
+ const telemetry = mergedAttentionTelemetry(config, baselineConfig, payload, lastSynced);
989
+ if (telemetry !== config.telemetry) next.telemetry = telemetry;
960
990
  // Sync can complete onboarding on another device, but never reopen it.
961
991
  // Resumable progress remains local until this installation completes it.
962
992
  if (
@@ -1420,6 +1450,8 @@ export const coreConfigSyncContributor: SyncContributor = {
1420
1450
  activeLayoutIndex: nextConfig.activeLayoutIndex,
1421
1451
  });
1422
1452
  }
1453
+ // An Attention Counts off from another device drops unsent counts before React commits.
1454
+ if (nextConfig.telemetry !== getState().config.telemetry) reportTelemetryConfig(nextConfig.telemetry);
1423
1455
  dispatch({ type: "SET_CONFIG", config: nextConfig });
1424
1456
  if (!removed) scheduleConfigSave(nextConfig);
1425
1457
  }
@@ -2,9 +2,11 @@ import { logFailure } from "../utils/log-failure";
2
2
  import { apiClient } from "../api-client";
3
3
  import type { AttentionAction, AttentionCountsPayload } from "../api-client/telemetry";
4
4
  import { CHART_COMPOSER_PANE_ID, TICKER_RESEARCH_PANE_ID, type TelemetryConfig } from "../types/config";
5
+ import { attentionNoticeShown } from "./attention-setting";
5
6
  import { telemetryOptedOut } from "./crash-reports";
6
- import { subscribeTelemetryConfig } from "./live-config";
7
+ import { currentTelemetryConfig, subscribeTelemetryConfig } from "./live-config";
7
8
  import { automationActive } from "./usage-counts";
9
+ import { cloudSyncController } from "../sync/controller";
8
10
 
9
11
  const FLUSH_MS = 60_000;
10
12
  const MAX_AGE_MS = 120_000;
@@ -13,12 +15,19 @@ const MAX_EVENTS = 100;
13
15
  const SYMBOL = /^[A-Z0-9^][A-Z0-9.^_:/=-]{0,63}$/;
14
16
  const ACTIONS = new Set<AttentionAction>(["des", "chart", "quote", "option_chain", "watchlist_add"]);
15
17
 
16
- /** Ticker-level counting always requires its own explicit consent. */
18
+ /**
19
+ * Whether `account`'s ticker opens count. On by default: an explicit `true`
20
+ * counts, `false` never does, and while the setting is untouched the account
21
+ * counts only once this installation has drawn the notice for it.
22
+ */
17
23
  export function attentionCountsEnabled(
18
24
  config: { telemetry?: TelemetryConfig } | null | undefined,
25
+ account: string | null,
19
26
  env: Record<string, string | undefined> = {},
20
27
  ): boolean {
21
- return config?.telemetry?.attention === true && !telemetryOptedOut(env);
28
+ const telemetry = config?.telemetry;
29
+ if (telemetryOptedOut(env) || telemetry?.attention === false) return false;
30
+ return telemetry?.attention === true || attentionNoticeShown(telemetry, account);
22
31
  }
23
32
 
24
33
  interface AttentionHost {
@@ -136,26 +145,75 @@ export class AttentionCounter {
136
145
  }
137
146
 
138
147
  let counter: AttentionCounter | null = null;
148
+ let collectionAllowed: (() => boolean) | null = null;
149
+
150
+ interface AttentionCounterOptions {
151
+ /** False under a privacy signal (`DO_NOT_TRACK`, browser Do Not Track or GPC): nothing counts and no notice shows. */
152
+ allowed(): boolean;
153
+ /** The config the surface launched with; the app's live switches replace it once reported. */
154
+ launchConfig(): { telemetry?: TelemetryConfig } | null | undefined;
155
+ /**
156
+ * Whether this surface has the signed-in account's synced settings, so an
157
+ * Attention Counts off made on another device has arrived before anything
158
+ * counts. Defaults to this session's cloud sync having pulled them; without
159
+ * cloud sync nothing counts. A popped-out desktop window gets them from the
160
+ * main window instead.
161
+ */
162
+ accountSettingsPulled?(): boolean;
163
+ }
164
+
165
+ function cloudSettingsPulled(): boolean {
166
+ return cloudSyncController.getStatus().lastPullAt !== null;
167
+ }
139
168
 
140
169
  /** Installed only by interactive terminal, desktop and web entry points. */
141
- export function installAttentionCounter(isEnabled: () => boolean): () => void {
170
+ export function installAttentionCounter({
171
+ allowed,
172
+ launchConfig,
173
+ accountSettingsPulled = cloudSettingsPulled,
174
+ }: AttentionCounterOptions): () => void {
142
175
  counter?.dispose();
143
176
  const next = new AttentionCounter({
144
- isEnabled,
177
+ isEnabled: () => allowed()
178
+ && accountSettingsPulled()
179
+ && attentionCountsEnabled(currentTelemetryConfig(launchConfig()), apiClient.getCurrentUser()?.id ?? null),
145
180
  verifiedSession: () => apiClient.getVerifiedSessionIdentity(),
146
181
  send: (payload, signal) => apiClient.reportAttentionCounts(payload, signal),
147
182
  });
148
183
  counter = next;
184
+ collectionAllowed = allowed;
149
185
  const config = subscribeTelemetryConfig(() => next.refresh());
150
186
  const auth = apiClient.subscribeCurrentUser(() => next.refresh());
151
187
  return () => {
152
188
  next.dispose();
153
189
  config();
154
190
  auth();
155
- if (counter === next) counter = null;
191
+ if (counter === next) {
192
+ counter = null;
193
+ collectionAllowed = null;
194
+ }
156
195
  };
157
196
  }
158
197
 
198
+ /**
199
+ * Whether this surface counts at all: an interactive entry point installed
200
+ * the collector and no privacy signal is set. The notice shows only here, so
201
+ * CLI commands, `shot`, tests and opted-out environments never draw it.
202
+ */
203
+ export function attentionCollectionAvailable(): boolean {
204
+ try {
205
+ return counter !== null && collectionAllowed?.() === true;
206
+ } catch {
207
+ // Intentional ignore: an unreadable privacy signal counts as set.
208
+ return false;
209
+ }
210
+ }
211
+
212
+ /** Sends what the installed counter holds now instead of after its delay. */
213
+ export async function __flushAttentionCountsForTests(): Promise<void> {
214
+ await counter?.flush();
215
+ }
216
+
159
217
  export function captureAttentionAction(): (symbol: string, action: AttentionAction) => void {
160
218
  return counter?.capture() ?? (() => {});
161
219
  }
@@ -0,0 +1,33 @@
1
+ import type { TelemetryConfig } from "../types/config";
2
+
3
+ /**
4
+ * The Attention Counts setting, apart from the collector. It is on by
5
+ * default: `telemetry.attention: false` turns it off and `true` is an
6
+ * explicit on. While it is unset, an account counts only after this
7
+ * installation has drawn the one-time notice for it; an explicit choice
8
+ * either way never needs the notice. While signed in, an off is the
9
+ * account's: cloud sync carries it to the account's other devices
10
+ * (`mergedAttentionTelemetry` in sync/core-contributors.ts).
11
+ */
12
+
13
+ /** What switching off says, from the notice or the command bar: counting stops, published totals stay. */
14
+ export const ATTENTION_OFF_MESSAGE = "Attention Counts is off. Totals already published can't be withdrawn.";
15
+
16
+ /** How many accounts an installation remembers showing the notice to. */
17
+ export const ATTENTION_NOTICE_ACCOUNTS = 20;
18
+
19
+ export function attentionNoticeShown(telemetry: TelemetryConfig | undefined, account: string | null): boolean {
20
+ return account !== null && (telemetry?.attentionNoticeShownTo?.includes(account) ?? false);
21
+ }
22
+
23
+ /** Whether `account` is waiting for the notice: the setting is untouched and this installation never showed it. */
24
+ export function attentionNoticeDue(telemetry: TelemetryConfig | undefined, account: string | null): boolean {
25
+ return account !== null && telemetry?.attention === undefined && !attentionNoticeShown(telemetry, account);
26
+ }
27
+
28
+ /** Records that the notice was drawn for `account`. The same object when it already was. */
29
+ export function withAttentionNoticeShown(telemetry: TelemetryConfig | undefined, account: string): TelemetryConfig | undefined {
30
+ if (attentionNoticeShown(telemetry, account)) return telemetry;
31
+ const accounts = [...(telemetry?.attentionNoticeShownTo ?? []), account].slice(-ATTENTION_NOTICE_ACCOUNTS);
32
+ return { ...telemetry, attentionNoticeShownTo: accounts };
33
+ }
@@ -2,6 +2,7 @@ import { logFailure } from "../utils/log-failure";
2
2
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
3
3
  import { homedir, release } from "os";
4
4
  import { join } from "path";
5
+ import { writePrivateFileSync } from "../utils/private-files";
5
6
  import type { CrashReportSurface } from "../api-client";
6
7
  import type { TelemetryConfig } from "../types/config";
7
8
  import {
@@ -109,7 +110,8 @@ export function createNodeCrashReporterHost(options: NodeCrashReporterHostOption
109
110
  const dir = dataDir();
110
111
  if (!dir) return;
111
112
  const reports = [...readSpool(dir), report].slice(-MAX_SPOOLED_REPORTS);
112
- writeFileSync(spoolPath(dir), JSON.stringify(reports), "utf-8");
113
+ // Readable by the user alone: a report's stack names the user's files.
114
+ writePrivateFileSync(spoolPath(dir), JSON.stringify(reports));
113
115
  },
114
116
  clearSpool: () => {
115
117
  const dir = dataDir();
@@ -0,0 +1,93 @@
1
+ import { dlopen, FFIType, ptr } from "bun:ffi";
2
+ import type { StallClockReading } from "../utils/main-thread-stall";
3
+
4
+ /** macOS `clock_gettime_nsec_np` clocks: awake time (stops in sleep), and this thread's CPU time. */
5
+ const DARWIN_CLOCK_UPTIME_RAW = 8;
6
+ const DARWIN_CLOCK_THREAD_CPUTIME_ID = 16;
7
+ /** Linux `clock_gettime` clocks: awake time (stops in suspend), and this thread's CPU time. */
8
+ const LINUX_CLOCK_MONOTONIC = 1;
9
+ const LINUX_CLOCK_THREAD_CPUTIME_ID = 3;
10
+ /** Windows counts both in 100 ns units. */
11
+ const WINDOWS_TICKS_PER_MS = 10_000;
12
+
13
+ type PlatformClocks = () => Pick<StallClockReading, "awakeMs" | "threadCpuMs">;
14
+
15
+ function processCpuMs(): number | null {
16
+ if (typeof process.cpuUsage !== "function") return null;
17
+ const usage = process.cpuUsage();
18
+ return (usage.user + usage.system) / 1_000;
19
+ }
20
+
21
+ /**
22
+ * Windows: QueryUnbiasedInterruptTime leaves out sleep and hibernation (the
23
+ * clock timers read keeps counting through them), and GetThreadTimes gives
24
+ * the calling thread's kernel and user time.
25
+ */
26
+ function windowsClocks(): PlatformClocks {
27
+ const kernel32 = dlopen("kernel32.dll", {
28
+ QueryUnbiasedInterruptTime: { args: [FFIType.ptr], returns: FFIType.bool },
29
+ // A handle as a 64-bit integer: the pseudo-handle is -2, which a pointer as a JS number would not keep exactly.
30
+ GetCurrentThread: { args: [], returns: FFIType.u64 },
31
+ GetThreadTimes: { args: [FFIType.u64, FFIType.ptr, FFIType.ptr, FFIType.ptr, FFIType.ptr], returns: FFIType.bool },
32
+ });
33
+ // A pseudo-handle: whichever thread passes it means itself.
34
+ const thread = kernel32.symbols.GetCurrentThread();
35
+ const awake = new BigUint64Array(1);
36
+ // Creation, exit, kernel and user time.
37
+ const times = new BigUint64Array(4);
38
+ return () => ({
39
+ awakeMs: kernel32.symbols.QueryUnbiasedInterruptTime(ptr(awake)) ? Number(awake[0]!) / WINDOWS_TICKS_PER_MS : null,
40
+ threadCpuMs: thread && kernel32.symbols.GetThreadTimes(thread, ptr(times), ptr(times, 8), ptr(times, 16), ptr(times, 24))
41
+ ? Number(times[2]! + times[3]!) / WINDOWS_TICKS_PER_MS
42
+ : null,
43
+ });
44
+ }
45
+
46
+ function darwinClocks(): PlatformClocks {
47
+ const libc = dlopen("/usr/lib/libSystem.B.dylib", {
48
+ clock_gettime_nsec_np: { args: [FFIType.u32], returns: FFIType.u64 },
49
+ });
50
+ const read = (clock: number): number | null => {
51
+ const nanoseconds = Number(libc.symbols.clock_gettime_nsec_np(clock));
52
+ return nanoseconds > 0 ? nanoseconds / 1e6 : null;
53
+ };
54
+ return () => ({ awakeMs: read(DARWIN_CLOCK_UPTIME_RAW), threadCpuMs: read(DARWIN_CLOCK_THREAD_CPUTIME_ID) });
55
+ }
56
+
57
+ function linuxClocks(): PlatformClocks {
58
+ const libc = dlopen("libc.so.6", {
59
+ clock_gettime: { args: [FFIType.i32, FFIType.ptr], returns: FFIType.i32 },
60
+ });
61
+ // struct timespec: seconds, nanoseconds.
62
+ const time = new BigInt64Array(2);
63
+ const read = (clock: number): number | null => (
64
+ libc.symbols.clock_gettime(clock, ptr(time)) === 0 ? Number(time[0]!) * 1_000 + Number(time[1]!) / 1e6 : null
65
+ );
66
+ return () => ({ awakeMs: read(LINUX_CLOCK_MONOTONIC), threadCpuMs: read(LINUX_CLOCK_THREAD_CPUTIME_ID) });
67
+ }
68
+
69
+ function platformClocks(): PlatformClocks | null {
70
+ try {
71
+ if (process.platform === "win32") return windowsClocks();
72
+ if (process.platform === "darwin") return darwinClocks();
73
+ if (process.platform === "linux") return linuxClocks();
74
+ } catch {
75
+ // Intentional ignore: without the platform's clocks a stall still counts, only its cause stays unknown.
76
+ }
77
+ return null;
78
+ }
79
+
80
+ /**
81
+ * The clocks the stall monitor reads on each check, for the thread that
82
+ * calls this: the thread's CPU time is the caller's own. A platform whose
83
+ * clocks cannot be loaded gives the process's CPU time alone.
84
+ */
85
+ export function createStallClocks(): () => StallClockReading {
86
+ const platform = platformClocks();
87
+ return () => ({
88
+ awakeMs: null,
89
+ threadCpuMs: null,
90
+ ...platform?.(),
91
+ processCpuMs: processCpuMs(),
92
+ });
93
+ }