@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,6 +1,6 @@
1
1
  import type { DebtHistoryPoint } from "../../../api-client/debt-maturities";
2
2
  import type { SeriesDefinition } from "../../../series/types";
3
- import { cachedSeriesWarning } from "../shared/series-warning";
3
+ import { cachedSeriesNote } from "../shared/series-warning";
4
4
  import { loadDebtMaturities } from "./client";
5
5
 
6
6
  type HistoryField = "totalPrincipal" | "next12MonthsShare" | "next3YearsShare";
@@ -36,10 +36,10 @@ export const DEBT_MATURITY_SERIES: readonly SeriesDefinition[] = FIELDS.map(({ i
36
36
  // An amount stays in the unit it is listed in: a filing in another currency is a gap, not a converted figure.
37
37
  const listed = (point: DebtHistoryPoint) => unit !== "USD" || point.currency === "USD";
38
38
  const other = resource.payload.history.find((point) => !listed(point))?.currency;
39
- const warning = other ? `Filings reported in ${other} are left out.` : cachedSeriesWarning(resource);
40
39
  return {
41
40
  points: resource.payload.history.map((point) => ({ date: point.asOf, value: listed(point) ? point[field] : null })),
42
- ...(warning ? { warning } : {}),
41
+ ...cachedSeriesNote(resource),
42
+ ...(other ? { warning: `Filings reported in ${other} are left out.` } : {}),
43
43
  };
44
44
  },
45
45
  }));
@@ -288,7 +288,7 @@ export const debugPlugin: GloomPlugin = {
288
288
  name: "Debug Log",
289
289
  icon: "D",
290
290
  component: DebugPane,
291
- reportFreshness: { status: "not-a-feed", basis: "debug log" },
291
+ reportFreshness: { status: "not-a-feed", basis: "debug log", local: true },
292
292
  defaultPosition: "right",
293
293
  defaultMode: "floating",
294
294
  defaultFloatingSize: { width: 120, height: 30 },
@@ -75,6 +75,19 @@ function statedRateSummary(stated: StatedForwardRate): QuoteSummaryDividendField
75
75
  interface DividendReadOptions {
76
76
  /** The forward rate fundamentals state (statedForwardRate), asked only when the summary is missing. */
77
77
  loadStatedForwardRate?: () => Promise<StatedForwardRate | null> | StatedForwardRate | null;
78
+ /** The history already asked for (requestDividendHistory), so it need not wait for the quote the price comes from. */
79
+ history?: Promise<DividendHistoryResponse>;
80
+ }
81
+
82
+ type DividendHistoryResponse = Awaited<ReturnType<typeof apiClient.getMarketDividends>>;
83
+
84
+ /** Asks Gloom Cloud for a listing's dividend history, for fetchDividendData to read once the price is known. */
85
+ export function requestDividendHistory(symbol: string, exchange = ""): Promise<DividendHistoryResponse> {
86
+ const pending = trackRequest("dividends", () => apiClient.getMarketDividends(symbol, exchange));
87
+ pending.catch(() => {
88
+ // Intentional ignore: fetchDividendData awaits this same promise and reports its failure.
89
+ });
90
+ return pending;
78
91
  }
79
92
 
80
93
  let connectionHealth: ConnectionHealthRegistry | null = null;
@@ -125,8 +138,22 @@ function declaredCashByExDate(declared: DeclaredCash | undefined, cashCurrency:
125
138
  ? [[row.exDate, { amount: row.amount / unit.divisor, currency: unit.currency }] as const] : []));
126
139
  }
127
140
 
141
+ /**
142
+ * The currency the company declares in when it pays the listing another
143
+ * (South32's dollars, which its rand line converts): the listing's amounts are
144
+ * conversions of it. Undefined for a listing paid in what was declared.
145
+ */
146
+ function declaredCurrencyOf(declared: DeclaredCash | undefined, cashCurrency: string): string | undefined {
147
+ const currency = resolveCurrencyUnit(declared?.currency).currency;
148
+ return currency && currency !== cashCurrency ? currency : undefined;
149
+ }
150
+
128
151
  export interface DividendData {
129
152
  payments: DividendPayment[];
153
+ /** The currency the company declares in, when its amounts here are converted from it (declaredCurrencyOf). */
154
+ convertedFrom?: string;
155
+ /** Beside convertedFrom, the sub-unit the source states the cash in (GBp, ZAc): converted amounts are written in it. */
156
+ convertedSubUnit?: string;
130
157
  /** Declared and unpaid, in `currency`; never in `payments` or any figure. Absent from older servers. */
131
158
  declared?: DeclaredDividendAction[];
132
159
  metrics: DividendMetrics;
@@ -155,7 +182,7 @@ export async function fetchDividendData(
155
182
  currentPriceCurrency?: string,
156
183
  options: DividendReadOptions = {},
157
184
  ): Promise<DividendData> {
158
- const response = await trackRequest("dividends", () => apiClient.getMarketDividends(symbol, exchange));
185
+ const response = await (options.history ?? requestDividendHistory(symbol, exchange));
159
186
  if (!response.data) throw new Error(`No dividend data found for ${symbol}`);
160
187
  const { actions: sourceActions, quote: rawQuote, summary, triedAt } = response.data;
161
188
  // In the major unit as the history arrives, as every other reader of it gets it.
@@ -180,6 +207,9 @@ export async function fetchDividendData(
180
207
  historyError = MISSING_DIVIDEND_CURRENCY;
181
208
  }
182
209
  const declaredCash = cashCurrency ? declaredCashByExDate(actions?.declaredCash, cashCurrency) : new Map();
210
+ const convertedFrom = cashCurrency ? declaredCurrencyOf(actions?.declaredCash, cashCurrency) : undefined;
211
+ const sourceUnit = sourceActions?.currency;
212
+ const convertedSubUnit = convertedFrom && sourceUnit && resolveCurrencyUnit(sourceUnit).divisor !== 1 ? sourceUnit : undefined;
183
213
  const payments = actions && cashCurrency ? actions.dividends.flatMap(action => {
184
214
  const payment = toDividendPayment(action.exDate, action.amount, cashCurrency);
185
215
  const declared = declaredCash.get(action.exDate);
@@ -203,7 +233,7 @@ export async function fetchDividendData(
203
233
  throw new Error(`No dividend data found for ${symbol}`);
204
234
  }
205
235
  return {
206
- payments, ...(declared.length > 0 ? { declared } : {}), metrics, price, currency: currency || undefined, historyAvailable,
236
+ payments, ...(declared.length > 0 ? { declared } : {}), ...(convertedFrom ? { convertedFrom } : {}), ...(convertedSubUnit ? { convertedSubUnit } : {}), metrics, price, currency: currency || undefined, historyAvailable,
207
237
  ...(suppliedPrice == null && price != null && quote ? dividendQuotePriceMetadata(quote) : {}),
208
238
  ...(historyError ? { historyError } : {}), ...(summaryError ? { summaryError } : {}),
209
239
  ...(statedFields && metrics.forwardRate != null ? { forwardFromFundamentals: true } : {}),
@@ -4,19 +4,21 @@ import type {
4
4
  HeadlessPaneDefinition,
5
5
  HeadlessPaneLoadArgs,
6
6
  } from "../../../types/plugin";
7
- import { fetchDividendData, statedForwardRate, type DividendData } from "./client";
7
+ import { fetchDividendData, requestDividendHistory, statedForwardRate, type DividendData } from "./client";
8
8
  import { dividendReferencePrice } from "../../../utils/trailing-dividend";
9
9
  import type { DividendPayment, GrowthPayments } from "./types";
10
- import { formatDividendYield, growthPaymentsText, PAYOUT_NOT_MEANINGFUL, staleHistoryText, toDividendRows, trailingPaymentsText } from "./view";
10
+ import {
11
+ convertedFromText, formatConvertedAmount, formatDividendYield, growthPaymentsText, PAYOUT_NOT_MEANINGFUL, staleHistoryText, toDividendRows, trailingPaymentsText,
12
+ } from "./view";
11
13
  import { formatDistributionAmount, formatPercent } from "../../../utils/format";
12
14
  import { forwardDividendBasis, noCurrentDividend, TRAILING_DIVIDEND_BASIS, UNEVEN_REGULAR_PAYMENTS } from "../../../utils/forward-dividend";
13
15
  import { currencyPriceMinDigits, formatMarketPriceWithCurrency } from "../../../market-data/market/format";
14
16
  import type { Quote } from "../../../types/financials";
15
17
  import { regularSessionValuationPrice } from "../../../market-data/quotes/regular-session-valuation";
16
18
  import { dividendPriceStatus, dividendQuotePriceMetadata } from "./reference-price";
17
- import { mapWithConcurrency } from "../../../utils/concurrency";
18
19
  import { logFailure } from "../../../utils/log-failure";
19
- import { projectDividendRanking, roundedRatio, type DividendRankingEntry } from "./ranking";
20
+ import { projectDividendRanking, roundedRatio } from "./ranking";
21
+ import { MAX_RANKED_SYMBOLS, readDividendRanking, type DividendRankingReads } from "./ranking-reads";
20
22
  import { REPORT_FILTER, REPORT_ROWS } from "../shared/headless-options";
21
23
  import type { FundBondFacts } from "../../../api-client/fund-facts";
22
24
  import { resolveTickerInstrumentKind } from "../../../tickers/instrument-kind";
@@ -27,31 +29,35 @@ import {
27
29
  type TaxEquivalentAnalysis, type TreasuryTenor,
28
30
  } from "../../../market-data/tax-equivalent";
29
31
 
30
- /** Symbols one `fn DVD` ranks at most, and how many it reads at once. */
31
- const MAX_RANKED_SYMBOLS = 25;
32
- const RANKING_CONCURRENCY = 4;
33
32
  /** The history options' defaults: a ranking lists no history, so only these leave it unchanged. */
34
33
  const DEFAULT_TYPE = "all";
35
34
  const DEFAULT_LIMIT = 40;
36
35
 
37
- const PAYMENT_COLUMNS = [
38
- { key: "exDate", header: "Ex-date" },
39
- { key: "amount", header: "Amount", align: "right" as const,
40
- format: (value: unknown, row: Record<string, unknown>) => typeof value === "number" && typeof row.currency === "string" && row.currency
41
- ? formatDistributionAmount(value, row.currency) : value == null ? "-" : String(value) },
42
- { key: "currency", header: "CCY" },
43
- { key: "type", header: "Type" },
44
- ];
36
+ /** The history's columns; amounts converted from the currency the company declares in go to two decimals of their source's unit. */
37
+ function paymentColumns(converted: boolean, subUnit?: string) {
38
+ const amount = converted ? (value: number, currency: string) => formatConvertedAmount(value, currency, subUnit) : formatDistributionAmount;
39
+ return [
40
+ { key: "exDate", header: "Ex-date" },
41
+ { key: "amount", header: "Amount", align: "right" as const,
42
+ format: (value: unknown, row: Record<string, unknown>) => typeof value === "number" && typeof row.currency === "string" && row.currency
43
+ ? amount(value, row.currency) : value == null ? "-" : String(value) },
44
+ { key: "currency", header: "CCY" },
45
+ { key: "type", header: "Type" },
46
+ ];
47
+ }
45
48
 
46
49
  /** After the amount, for a company declaring in another currency than the listing pays: what it declared. */
47
50
  const DECLARED_COLUMN = { key: "declared", header: "Declared", align: "right" as const,
48
51
  format: (value: unknown, row: Record<string, unknown>) => typeof value === "number" && typeof row.declaredCurrency === "string"
49
52
  ? formatDistributionAmount(value, row.declaredCurrency) : "-" };
50
53
 
54
+ /** What a ticker's read needs: the market data, Gloom Cloud, and the listing a typed key stands for. */
55
+ export type DividendReadContext = Pick<HeadlessPaneContext, "marketData" | "apiClient" | "resolveInstrument">;
56
+
51
57
  export interface DividendYieldHeadlessDependencies {
52
- loadData(symbol: string, context: HeadlessPaneContext): Promise<DividendData>;
58
+ loadData(symbol: string, context: DividendReadContext): Promise<DividendData>;
53
59
  /** A US-listed fund's bond facts (SEC yield, yield to worst, duration); null for any other listing. */
54
- loadBond?(symbol: string, context: HeadlessPaneContext): Promise<FundBondFacts | null>;
60
+ loadBond?(symbol: string, context: DividendReadContext): Promise<FundBondFacts | null>;
55
61
  /** The Treasury par curve GC shows, for the muni/Treasury ratio; null when it cannot be read. */
56
62
  loadTreasury?(context: HeadlessPaneContext): Promise<TreasuryTenor[] | null>;
57
63
  }
@@ -68,6 +74,8 @@ const defaultDependencies: DividendYieldHeadlessDependencies = {
68
74
  const instrument = await context.resolveInstrument?.(symbol);
69
75
  const listingSymbol = instrument?.symbol ?? symbol;
70
76
  const exchange = instrument?.exchange ?? "";
77
+ // The history is asked for beside the quote, not after it: only its yields need the price.
78
+ const history = requestDividendHistory(listingSymbol, exchange);
71
79
  try {
72
80
  const quote = await context.marketData.getQuote(listingSymbol, exchange);
73
81
  // The regular session's price: after the close, the close rather than an after-hours print.
@@ -79,6 +87,7 @@ const defaultDependencies: DividendYieldHeadlessDependencies = {
79
87
  currentPrice = null;
80
88
  }
81
89
  const data = await fetchDividendData(listingSymbol, currentPrice, exchange, currentPriceCurrency, {
90
+ history,
82
91
  // Read only when the summary is missing: the forward rate fundamentals state, as `fundamentals` shows it.
83
92
  loadStatedForwardRate: async () => statedForwardRate(await context.marketData.getTickerFinancials(listingSymbol, exchange)),
84
93
  });
@@ -102,6 +111,25 @@ const defaultDependencies: DividendYieldHeadlessDependencies = {
102
111
  loadTreasury: (context) => loadTreasuryTenors(context.apiClient),
103
112
  };
104
113
 
114
+ /**
115
+ * How a ranking reads each ticker: `fn DVD`'s, and the DVD pane's with the
116
+ * app's market data, so both read the same listing at the same price. A
117
+ * fund's bond facts never fail its row: without them it ranks as before.
118
+ */
119
+ export function dividendRankingReads(
120
+ context: DividendReadContext,
121
+ dependencies: Pick<DividendYieldHeadlessDependencies, "loadData" | "loadBond"> = defaultDependencies,
122
+ ): DividendRankingReads {
123
+ const { loadBond } = dependencies;
124
+ return {
125
+ loadData: (symbol) => dependencies.loadData(symbol, context),
126
+ ...loadBond ? {
127
+ loadBond: (symbol: string) => loadBond(symbol, context).catch((error) => error instanceof BondClassificationError ? null
128
+ : logFailure("dividend-yield.headless", "Read bond fund facts for ranking failed", null)(error)),
129
+ } : {},
130
+ };
131
+ }
132
+
105
133
  /** A growth figure, or what its short year holds: "n/a: 1 of 2 payments". */
106
134
  function growthText(growth: number | null, payments: GrowthPayments | null | undefined): string {
107
135
  return growth == null && payments ? growthPaymentsText(payments) : formatPercent(growth ?? undefined);
@@ -147,10 +175,16 @@ export function projectDividendYieldHeadless(
147
175
  ...(row.declared ? { declared: row.declared.amount, declaredCurrency: row.declared.currency } : {}),
148
176
  };
149
177
  });
178
+ // A listing paid in another currency than its company declares (South32's rand) has its amounts converted from that one.
179
+ const convertedFrom = data.convertedFrom;
180
+ const subUnit = data.convertedSubUnit;
181
+ const paymentColumnList = paymentColumns(!!convertedFrom, subUnit);
150
182
  const declaredColumns = rows.some((row) => row.declared)
151
- ? [...PAYMENT_COLUMNS.slice(0, 2), DECLARED_COLUMN, ...PAYMENT_COLUMNS.slice(2)] : PAYMENT_COLUMNS;
183
+ ? [...paymentColumnList.slice(0, 2), DECLARED_COLUMN, ...paymentColumnList.slice(2)] : paymentColumnList;
152
184
  const metrics = data.metrics;
153
185
  const currency = data.currency ?? data.payments[0]?.currency ?? "";
186
+ const cashRate = (value: number | null) => !currency ? "—" : !convertedFrom ? formatDistributionAmount(value ?? undefined, currency)
187
+ : value == null ? formatConvertedAmount(value, currency) : `${formatConvertedAmount(value, currency, subUnit)} (${convertedFromText(convertedFrom)})`;
154
188
  const priceStatus = dividendPriceStatus(data.price, data.priceAsOf, data.priceStale);
155
189
  // A regular payer's year with fewer payments than its cadence pays: the yield says how many it counts.
156
190
  const shortfall = trailingPaymentsText(metrics.trailingPayments, { cadence: true });
@@ -180,8 +214,8 @@ export function projectDividendYieldHeadless(
180
214
  value: figure.value,
181
215
  formatted: `${figure.text}${figure.date ? ` (as of ${figure.date})` : ""}`,
182
216
  })) : []),
183
- { label: "Trailing rate", value: metrics.trailingRate, formatted: currency ? formatDistributionAmount(metrics.trailingRate ?? undefined, currency) : "—" },
184
- { label: "Forward rate", value: metrics.forwardRate, formatted: currency ? formatDistributionAmount(metrics.forwardRate ?? undefined, currency) : "—" },
217
+ { label: "Trailing rate", value: metrics.trailingRate, formatted: cashRate(metrics.trailingRate) },
218
+ { label: "Forward rate", value: metrics.forwardRate, formatted: cashRate(metrics.forwardRate) },
185
219
  { label: "Earnings Payout", value: roundedRatio(metrics.payoutRatio), formatted: metrics.payoutNotMeaningful ? PAYOUT_NOT_MEANINGFUL : formatDividendYield(metrics.payoutRatio) },
186
220
  { label: "1Y Cash Growth", value: roundedRatio(metrics.growth1Y), formatted: growthText(metrics.growth1Y, metrics.growth1YPayments) },
187
221
  { label: "3Y Cash CAGR", value: roundedRatio(metrics.growth3Y), formatted: growthText(metrics.growth3Y, metrics.growth3YPayments) },
@@ -198,7 +232,7 @@ export function projectDividendYieldHeadless(
198
232
  entries: taxEquivalentLines(tax).map((line) => ({ label: line.label, value: line.value, formatted: line.text })),
199
233
  }] : []),
200
234
  {
201
- title: "Dividend history",
235
+ title: convertedFrom ? `Dividend history (amounts ${convertedFromText(convertedFrom)})` : "Dividend history",
202
236
  columns: declaredColumns,
203
237
  rows,
204
238
  // Each amount is in its own row's currency.
@@ -211,6 +245,8 @@ export function projectDividendYieldHeadless(
211
245
  truncated: rows.length < matching.length,
212
246
  type,
213
247
  currency: data.currency ?? data.payments[0]?.currency ?? null,
248
+ // The currency the company declares in, when the amounts are converted from it; null for one paid as declared.
249
+ convertedFrom: convertedFrom ?? null,
214
250
  historyAvailable: data.historyAvailable ?? true,
215
251
  historyError: data.historyError ?? null,
216
252
  summaryError: data.summaryError ?? null,
@@ -321,18 +357,7 @@ export function createDividendYieldHeadless(
321
357
  if (historyOptions.length > 0) {
322
358
  throw new Error(`${historyOptions.join(" and ")} ${historyOptions.length === 1 ? "filters" : "filter"} one ticker's payment history, and a ranking of several lists none. Drop ${historyOptions.length === 1 ? "it" : "them"}, or run DVD on one ticker.`);
323
359
  }
324
- const entries = await mapWithConcurrency(args.symbols, RANKING_CONCURRENCY, async (symbol): Promise<DividendRankingEntry> => {
325
- // A fund's bond facts never fail its row: without them it ranks as before.
326
- const bond = dependencies.loadBond?.(symbol, context)
327
- .catch((error) => error instanceof BondClassificationError ? null
328
- : logFailure("dividend-yield.headless", "Read bond fund facts for ranking failed", null)(error)) ?? null;
329
- try {
330
- return { symbol, data: await dependencies.loadData(symbol, context), bond: await bond };
331
- } catch (error) {
332
- return { symbol, error: error instanceof Error ? error.message : String(error) };
333
- }
334
- });
335
- return projectDividendRanking(entries);
360
+ return projectDividendRanking(await readDividendRanking(args.symbols, dividendRankingReads(context, dependencies)));
336
361
  },
337
362
  };
338
363
  }
@@ -8,6 +8,9 @@ import {
8
8
  } from "./client";
9
9
  import { DividendYieldPane } from "./pane";
10
10
  import { dividendYieldHeadless } from "./headless";
11
+ import { RANK_SYMBOLS_SETTING } from "./ranking-view";
12
+ import { MAX_RANKED_SYMBOLS } from "./ranking-reads";
13
+ import type { PaneTemplateCreateOptions, PaneTemplateDef } from "../../../types/plugin";
11
14
 
12
15
  function createDividendYieldModule({
13
16
  component = DividendYieldPane,
@@ -65,7 +68,7 @@ function createDividendYieldModule({
65
68
 
66
69
  paneTemplates: [
67
70
  {
68
- ...createTickerSurfacePaneTemplate({
71
+ ...withRanking(createTickerSurfacePaneTemplate({
69
72
  id: "dividend-yield-pane",
70
73
  paneId: "dividend-yield",
71
74
  label: "Dividend Yield",
@@ -75,11 +78,43 @@ function createDividendYieldModule({
75
78
  "dividend growth", "payout ratio", "drip", "reinvest", "reinvestment", "dividend reinvestment", "rank dividend payers",
76
79
  "sec yield", "distribution yield", "muni", "munis", "municipal", "municipal bonds", "tax-exempt", "tax-equivalent yield", "muni treasury ratio"],
77
80
  shortcut: "DVD",
78
- }),
81
+ })),
79
82
  headless,
80
83
  },
81
84
  ],
82
85
  };
83
86
  }
84
87
 
88
+ /**
89
+ * The tickers a DVD is opened with when there are several: the list a command
90
+ * passes (`shot DVD KO JNJ`), or a comma list typed as its argument.
91
+ */
92
+ function rankedSymbols(options: PaneTemplateCreateOptions | undefined): string[] {
93
+ const list = options?.symbols?.length ? options.symbols : (options?.arg ?? options?.symbol ?? "").split(",");
94
+ // The first 25, as `fn DVD` ranks at most.
95
+ const symbols = [...new Set(list.map((symbol) => symbol.trim().toUpperCase()).filter(Boolean))].slice(0, MAX_RANKED_SYMBOLS);
96
+ return symbols.length > 1 ? symbols : [];
97
+ }
98
+
99
+ /** DVD with several tickers opens one pane that ranks them by yield, as `fn DVD A B C` does; with one, its own pane. */
100
+ function withRanking(template: PaneTemplateDef): PaneTemplateDef {
101
+ return {
102
+ ...template,
103
+ // `fn` and `shot` pass a typed list as `symbols`, so `shot DVD KO JNJ` draws the ranking `fn DVD KO JNJ` prints.
104
+ opensTickerList: true,
105
+ canCreate: (context, options) => rankedSymbols(options).length > 1 || (template.canCreate?.(context, options) ?? true),
106
+ createInstance: (context, options) => {
107
+ const symbols = rankedSymbols(options);
108
+ if (symbols.length < 2) return template.createInstance?.(context, options) ?? null;
109
+ return {
110
+ instanceId: `dividend-yield:rank:${symbols.map((symbol) => encodeURIComponent(symbol)).join(",")}`,
111
+ title: `DVD ${symbols.join(", ")}`,
112
+ binding: { kind: "none" },
113
+ placement: "floating",
114
+ settings: { [RANK_SYMBOLS_SETTING]: symbols },
115
+ };
116
+ },
117
+ };
118
+ }
119
+
85
120
  export const dividendYieldModule = createDividendYieldModule();
@@ -31,7 +31,7 @@ import { isCloudSessionRequired, useResearchCloudSession } from "../shared/resea
31
31
  import { fetchDividendData, repriceDividendMetrics, statedForwardRate, type DividendData } from "./client";
32
32
  import { dividendReferencePrice } from "../../../utils/trailing-dividend";
33
33
  import { useTickerQuoteStream } from "../../../state/hooks/live-ticker-financials";
34
- import { buildTrailingCashChartPoints, formatDividendYield, growthPaymentsDetail, PAYOUT_NOT_MEANINGFUL, staleHistoryText, toDividendRows, trailingPaymentsText, type DividendRow } from "./view";
34
+ import { buildTrailingCashChartPoints, convertedFromText, formatConvertedAmount, formatDividendYield, growthPaymentsDetail, PAYOUT_NOT_MEANINGFUL, staleHistoryText, toDividendRows, trailingPaymentsText, type DividendRow } from "./view";
35
35
  import {
36
36
  DEFAULT_SORT_PREFERENCE,
37
37
  buildDividendColumns,
@@ -46,11 +46,14 @@ import type { FundBondFacts } from "../../../api-client/fund-facts";
46
46
  import { resolveTickerInstrumentKind } from "../../../tickers/instrument-kind";
47
47
  import { mayBeUsEquityOrFundTicker } from "../../../utils/sec";
48
48
  import { useFundFacts } from "../sec/fund-facts-view";
49
+ import { usePaneInstance } from "../../../state/app/context";
50
+ import { DividendRankingView, rankSymbolsOf } from "./ranking-view";
49
51
  import { bondFigures } from "../../../market-data/bond-funds";
50
52
 
51
- function formatRate(value: number | null, currency: string): string {
53
+ /** A per-share rate; one converted from the currency the company declares in goes to two decimals of its source's unit. */
54
+ function formatRate(value: number | null, currency: string, converted = false, subUnit?: string): string {
52
55
  if (value == null) return "—";
53
- return formatDistributionAmount(value, currency);
56
+ return converted ? formatConvertedAmount(value, currency, subUnit) : formatDistributionAmount(value, currency);
54
57
  }
55
58
 
56
59
  function formatGrowth(value: number | null): string {
@@ -90,7 +93,9 @@ function formatFrequency(freq: DividendMetrics["paymentFrequency"]): string {
90
93
  * say they are not its SEC yield, and the SEC yield follows them with its date.
91
94
  */
92
95
  function buildMetricItems(metrics: DividendMetrics, currency: string, hasHistory: boolean, short: boolean,
93
- declared: DividendData["declared"], bond: FundBondFacts | null = null): StatItem[] {
96
+ declared: DividendData["declared"], bond: FundBondFacts | null = null, convertedFrom?: string, convertedSubUnit?: string): StatItem[] {
97
+ // The rates of a listing paid in a conversion of what its company declares say what they were converted from.
98
+ const converted = convertedFrom ? { detail: convertedFromText(convertedFrom) } : {};
94
99
  const exDate: StatItem[] = metrics.nextExDividendDate
95
100
  ? [{ id: "next-ex", label: "Next ex", value: formatDate(metrics.nextExDividendDate) }]
96
101
  : hasHistory ? [] : [{ id: "last-ex", label: "Last ex", value: formatDate(metrics.lastExDividendDate) }];
@@ -116,8 +121,10 @@ function buildMetricItems(metrics: DividendMetrics, currency: string, hasHistory
116
121
  ...(forwardNote ? { detail: forwardNote } : {}) },
117
122
  // The 30-day SEC yield as the fund's sponsor publishes it, dated.
118
123
  ...(sec ? [{ id: "sec-yield", label: short ? "SEC yld" : "SEC yield", value: sec.text, ...(sec.date ? { detail: `as of ${sec.date}` } : {}) }] : []),
119
- { id: "ttm-rate", label: short ? "TTM/sh" : "TTM/share", value: formatRate(metrics.trailingRate, currency) },
120
- { id: "forward-rate", label: short ? "Fwd/sh" : "Fwd/share", value: formatRate(metrics.forwardRate, currency) },
124
+ { id: "ttm-rate", label: short ? "TTM/sh" : "TTM/share", value: formatRate(metrics.trailingRate, currency, !!convertedFrom, convertedSubUnit),
125
+ ...metrics.trailingRate != null ? converted : {} },
126
+ { id: "forward-rate", label: short ? "Fwd/sh" : "Fwd/share", value: formatRate(metrics.forwardRate, currency, !!convertedFrom, convertedSubUnit),
127
+ ...metrics.forwardRate != null ? converted : {} },
121
128
  growthItem("growth-1y", short ? "1Y chg" : "1Y growth", metrics.growth1Y, metrics.growth1YPayments),
122
129
  growthItem("cagr-3y", "3Y CAGR", metrics.growth3Y, metrics.growth3YPayments),
123
130
  { id: "payout", label: "Payout", value: metrics.payoutNotMeaningful ? PAYOUT_NOT_MEANINGFUL : metrics.payoutRatio != null ? `${(metrics.payoutRatio * 100).toFixed(1)}%` : "—" },
@@ -144,13 +151,15 @@ const rowKey = (row: DividendRow) => row.key;
144
151
  function renderCell(
145
152
  row: DividendRow,
146
153
  column: DividendColumn,
154
+ converted = false,
155
+ subUnit?: string,
147
156
  ): DataTableCell {
148
157
  switch (column.id) {
149
158
  case "exDate":
150
159
  return { text: row.exDate, color: colors.textDim };
151
160
  case "amount":
152
161
  return {
153
- text: formatDistributionAmount(row.amount, row.currency),
162
+ text: converted ? formatConvertedAmount(row.amount, row.currency, subUnit) : formatDistributionAmount(row.amount, row.currency),
154
163
  color: colors.textBright,
155
164
  attributes: TextAttributes.BOLD,
156
165
  };
@@ -161,9 +170,15 @@ function renderCell(
161
170
  }
162
171
  }
163
172
 
164
- export function DividendYieldPane({ focused, width, height, loadData = fetchDividendData }: {
165
- focused: boolean; width: number; height: number; loadData?: typeof fetchDividendData;
166
- }) {
173
+ type DividendPaneProps = { focused: boolean; width: number; height: number; loadData?: typeof fetchDividendData };
174
+
175
+ /** One ticker's dividends, or several ranked by yield when the pane was opened with a list (`shot DVD KO JNJ`). */
176
+ export function DividendYieldPane(props: DividendPaneProps) {
177
+ const ranked = rankSymbolsOf(usePaneInstance()?.settings);
178
+ return ranked.length > 1 ? <DividendRankingView symbols={ranked} {...props} /> : <DividendTickerPane {...props} />;
179
+ }
180
+
181
+ function DividendTickerPane({ focused, width, height, loadData = fetchDividendData }: DividendPaneProps) {
167
182
  const { symbol, ticker, financials } = usePaneTicker();
168
183
  const { nativePaneChrome } = useUiCapabilities();
169
184
  // Yields are repriced from the quote on every render, so streaming it is all
@@ -275,13 +290,17 @@ export function DividendYieldPane({ focused, width, height, loadData = fetchDivi
275
290
  });
276
291
  // The header row, plus the scrollbar row once the columns overflow the pane.
277
292
  const tableChromeRows = chartTableChromeRows(columns, width);
278
- const fullFigures = metrics ? buildMetricItems(metrics, currency, sortedRows.length > 0, false, data?.declared, bond) : [];
293
+ const convertedFrom = historyData?.convertedFrom;
294
+ const convertedSubUnit = historyData?.convertedSubUnit;
295
+ const renderRowCell = useCallback((row: DividendRow, column: DividendColumn) => renderCell(row, column, !!convertedFrom, convertedSubUnit),
296
+ [convertedFrom, convertedSubUnit]);
297
+ const fullFigures = metrics ? buildMetricItems(metrics, currency, sortedRows.length > 0, false, data?.declared, bond, convertedFrom, convertedSubUnit) : [];
279
298
  const keptFigures = chartTableLayout({
280
299
  width, height, figures: fullFigures, tableRows: sortedRows.length, tableChromeRows, chart: chartPoints.length >= 2 ? {} : null,
281
300
  }).figures;
282
301
  // The desktop grid never clips a label; the terminal shortens them rather than clip.
283
302
  const figures = metrics && !nativePaneChrome && labelsClip(keptFigures, width)
284
- ? buildMetricItems(metrics, currency, sortedRows.length > 0, true, data?.declared, bond) : fullFigures;
303
+ ? buildMetricItems(metrics, currency, sortedRows.length > 0, true, data?.declared, bond, convertedFrom, convertedSubUnit) : fullFigures;
285
304
 
286
305
  const handleHeaderClick = useCallback((columnId: string) => {
287
306
  setSortPreference((current) => nextHeaderSort(current, columnId as DividendColumnId, {
@@ -324,7 +343,7 @@ export function DividendYieldPane({ focused, width, height, loadData = fetchDivi
324
343
  sortDirection={sortPreference.direction}
325
344
  onHeaderClick={handleHeaderClick}
326
345
  getItemKey={rowKey}
327
- renderCell={renderCell}
346
+ renderCell={renderRowCell}
328
347
  selectedTextOverridesCellColor
329
348
  emptyStateTitle={emptyTitle}
330
349
  />
@@ -0,0 +1,73 @@
1
+ import { ApiRequestError } from "../../../api-client/errors";
2
+ import type { FundBondFacts } from "../../../api-client/fund-facts";
3
+ import { createConcurrencyLimiter } from "../../../utils/concurrency";
4
+ import type { DividendData } from "./client";
5
+ import type { DividendRankingEntry } from "./ranking";
6
+
7
+ /** Tickers one ranking reads at most: `fn DVD`, `shot DVD` and the DVD pane alike. */
8
+ export const MAX_RANKED_SYMBOLS = 25;
9
+ /** Tickers read at once across every ranking in the process, so a ranking never floods Gloom Cloud. */
10
+ const RANKING_CONCURRENCY = 4;
11
+ /** A rate-limited read waits what the server asks, a second when it names nothing, and never longer than this. */
12
+ const RATE_LIMIT_WAIT_MS = 1_000;
13
+ const RATE_LIMIT_WAIT_MAX_MS = 5_000;
14
+
15
+ /** The one gate every ranking's reads pass through. */
16
+ const rankingReads = createConcurrencyLimiter(RANKING_CONCURRENCY);
17
+
18
+ /** How a ranking reads one ticker. */
19
+ export interface DividendRankingReads {
20
+ loadData(symbol: string): Promise<DividendData>;
21
+ /** A fund's bond facts, null for any other listing. It never rejects: without them the fund ranks as before. */
22
+ loadBond?(symbol: string): Promise<FundBondFacts | null>;
23
+ }
24
+
25
+ interface RankingReadOptions {
26
+ /** How the one retry of a rate-limited read waits; tests pass their own. */
27
+ sleep?: (ms: number) => Promise<void>;
28
+ }
29
+
30
+ const wait = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
31
+
32
+ /** The 429 a read was refused with, wrapped or not; null for any other failure. */
33
+ function rateLimited(error: unknown): ApiRequestError | null {
34
+ for (let current: unknown = error, depth = 0; current && typeof current === "object" && depth < 4; depth += 1) {
35
+ if (current instanceof ApiRequestError) return current.status === 429 ? current : null;
36
+ current = (current as { cause?: unknown }).cause;
37
+ }
38
+ return null;
39
+ }
40
+
41
+ /** A read Gloom Cloud refused with 429, asked once more after the wait it names; a second refusal is the row's error. */
42
+ async function retryRateLimitedOnce<T>(read: () => Promise<T>, sleep: (ms: number) => Promise<void>): Promise<T> {
43
+ try {
44
+ return await read();
45
+ } catch (error) {
46
+ const limited = rateLimited(error);
47
+ if (!limited) throw error;
48
+ await sleep(Math.min(Math.max(0, limited.retryAfterMs ?? RATE_LIMIT_WAIT_MS), RATE_LIMIT_WAIT_MAX_MS));
49
+ return read();
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Each ticker of a ranking, read into its entry in the order given: the first
55
+ * 25, at most four at once through the gate every ranking shares, so `fn`,
56
+ * `shot` and any open DVD pane together never ask Gloom Cloud for more. A
57
+ * ticker that fails keeps its row with its error, after one more try when it
58
+ * was rate limited.
59
+ */
60
+ export function readDividendRanking(
61
+ symbols: readonly string[],
62
+ reads: DividendRankingReads,
63
+ { sleep = wait }: RankingReadOptions = {},
64
+ ): Promise<DividendRankingEntry[]> {
65
+ return Promise.all(symbols.slice(0, MAX_RANKED_SYMBOLS).map((symbol) => rankingReads(async (): Promise<DividendRankingEntry> => {
66
+ const bond = reads.loadBond?.(symbol) ?? null;
67
+ try {
68
+ return { symbol, data: await retryRateLimitedOnce(() => reads.loadData(symbol), sleep), bond: await bond };
69
+ } catch (error) {
70
+ return { symbol, error: error instanceof Error ? error.message : String(error) };
71
+ }
72
+ })));
73
+ }