gloomberb 0.18.0 → 0.18.2

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 (365) hide show
  1. package/package.json +1 -1
  2. package/src/api-client/commodity-balances.ts +6 -0
  3. package/src/api-client/company-kpis.ts +9 -0
  4. package/src/api-client/crypto-markets.ts +9 -0
  5. package/src/api-client/data.ts +6 -5
  6. package/src/api-client/equity-screener.ts +2 -0
  7. package/src/api-client/market-movers.ts +7 -0
  8. package/src/api-client/types.ts +14 -0
  9. package/src/app/pane-runtime/plugin-bindings.ts +3 -1
  10. package/src/app/pane-runtime/ticker-open-runtime.ts +17 -5
  11. package/src/app/runtime/plugin-bindings.ts +6 -2
  12. package/src/brokers/logos.ts +66 -0
  13. package/src/brokers/profile-form.ts +3 -0
  14. package/src/brokers/remote-broker-adapter.ts +13 -3
  15. package/src/capabilities/index.ts +1 -1
  16. package/src/capabilities/service-capabilities.ts +7 -1
  17. package/src/cli/base-converter.ts +60 -18
  18. package/src/cli/commands/fred-series.ts +15 -5
  19. package/src/cli/commands/market.ts +290 -159
  20. package/src/cli/commands/news-search.ts +35 -8
  21. package/src/cli/commands/overview.ts +31 -6
  22. package/src/cli/commands/system.ts +35 -1
  23. package/src/cli/commands/ticker.ts +105 -52
  24. package/src/cli/currency-hints.ts +27 -0
  25. package/src/cli/fx-pair.ts +41 -4
  26. package/src/cli/glossary.ts +57 -4
  27. package/src/cli/helpers.ts +36 -3
  28. package/src/cli/index.ts +4 -1
  29. package/src/cli/known-gaps.ts +37 -1
  30. package/src/cli/listing-arg.ts +37 -2
  31. package/src/cli/pane-functions/catalog.ts +3 -1
  32. package/src/cli/pane-functions/discovery.ts +15 -1
  33. package/src/cli/pane-functions/dom.ts +9 -4
  34. package/src/cli/pane-functions/freshness.ts +4 -4
  35. package/src/cli/pane-functions/function-help.ts +31 -7
  36. package/src/cli/pane-functions/headless.ts +6 -5
  37. package/src/cli/pane-functions/index.ts +3 -2
  38. package/src/cli/pane-functions/lock-note.ts +7 -4
  39. package/src/cli/pane-functions/resolver.ts +2 -1
  40. package/src/cli/quote-answer.ts +179 -0
  41. package/src/cli/series-search.ts +2 -1
  42. package/src/cli/symbol-list.ts +51 -16
  43. package/src/cli/types.ts +4 -0
  44. package/src/components/chart/composite/composite-chart.tsx +22 -7
  45. package/src/components/chart/composite/format.ts +13 -1
  46. package/src/components/chart/composite/level-labels.ts +30 -0
  47. package/src/components/chart/composite/levels.ts +17 -6
  48. package/src/components/chart/composite/panel-surface.tsx +64 -16
  49. package/src/components/chart/composite/price-axis-labels.tsx +2 -1
  50. package/src/components/chart/composite/types.ts +4 -0
  51. package/src/components/command-bar/routes/ticker-search/actions.ts +6 -0
  52. package/src/components/command-bar/workflow/collection-actions.ts +4 -0
  53. package/src/components/index.ts +2 -0
  54. package/src/components/layout/pane/content.tsx +16 -14
  55. package/src/components/layout/pane/footer/index.tsx +22 -3
  56. package/src/components/layout/pane/footer/model.ts +99 -4
  57. package/src/components/layout/pane/header-tabs.tsx +15 -1
  58. package/src/components/onboarding/onboarding-frame.tsx +9 -11
  59. package/src/components/onboarding/use-onboarding-broker-form.ts +5 -2
  60. package/src/components/onboarding/wizard-model.ts +10 -3
  61. package/src/components/onboarding/wizard-positions.ts +8 -0
  62. package/src/components/ui/button.tsx +11 -4
  63. package/src/components/ui/choice-dialog.tsx +8 -5
  64. package/src/components/ui/display.tsx +11 -6
  65. package/src/components/ui/fields.tsx +100 -20
  66. package/src/components/ui/icon.tsx +6 -1
  67. package/src/components/ui/list-row-lines.tsx +87 -0
  68. package/src/components/ui/list-view.tsx +14 -0
  69. package/src/components/ui/logo-mark.tsx +64 -0
  70. package/src/components/ui/number-step.ts +54 -0
  71. package/src/components/ui/select-button.tsx +5 -4
  72. package/src/components/ui/select-field.tsx +3 -3
  73. package/src/components/ui/status.tsx +17 -3
  74. package/src/components/ui/tabs.tsx +9 -1
  75. package/src/components/ui/toggle.tsx +17 -3
  76. package/src/core/state/app/instrument.ts +8 -3
  77. package/src/data/config/store/normalize.ts +4 -1
  78. package/src/data/plugin-cache.ts +32 -6
  79. package/src/docs/snapshot.json +57 -22
  80. package/src/i18n/es.ts +9 -3
  81. package/src/i18n/ja.ts +9 -3
  82. package/src/i18n/ko.ts +9 -3
  83. package/src/i18n/zh-cn.ts +10 -4
  84. package/src/i18n/zh-tw.ts +10 -4
  85. package/src/market-data/commodity-names.ts +80 -0
  86. package/src/market-data/coordinator/auxiliary.ts +3 -1
  87. package/src/market-data/coordinator/chart.ts +3 -1
  88. package/src/market-data/futures-board.ts +38 -1
  89. package/src/market-data/futures-contract.ts +91 -0
  90. package/src/market-data/fx-closes.ts +78 -1
  91. package/src/market-data/market/change-basis.ts +219 -0
  92. package/src/market-data/market/format.ts +28 -12
  93. package/src/market-data/market/freshness.ts +38 -0
  94. package/src/market-data/market/status.ts +55 -9
  95. package/src/market-data/market/year-to-date.ts +46 -0
  96. package/src/market-data/quotes/contributions.ts +12 -0
  97. package/src/market-data/quotes/delisting.ts +34 -0
  98. package/src/market-data/quotes/live-valuation.ts +23 -0
  99. package/src/market-data/quotes/resolution.ts +8 -2
  100. package/src/market-data/tax-equivalent.ts +10 -2
  101. package/src/notifications/app-notifier.ts +2 -2
  102. package/src/notifications/notification-log.ts +8 -6
  103. package/src/plugins/builtin/alerts/cli.ts +26 -18
  104. package/src/plugins/builtin/alerts/headless.ts +1 -1
  105. package/src/plugins/builtin/alerts/index.tsx +13 -4
  106. package/src/plugins/builtin/analytics/index.tsx +1 -1
  107. package/src/plugins/builtin/analytics/risk-client.ts +44 -11
  108. package/src/plugins/builtin/analytics/risk-fx.ts +61 -0
  109. package/src/plugins/builtin/analytics/risk-headless.ts +10 -1
  110. package/src/plugins/builtin/analytics/risk-model.ts +79 -16
  111. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
  112. package/src/plugins/builtin/analytics/risk-test-data.ts +3 -0
  113. package/src/plugins/builtin/attention/trending.tsx +8 -6
  114. package/src/plugins/builtin/backtest/client.ts +5 -2
  115. package/src/plugins/builtin/backtest/window.ts +16 -3
  116. package/src/plugins/builtin/broker-manager/add-flow.tsx +40 -8
  117. package/src/plugins/builtin/broker-manager/detail.tsx +26 -1
  118. package/src/plugins/builtin/broker-manager/index.tsx +20 -9
  119. package/src/plugins/builtin/broker-manager/model.ts +5 -0
  120. package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -1
  121. package/src/plugins/builtin/broker-manager/table.ts +5 -1
  122. package/src/plugins/builtin/broker-trading/account-picker.tsx +91 -0
  123. package/src/plugins/builtin/broker-trading/chart-levels.ts +54 -0
  124. package/src/plugins/builtin/broker-trading/command.ts +103 -0
  125. package/src/plugins/builtin/broker-trading/controller.ts +688 -0
  126. package/src/plugins/builtin/broker-trading/dock.tsx +182 -0
  127. package/src/plugins/builtin/broker-trading/headless.ts +76 -0
  128. package/src/plugins/builtin/broker-trading/index.tsx +77 -0
  129. package/src/plugins/builtin/broker-trading/order-feed.ts +383 -0
  130. package/src/plugins/builtin/broker-trading/orders-data.ts +133 -0
  131. package/src/plugins/builtin/broker-trading/orders.tsx +437 -0
  132. package/src/plugins/builtin/broker-trading/overview-orders.tsx +121 -0
  133. package/src/plugins/builtin/broker-trading/pane-model.ts +207 -0
  134. package/src/plugins/builtin/broker-trading/pane.tsx +505 -0
  135. package/src/plugins/builtin/broker-trading/strategy.ts +153 -0
  136. package/src/plugins/builtin/broker-trading/ticket-parts.tsx +117 -0
  137. package/src/plugins/builtin/broker-trading/ticket.tsx +573 -0
  138. package/src/plugins/builtin/buildout/data-runtime.ts +1 -1
  139. package/src/plugins/builtin/buildout/format.ts +1 -0
  140. package/src/plugins/builtin/buildout/model/index.ts +16 -7
  141. package/src/plugins/builtin/buildout/pane/index.tsx +3 -2
  142. package/src/plugins/builtin/buildout/pane/status.tsx +3 -3
  143. package/src/plugins/builtin/chart-composer/cli-options.ts +19 -0
  144. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +2 -1
  145. package/src/plugins/builtin/chart-composer/headless-schema.ts +3 -3
  146. package/src/plugins/builtin/chart-composer/headless.ts +2 -1
  147. package/src/plugins/builtin/chart-composer/option-range.ts +39 -0
  148. package/src/plugins/builtin/chart-composer/pane.tsx +31 -11
  149. package/src/plugins/builtin/chart-composer/presets.ts +2 -2
  150. package/src/plugins/builtin/chart-composer/series-catalog.ts +4 -2
  151. package/src/plugins/builtin/chart-composer/shortcuts.ts +3 -3
  152. package/src/plugins/builtin/chart-composer/viewport-labels.ts +5 -2
  153. package/src/plugins/builtin/chat/index.tsx +1 -1
  154. package/src/plugins/builtin/chat/status-widget.tsx +1 -1
  155. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +6 -1
  156. package/src/plugins/builtin/chat/unread-inbox.ts +1 -1
  157. package/src/plugins/builtin/cloud/team/pane-sections.tsx +10 -3
  158. package/src/plugins/builtin/cloud/team/pane.tsx +30 -9
  159. package/src/plugins/builtin/commodity-balances/client.ts +6 -4
  160. package/src/plugins/builtin/commodity-balances/headless.ts +41 -7
  161. package/src/plugins/builtin/commodity-balances/index.tsx +19 -4
  162. package/src/plugins/builtin/commodity-balances/model.ts +22 -0
  163. package/src/plugins/builtin/commodity-balances/pane.tsx +36 -5
  164. package/src/plugins/builtin/company-kpis/client.ts +3 -2
  165. package/src/plugins/builtin/company-kpis/headless.ts +5 -7
  166. package/src/plugins/builtin/company-kpis/model.ts +85 -2
  167. package/src/plugins/builtin/company-kpis/pane.tsx +26 -12
  168. package/src/plugins/builtin/composite-plugins.ts +3 -2
  169. package/src/plugins/builtin/cot/headless.ts +1 -1
  170. package/src/plugins/builtin/cot/index.tsx +1 -1
  171. package/src/plugins/builtin/cot/model.ts +35 -12
  172. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  173. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +2 -1
  174. package/src/plugins/builtin/credit-conditions/index.tsx +2 -1
  175. package/src/plugins/builtin/crypto-board/client.ts +33 -2
  176. package/src/plugins/builtin/crypto-board/headless.ts +5 -2
  177. package/src/plugins/builtin/crypto-board/model.ts +28 -8
  178. package/src/plugins/builtin/custom-view/loader.ts +3 -2
  179. package/src/plugins/builtin/custom-view/pane.tsx +5 -4
  180. package/src/plugins/builtin/custom-view/view-spec.ts +1 -0
  181. package/src/plugins/builtin/dividend-yield/headless.ts +30 -8
  182. package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
  183. package/src/plugins/builtin/earnings-calls/headless.ts +4 -4
  184. package/src/plugins/builtin/earnings-calls/lock-note.ts +12 -3
  185. package/src/plugins/builtin/econ/calendar-model.ts +38 -2
  186. package/src/plugins/builtin/econ/fred-catalog.ts +6 -2
  187. package/src/plugins/builtin/econ/fred-series-map.ts +12 -1
  188. package/src/plugins/builtin/econ/index.tsx +31 -4
  189. package/src/plugins/builtin/econ/print-watch.ts +279 -0
  190. package/src/plugins/builtin/equity-screener/headless.ts +20 -5
  191. package/src/plugins/builtin/equity-screener/model.ts +40 -2
  192. package/src/plugins/builtin/equity-screener/pane.tsx +24 -21
  193. package/src/plugins/builtin/estimate-revisions/headless.ts +3 -2
  194. package/src/plugins/builtin/estimate-revisions/lock-note.ts +29 -7
  195. package/src/plugins/builtin/executives/data.ts +24 -8
  196. package/src/plugins/builtin/executives/index.tsx +2 -18
  197. package/src/plugins/builtin/executives/pane.tsx +34 -10
  198. package/src/plugins/builtin/exposure/client.ts +4 -1
  199. package/src/plugins/builtin/exposure/editor.tsx +3 -3
  200. package/src/plugins/builtin/exposure/evidence.ts +4 -1
  201. package/src/plugins/builtin/exposure/holdings.ts +3 -1
  202. package/src/plugins/builtin/exposure/pane.tsx +37 -16
  203. package/src/plugins/builtin/filing-events/index.tsx +9 -3
  204. package/src/plugins/builtin/futures/table.tsx +6 -3
  205. package/src/plugins/builtin/futures-curve/pane.tsx +2 -2
  206. package/src/plugins/builtin/insider/base-values.ts +72 -0
  207. package/src/plugins/builtin/insider/headless.ts +29 -4
  208. package/src/plugins/builtin/jobs/headless.ts +2 -2
  209. package/src/plugins/builtin/jobs/lock-note.ts +14 -3
  210. package/src/plugins/builtin/market-halts/model.ts +31 -6
  211. package/src/plugins/builtin/market-halts/pane.tsx +3 -0
  212. package/src/plugins/builtin/market-movers/client.ts +2 -2
  213. package/src/plugins/builtin/market-movers/index.tsx +0 -3
  214. package/src/plugins/builtin/market-movers/session-body.tsx +27 -11
  215. package/src/plugins/builtin/market-movers/session.ts +25 -10
  216. package/src/plugins/builtin/market-valuation/pane.tsx +2 -1
  217. package/src/plugins/builtin/members/pane.tsx +8 -3
  218. package/src/plugins/builtin/news/wire/feed-config.ts +33 -13
  219. package/src/plugins/builtin/news/wire/index.tsx +7 -2
  220. package/src/plugins/builtin/news/wire/letters/client.ts +1 -0
  221. package/src/plugins/builtin/news/wire/news/command-bar-search.ts +6 -3
  222. package/src/plugins/builtin/news/wire/news/detail-view.tsx +2 -1
  223. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +11 -3
  224. package/src/plugins/builtin/news/wire/news/table.tsx +2 -1
  225. package/src/plugins/builtin/news/wire/rss/cli.ts +3 -2
  226. package/src/plugins/builtin/news/wire/rss/feed-xml.ts +6 -1
  227. package/src/plugins/builtin/news/wire/rss/managed-feed.ts +1 -0
  228. package/src/plugins/builtin/news/wire/rss/source.ts +13 -4
  229. package/src/plugins/builtin/notes/cli.ts +33 -8
  230. package/src/plugins/builtin/notes/export.ts +49 -3
  231. package/src/plugins/builtin/notes/files.ts +14 -3
  232. package/src/plugins/builtin/notes/index.tsx +3 -3
  233. package/src/plugins/builtin/notes/quick-notes-pane.tsx +45 -10
  234. package/src/plugins/builtin/notes/store.ts +13 -3
  235. package/src/plugins/builtin/options/footer.ts +25 -20
  236. package/src/plugins/builtin/options/live-quotes.ts +33 -18
  237. package/src/plugins/builtin/options/view.tsx +77 -22
  238. package/src/plugins/builtin/options-calculator/pane.tsx +4 -2
  239. package/src/plugins/builtin/options-scenario/pane.tsx +41 -6
  240. package/src/plugins/builtin/portfolio-list/account-metrics.ts +3 -0
  241. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -7
  242. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +166 -15
  243. package/src/plugins/builtin/portfolio-list/cli/render.ts +69 -15
  244. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +2 -2
  245. package/src/plugins/builtin/portfolio-list/headless.ts +3 -1
  246. package/src/plugins/builtin/portfolio-list/pane/data.ts +23 -1
  247. package/src/plugins/builtin/portfolio-list/pane/index.tsx +44 -17
  248. package/src/plugins/builtin/portfolio-list/settings.ts +20 -2
  249. package/src/plugins/builtin/portfolio-list/summary/index.tsx +22 -1
  250. package/src/plugins/builtin/portfolio-list/summary/live-accounts.ts +42 -7
  251. package/src/plugins/builtin/portfolio-list/summary/totals.ts +24 -3
  252. package/src/plugins/builtin/primary-dealers/client.ts +137 -62
  253. package/src/plugins/builtin/primary-dealers/headless.ts +30 -5
  254. package/src/plugins/builtin/primary-dealers/index.tsx +3 -2
  255. package/src/plugins/builtin/primary-dealers/model.ts +179 -129
  256. package/src/plugins/builtin/primary-dealers/pane.tsx +135 -15
  257. package/src/plugins/builtin/research/analyst-model.ts +5 -1
  258. package/src/plugins/builtin/risk-factors/index.tsx +12 -3
  259. package/src/plugins/builtin/scanner/feed.ts +6 -1
  260. package/src/plugins/builtin/scanner/headless.ts +1 -1
  261. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  262. package/src/plugins/builtin/series-inventory.ts +1 -0
  263. package/src/plugins/builtin/shared/broker-reconnect.ts +105 -0
  264. package/src/plugins/builtin/shared/cloud-denied.ts +7 -6
  265. package/src/plugins/builtin/shared/cloud-upgrade.ts +10 -11
  266. package/src/plugins/builtin/shared/free-routes.ts +24 -0
  267. package/src/plugins/builtin/shared/headless-options.ts +11 -0
  268. package/src/plugins/builtin/shared/pro-feature-wall.tsx +9 -4
  269. package/src/plugins/builtin/shared/pro-lock-note.ts +89 -7
  270. package/src/plugins/builtin/shared/research-trading.tsx +62 -0
  271. package/src/plugins/builtin/social-mentions/headless.ts +1 -1
  272. package/src/plugins/builtin/social-mentions/lock-note.ts +1 -1
  273. package/src/plugins/builtin/themes/headless.ts +8 -2
  274. package/src/plugins/builtin/themes/model.ts +43 -10
  275. package/src/plugins/builtin/themes/pane.tsx +8 -6
  276. package/src/plugins/builtin/thirteenf/api.ts +4 -1
  277. package/src/plugins/builtin/thirteenf/data.ts +5 -1
  278. package/src/plugins/builtin/thirteenf/headless.ts +3 -1
  279. package/src/plugins/builtin/thirteenf/pane.tsx +8 -3
  280. package/src/plugins/builtin/ticker-detail/overview/model.ts +44 -10
  281. package/src/plugins/builtin/ticker-detail/overview/option-contract.ts +64 -0
  282. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +82 -21
  283. package/src/plugins/builtin/ticker-detail/pane.tsx +21 -14
  284. package/src/plugins/builtin/ticker-detail/quote-footer.ts +5 -1
  285. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +37 -8
  286. package/src/plugins/builtin/time-sales/pane.tsx +2 -2
  287. package/src/plugins/builtin/trace-bonds/client.ts +14 -3
  288. package/src/plugins/builtin/trace-bonds/headless.ts +1 -1
  289. package/src/plugins/builtin/vol-surface/pane.tsx +2 -2
  290. package/src/plugins/builtin-editorial.ts +1 -1
  291. package/src/plugins/compat.ts +3 -0
  292. package/src/plugins/external-runtime.ts +5 -0
  293. package/src/plugins/loader.ts +6 -0
  294. package/src/plugins/plugin-icon.ts +61 -0
  295. package/src/plugins/registry/host-actions.ts +2 -1
  296. package/src/plugins/registry/index.ts +9 -1
  297. package/src/plugins/runtime/context.tsx +5 -0
  298. package/src/plugins/runtime/index.tsx +3 -0
  299. package/src/portfolio/column-values.ts +13 -4
  300. package/src/portfolio/cost-fx.ts +61 -0
  301. package/src/portfolio/mutations.ts +16 -2
  302. package/src/portfolio/position-metrics.ts +45 -4
  303. package/src/portfolio/position-quantity.ts +43 -0
  304. package/src/public/__snapshots__/public-api.test.ts.snap +5 -0
  305. package/src/public/broker.ts +8 -0
  306. package/src/public/market-data.ts +5 -0
  307. package/src/renderers/dom/desktop/control-styles.ts +7 -1
  308. package/src/renderers/dom/desktop/controls.tsx +39 -7
  309. package/src/renderers/dom/desktop/icons.tsx +14 -0
  310. package/src/renderers/dom/desktop/list-view.tsx +51 -19
  311. package/src/renderers/dom/desktop/menu.tsx +10 -1
  312. package/src/renderers/dom/desktop/select-field.tsx +8 -6
  313. package/src/renderers/dom/host/style.ts +6 -1
  314. package/src/renderers/dom/host/tabs.tsx +6 -0
  315. package/src/renderers/dom/styles.css +12 -5
  316. package/src/renderers/electrobun/bun/core-capabilities.ts +12 -0
  317. package/src/renderers/electrobun/bun/external-plugins.ts +3 -0
  318. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  319. package/src/renderers/electrobun/view/broker-remote-client.ts +16 -0
  320. package/src/renderers/electrobun/view/external-plugins.ts +1 -0
  321. package/src/series/validate.ts +2 -2
  322. package/src/sources/gloomberb-cloud/fred-series.ts +27 -2
  323. package/src/sources/gloomberb-cloud/index.ts +50 -17
  324. package/src/sources/gloomberb-cloud/normalizers.ts +4 -0
  325. package/src/sources/history-result.ts +3 -0
  326. package/src/sources/provider-errors.ts +3 -1
  327. package/src/sources/provider-router/history.ts +49 -6
  328. package/src/sources/provider-router/index.ts +2 -1
  329. package/src/state/app/activity.ts +13 -0
  330. package/src/sync/controller.ts +4 -0
  331. package/src/sync/core-contributors.ts +63 -11
  332. package/src/sync/local-only.ts +121 -0
  333. package/src/sync/react.ts +6 -0
  334. package/src/sync/types.ts +3 -0
  335. package/src/team/collections.ts +2 -0
  336. package/src/tickers/listing-fate.ts +27 -8
  337. package/src/tickers/metadata.ts +10 -0
  338. package/src/tickers/search/index.ts +24 -3
  339. package/src/time-series/price-comparison.ts +35 -0
  340. package/src/time-series/resolve.ts +52 -11
  341. package/src/time-series/spec.ts +1 -1
  342. package/src/time-series/types.ts +5 -0
  343. package/src/types/broker.ts +56 -1
  344. package/src/types/config.ts +6 -0
  345. package/src/types/data-provider.ts +5 -1
  346. package/src/types/financials.ts +23 -0
  347. package/src/types/headless.ts +12 -2
  348. package/src/types/instrument.ts +4 -0
  349. package/src/types/plugin.ts +24 -6
  350. package/src/types/price-history.ts +6 -1
  351. package/src/types/ticker.ts +9 -0
  352. package/src/types/trading.ts +67 -0
  353. package/src/ui/host.tsx +4 -0
  354. package/src/utils/base-currency.ts +108 -0
  355. package/src/utils/currency-names.ts +128 -0
  356. package/src/utils/currency-pair.ts +2 -1
  357. package/src/utils/currency-units.ts +41 -1
  358. package/src/utils/dom-screenshot.ts +22 -9
  359. package/src/utils/exchange-notation.generated.ts +360 -0
  360. package/src/utils/exchanges.ts +15 -322
  361. package/src/utils/format.ts +31 -1
  362. package/src/utils/instrument-identity.ts +38 -1
  363. package/src/utils/options.ts +46 -1
  364. package/src/utils/price-history.ts +4 -0
  365. package/src/utils/reported-money.ts +8 -4
@@ -22,6 +22,12 @@ export interface ListViewItem {
22
22
  disabled?: boolean;
23
23
  /** Tints the label, e.g. a status in its tone; the words carry the meaning. */
24
24
  color?: string;
25
+ /**
26
+ * Artwork before the label on the desktop and the web, such as a broker's
27
+ * logo (`LogoMark`). It is decoration: the terminal does not draw it, and
28
+ * automation and screen readers read only `label`.
29
+ */
30
+ leading?: ReactNode;
25
31
  }
26
32
 
27
33
  export interface ListRowState {
@@ -39,6 +45,12 @@ export interface ListViewProps {
39
45
  renderRow?: (item: ListViewItem, state: ListRowState, index: number) => ReactNode;
40
46
  getRowBackgroundColor?: (item: ListViewItem, state: ListRowState, index: number) => string | undefined;
41
47
  showSelectedDescription?: boolean;
48
+ /**
49
+ * Desktop and web: every row sets its `description` on a second line under
50
+ * its label, and is two rows tall unless `rowHeight` says otherwise. The
51
+ * terminal keeps one line per row, with `detail` at its end.
52
+ */
53
+ rowDescriptions?: boolean;
42
54
  emptyMessage?: string;
43
55
  bgColor?: string;
44
56
  selectedBgColor?: string;
@@ -173,6 +185,7 @@ export function ListView({
173
185
  renderRow,
174
186
  getRowBackgroundColor,
175
187
  showSelectedDescription = false,
188
+ rowDescriptions,
176
189
  emptyMessage = "Nothing to show.",
177
190
  bgColor,
178
191
  selectedBgColor,
@@ -243,6 +256,7 @@ export function ListView({
243
256
  renderRow={renderRow}
244
257
  getRowBackgroundColor={getRowBackgroundColor}
245
258
  showSelectedDescription={showSelectedDescription}
259
+ rowDescriptions={rowDescriptions}
246
260
  emptyMessage={emptyMessage}
247
261
  bgColor={bgColor}
248
262
  selectedBgColor={selectedBgColor}
@@ -0,0 +1,64 @@
1
+ import type { CSSProperties } from "react";
2
+ import { Box, ImageSurface, Text, TextAttributes, useUiCapabilities } from "../../ui";
3
+ import { useThemeColors } from "../../theme/theme-context";
4
+
5
+ /**
6
+ * `row`: beside a label in one row (a table cell's `leading`, a list row).
7
+ * `lead`: the art of a two-line list row; it fills the square `ListRowLines`
8
+ * sizes to the two lines beside it.
9
+ */
10
+ type LogoMarkSize = "row" | "lead";
11
+
12
+ const TILE: Record<LogoMarkSize, { side: string; radius: string; type: string }> = {
13
+ // A pixel above and below in its row, as a chip.
14
+ row: { side: "calc(var(--cell-h) - 2px)", radius: "var(--gloom-radius-sm)", type: "var(--gloom-type-caption)" },
15
+ lead: { side: "100%", radius: "var(--gloom-radius-md)", type: "var(--gloom-type-ui)" },
16
+ };
17
+
18
+ // Sized by its capital and baseline, so flex centring puts the letter in the
19
+ // middle of the tile whatever the font's ascent and descent (as chrome text).
20
+ const LETTER_STYLE: Record<LogoMarkSize, CSSProperties> = {
21
+ row: { fontSize: TILE.row.type, lineHeight: 1, textBox: "trim-both cap alphabetic" } as CSSProperties,
22
+ lead: { fontSize: TILE.lead.type, lineHeight: 1, textBox: "trim-both cap alphabetic" } as CSSProperties,
23
+ };
24
+
25
+ export interface LogoMarkProps {
26
+ /** The logo, an image URL; without one, or when it fails to load, the tile shows `name`'s initial. */
27
+ src?: string | null;
28
+ /** What the logo stands for, such as a broker. Only its initial is drawn; the label beside it names it. */
29
+ name: string;
30
+ size?: LogoMarkSize;
31
+ }
32
+
33
+ function initialOf(name: string): string {
34
+ const letter = [...name.trim()].find((character) => /[\p{L}\p{N}]/u.test(character));
35
+ return letter ? letter.toLocaleUpperCase() : "?";
36
+ }
37
+
38
+ /**
39
+ * A logo in a square tile, for lists of brokers and the accounts they hold, on
40
+ * the desktop and the web. Decoration: hidden from assistive technology, so
41
+ * the label beside it stays the name. The terminal draws nothing.
42
+ */
43
+ export function LogoMark({ src, name, size = "row" }: LogoMarkProps) {
44
+ const desktop = useUiCapabilities().nativePaneChrome === true;
45
+ const colors = useThemeColors();
46
+ if (!desktop) return null;
47
+ const tile = TILE[size];
48
+ return (
49
+ <ImageSurface
50
+ src={src ?? undefined}
51
+ alt=""
52
+ objectFit="cover"
53
+ aria-hidden="true"
54
+ data-gloom-role="logo-mark"
55
+ style={{ width: tile.side, height: tile.side, flexShrink: 0, borderRadius: tile.radius }}
56
+ >
57
+ <Box width="100%" height="100%" alignItems="center" justifyContent="center" backgroundColor={colors.border}>
58
+ <Text fg={colors.textBright} attributes={TextAttributes.BOLD} style={LETTER_STYLE[size]}>
59
+ {initialOf(name)}
60
+ </Text>
61
+ </Box>
62
+ </ImageSurface>
63
+ );
64
+ }
@@ -0,0 +1,54 @@
1
+ /** Decimal stepping on a zero-based increment grid, without binary rounding drift. */
2
+ function decimal(value: string | number): { units: bigint; places: number } | undefined {
3
+ const text = String(value).trim();
4
+ const match = /^([+-]?)(\d*)(?:\.(\d*))?(?:e([+-]?\d+))?$/i.exec(text);
5
+ if (!match || !`${match[2]}${match[3] ?? ""}` || !Number.isFinite(Number(text))) return;
6
+ const exponent = Number(match[4] ?? 0);
7
+ const places = (match[3]?.length ?? 0) - exponent;
8
+ if (Math.abs(places) > 18 || text.length > 100) return;
9
+ const units = BigInt(`${match[1] === "-" ? "-" : ""}${match[2] || "0"}${match[3] ?? ""}`);
10
+ return places < 0 ? { units: units * 10n ** BigInt(-places), places: 0 } : { units, places };
11
+ }
12
+ function floorDivide(a: bigint, b: bigint): bigint { return a / b - (a < 0n && a % b !== 0n ? 1n : 0n); }
13
+ function ceilDivide(a: bigint, b: bigint): bigint { return -floorDivide(-a, b); }
14
+ function format(units: bigint, places: number): string {
15
+ const digits = (units < 0n ? -units : units).toString().padStart(places + 1, "0");
16
+ const text = places ? `${digits.slice(0, -places)}.${digits.slice(-places)}`.replace(/\.?0+$/, "") : digits;
17
+ return `${units < 0n ? "-" : ""}${text}`;
18
+ }
19
+ export function stepNumber(value: string, direction: 1 | -1, step: number, min?: number, max?: number): string | undefined {
20
+ const current = decimal(value === "" ? 0 : value), increment = decimal(step);
21
+ const lower = min === undefined ? undefined : decimal(min), upper = max === undefined ? undefined : decimal(max);
22
+ if (!current || !increment || increment.units <= 0n || min !== undefined && !lower || max !== undefined && !upper) return;
23
+ const places = Math.max(current.places, increment.places, lower?.places ?? 0, upper?.places ?? 0);
24
+ const scaled = (n: { units: bigint; places: number }) => n.units * 10n ** BigInt(places - n.places);
25
+ const at = scaled(current), by = scaled(increment);
26
+ let ticks = direction > 0 ? floorDivide(at, by) + 1n : ceilDivide(at, by) - 1n;
27
+ const low = lower && ceilDivide(scaled(lower), by), high = upper && floorDivide(scaled(upper), by);
28
+ if (low !== undefined && high !== undefined && low > high) return;
29
+ if (low !== undefined && ticks < low) ticks = low;
30
+ if (high !== undefined && ticks > high) ticks = high;
31
+ const next = ticks * by;
32
+ // A bound must not make a plus go down or a minus go up, except an empty field.
33
+ if (value !== "" && (direction > 0 ? next <= at : next >= at)) return;
34
+ const text = format(next, places), roundTrip = decimal(Number(text));
35
+ if (!roundTrip || roundTrip.places > places || scaled(roundTrip) !== next) return;
36
+ return text;
37
+ }
38
+
39
+ /** Smallest whole-number multiple, for brokers combining a step with whole quantities. */
40
+ export function wholeNumberStep(step: number): number | undefined {
41
+ const value = decimal(step);
42
+ if (!value || value.units <= 0n) return;
43
+ let a = value.units, b = 10n ** BigInt(value.places);
44
+ while (b) { const remainder = a % b; a = b; b = remainder; }
45
+ const result = Number(value.units / a);
46
+ return Number.isSafeInteger(result) ? result : undefined;
47
+ }
48
+
49
+ export function matchesNumberStep(value: number, step: number): boolean {
50
+ const n = decimal(value), s = decimal(step);
51
+ if (!n || !s || s.units <= 0n) return false;
52
+ const places = Math.max(n.places, s.places);
53
+ return n.units * 10n ** BigInt(places - n.places) % (s.units * 10n ** BigInt(places - s.places)) === 0n;
54
+ }
@@ -48,9 +48,10 @@ export interface SelectButtonProps<T extends string = string> {
48
48
  * `inline`: the label and the value in a row, in a toolbar. `field`: a
49
49
  * bordered field. `form`: a field in a form of text fields; on the desktop
50
50
  * and the web its label sits above it, bold, as a TextField's does; the
51
- * terminal draws it inline.
51
+ * terminal draws it inline. `well`: a field without its border, on a
52
+ * recessed fill, for a dense panel such as the order ticket.
52
53
  */
53
- variant?: "inline" | "field" | "form";
54
+ variant?: "inline" | "field" | "form" | "well";
54
55
  width?: number;
55
56
  onFocus?: () => void;
56
57
  /**
@@ -189,8 +190,8 @@ export function SelectButton<T extends string = string>({
189
190
  }
190
191
 
191
192
  // The terminal draws a form select as it draws an inline one.
192
- if (variant === "field") {
193
- return <Button label={showLabel ? `${label}: ${current?.short ?? current?.label ?? ""}` : current?.short ?? current?.label ?? label} width={width} active={emphasized} disabled={disabled} onPress={openDialog} stopPropagation />;
193
+ if (variant === "field" || variant === "well") {
194
+ return <Button label={showLabel ? `${label}: ${current?.short ?? current?.label ?? ""}` : current?.short ?? current?.label ?? label} width={width} variant={variant === "well" ? "quiet" : undefined} active={emphasized} disabled={disabled} onPress={openDialog} stopPropagation />;
194
195
  }
195
196
 
196
197
  return (
@@ -22,10 +22,10 @@ export interface SelectFieldProps {
22
22
  width?: number | string;
23
23
  height?: number;
24
24
  /**
25
- * `field` is the form control used in dialogs. `inline` drops the box so a
26
- * dense row reads as text; both open the same Menu.
25
+ * `field` is the framed form control used in dialogs. `well` uses a quiet
26
+ * filled surface; `inline` reads as text. All open the same Menu.
27
27
  */
28
- variant?: "field" | "inline";
28
+ variant?: "field" | "inline" | "well";
29
29
  includeUnsetOption?: boolean;
30
30
  /**
31
31
  * Return focus to the trigger when the menu closes (inside a dialog). A list
@@ -5,6 +5,7 @@ import { Box, Text, useUiCapabilities } from "../../ui";
5
5
  import { formatRelativeAge } from "../../utils/datetime-format";
6
6
  import { Spinner } from "./loading";
7
7
  import { ButtonActionScope } from "./action-scope";
8
+ import { blendForContrast, higherContrast } from "../../theme/color-utils";
8
9
  import { Button } from "./button";
9
10
 
10
11
  /**
@@ -233,14 +234,27 @@ function EmptyStateBody({ title, message, hint, actions, retry, status = "empty"
233
234
  export interface NoticeProps {
234
235
  children: ReactNode;
235
236
  tone?: "muted" | "positive" | "warning" | "negative";
237
+ variant?: "inline" | "callout";
236
238
  }
237
239
 
238
240
  /** Inline feedback leaves existing content visible, including stale data after a refresh failure. */
239
- export function Notice({ children, tone = "warning" }: NoticeProps) {
241
+ export function Notice({ children, tone = "warning", variant = "inline" }: NoticeProps) {
240
242
  const colors = useThemeColors();
243
+ const native = useUiCapabilities().nativePaneChrome;
244
+ const color = tone === "muted" ? colors.textDim : colors[tone];
245
+ const status = tone === "negative" ? "error" : "notice";
246
+ // Inline feedback keeps its original single-text shape: a flex row here collapses inside content-sized parents.
247
+ if (variant !== "callout") {
248
+ return (
249
+ <Box data-gloom-status={status} data-gloom-ui="notice">
250
+ <Text fg={color} wrapText>{children}</Text>
251
+ </Box>
252
+ );
253
+ }
254
+ const foreground = blendForContrast(color, colors.panel, higherContrast("#000000", "#ffffff", colors.panel), 4.5);
241
255
  return (
242
- <Box data-gloom-status={tone === "negative" ? "error" : "notice"} data-gloom-ui="notice">
243
- <Text fg={tone === "muted" ? colors.textDim : colors[tone]} wrapText>{children}</Text>
256
+ <Box flexDirection="row" gap={1} paddingX={1} paddingY={native ? 0.35 : 0} backgroundColor={colors.panel} border={["left"]} borderColor={color} data-gloom-status={status} data-gloom-ui="notice" role={native ? tone === "negative" ? "alert" : "status" : undefined} aria-atomic={native ? true : undefined}>
257
+ <Text fg={foreground}>!</Text><Box flexGrow={1} minWidth={0}><Text fg={foreground} wrapText>{children}</Text></Box>
244
258
  </Box>
245
259
  );
246
260
  }
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
1
+ import { useCallback, useEffect, useId, useMemo, useRef, useState, type ReactNode } from "react";
2
2
  import { usePaneMenuItems, usePaneTabStripKeys } from "../layout/pane/footer/registration";
3
3
  import type { ContextMenuItem } from "../../types/context-menu";
4
4
  import { useShortcut } from "../../react/input";
@@ -22,6 +22,14 @@ type TabPointerEvent = {
22
22
  interface TabItem {
23
23
  label: string;
24
24
  value: string;
25
+ /**
26
+ * Artwork before the label on the desktop and the web, such as the logo of
27
+ * the broker a portfolio's account is at (`LogoMark`). It is decoration: the
28
+ * terminal does not draw it, and automation and screen readers read only
29
+ * `label`. Keep the same element between renders (useMemo): a title-bar
30
+ * strip redraws when it changes.
31
+ */
32
+ leading?: ReactNode;
25
33
  disabled?: boolean;
26
34
  /** Text color while inactive; used for group markers such as a team accent. */
27
35
  fg?: string;
@@ -9,9 +9,12 @@ interface SegmentedControlOption {
9
9
  label: string;
10
10
  value: string;
11
11
  disabled?: boolean;
12
+ tone?: "positive" | "negative";
12
13
  }
13
14
 
14
15
  export interface SegmentedControlProps {
16
+ /** Name of the choice group for assistive technology and semantic access. */
17
+ accessibleLabel?: string;
15
18
  options: SegmentedControlOption[];
16
19
  value: string;
17
20
  onChange?: (value: string) => void;
@@ -21,9 +24,13 @@ export interface SegmentedControlProps {
21
24
  shortcutScope?: string;
22
25
  width?: number | "100%";
23
26
  wrap?: boolean;
27
+ size?: "default" | "large";
28
+ /** One filled selection without an enclosing frame. */
29
+ variant?: "default" | "flat";
24
30
  }
25
31
 
26
32
  export function SegmentedControl({
33
+ accessibleLabel,
27
34
  options,
28
35
  value,
29
36
  onChange,
@@ -32,13 +39,15 @@ export function SegmentedControl({
32
39
  shortcutScope,
33
40
  width,
34
41
  wrap = false,
42
+ size = "default",
43
+ variant = "default",
35
44
  }: SegmentedControlProps) {
36
45
  const colors = useThemeColors();
37
46
  const ui = useUiHost();
38
47
  const HostSegmentedControl = ui.SegmentedControl as ComponentType<SegmentedControlProps> | undefined;
39
48
  useRemoteUiNode({
40
49
  role: "select",
41
- label: "Segmented control",
50
+ label: accessibleLabel ?? "Segmented control",
42
51
  actions: {
43
52
  select: (input) => {
44
53
  const next = typeof input === "string" ? input : (input as { value?: string } | null)?.value;
@@ -78,12 +87,15 @@ export function SegmentedControl({
78
87
  if (HostSegmentedControl) {
79
88
  return (
80
89
  <HostSegmentedControl
90
+ accessibleLabel={accessibleLabel}
81
91
  options={options}
82
92
  value={value}
83
93
  onChange={onChange}
84
94
  focused={focused}
85
95
  width={width}
86
96
  wrap={wrap}
97
+ size={size}
98
+ variant={variant}
87
99
  />
88
100
  );
89
101
  }
@@ -100,7 +112,9 @@ export function SegmentedControl({
100
112
  return (
101
113
  <Box
102
114
  key={option.value}
103
- backgroundColor={active ? (focused ? colors.borderFocused : colors.selected) : colors.panel}
115
+ flexGrow={size === "large" ? 1 : undefined}
116
+ justifyContent="center"
117
+ backgroundColor={option.disabled ? colors.panel : active && option.tone ? colors[option.tone] : active ? (focused ? colors.borderFocused : colors.selected) : colors.panel}
104
118
  onMouseDown={() => {
105
119
  if (!option.disabled) onChange?.(option.value);
106
120
  }}
@@ -109,7 +123,7 @@ export function SegmentedControl({
109
123
  <Text
110
124
  // The focused fill is the primary button's, so its text is too:
111
125
  // selected text on it is unreadable in the white theme.
112
- fg={option.disabled ? colors.textMuted : active ? (focused ? colors.bg : colors.selectedText) : colors.textDim}
126
+ fg={option.disabled ? colors.textMuted : active && option.tone ? colors.bg : option.tone ? colors[option.tone] : active ? (focused ? colors.bg : colors.selectedText) : colors.textDim}
113
127
  attributes={active ? TextAttributes.BOLD : 0}
114
128
  >
115
129
  {` ${option.label} `}
@@ -3,7 +3,7 @@ import { findPaneInstance, type PaneBinding } from "../../../types/config";
3
3
  import type { BrokerContractRef, TickerListingRef } from "../../../types/instrument";
4
4
  import type { TickerRecord } from "../../../types/ticker";
5
5
  import { instrumentFromTicker, type InstrumentRef } from "../../../market-data/request-types";
6
- import { scopedBrokerContractIdentityKey } from "../../../utils/instrument-identity";
6
+ import { heldOnListing, sameBrokerContract } from "../../../utils/instrument-identity";
7
7
  import { isBrokerPortfolioId } from "../../../utils/broker-instances";
8
8
  import { searchContractKey } from "../../../tickers/search/identity";
9
9
  import { canonicalExchange } from "../../../utils/exchanges";
@@ -77,10 +77,15 @@ function contractListingExchange(ticker: TickerRecord, instrument: BrokerContrac
77
77
  export function tickerForInstrument(ticker: TickerRecord | null, instrument: BrokerContractRef | null | undefined, listing?: TickerListingRef): TickerRecord | null {
78
78
  if (!ticker || instrument === undefined) return null;
79
79
  if (!instrument && !listing && !ticker.metadata.broker_contracts?.length) return ticker;
80
- const key = instrument ? scopedBrokerContractIdentityKey(instrument) : null;
80
+ // A position is this contract's however the broker routed it: SPY held through SMART is the ARCX listing's.
81
81
  const positions = ticker.metadata.positions.filter((position) => {
82
82
  const selected = instrumentFromTicker({ ...ticker, metadata: { ...ticker.metadata, positions: [position] } }, null, { portfolioId: position.portfolio });
83
- return selected && (selected.instrument ? scopedBrokerContractIdentityKey(selected.instrument) : null) === key;
83
+ const held = selected?.instrument;
84
+ if (!selected) return false;
85
+ if (!held) return !instrument;
86
+ // A pane opened on a listing from search has no broker contract; a share held there is still its.
87
+ if (!instrument) return !!listing && heldOnListing(held, listing);
88
+ return held.brokerId === instrument.brokerId && (held.brokerInstanceId ?? "") === (instrument.brokerInstanceId ?? "") && sameBrokerContract(held, instrument);
84
89
  });
85
90
  return { ...ticker, metadata: {
86
91
  ...ticker.metadata,
@@ -24,6 +24,7 @@ import { migrateSavedConfig, type ConfigMigrationHost } from "./migrations";
24
24
  import { encodeBuiltinDisabledPluginIds, expandBuiltinPluginGroups } from "../../../plugins/ownership";
25
25
  import { sanitizeSavedPaneState } from "./pane-state";
26
26
  import { isRecord } from "../../../utils/guards";
27
+ import { normalizeStoredBaseCurrency } from "../../../utils/base-currency";
27
28
  import { debugLog } from "../../../utils/debug-log";
28
29
  import { DEFAULT_THEME, themes } from "../../../theme/themes";
29
30
  import { sanitizeStarPrompt } from "../../../app/star-prompt/model";
@@ -80,7 +81,9 @@ export function normalizeLoadedConfig(
80
81
  const config: AppConfig = {
81
82
  dataDir,
82
83
  configVersion: CURRENT_CONFIG_VERSION,
83
- baseCurrency: typeof candidate.baseCurrency === "string" ? candidate.baseCurrency : defaults.baseCurrency,
84
+ // A name or sign an older version saved (Canadian dollar, CA$) reads as its code; nothing turns into USD unasked.
85
+ baseCurrency: typeof candidate.baseCurrency === "string" && candidate.baseCurrency.trim()
86
+ ? normalizeStoredBaseCurrency(candidate.baseCurrency) : defaults.baseCurrency,
84
87
  refreshIntervalMinutes: typeof candidate.refreshIntervalMinutes === "number" ? candidate.refreshIntervalMinutes : defaults.refreshIntervalMinutes,
85
88
  portfolios: sanitizePortfolios(candidate.portfolios, defaults.portfolios),
86
89
  watchlists: sanitizeWatchlists(candidate.watchlists, defaults.watchlists),
@@ -44,6 +44,24 @@ export function createPluginCache<T, Stored = T>(options: {
44
44
  source: options.source,
45
45
  };
46
46
  };
47
+ /**
48
+ * Listeners by key, kept apart from the queries so they outlive a reset:
49
+ * a pane that subscribed before the plugin attached its store still hears
50
+ * the loads made after.
51
+ */
52
+ const listeners = new Map<string, Set<() => void>>();
53
+ const emit = (key: string) => {
54
+ for (const listener of [...listeners.get(key) ?? []]) listener();
55
+ };
56
+ const queryFor = (key: string): CachedQuery<T> => {
57
+ let query = queries.get(key);
58
+ if (!query) {
59
+ query = new CachedQuery({ read: (allowExpired) => read(key, allowExpired) });
60
+ query.subscribe(() => emit(key));
61
+ queries.set(key, query);
62
+ }
63
+ return query;
64
+ };
47
65
  const reset = () => {
48
66
  generation += 1;
49
67
  persistence = null;
@@ -53,21 +71,29 @@ export function createPluginCache<T, Stored = T>(options: {
53
71
  };
54
72
  return {
55
73
  attach(next: PluginPersistence) {
56
- if (persistence !== next) reset();
74
+ if (persistence === next) return;
75
+ reset();
57
76
  persistence = next;
77
+ // What a listener reads now comes from another store.
78
+ for (const key of [...listeners.keys()]) emit(key);
58
79
  },
59
80
  reset,
81
+ subscribe(key: string, listener: () => void): () => void {
82
+ const keyed = listeners.get(key) ?? new Set<() => void>();
83
+ listeners.set(key, keyed);
84
+ keyed.add(listener);
85
+ return () => {
86
+ keyed.delete(listener);
87
+ if (keyed.size === 0 && listeners.get(key) === keyed) listeners.delete(key);
88
+ };
89
+ },
60
90
  get(key: string, { allowExpired = false } = {}): PluginCacheResult<T> | null {
61
91
  const cached = read(key, allowExpired) ?? queries.get(key)?.getSnapshot().result;
62
92
  if (!cached || (!allowExpired && cached.expiresAt <= Date.now())) return null;
63
93
  return { data: cached.value, fetchedAt: cached.fetchedAt, stale: cached.staleAt <= Date.now(), source: "cache" };
64
94
  },
65
95
  async load(key: string, loader: () => Promise<T>, { force = false, replace = false } = {}): Promise<PluginCacheResult<T>> {
66
- let query = queries.get(key);
67
- if (!query) {
68
- query = new CachedQuery({ read: (allowExpired) => read(key, allowExpired) });
69
- queries.set(key, query);
70
- }
96
+ const query = queryFor(key);
71
97
  const owner = generation;
72
98
  const before = query.getSnapshot().result;
73
99
  const result = await query.load({ force, replace, fetch: async () => {