gloomberb 0.18.1 → 0.18.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 (416) hide show
  1. package/package.json +1 -1
  2. package/patches/electrobun@1.18.1.patch +106 -5
  3. package/src/api-client/commodity-balances.ts +6 -0
  4. package/src/api-client/company-kpis.ts +9 -0
  5. package/src/api-client/crypto-markets.ts +9 -0
  6. package/src/api-client/data.ts +21 -5
  7. package/src/api-client/equity-screener.ts +2 -0
  8. package/src/api-client/index.ts +2 -0
  9. package/src/api-client/request.ts +5 -3
  10. package/src/api-client/telemetry.ts +17 -0
  11. package/src/api-client/types.ts +5 -0
  12. package/src/app/global-shortcuts.ts +1 -16
  13. package/src/app/pane-runtime/plugin-bindings.ts +3 -1
  14. package/src/app/pane-runtime/ticker-open-runtime.ts +17 -5
  15. package/src/app/runtime/plugin-bindings.ts +6 -2
  16. package/src/app/runtime/update.ts +28 -8
  17. package/src/architecture/pane-tabs-allowlist.json +65 -0
  18. package/src/brokers/logos.ts +66 -0
  19. package/src/brokers/profile-form.ts +3 -0
  20. package/src/brokers/remote-broker-adapter.ts +13 -3
  21. package/src/capabilities/index.ts +1 -1
  22. package/src/capabilities/service-capabilities.ts +7 -1
  23. package/src/cli/base-converter.ts +60 -18
  24. package/src/cli/commands/market.ts +354 -187
  25. package/src/cli/commands/news-search.ts +35 -8
  26. package/src/cli/commands/system.ts +35 -1
  27. package/src/cli/commands/ticker.ts +167 -60
  28. package/src/cli/currency-hints.ts +27 -0
  29. package/src/cli/fx-pair.ts +41 -4
  30. package/src/cli/glossary.ts +57 -4
  31. package/src/cli/help.ts +1 -1
  32. package/src/cli/helpers.ts +36 -3
  33. package/src/cli/index.ts +12 -1
  34. package/src/cli/issuer-membership.ts +103 -0
  35. package/src/cli/known-gaps.ts +37 -1
  36. package/src/cli/listing-arg.ts +51 -3
  37. package/src/cli/name-entries.ts +51 -12
  38. package/src/cli/pane-functions/capabilities.ts +2 -0
  39. package/src/cli/pane-functions/catalog.ts +19 -1
  40. package/src/cli/pane-functions/discovery.ts +15 -1
  41. package/src/cli/pane-functions/dom.ts +11 -5
  42. package/src/cli/pane-functions/freshness.ts +4 -4
  43. package/src/cli/pane-functions/function-help.ts +27 -3
  44. package/src/cli/pane-functions/headless.ts +31 -11
  45. package/src/cli/pane-functions/index.ts +67 -7
  46. package/src/cli/pane-functions/lock-note.ts +7 -4
  47. package/src/cli/pane-functions/resolver.ts +21 -6
  48. package/src/cli/quote-answer.ts +179 -0
  49. package/src/cli/report-tables.ts +136 -18
  50. package/src/cli/result.ts +12 -4
  51. package/src/cli/series-search.ts +2 -1
  52. package/src/cli/symbol-list.ts +47 -11
  53. package/src/cli/types.ts +4 -0
  54. package/src/components/chart/composite/composite-chart.tsx +25 -7
  55. package/src/components/chart/composite/drawing-store.ts +26 -6
  56. package/src/components/chart/composite/format.ts +13 -1
  57. package/src/components/chart/composite/interactions.ts +2 -1
  58. package/src/components/chart/composite/level-labels.ts +30 -0
  59. package/src/components/chart/composite/levels.ts +17 -6
  60. package/src/components/chart/composite/panel-surface.tsx +69 -8
  61. package/src/components/chart/composite/types.ts +6 -0
  62. package/src/components/chart/composite/use-composite-chart-keyboard.ts +1 -1
  63. package/src/components/command-bar/routes/root/command-items.ts +32 -6
  64. package/src/components/command-bar/routes/ticker-search/actions.ts +6 -0
  65. package/src/components/data-table/view.tsx +23 -3
  66. package/src/components/index.ts +3 -0
  67. package/src/components/layout/detached-pane-shell.tsx +1 -0
  68. package/src/components/layout/floating-pane.tsx +1 -0
  69. package/src/components/layout/header.tsx +228 -55
  70. package/src/components/layout/pane/content.tsx +16 -14
  71. package/src/components/layout/pane/footer/index.tsx +22 -3
  72. package/src/components/layout/pane/footer/model.ts +99 -4
  73. package/src/components/layout/pane/header-tabs.tsx +15 -1
  74. package/src/components/layout/pane/header.tsx +10 -3
  75. package/src/components/layout/pane/index.tsx +1 -0
  76. package/src/components/layout/pane/pane-tabs.tsx +9 -1
  77. package/src/components/onboarding/onboarding-frame.tsx +9 -11
  78. package/src/components/onboarding/use-onboarding-broker-form.ts +5 -2
  79. package/src/components/onboarding/wizard-model.ts +10 -3
  80. package/src/components/ui/button.tsx +11 -4
  81. package/src/components/ui/choice-dialog.tsx +8 -5
  82. package/src/components/ui/display.tsx +11 -6
  83. package/src/components/ui/fields.tsx +100 -20
  84. package/src/components/ui/icon.tsx +6 -1
  85. package/src/components/ui/list-row-lines.tsx +87 -0
  86. package/src/components/ui/list-view.tsx +14 -0
  87. package/src/components/ui/logo-mark.tsx +64 -0
  88. package/src/components/ui/number-step.ts +54 -0
  89. package/src/components/ui/select-button.tsx +5 -4
  90. package/src/components/ui/select-field.tsx +3 -3
  91. package/src/components/ui/stat-grid.tsx +28 -3
  92. package/src/components/ui/status.tsx +20 -4
  93. package/src/components/ui/tabs.tsx +9 -1
  94. package/src/components/ui/toggle.tsx +17 -3
  95. package/src/core/app-services.ts +4 -0
  96. package/src/core/state/app/instrument.ts +8 -3
  97. package/src/data/config/store/normalize.ts +4 -1
  98. package/src/data/plugin-cache.ts +32 -6
  99. package/src/docs/snapshot.json +5 -5
  100. package/src/i18n/es.ts +31 -3
  101. package/src/i18n/ja.ts +31 -3
  102. package/src/i18n/ko.ts +31 -3
  103. package/src/i18n/zh-cn.ts +32 -4
  104. package/src/i18n/zh-tw.ts +32 -4
  105. package/src/market-data/commodity-names.ts +80 -0
  106. package/src/market-data/coordinator/auxiliary.ts +3 -1
  107. package/src/market-data/coordinator/chart.ts +3 -1
  108. package/src/market-data/futures-board.ts +38 -1
  109. package/src/market-data/futures-contract.ts +91 -0
  110. package/src/market-data/fx-closes.ts +78 -1
  111. package/src/market-data/market/change-basis.ts +219 -0
  112. package/src/market-data/market/format.ts +28 -12
  113. package/src/market-data/market/freshness.ts +38 -0
  114. package/src/market-data/market/status.ts +55 -9
  115. package/src/market-data/market/year-to-date.ts +46 -0
  116. package/src/market-data/quotes/contributions.ts +12 -0
  117. package/src/market-data/quotes/delisting.ts +34 -0
  118. package/src/market-data/quotes/live-valuation.ts +23 -0
  119. package/src/market-data/quotes/resolution.ts +8 -2
  120. package/src/market-data/tax-equivalent.ts +10 -2
  121. package/src/notifications/app-notifier.ts +2 -2
  122. package/src/notifications/notification-log.ts +8 -6
  123. package/src/plugins/builtin/alerts/alert-engine.ts +3 -1
  124. package/src/plugins/builtin/alerts/cli.ts +26 -18
  125. package/src/plugins/builtin/alerts/events.ts +1 -0
  126. package/src/plugins/builtin/alerts/headless.ts +1 -1
  127. package/src/plugins/builtin/alerts/index.tsx +13 -4
  128. package/src/plugins/builtin/alerts/research-rules.ts +1 -0
  129. package/src/plugins/builtin/analytics/risk-pane.tsx +10 -7
  130. package/src/plugins/builtin/attention/pane.tsx +16 -14
  131. package/src/plugins/builtin/attention/trending.tsx +8 -6
  132. package/src/plugins/builtin/awards/pane.tsx +6 -6
  133. package/src/plugins/builtin/backtest/client.ts +5 -2
  134. package/src/plugins/builtin/backtest/export.ts +1 -1
  135. package/src/plugins/builtin/backtest/window.ts +16 -3
  136. package/src/plugins/builtin/broker-manager/add-flow.tsx +40 -8
  137. package/src/plugins/builtin/broker-manager/detail.tsx +26 -1
  138. package/src/plugins/builtin/broker-manager/index.tsx +20 -9
  139. package/src/plugins/builtin/broker-manager/model.ts +5 -0
  140. package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -1
  141. package/src/plugins/builtin/broker-manager/table.ts +5 -1
  142. package/src/plugins/builtin/broker-trading/account-picker.tsx +91 -0
  143. package/src/plugins/builtin/broker-trading/chart-levels.ts +54 -0
  144. package/src/plugins/builtin/broker-trading/command.ts +103 -0
  145. package/src/plugins/builtin/broker-trading/controller.ts +688 -0
  146. package/src/plugins/builtin/broker-trading/dock.tsx +184 -0
  147. package/src/plugins/builtin/broker-trading/headless.ts +76 -0
  148. package/src/plugins/builtin/broker-trading/index.tsx +77 -0
  149. package/src/plugins/builtin/broker-trading/order-feed.ts +383 -0
  150. package/src/plugins/builtin/broker-trading/orders-data.ts +133 -0
  151. package/src/plugins/builtin/broker-trading/orders.tsx +437 -0
  152. package/src/plugins/builtin/broker-trading/overview-orders.tsx +121 -0
  153. package/src/plugins/builtin/broker-trading/pane-model.ts +207 -0
  154. package/src/plugins/builtin/broker-trading/pane.tsx +507 -0
  155. package/src/plugins/builtin/broker-trading/strategy.ts +153 -0
  156. package/src/plugins/builtin/broker-trading/ticket-parts.tsx +117 -0
  157. package/src/plugins/builtin/broker-trading/ticket.tsx +578 -0
  158. package/src/plugins/builtin/buildout/data-runtime.ts +1 -1
  159. package/src/plugins/builtin/buildout/format.ts +1 -0
  160. package/src/plugins/builtin/buildout/model/index.ts +16 -7
  161. package/src/plugins/builtin/buildout/pane/index.tsx +11 -7
  162. package/src/plugins/builtin/buildout/pane/status.tsx +3 -3
  163. package/src/plugins/builtin/catalysts/client.ts +2 -2
  164. package/src/plugins/builtin/catalysts/pane.tsx +3 -3
  165. package/src/plugins/builtin/chart-composer/cli-options.ts +19 -0
  166. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +2 -1
  167. package/src/plugins/builtin/chart-composer/headless-schema.ts +3 -3
  168. package/src/plugins/builtin/chart-composer/headless.ts +2 -1
  169. package/src/plugins/builtin/chart-composer/option-range.ts +39 -0
  170. package/src/plugins/builtin/chart-composer/pane.tsx +32 -11
  171. package/src/plugins/builtin/chart-composer/presets.ts +2 -2
  172. package/src/plugins/builtin/chart-composer/series-catalog.ts +4 -2
  173. package/src/plugins/builtin/chart-composer/shortcuts.ts +3 -3
  174. package/src/plugins/builtin/chart-composer/viewport-labels.ts +5 -2
  175. package/src/plugins/builtin/chat/index.tsx +1 -1
  176. package/src/plugins/builtin/chat/status-widget.tsx +1 -1
  177. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +6 -1
  178. package/src/plugins/builtin/chat/unread-inbox.ts +1 -1
  179. package/src/plugins/builtin/commodity-balances/client.ts +6 -4
  180. package/src/plugins/builtin/commodity-balances/headless.ts +41 -7
  181. package/src/plugins/builtin/commodity-balances/index.tsx +19 -4
  182. package/src/plugins/builtin/commodity-balances/model.ts +22 -0
  183. package/src/plugins/builtin/commodity-balances/pane.tsx +39 -9
  184. package/src/plugins/builtin/company-attention/app-rank.tsx +4 -5
  185. package/src/plugins/builtin/company-attention/pane.tsx +13 -11
  186. package/src/plugins/builtin/company-kpis/client.ts +3 -2
  187. package/src/plugins/builtin/company-kpis/headless.ts +5 -7
  188. package/src/plugins/builtin/company-kpis/model.ts +85 -2
  189. package/src/plugins/builtin/company-kpis/pane.tsx +30 -15
  190. package/src/plugins/builtin/composite-plugins.ts +3 -2
  191. package/src/plugins/builtin/correlation/headless.ts +2 -1
  192. package/src/plugins/builtin/correlation/relationship/model.ts +1 -0
  193. package/src/plugins/builtin/correlation/settings.ts +1 -0
  194. package/src/plugins/builtin/cot/headless.ts +1 -1
  195. package/src/plugins/builtin/cot/index.tsx +1 -1
  196. package/src/plugins/builtin/cot/model.ts +35 -12
  197. package/src/plugins/builtin/cot/pane.tsx +4 -3
  198. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  199. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +2 -1
  200. package/src/plugins/builtin/credit-conditions/index.tsx +2 -1
  201. package/src/plugins/builtin/credit-documents/pane.tsx +8 -7
  202. package/src/plugins/builtin/crypto-board/client.ts +33 -2
  203. package/src/plugins/builtin/crypto-board/headless.ts +5 -2
  204. package/src/plugins/builtin/crypto-board/model.ts +28 -8
  205. package/src/plugins/builtin/debt-maturities/pane.tsx +4 -3
  206. package/src/plugins/builtin/dividend-yield/headless.ts +30 -8
  207. package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
  208. package/src/plugins/builtin/doe/pane.tsx +1 -1
  209. package/src/plugins/builtin/earnings-calls/headless.ts +4 -4
  210. package/src/plugins/builtin/earnings-calls/lock-note.ts +12 -3
  211. package/src/plugins/builtin/econ/calendar-model.ts +35 -0
  212. package/src/plugins/builtin/econ/index.tsx +31 -4
  213. package/src/plugins/builtin/econ/print-watch.ts +279 -0
  214. package/src/plugins/builtin/equity-screener/headless.ts +30 -5
  215. package/src/plugins/builtin/equity-screener/model.ts +68 -2
  216. package/src/plugins/builtin/equity-screener/pane.tsx +129 -119
  217. package/src/plugins/builtin/estimate-revisions/headless.ts +3 -2
  218. package/src/plugins/builtin/estimate-revisions/lock-note.ts +29 -7
  219. package/src/plugins/builtin/exposure/client.ts +4 -1
  220. package/src/plugins/builtin/exposure/editor.tsx +3 -3
  221. package/src/plugins/builtin/exposure/evidence.ts +4 -1
  222. package/src/plugins/builtin/exposure/holdings.ts +3 -1
  223. package/src/plugins/builtin/exposure/pane.tsx +43 -21
  224. package/src/plugins/builtin/filing-events/index.tsx +9 -3
  225. package/src/plugins/builtin/futures/table.tsx +6 -3
  226. package/src/plugins/builtin/futures-curve/pane.tsx +2 -2
  227. package/src/plugins/builtin/gpu/board.tsx +4 -1
  228. package/src/plugins/builtin/gpu/changes.tsx +10 -10
  229. package/src/plugins/builtin/gpu/equities.tsx +4 -1
  230. package/src/plugins/builtin/gpu/history.tsx +4 -2
  231. package/src/plugins/builtin/gpu/pane.tsx +5 -3
  232. package/src/plugins/builtin/holders/client.ts +2 -1
  233. package/src/plugins/builtin/holders/headless.ts +2 -1
  234. package/src/plugins/builtin/holders/thirteenf-match.ts +3 -1
  235. package/src/plugins/builtin/insider/base-values.ts +76 -0
  236. package/src/plugins/builtin/insider/client.ts +3 -0
  237. package/src/plugins/builtin/insider/headless.ts +48 -10
  238. package/src/plugins/builtin/insider/index.tsx +17 -8
  239. package/src/plugins/builtin/insider/issuer.ts +34 -4
  240. package/src/plugins/builtin/insider/signals-client.ts +2 -2
  241. package/src/plugins/builtin/insider/table-model.ts +35 -13
  242. package/src/plugins/builtin/insider/table.tsx +5 -2
  243. package/src/plugins/builtin/iv-history/events.ts +145 -0
  244. package/src/plugins/builtin/iv-history/format.ts +3 -0
  245. package/src/plugins/builtin/iv-history/headless.ts +91 -7
  246. package/src/plugins/builtin/jobs/headless.ts +2 -2
  247. package/src/plugins/builtin/jobs/lock-note.ts +14 -3
  248. package/src/plugins/builtin/jodi/pane.tsx +4 -4
  249. package/src/plugins/builtin/market-halts/model.ts +31 -6
  250. package/src/plugins/builtin/market-halts/pane.tsx +3 -0
  251. package/src/plugins/builtin/market-movers/client.ts +4 -3
  252. package/src/plugins/builtin/market-movers/screener.ts +4 -3
  253. package/src/plugins/builtin/market-valuation/pane.tsx +2 -1
  254. package/src/plugins/builtin/members/pane.tsx +8 -3
  255. package/src/plugins/builtin/news/wire/feed-config.ts +29 -12
  256. package/src/plugins/builtin/news/wire/index.tsx +7 -2
  257. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +11 -3
  258. package/src/plugins/builtin/news/wire/rss/cli.ts +3 -2
  259. package/src/plugins/builtin/news/wire/rss/source.ts +13 -4
  260. package/src/plugins/builtin/notes/cli.ts +1 -1
  261. package/src/plugins/builtin/notes/export.ts +17 -6
  262. package/src/plugins/builtin/notes/files.ts +16 -5
  263. package/src/plugins/builtin/options/enrichment-model.ts +10 -9
  264. package/src/plugins/builtin/options/footer.ts +25 -20
  265. package/src/plugins/builtin/options/live-quotes.ts +33 -18
  266. package/src/plugins/builtin/options/model-spot.ts +70 -4
  267. package/src/plugins/builtin/options/view.tsx +118 -35
  268. package/src/plugins/builtin/options-calculator/pane.tsx +7 -4
  269. package/src/plugins/builtin/options-positioning/headless.ts +11 -3
  270. package/src/plugins/builtin/options-positioning/model.ts +24 -0
  271. package/src/plugins/builtin/options-positioning/pane.tsx +10 -12
  272. package/src/plugins/builtin/options-scenario/client.ts +19 -1
  273. package/src/plugins/builtin/options-scenario/headless.ts +13 -6
  274. package/src/plugins/builtin/options-scenario/pane.tsx +54 -11
  275. package/src/plugins/builtin/plugin-marketplace/pane.tsx +5 -4
  276. package/src/plugins/builtin/portfolio-list/account-metrics.ts +3 -0
  277. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -5
  278. package/src/plugins/builtin/portfolio-list/cli/render.ts +34 -10
  279. package/src/plugins/builtin/portfolio-list/headless.ts +1 -1
  280. package/src/plugins/builtin/portfolio-list/pane/data.ts +23 -1
  281. package/src/plugins/builtin/portfolio-list/pane/index.tsx +33 -14
  282. package/src/plugins/builtin/portfolio-list/settings.ts +20 -2
  283. package/src/plugins/builtin/portfolio-list/summary/index.tsx +12 -0
  284. package/src/plugins/builtin/portfolio-list/summary/totals.ts +24 -3
  285. package/src/plugins/builtin/power/client.ts +2 -2
  286. package/src/plugins/builtin/power/pane.tsx +36 -31
  287. package/src/plugins/builtin/primary-dealers/client.ts +137 -62
  288. package/src/plugins/builtin/primary-dealers/headless.ts +30 -5
  289. package/src/plugins/builtin/primary-dealers/index.tsx +3 -2
  290. package/src/plugins/builtin/primary-dealers/model.ts +179 -129
  291. package/src/plugins/builtin/primary-dealers/pane.tsx +135 -15
  292. package/src/plugins/builtin/research/analyst-model.ts +5 -1
  293. package/src/plugins/builtin/reverse-dcf/client.ts +2 -1
  294. package/src/plugins/builtin/reverse-dcf/model.ts +4 -3
  295. package/src/plugins/builtin/risk-factors/index.tsx +12 -3
  296. package/src/plugins/builtin/scanner/feed.ts +6 -1
  297. package/src/plugins/builtin/scanner/headless.ts +1 -1
  298. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  299. package/src/plugins/builtin/seasonality/client.ts +2 -1
  300. package/src/plugins/builtin/sectors/client.ts +4 -3
  301. package/src/plugins/builtin/series-inventory.ts +1 -0
  302. package/src/plugins/builtin/shared/cloud-denied.ts +7 -6
  303. package/src/plugins/builtin/shared/cloud-resource.ts +15 -2
  304. package/src/plugins/builtin/shared/cloud-upgrade.ts +10 -11
  305. package/src/plugins/builtin/shared/free-routes.ts +24 -0
  306. package/src/plugins/builtin/shared/headless-options.ts +11 -0
  307. package/src/plugins/builtin/shared/pro-lock-note.ts +89 -7
  308. package/src/plugins/builtin/shared/research-trading.tsx +64 -0
  309. package/src/plugins/builtin/shared/series-list-detail.tsx +3 -1
  310. package/src/plugins/builtin/shared/volatility/options.ts +2 -1
  311. package/src/plugins/builtin/social-mentions/client.ts +2 -1
  312. package/src/plugins/builtin/social-mentions/headless.ts +1 -1
  313. package/src/plugins/builtin/social-mentions/lock-note.ts +1 -1
  314. package/src/plugins/builtin/themes/headless.ts +8 -2
  315. package/src/plugins/builtin/themes/model.ts +43 -10
  316. package/src/plugins/builtin/themes/pane.tsx +8 -6
  317. package/src/plugins/builtin/ticker-detail/overview/model.ts +44 -10
  318. package/src/plugins/builtin/ticker-detail/overview/option-contract.ts +64 -0
  319. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +82 -21
  320. package/src/plugins/builtin/ticker-detail/pane.tsx +21 -14
  321. package/src/plugins/builtin/ticker-detail/quote-footer.ts +5 -1
  322. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +37 -8
  323. package/src/plugins/builtin/time-sales/pane.tsx +2 -2
  324. package/src/plugins/builtin/trace-bonds/client.ts +14 -3
  325. package/src/plugins/builtin/trace-bonds/headless.ts +1 -1
  326. package/src/plugins/builtin/vol-surface/client.ts +23 -3
  327. package/src/plugins/builtin/vol-surface/headless.ts +33 -14
  328. package/src/plugins/builtin/vol-surface/model.ts +30 -10
  329. package/src/plugins/builtin/vol-surface/pane.tsx +35 -15
  330. package/src/plugins/builtin/vol-surface/stored.ts +34 -3
  331. package/src/plugins/builtin/vol-surface/tables.ts +14 -0
  332. package/src/plugins/builtin-editorial.ts +1 -1
  333. package/src/plugins/compat.ts +3 -0
  334. package/src/plugins/external-runtime.ts +5 -0
  335. package/src/plugins/loader.ts +6 -0
  336. package/src/plugins/plugin-icon.ts +61 -0
  337. package/src/plugins/registry/host-actions.ts +2 -1
  338. package/src/plugins/registry/index.ts +9 -1
  339. package/src/plugins/runtime/context.tsx +5 -0
  340. package/src/plugins/runtime/index.tsx +3 -0
  341. package/src/portfolio/column-values.ts +13 -4
  342. package/src/portfolio/cost-fx.ts +5 -28
  343. package/src/public/__snapshots__/public-api.test.ts.snap +5 -0
  344. package/src/public/broker.ts +8 -0
  345. package/src/public/market-data.ts +5 -0
  346. package/src/renderers/dom/desktop/control-styles.ts +7 -1
  347. package/src/renderers/dom/desktop/controls.tsx +40 -8
  348. package/src/renderers/dom/desktop/icons.tsx +14 -0
  349. package/src/renderers/dom/desktop/list-view.tsx +51 -19
  350. package/src/renderers/dom/desktop/menu.tsx +10 -1
  351. package/src/renderers/dom/desktop/query-bar.tsx +2 -0
  352. package/src/renderers/dom/desktop/select-field.tsx +8 -6
  353. package/src/renderers/dom/host/style.ts +6 -1
  354. package/src/renderers/dom/host/tabs.tsx +22 -5
  355. package/src/renderers/dom/host/top-surface.ts +127 -0
  356. package/src/renderers/dom/styles.css +51 -5
  357. package/src/renderers/electrobun/bun/core-capabilities.ts +21 -1
  358. package/src/renderers/electrobun/bun/desktop/backend.ts +10 -0
  359. package/src/renderers/electrobun/bun/desktop/update.ts +210 -30
  360. package/src/renderers/electrobun/bun/external-plugins.ts +3 -0
  361. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  362. package/src/renderers/electrobun/view/broker-remote-client.ts +16 -0
  363. package/src/renderers/electrobun/view/external-plugins.ts +1 -0
  364. package/src/renderers/electrobun/view/news-source.ts +4 -2
  365. package/src/renderers/electrobun/view/remote/asset-data-client.ts +8 -1
  366. package/src/renderers/opentui/start.tsx +7 -0
  367. package/src/series/validate.ts +2 -2
  368. package/src/sources/gloomberb-cloud/index.ts +57 -17
  369. package/src/sources/gloomberb-cloud/normalizers.ts +4 -0
  370. package/src/sources/history-result.ts +3 -0
  371. package/src/sources/provider-errors.ts +3 -1
  372. package/src/sources/provider-router/history.ts +49 -6
  373. package/src/sources/provider-router/index.ts +18 -1
  374. package/src/state/app/activity.ts +13 -0
  375. package/src/sync/controller.ts +4 -0
  376. package/src/sync/core-contributors.ts +62 -11
  377. package/src/sync/local-only.ts +121 -0
  378. package/src/sync/react.ts +6 -0
  379. package/src/sync/types.ts +3 -0
  380. package/src/telemetry/crash-reports.ts +4 -0
  381. package/src/telemetry/update-failures-node.ts +59 -0
  382. package/src/telemetry/update-failures.ts +307 -0
  383. package/src/tickers/company-name-listing.ts +9 -7
  384. package/src/tickers/company-name.ts +43 -0
  385. package/src/tickers/listing-fate.ts +57 -8
  386. package/src/tickers/search/index.ts +24 -3
  387. package/src/time-series/price-comparison.ts +35 -0
  388. package/src/time-series/resolve.ts +52 -11
  389. package/src/time-series/spec.ts +1 -1
  390. package/src/time-series/types.ts +5 -0
  391. package/src/types/broker.ts +56 -1
  392. package/src/types/config.ts +6 -0
  393. package/src/types/data-provider.ts +11 -1
  394. package/src/types/financials.ts +23 -0
  395. package/src/types/headless.ts +19 -2
  396. package/src/types/instrument.ts +4 -0
  397. package/src/types/issuer-listings.ts +28 -0
  398. package/src/types/plugin.ts +24 -6
  399. package/src/types/price-history.ts +6 -1
  400. package/src/types/trading.ts +67 -0
  401. package/src/ui/host.tsx +4 -0
  402. package/src/updater/failure.ts +163 -0
  403. package/src/updater/index.ts +87 -23
  404. package/src/utils/base-currency.ts +108 -0
  405. package/src/utils/currency-names.ts +128 -0
  406. package/src/utils/currency-pair.ts +28 -1
  407. package/src/utils/currency-units.ts +32 -1
  408. package/src/utils/dom-screenshot.ts +22 -9
  409. package/src/utils/exchange-notation.generated.ts +360 -0
  410. package/src/utils/exchanges.ts +15 -322
  411. package/src/utils/format.ts +31 -1
  412. package/src/utils/instrument-identity.ts +38 -1
  413. package/src/utils/options.ts +46 -1
  414. package/src/utils/price-history.ts +4 -0
  415. package/src/utils/reported-money.ts +8 -4
  416. package/src/utils/sec.ts +68 -0
@@ -15,6 +15,7 @@ import type { Quote } from "../../../types/financials";
15
15
  import { regularSessionValuationPrice } from "../../../market-data/quotes/regular-session-valuation";
16
16
  import { dividendPriceStatus, dividendQuotePriceMetadata } from "./reference-price";
17
17
  import { mapWithConcurrency } from "../../../utils/concurrency";
18
+ import { logFailure } from "../../../utils/log-failure";
18
19
  import { projectDividendRanking, roundedRatio, type DividendRankingEntry } from "./ranking";
19
20
  import { REPORT_FILTER, REPORT_ROWS } from "../shared/headless-options";
20
21
  import type { FundBondFacts } from "../../../api-client/fund-facts";
@@ -50,6 +51,9 @@ export interface DividendYieldHeadlessDependencies {
50
51
  loadTreasury?(context: HeadlessPaneContext): Promise<TreasuryTenor[] | null>;
51
52
  }
52
53
 
54
+ /** Already logged at the quote boundary; keep it distinct from absent bond facts. */
55
+ class BondClassificationError extends Error {}
56
+
53
57
  const defaultDependencies: DividendYieldHeadlessDependencies = {
54
58
  async loadData(symbol, context) {
55
59
  let currentPrice: number | null = null;
@@ -63,7 +67,8 @@ const defaultDependencies: DividendYieldHeadlessDependencies = {
63
67
  currentPrice = regularSessionValuationPrice(quote);
64
68
  currentPriceCurrency = quote.currency;
65
69
  referenceQuote = quote;
66
- } catch {
70
+ } catch (error) {
71
+ logFailure("dividend-yield.headless", "Read dividend reference quote failed")(error);
67
72
  currentPrice = null;
68
73
  }
69
74
  const data = await fetchDividendData(symbol, currentPrice, exchange, currentPriceCurrency);
@@ -75,7 +80,11 @@ const defaultDependencies: DividendYieldHeadlessDependencies = {
75
80
  async loadBond(symbol, context) {
76
81
  const instrument = await context.resolveInstrument?.(symbol);
77
82
  const exchange = instrument?.exchange ?? "";
78
- const quote = await context.marketData.getQuote(symbol, exchange).catch(() => undefined);
83
+ const quote = await context.marketData.getQuote(symbol, exchange)
84
+ .catch((error) => {
85
+ logFailure("dividend-yield.headless", "Read bond fund classification quote failed")(error);
86
+ throw new BondClassificationError("Bond fund quote check failed");
87
+ });
79
88
  const venue = exchange || quote?.exchangeName || "";
80
89
  if (resolveTickerInstrumentKind(null, { quote }) !== "fund" || (venue && !isUsListingExchange(venue))) return null;
81
90
  return fetchFundBondFacts(instrument?.symbol ?? symbol, context.apiClient);
@@ -125,7 +134,7 @@ export function projectDividendYieldHeadless(
125
134
  const metrics = data.metrics;
126
135
  const currency = data.currency ?? data.payments[0]?.currency ?? "";
127
136
  const priceStatus = dividendPriceStatus(data.price, data.priceAsOf, data.priceStale);
128
- const errors = [data.historyError, data.summaryError].filter((error): error is string => !!error);
137
+ const errors = [data.historyError, data.summaryError, tax?.checkError].filter((error): error is string => !!error);
129
138
 
130
139
  return {
131
140
  ...(errors.length > 0 ? { complete: false, errors } : {}),
@@ -249,15 +258,26 @@ export function createDividendYieldHeadless(
249
258
  // One symbol is its full report; several are one table ranked by yield.
250
259
  if (args.symbols.length <= 1) {
251
260
  const symbol = args.symbols[0] ?? String(args.argument);
261
+ let bondCheckError: string | undefined;
252
262
  const [data, bond, treasury] = await Promise.all([
253
263
  dependencies.loadData(symbol, context),
254
- // A fund's bond facts never fail the report: without them its yields read as before.
255
- dependencies.loadBond?.(symbol, context).catch(() => null) ?? null,
256
- rates ? dependencies.loadTreasury?.(context).catch(() => null) ?? null : null,
264
+ // Optional enrichment without a tax rate; a requested tax check must distinguish failure from absence.
265
+ dependencies.loadBond?.(symbol, context)
266
+ .catch((error) => {
267
+ if (error instanceof BondClassificationError) {
268
+ bondCheckError = error.message;
269
+ } else {
270
+ logFailure("dividend-yield.headless", "Read bond fund facts for report failed")(error);
271
+ bondCheckError = "Bond fund facts check failed";
272
+ }
273
+ return null;
274
+ }) ?? null,
275
+ rates ? dependencies.loadTreasury?.(context)
276
+ .catch(logFailure("dividend-yield.headless", "Read Treasury curve for tax comparison failed", null)) ?? null : null,
257
277
  ]);
258
278
  const forward = data.metrics.lapse ? null : distributionBasis(data.metrics.forwardYield, "forward", data.metrics.forwardRateAsOf ?? null);
259
279
  const tax = rates ? analyzeTaxEquivalent({
260
- symbol: symbol.toUpperCase(), bond, rates, treasury,
280
+ symbol: symbol.toUpperCase(), bond, rates, treasury, checkError: bondCheckError,
261
281
  distribution: forward ?? distributionBasis(data.metrics.trailingYield, "trailing"),
262
282
  }) : null;
263
283
  return projectDividendYieldHeadless(data, args, { bond, tax });
@@ -275,7 +295,9 @@ export function createDividendYieldHeadless(
275
295
  }
276
296
  const entries = await mapWithConcurrency(args.symbols, RANKING_CONCURRENCY, async (symbol): Promise<DividendRankingEntry> => {
277
297
  // A fund's bond facts never fail its row: without them it ranks as before.
278
- const bond = dependencies.loadBond?.(symbol, context).catch(() => null) ?? null;
298
+ const bond = dependencies.loadBond?.(symbol, context)
299
+ .catch((error) => error instanceof BondClassificationError ? null
300
+ : logFailure("dividend-yield.headless", "Read bond fund facts for ranking failed", null)(error)) ?? null;
279
301
  try {
280
302
  return { symbol, data: await dependencies.loadData(symbol, context), bond: await bond };
281
303
  } catch (error) {
@@ -3,6 +3,7 @@ import { regularSessionValuationPrice } from "../../../market-data/quotes/regula
3
3
  import { resolveCurrencyUnit } from "../../../utils/currency-units";
4
4
  import { cashInMajorUnits } from "../../../utils/dividend-cash";
5
5
  import { dividendReferencePrice } from "../../../utils/trailing-dividend";
6
+ import { logFailure } from "../../../utils/log-failure";
6
7
  import { buildDividendMetrics, INCOMPLETE_DIVIDEND_HISTORY, MISSING_DIVIDEND_CURRENCY, toDividendPayment, type DividendData } from "./client";
7
8
  import { dividendQuotePriceMetadata } from "./reference-price";
8
9
 
@@ -17,7 +18,8 @@ export async function fetchProviderDividendData(
17
18
  if (!provider.getCorporateActions) throw new Error("Dividend history source unavailable.");
18
19
  const [sourceActions, quote] = await Promise.all([
19
20
  provider.getCorporateActions(symbol, exchange),
20
- provider.getQuote(symbol, exchange).catch(() => null),
21
+ provider.getQuote(symbol, exchange)
22
+ .catch(logFailure("dividend-yield.provider-client", "Read dividend reference quote failed", null)),
21
23
  ]);
22
24
  // In the major unit as the history arrives, as the provider router serves it.
23
25
  const actions = cashInMajorUnits(sourceActions);
@@ -129,7 +129,7 @@ export function DoePane({ width, height, focused }: PaneProps) {
129
129
  const data = resource.data?.payload ?? null;
130
130
  // The server not serving the board yet is a state of the data, not a failure.
131
131
  const notAvailable = !data && resource.error === DOE_NOT_AVAILABLE;
132
- const { strip, rows: tabRows } = usePaneTabs(data ? { tabs: [...DOE_TABS], activeValue: tab, onSelect: setTab, focused, dense: true } : null);
132
+ const { strip, rows: tabRows } = usePaneTabs({ tabs: [...DOE_TABS], activeValue: tab, onSelect: setTab, focused, dense: true });
133
133
  useAutoRefresh(resource.updatedAt, resource.load);
134
134
  usePaneRefreshKey(() => void resource.reload(), { focused });
135
135
  usePaneNoticeFooter({ registrationId: "doe:notices", focused,
@@ -70,8 +70,8 @@ const defaultDependencies: EarningsCallsHeadlessDependencies = {
70
70
  };
71
71
 
72
72
  /** A refused read says what the lock note says (CALLS is Pro whole), so a free account meets the same words as a signed-out one. */
73
- function explainDenied(ticker: string | null) {
74
- return (error: unknown): never => { throw explainRefusal(error, callsLockNote, { symbol: ticker }); };
73
+ function explainDenied(ticker: string | null, context: HeadlessPaneContext) {
74
+ return async (error: unknown): Promise<never> => { throw await explainRefusal(error, callsLockNote, { symbol: ticker, context }); };
75
75
  }
76
76
 
77
77
  function callRow(call: CloudEarningsCallPayload) {
@@ -285,7 +285,7 @@ export function createEarningsCallsHeadless(
285
285
  },
286
286
  async load(args, context) {
287
287
  const ticker = typeof args.argument === "string" ? args.argument : null;
288
- const calls = await dependencies.loadCalls(ticker, 200, context).catch(explainDenied(ticker));
288
+ const calls = await dependencies.loadCalls(ticker, 200, context).catch(explainDenied(ticker, context));
289
289
  if (!ticker || !readsTranscript(args)) {
290
290
  return projectEarningsCallsHeadless(calls, { ...args, options: { ...args.options, limit: args.options.limit ?? 50 } });
291
291
  }
@@ -299,7 +299,7 @@ export function createEarningsCallsHeadless(
299
299
  }
300
300
  throw new Error(`No ${quarter} earnings call found for ${ticker}.`);
301
301
  }
302
- const transcript = await dependencies.loadTranscript(selected.id, context).catch(explainDenied(ticker));
302
+ const transcript = await dependencies.loadTranscript(selected.id, context).catch(explainDenied(ticker, context));
303
303
  if (isPendingTranscript(transcript)) {
304
304
  throw new Error(`Transcript for ${ticker} ${quarter} is still being produced.`);
305
305
  }
@@ -1,17 +1,26 @@
1
1
  import type { PaneLock } from "../../../types/plugin";
2
- import { proLockNote } from "../shared/pro-lock-note";
2
+ import { resolveHeadlessInstrument } from "../shared/headless-market-data";
3
+ import { proLockNote, type FreeRoute } from "../shared/pro-lock-note";
4
+
5
+ const EVENTS_ROUTE: FreeRoute = {
6
+ text: (ticker, company) => `gloomberb events ${ticker} lists ${company}'s earnings dates, with the EPS reported at each and the next estimate.`,
7
+ answers: async (symbol, context) => {
8
+ const { symbol: ticker, exchange } = await resolveHeadlessInstrument(context, symbol);
9
+ return ((await context.marketData.getCorporateActions?.(ticker, exchange))?.earnings.length ?? 0) > 0;
10
+ },
11
+ };
3
12
 
4
13
  /**
5
14
  * CALLS is Pro whole, the list of calls included. `events` needs no account
6
15
  * and lists a company's earnings dates, each with the EPS it reported and the
7
16
  * next one's estimate, without the call or its transcript.
8
17
  */
9
- export function callsLockNote(lock: PaneLock): string {
18
+ export function callsLockNote(lock: PaneLock): Promise<string> {
10
19
  return proLockNote(lock, {
11
20
  token: "CALLS",
12
21
  argument: "optional",
13
22
  does: "it reads each earnings call into a transcript with speaker turns, analyst Q&A and extracted guidance",
14
23
  shows: "it",
15
- free: (ticker, company) => `gloomberb events ${ticker} lists ${company}'s earnings dates, with the EPS reported at each and the next estimate.`,
24
+ free: [EVENTS_ROUTE],
16
25
  });
17
26
  }
@@ -191,6 +191,37 @@ export function matchesCountry(event: EconEvent, filter: CountryFilter): boolean
191
191
  return event.country === filter;
192
192
  }
193
193
 
194
+ /**
195
+ * How long after its release time a high-impact print is still news: the
196
+ * calendar is asked for it this long, and a print that turns up later is
197
+ * recorded without a toast.
198
+ */
199
+ export const PRINT_PENDING_MS = 20 * 60 * 1000;
200
+
201
+ /** High-impact releases whose time has come in the last {@link PRINT_PENDING_MS} and that have no actual yet. */
202
+ export function pendingHighImpactPrints(events: readonly EconEvent[], now: number): EconEvent[] {
203
+ return events.filter((event) => {
204
+ if (event.impact !== "high" || event.actual) return false;
205
+ const releasedFor = now - event.date.getTime();
206
+ return releasedFor >= 0 && releasedFor <= PRINT_PENDING_MS;
207
+ });
208
+ }
209
+
210
+ export function hasPendingHighImpactPrint(events: readonly EconEvent[], now: number): boolean {
211
+ return pendingHighImpactPrints(events, now).length > 0;
212
+ }
213
+
214
+ /** High-impact rows that were blank in `previous` and have an actual in `next`. */
215
+ export function landedHighImpactPrints(
216
+ previous: ReadonlyMap<string, string | null>,
217
+ next: readonly EconEvent[],
218
+ ): EconEvent[] {
219
+ return next.filter((event) => {
220
+ if (event.impact !== "high" || !event.actual || !previous.has(event.id)) return false;
221
+ return !previous.get(event.id);
222
+ });
223
+ }
224
+
194
225
  export function formatCountdown(ms: number): string {
195
226
  if (ms <= 60_000) return "in <1m";
196
227
  const totalMin = Math.floor(ms / 60_000);
@@ -229,6 +260,10 @@ export function getCalendarCache(options?: { allowExpired?: boolean }): EconCale
229
260
  return cache.get(CACHE_KEY, options);
230
261
  }
231
262
 
263
+ export function subscribeCalendar(listener: () => void): () => void {
264
+ return cache.subscribe(CACHE_KEY, listener);
265
+ }
266
+
232
267
  export interface EconCalendarLoadResult extends EconCalendarCacheEntry {
233
268
  refreshError?: string;
234
269
  }
@@ -20,6 +20,7 @@ import type { PluginModule } from "../plugin-module";
20
20
  import { colors, blendHex } from "../../../theme/colors";
21
21
  import type { EconEvent } from "./types";
22
22
  import { FRED_CATALOG_SERIES } from "./fred-catalog";
23
+ import { ECON_PRINT_NOTIFICATIONS_ENABLED_KEY, retainVisiblePrintRefresh, setupHighImpactPrintNotifications } from "./print-watch";
23
24
  import { EconDetailView } from "./detail-view";
24
25
  import {
25
26
  COUNTRY_CYCLE,
@@ -35,6 +36,7 @@ import {
35
36
  formatCountdown,
36
37
  formatStaleness,
37
38
  getCalendarCache,
39
+ subscribeCalendar,
38
40
  impactIndicator,
39
41
  loadCalendar,
40
42
  matchesCountry,
@@ -75,10 +77,16 @@ function EconCalendarPane({ focused, width, height }: PaneProps) {
75
77
  // loadCalendar serves a fresh cache without a request, so the pane can always
76
78
  // ask and still follow the global cadence once the cache goes stale.
77
79
  const calendar = useAsyncResource<CalendarResult>(loadEvents, { initialData: cachedEvents });
80
+ // A load made elsewhere, such as the print watch asking while a release is
81
+ // due, reaches the open pane through the cache; the newer of the two wins.
82
+ const [cacheVersion, setCacheVersion] = useState(0);
83
+ useEffect(() => subscribeCalendar(() => setCacheVersion((version) => version + 1)), []);
84
+ const cached = useMemo(() => getCalendarCache(), [cacheVersion]);
85
+ const shown = cached && (!calendar.data || cached.fetchedAt > calendar.data.fetchedAt) ? cached : calendar.data;
78
86
  const { loading } = calendar;
79
- const events = calendar.data?.data ?? NO_EVENTS;
80
- const stale = calendar.data?.stale ?? false;
81
- const fetchedAt = calendar.data?.fetchedAt ?? null;
87
+ const events = shown?.data ?? NO_EVENTS;
88
+ const stale = shown?.stale ?? false;
89
+ const fetchedAt = shown?.fetchedAt ?? null;
82
90
  // A cache that could not be refreshed arrives as a result, not a failure.
83
91
  const error = calendar.error ?? (loading ? null : calendar.data?.refreshError ?? null);
84
92
  // The selected row and the open event are remembered by event id, so a
@@ -94,8 +102,11 @@ function EconCalendarPane({ focused, width, height }: PaneProps) {
94
102
 
95
103
  useAutoRefresh(stale ? null : fetchedAt, calendar.load);
96
104
 
97
- // Tick every 30s to update staleness + countdown, only while the pane can be seen.
98
105
  const paneVisible = usePaneVisible();
106
+ // In view, a due high-impact print is asked for on the print watch's timing
107
+ // and lands here through the cache; a covered or hidden pane asks nothing.
108
+ useEffect(() => (paneVisible ? retainVisiblePrintRefresh() : undefined), [paneVisible]);
109
+ // Tick every 30s to update staleness + countdown, only while the pane can be seen.
99
110
  useEffect(() => {
100
111
  if (!paneVisible) return;
101
112
  setNow(Date.now());
@@ -437,6 +448,8 @@ function EconCalendarPane({ focused, width, height }: PaneProps) {
437
448
  );
438
449
  }
439
450
 
451
+ let stopPrintWatch: (() => void) | null = null;
452
+
440
453
  export const economicCalendarModule: PluginModule = {
441
454
  series: FRED_CATALOG_SERIES,
442
455
  panes: [{
@@ -449,6 +462,16 @@ export const economicCalendarModule: PluginModule = {
449
462
  defaultMode: "floating",
450
463
  defaultFloatingSize: { width: 100, height: 30 },
451
464
  tableExport: true,
465
+ settings: {
466
+ title: "Economic Calendar Settings",
467
+ fields: [{
468
+ key: ECON_PRINT_NOTIFICATIONS_ENABLED_KEY,
469
+ label: "Notifications",
470
+ description: "Notify when a high-impact release prints, even while this pane is closed.",
471
+ type: "toggle",
472
+ storage: "plugin",
473
+ }],
474
+ },
452
475
  }],
453
476
  paneTemplates: [{
454
477
  id: "econ-calendar-pane",
@@ -461,8 +484,12 @@ export const economicCalendarModule: PluginModule = {
461
484
  }],
462
485
  setup(ctx) {
463
486
  attachEconCalendarPersistence(ctx.persistence);
487
+ stopPrintWatch?.();
488
+ stopPrintWatch = setupHighImpactPrintNotifications(ctx);
464
489
  },
465
490
  dispose() {
491
+ stopPrintWatch?.();
492
+ stopPrintWatch = null;
466
493
  resetEconCalendarPersistence();
467
494
  },
468
495
  };
@@ -0,0 +1,279 @@
1
+ import type { GloomPluginContext } from "../../../types/plugin";
2
+ import {
3
+ getCalendarCache,
4
+ landedHighImpactPrints,
5
+ loadCalendar,
6
+ pendingHighImpactPrints,
7
+ PRINT_PENDING_MS,
8
+ subscribeCalendar,
9
+ type EconCalendarCacheEntry,
10
+ } from "./calendar-model";
11
+ import type { EconEvent } from "./types";
12
+
13
+ /** The ECO setting that turns the print toast on. Off until the user turns it on, as breaking news is. */
14
+ export const ECON_PRINT_NOTIFICATIONS_ENABLED_KEY = "econPrintNotificationsEnabled";
15
+
16
+ const TOAST_DURATION_MS = 20_000;
17
+ /**
18
+ * The waits between asks while a print is due: short just after the release
19
+ * time, when most prints land, then longer until the window closes, which
20
+ * keeps a release to about twenty asks per app instead of eighty.
21
+ */
22
+ const PRINT_BACKOFF_MS = [15_000, 30_000, 60_000] as const;
23
+ /** Each wait moves by up to a fifth either way, so apps that started together drift apart. */
24
+ const PRINT_JITTER = 0.2;
25
+ /** How often the watch looks at the calendar it already has while nothing is due. */
26
+ const PRINT_IDLE_MS = 60_000;
27
+ /** While idle, a calendar this old is asked for again, so a release added since is known. */
28
+ const CALENDAR_RECHECK_MS = 6 * 60 * 60 * 1000;
29
+ /** With no calendar at all, how long a failed ask waits before the next. */
30
+ const NO_CALENDAR_RETRY_MS = 5 * 60 * 1000;
31
+
32
+ type CalendarRead = Pick<EconCalendarCacheEntry, "data" | "fetchedAt">;
33
+
34
+ export interface PrintWatchDeps {
35
+ now: () => number;
36
+ /** Asks for the calendar; `force` skips a fresh cache. */
37
+ load: (force: boolean) => Promise<CalendarRead>;
38
+ /** The cached calendar, however old, without a request. */
39
+ read: () => CalendarRead | null;
40
+ /** Called whenever anyone, a pane included, loads the calendar. */
41
+ subscribe: (listener: () => void) => () => void;
42
+ schedule: (run: () => Promise<void> | void, delay: number) => () => void;
43
+ random: () => number;
44
+ }
45
+
46
+ const DEFAULT_DEPS: PrintWatchDeps = {
47
+ now: Date.now,
48
+ load: (force) => loadCalendar(force),
49
+ read: () => getCalendarCache({ allowExpired: true }),
50
+ subscribe: subscribeCalendar,
51
+ schedule: (run, delay) => {
52
+ const timer = setTimeout(run, delay);
53
+ return () => clearTimeout(timer);
54
+ },
55
+ random: Math.random,
56
+ };
57
+
58
+ function printBody(event: EconEvent): string {
59
+ return event.forecast ? `${event.event} ${event.actual} vs ${event.forecast}` : `${event.event} ${event.actual}`;
60
+ }
61
+
62
+ type PrintWatchContext = Pick<GloomPluginContext, "notify" | "showPane">;
63
+
64
+ /**
65
+ * Toasts a high-impact print once, when it lands within
66
+ * {@link PRINT_PENDING_MS} of its release time. While one is due the
67
+ * calendar is asked for on {@link PRINT_BACKOFF_MS}, and every read is
68
+ * compared: the watch's own, its idle look at the cache, and a pane's
69
+ * refresh. The first read only records what has already printed.
70
+ */
71
+ export function startHighImpactPrintWatch(
72
+ ctx: PrintWatchContext,
73
+ overrides: Partial<PrintWatchDeps> = {},
74
+ ): () => void {
75
+ const deps: PrintWatchDeps = { ...DEFAULT_DEPS, ...overrides };
76
+ let stopped = false;
77
+ let running = false;
78
+ let cancel: (() => void) | null = null;
79
+ /** The newest calendar the watch has seen. */
80
+ let latest: CalendarRead | null = null;
81
+ let lastAskAt = Number.NEGATIVE_INFINITY;
82
+ let primed = false;
83
+ /**
84
+ * Each high-impact row not yet past its window, by id: null while blank,
85
+ * then its number. A number stays once seen, so a print that blanks and
86
+ * comes back is not a new print.
87
+ */
88
+ const actuals = new Map<string, string | null>();
89
+ const expiries = new Map<string, number>();
90
+ let attempt = 0;
91
+ let dueIds = new Set<string>();
92
+
93
+ const toast = (event: EconEvent) => {
94
+ ctx.notify({
95
+ title: event.country,
96
+ body: printBody(event),
97
+ type: "info",
98
+ desktop: "always",
99
+ sound: "Glass",
100
+ duration: TOAST_DURATION_MS,
101
+ refId: `econ:${event.id}`,
102
+ action: { label: "Open", onClick: () => ctx.showPane("econ-calendar") },
103
+ });
104
+ };
105
+
106
+ /** `now` is when the read was asked for, so a print the last ask in the window brings back still counts. */
107
+ const observe = (read: CalendarRead | null, now = deps.now()) => {
108
+ if (stopped || !read) return;
109
+ if (!latest || read.fetchedAt >= latest.fetchedAt) latest = read;
110
+ const landed = primed ? landedHighImpactPrints(actuals, read.data) : [];
111
+ for (const event of read.data) {
112
+ if (event.impact !== "high") continue;
113
+ const expiresAt = event.date.getTime() + PRINT_PENDING_MS;
114
+ if (expiresAt < now) continue;
115
+ expiries.set(event.id, expiresAt);
116
+ if (event.actual) actuals.set(event.id, event.actual);
117
+ else if (!actuals.has(event.id)) actuals.set(event.id, null);
118
+ }
119
+ for (const [id, expiresAt] of expiries) {
120
+ if (expiresAt >= now) continue;
121
+ expiries.delete(id);
122
+ actuals.delete(id);
123
+ }
124
+ primed = true;
125
+ // A print that turns up after its window is recorded, not announced late.
126
+ for (const event of landed) {
127
+ if (now - event.date.getTime() <= PRINT_PENDING_MS) toast(event);
128
+ }
129
+ };
130
+
131
+ /** Whether a print in the newest calendar is due and still within its window. */
132
+ const printDue = (now: number): boolean => {
133
+ const pending = latest ? pendingHighImpactPrints(latest.data, now) : [];
134
+ // A release that has just come due starts the waits over.
135
+ if (pending.length === 0 || pending.some((event) => !dueIds.has(event.id))) attempt = 0;
136
+ dueIds = new Set(pending.map((event) => event.id));
137
+ return pending.length > 0;
138
+ };
139
+
140
+ const backoff = (): number => {
141
+ const base = PRINT_BACKOFF_MS[Math.min(attempt, PRINT_BACKOFF_MS.length - 1)]!;
142
+ attempt += 1;
143
+ return Math.round(base * (1 + (deps.random() * 2 - 1) * PRINT_JITTER));
144
+ };
145
+
146
+ const askWhileIdle = (now: number): boolean => (latest
147
+ ? now - Math.max(latest.fetchedAt, lastAskAt) >= CALENDAR_RECHECK_MS
148
+ : now - lastAskAt >= NO_CALENDAR_RETRY_MS);
149
+
150
+ const arm = (delay: number) => {
151
+ if (stopped || cancel) return;
152
+ cancel = deps.schedule(() => {
153
+ cancel = null;
154
+ return run();
155
+ }, delay);
156
+ };
157
+
158
+ const run = async () => {
159
+ if (stopped || running) return;
160
+ observe(deps.read());
161
+ const before = deps.now();
162
+ const due = printDue(before);
163
+ if (!due && !askWhileIdle(before)) {
164
+ arm(PRINT_IDLE_MS);
165
+ return;
166
+ }
167
+ running = true;
168
+ lastAskAt = before;
169
+ try {
170
+ // A due print is asked for past the cache; otherwise a fresh cache answers without a request.
171
+ observe(await deps.load(due), before);
172
+ } catch {
173
+ // The next ask follows the same schedule.
174
+ } finally {
175
+ running = false;
176
+ }
177
+ if (stopped) return;
178
+ // Past the window nothing is due, so the asks stop and the watch goes back to idle looks.
179
+ arm(printDue(deps.now()) ? backoff() : PRINT_IDLE_MS);
180
+ };
181
+
182
+ // A load the watch is waiting on is compared when it answers, at the time it was asked.
183
+ const unsubscribe = deps.subscribe(() => {
184
+ if (!running) observe(deps.read());
185
+ });
186
+ void run();
187
+ return () => {
188
+ stopped = true;
189
+ unsubscribe();
190
+ cancel?.();
191
+ cancel = null;
192
+ };
193
+ }
194
+
195
+ type PrintNotificationContext = PrintWatchContext & Pick<GloomPluginContext, "configState" | "on">;
196
+
197
+ function printNotificationsEnabled(ctx: Pick<GloomPluginContext, "configState">): boolean {
198
+ return ctx.configState.get<boolean>(ECON_PRINT_NOTIFICATIONS_ENABLED_KEY) === true;
199
+ }
200
+
201
+ /**
202
+ * The one print watch, run while anything wants it: the Notifications
203
+ * setting, or an ECO pane in view, which fills a print in as it lands. Every
204
+ * reason shares its timing and its requests; only the setting toasts.
205
+ */
206
+ export interface PrintRefresh {
207
+ /** The plugin's context, for toasts and their Open; null once it is disposed. */
208
+ attach(ctx: PrintWatchContext | null): void;
209
+ setNotifying(on: boolean): void;
210
+ /** An ECO pane can be seen; the returned release says it no longer can. */
211
+ retainVisiblePane(): () => void;
212
+ }
213
+
214
+ export function createPrintRefresh(
215
+ start: (ctx: PrintWatchContext) => () => void = startHighImpactPrintWatch,
216
+ ): PrintRefresh {
217
+ let host: PrintWatchContext | null = null;
218
+ let notifying = false;
219
+ let visiblePanes = 0;
220
+ let stopWatch: (() => void) | null = null;
221
+ // A print that lands while only a pane wants the watch is recorded, not toasted.
222
+ const watchContext: PrintWatchContext = {
223
+ notify: (notice) => {
224
+ if (notifying) host?.notify(notice);
225
+ },
226
+ showPane: (paneId) => host?.showPane(paneId),
227
+ };
228
+ const sync = () => {
229
+ const wanted = notifying || visiblePanes > 0;
230
+ if (wanted && !stopWatch) stopWatch = start(watchContext);
231
+ if (!wanted && stopWatch) {
232
+ stopWatch();
233
+ stopWatch = null;
234
+ }
235
+ };
236
+ return {
237
+ attach(ctx) {
238
+ host = ctx;
239
+ },
240
+ setNotifying(on) {
241
+ notifying = on;
242
+ sync();
243
+ },
244
+ retainVisiblePane() {
245
+ visiblePanes += 1;
246
+ sync();
247
+ let released = false;
248
+ return () => {
249
+ if (released) return;
250
+ released = true;
251
+ visiblePanes -= 1;
252
+ sync();
253
+ };
254
+ },
255
+ };
256
+ }
257
+
258
+ const printRefresh = createPrintRefresh();
259
+
260
+ /** For the ECO pane: while it can be seen, a due high-impact print is asked for on the watch's timing. */
261
+ export function retainVisiblePrintRefresh(): () => void {
262
+ return printRefresh.retainVisiblePane();
263
+ }
264
+
265
+ /** Toasts prints while the Notifications setting is on; off, only a pane in view keeps the watch asking. */
266
+ export function setupHighImpactPrintNotifications(
267
+ ctx: PrintNotificationContext,
268
+ refresh: PrintRefresh = printRefresh,
269
+ ): () => void {
270
+ refresh.attach(ctx);
271
+ const sync = () => refresh.setNotifying(printNotificationsEnabled(ctx));
272
+ const disposeConfigListener = ctx.on("config:changed", sync);
273
+ sync();
274
+ return () => {
275
+ disposeConfigListener();
276
+ refresh.setNotifying(false);
277
+ refresh.attach(null);
278
+ };
279
+ }