gloomberb 0.10.1 → 0.10.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 (351) hide show
  1. package/package.json +8 -1
  2. package/src/api-client/auth.ts +1 -0
  3. package/src/api-client/index.test.ts +359 -108
  4. package/src/api-client/index.ts +24 -13
  5. package/src/api-client/socket.ts +70 -35
  6. package/src/api-client/types.ts +1 -1
  7. package/src/app/language-observer.test.tsx +82 -0
  8. package/src/app/language-observer.tsx +13 -0
  9. package/src/app/pane-runtime/index.tsx +2 -2
  10. package/src/app/pane-runtime/pane-settings-runtime.tsx +2 -2
  11. package/src/app/pane-runtime/pane-template-runtime.ts +2 -2
  12. package/src/app/pane-runtime/plugin-bindings.ts +2 -2
  13. package/src/app/pane-runtime/ticker-open-runtime.ts +2 -2
  14. package/src/app/runtime/broker-import.ts +2 -2
  15. package/src/app/runtime/plugin-bindings.ts +2 -2
  16. package/src/app/runtime/startup.ts +2 -2
  17. package/src/app.tsx +16 -7
  18. package/src/brokers/account-cache.ts +6 -6
  19. package/src/brokers/broker-ticker-sync.ts +3 -3
  20. package/src/brokers/remote-broker-adapter.ts +2 -2
  21. package/src/brokers/sync-broker-instance.ts +5 -6
  22. package/src/cli/commands/api.ts +3 -6
  23. package/src/cli/commands/automation.ts +7 -15
  24. package/src/cli/commands/market.ts +26 -61
  25. package/src/cli/commands/overview.ts +34 -45
  26. package/src/cli/commands/search.ts +25 -27
  27. package/src/cli/commands/system.ts +27 -57
  28. package/src/cli/commands/ticker.ts +62 -63
  29. package/src/cli/context.test.ts +63 -0
  30. package/src/cli/context.ts +15 -1
  31. package/src/cli/index.ts +1 -1
  32. package/src/cli/pane-functions/data.ts +2 -19
  33. package/src/cli/pane-functions/index.ts +26 -25
  34. package/src/cli/pane-functions/report.ts +1 -1
  35. package/src/cli/pane-functions/screenshot.ts +3 -3
  36. package/src/cli/scoped-context.ts +41 -0
  37. package/src/components/chart/composite/composite-chart.test.tsx +615 -18
  38. package/src/components/chart/composite/composite-chart.tsx +179 -79
  39. package/src/components/chart/composite/format.test.ts +20 -7
  40. package/src/components/chart/composite/index.ts +0 -21
  41. package/src/components/chart/composite/interactions.test.ts +90 -3
  42. package/src/components/chart/composite/interactions.ts +189 -24
  43. package/src/components/chart/composite/price-series.ts +0 -2
  44. package/src/components/chart/composite/rasterizer.ts +10 -9
  45. package/src/components/chart/composite/renderers.test.ts +108 -2
  46. package/src/components/chart/composite/scene.test.ts +36 -0
  47. package/src/components/chart/composite/scene.ts +5 -2
  48. package/src/components/chart/composite/text-renderer.ts +37 -25
  49. package/src/components/chart/composite/time-axis.test.ts +243 -0
  50. package/src/components/chart/composite/time-axis.ts +608 -0
  51. package/src/components/chart/composite/types.ts +12 -1
  52. package/src/components/chart/core/data.ts +2 -159
  53. package/src/components/chart/core/date-window.ts +6 -225
  54. package/src/components/chart/core/palette.ts +1 -12
  55. package/src/components/chart/core/pointer.ts +0 -142
  56. package/src/components/chart/core/types.ts +1 -58
  57. package/src/components/chart/cursor-motion.ts +2 -4
  58. package/src/components/chart/indicators/types.ts +0 -9
  59. package/src/components/chart/market-session.ts +0 -11
  60. package/src/components/chart/native/kitty/protocol.ts +1 -1
  61. package/src/components/chart/native/raster/layout.ts +3 -10
  62. package/src/components/chart/static/chart/axis-overlays.test.tsx +64 -0
  63. package/src/components/chart/static/chart/axis-overlays.tsx +59 -14
  64. package/src/components/chart/static/chart/surface.tsx +1 -2
  65. package/src/components/command-bar/action-runtime.ts +2 -2
  66. package/src/components/command-bar/commands/direct/index.ts +1 -1
  67. package/src/components/command-bar/panel/body.tsx +2 -2
  68. package/src/components/command-bar/panel/index.tsx +11 -9
  69. package/src/components/command-bar/routes/ticker-search/actions.ts +2 -2
  70. package/src/components/command-bar/surface/index.tsx +2 -2
  71. package/src/components/command-bar/workflow/body.tsx +3 -3
  72. package/src/components/command-bar/workflow/collection-actions.ts +2 -2
  73. package/src/components/command-bar/workflow/coordinator.ts +2 -2
  74. package/src/components/command-bar/workflow/field-row.tsx +25 -11
  75. package/src/components/command-bar/workflow/tickers.ts +2 -2
  76. package/src/components/feed-data-table/stack-view.test.tsx +28 -36
  77. package/src/components/feed-data-table/stack-view.tsx +3 -1
  78. package/src/components/layout/detached-pane-shell.tsx +6 -6
  79. package/src/components/layout/header.tsx +13 -9
  80. package/src/components/layout/pane/content.test.tsx +42 -3
  81. package/src/components/layout/pane/content.tsx +3 -1
  82. package/src/components/layout/pane/footer/index.test.tsx +45 -2
  83. package/src/components/layout/pane/footer/registration.tsx +3 -1
  84. package/src/components/layout/shell/index.tsx +1 -2
  85. package/src/components/layout/shell/native/surfaces.ts +1 -1
  86. package/src/components/layout/shell/native/window-state.ts +1 -1
  87. package/src/components/layout/status-bar.tsx +12 -8
  88. package/src/components/layout/window-controls.tsx +8 -6
  89. package/src/components/onboarding/onboarding-wizard.tsx +3 -1
  90. package/src/components/pane-settings-dialog/desktop.tsx +0 -2
  91. package/src/components/pane-settings-dialog.tsx +0 -2
  92. package/src/components/pane-template-wizard.tsx +10 -9
  93. package/src/components/price-performance.tsx +2 -113
  94. package/src/components/price-sparkline/model.ts +15 -17
  95. package/src/components/price-sparkline/view.tsx +8 -7
  96. package/src/components/speedometer-gauge/desktop.tsx +2 -1
  97. package/src/components/speedometer-gauge/index.tsx +0 -2
  98. package/src/components/speedometer-gauge/terminal.tsx +24 -5
  99. package/src/components/ticker/badge/index.tsx +1 -18
  100. package/src/components/ui/data-table/opentui/index.tsx +3 -3
  101. package/src/components/ui/frame.tsx +1 -2
  102. package/src/components/ui/page-stack-view.test.tsx +126 -0
  103. package/src/components/ui/page-stack-view.tsx +24 -12
  104. package/src/components/ui/tabs.tsx +7 -3
  105. package/src/components/ui/ui.test.tsx +23 -2
  106. package/src/components/use-external-link-footer.ts +4 -2
  107. package/src/core/app-service-ports.ts +46 -0
  108. package/src/core/app-services.test.ts +66 -0
  109. package/src/core/app-services.ts +17 -15
  110. package/src/data/config/chart-settings.ts +3 -10
  111. package/src/data/config/store/index.test.ts +99 -2
  112. package/src/data/config/store/migrations.ts +278 -0
  113. package/src/data/config/store/normalize.ts +44 -186
  114. package/src/data/resource-store.ts +73 -12
  115. package/src/data/sqlite/cache.test.ts +104 -15
  116. package/src/i18n/index.test.ts +17 -0
  117. package/src/i18n/index.ts +23 -6
  118. package/src/i18n/ja.ts +13 -0
  119. package/src/i18n/ko.ts +13 -0
  120. package/src/i18n/react.ts +6 -0
  121. package/src/i18n/zh-cn.ts +13 -0
  122. package/src/i18n/zh-tw.ts +13 -0
  123. package/src/market-data/coordinator/auxiliary.ts +7 -3
  124. package/src/market-data/coordinator/chart.ts +1 -1
  125. package/src/market-data/coordinator/index.test.ts +26 -0
  126. package/src/market-data/coordinator/index.ts +11 -5
  127. package/src/market-data/coordinator/quotes.ts +4 -40
  128. package/src/market-data/coordinator/subscriptions.test.ts +89 -1
  129. package/src/market-data/hooks.tsx +47 -1
  130. package/src/market-data/market/status.ts +6 -6
  131. package/src/market-data/performance.ts +1 -1
  132. package/src/market-data/quote-subscription-target.test.ts +37 -0
  133. package/src/market-data/quote-subscription-target.ts +60 -0
  134. package/src/market-data/quotes/freshness.ts +1 -0
  135. package/src/market-data/quotes/resolution.ts +4 -0
  136. package/src/market-data/request-types.ts +3 -3
  137. package/src/news/aggregator.test.ts +97 -1
  138. package/src/news/aggregator.ts +100 -57
  139. package/src/news/hooks.test.tsx +64 -0
  140. package/src/news/hooks.ts +18 -25
  141. package/src/plugins/builtin/account-management/footer.ts +4 -2
  142. package/src/plugins/builtin/account-management/pane.tsx +9 -4
  143. package/src/plugins/builtin/ai/agent-history.ts +11 -7
  144. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +0 -5
  145. package/src/plugins/builtin/ai/ask-ai.test.tsx +3 -4
  146. package/src/plugins/builtin/ai/index.test.ts +2 -2
  147. package/src/plugins/builtin/ai/index.tsx +10 -11
  148. package/src/plugins/builtin/ai/pane-settings.ts +1 -1
  149. package/src/plugins/builtin/ai/pi/runtime.ts +6 -5
  150. package/src/plugins/builtin/ai/providers.ts +1 -9
  151. package/src/plugins/builtin/ai/runner.ts +0 -6
  152. package/src/plugins/builtin/ai/screener/footer.ts +4 -1
  153. package/src/plugins/builtin/ai/screener/pane.test.tsx +9 -9
  154. package/src/plugins/builtin/ai/workspace/model.ts +2 -27
  155. package/src/plugins/builtin/ai/workspace/pane.tsx +2 -2
  156. package/src/plugins/builtin/analytics/metrics.ts +1 -7
  157. package/src/plugins/builtin/broker-manager/detail.tsx +3 -2
  158. package/src/plugins/builtin/broker-manager/index.tsx +4 -2
  159. package/src/plugins/builtin/chart-composer/editor/controller.ts +643 -0
  160. package/src/plugins/builtin/chart-composer/editor/desktop-view.tsx +134 -0
  161. package/src/plugins/builtin/chart-composer/editor/model.test.ts +85 -0
  162. package/src/plugins/builtin/chart-composer/editor/model.ts +292 -0
  163. package/src/plugins/builtin/chart-composer/editor/opentui-view.tsx +108 -0
  164. package/src/plugins/builtin/chart-composer/editor.tsx +65 -947
  165. package/src/plugins/builtin/chart-composer/index.tsx +0 -6
  166. package/src/plugins/builtin/chart-composer/pane.tsx +69 -36
  167. package/src/plugins/builtin/chart-composer/presets.ts +1 -19
  168. package/src/plugins/builtin/chart-composer/settings.ts +1 -1
  169. package/src/plugins/builtin/chat/content/composer-runtime.ts +1 -1
  170. package/src/plugins/builtin/chat/content/shortcuts.ts +0 -1
  171. package/src/plugins/builtin/chat/controller/fetch.ts +8 -2
  172. package/src/plugins/builtin/chat/controller/persistence.ts +2 -0
  173. package/src/plugins/builtin/chat/controller/session.test.ts +25 -0
  174. package/src/plugins/builtin/chat/controller/session.ts +2 -0
  175. package/src/plugins/builtin/chat/controller/state.ts +7 -0
  176. package/src/plugins/builtin/chat/controller.test.ts +73 -20
  177. package/src/plugins/builtin/chat/message/desktop.tsx +2 -0
  178. package/src/plugins/builtin/cloud-tweets/footer.ts +9 -28
  179. package/src/plugins/builtin/cloud-tweets/pane.tsx +0 -5
  180. package/src/plugins/builtin/cloud-tweets/table.tsx +12 -17
  181. package/src/plugins/builtin/correlation/compute.ts +4 -10
  182. package/src/plugins/builtin/econ/index.tsx +17 -19
  183. package/src/plugins/builtin/holders/types.ts +0 -1
  184. package/src/plugins/builtin/insider/index.tsx +6 -31
  185. package/src/plugins/builtin/news/index.tsx +5 -7
  186. package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +6 -8
  187. package/src/plugins/builtin/news/wire/news/footer.ts +10 -11
  188. package/src/plugins/builtin/news/wire/read-state.test.ts +7 -0
  189. package/src/plugins/builtin/news/wire/read-state.ts +18 -28
  190. package/src/plugins/builtin/news/wire/rss/parser.ts +1 -11
  191. package/src/plugins/builtin/options/footer.test.ts +115 -0
  192. package/src/plugins/builtin/options/footer.ts +110 -0
  193. package/src/plugins/builtin/options/live-quotes.test.ts +238 -0
  194. package/src/plugins/builtin/options/live-quotes.ts +147 -0
  195. package/src/plugins/builtin/options/view.test.tsx +71 -1
  196. package/src/plugins/builtin/options/view.tsx +39 -24
  197. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +103 -117
  198. package/src/plugins/builtin/portfolio-list/cli/render.ts +11 -5
  199. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +120 -135
  200. package/src/plugins/builtin/portfolio-list/header.tsx +9 -4
  201. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +6 -1
  202. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +4 -2
  203. package/src/plugins/builtin/research/corporate-actions-pane.tsx +24 -52
  204. package/src/plugins/builtin/sec/filing-content.test.ts +41 -0
  205. package/src/plugins/builtin/sec/filing-content.ts +94 -0
  206. package/src/plugins/builtin/sec/index.tsx +21 -104
  207. package/src/plugins/builtin/sectors/sector-model.ts +1 -7
  208. package/src/plugins/builtin/shared/cloud-upgrade.ts +1 -0
  209. package/src/plugins/builtin/shared/pane-footer.ts +82 -0
  210. package/src/plugins/builtin/shared/read-state.ts +64 -3
  211. package/src/plugins/builtin/substack/read-state.ts +16 -41
  212. package/src/plugins/builtin/thirteenf/pane.tsx +24 -73
  213. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +10 -10
  214. package/src/plugins/builtin/yield-curve/index.tsx +11 -9
  215. package/src/plugins/ibkr/dialogs.tsx +3 -3
  216. package/src/plugins/ibkr/gateway/fundamentals.ts +1 -1
  217. package/src/plugins/ibkr/gateway/history-requests.ts +2 -2
  218. package/src/plugins/ibkr/gateway/history.ts +2 -2
  219. package/src/plugins/ibkr/gateway/selection.ts +2 -2
  220. package/src/plugins/ibkr/gateway/service/index.ts +2 -2
  221. package/src/plugins/ibkr/gateway/service/native.ts +2 -2
  222. package/src/plugins/plugin-persistence.ts +3 -4
  223. package/src/plugins/prediction-markets/controller/keyboard.ts +1 -1
  224. package/src/plugins/prediction-markets/controller/live-updates.ts +6 -5
  225. package/src/plugins/prediction-markets/detail/book.tsx +2 -2
  226. package/src/plugins/prediction-markets/detail/outcomes.tsx +3 -1
  227. package/src/plugins/prediction-markets/detail/pane.tsx +3 -1
  228. package/src/plugins/prediction-markets/detail/trades.tsx +3 -3
  229. package/src/plugins/registry/context.ts +3 -4
  230. package/src/plugins/registry/index.ts +5 -6
  231. package/src/react/input.test.ts +126 -3
  232. package/src/react/input.ts +120 -1
  233. package/src/remote/app-host.tsx +2 -13
  234. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +44 -60
  235. package/src/renderers/electrobun/bun/desktop/capability-bridge.ts +15 -13
  236. package/src/renderers/electrobun/bun/desktop/host-requests.ts +26 -22
  237. package/src/renderers/electrobun/bun/desktop/http-fetch.ts +5 -1
  238. package/src/renderers/electrobun/bun/desktop/initialization.ts +11 -6
  239. package/src/renderers/electrobun/bun/desktop/media.ts +5 -3
  240. package/src/renderers/electrobun/bun/desktop/plugin-state.ts +21 -22
  241. package/src/renderers/electrobun/bun/desktop/window-controls.ts +3 -1
  242. package/src/renderers/electrobun/bun/desktop/workspace/requests.ts +18 -18
  243. package/src/renderers/electrobun/bun/index.ts +123 -83
  244. package/src/renderers/electrobun/shared/protocol.ts +146 -7
  245. package/src/renderers/electrobun/view/app-services.ts +5 -8
  246. package/src/renderers/electrobun/view/backend-rpc.ts +18 -8
  247. package/src/renderers/electrobun/view/broker-remote-client.ts +2 -2
  248. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +5 -22
  249. package/src/renderers/electrobun/view/config-host.ts +1 -1
  250. package/src/renderers/electrobun/view/context-menu.ts +1 -17
  251. package/src/renderers/electrobun/view/data-table/index.tsx +0 -1
  252. package/src/renderers/electrobun/view/data-table/row.tsx +0 -4
  253. package/src/renderers/electrobun/view/desktop/control-styles.ts +37 -34
  254. package/src/renderers/electrobun/view/desktop/controls.tsx +36 -33
  255. package/src/renderers/electrobun/view/desktop/list-view.tsx +15 -12
  256. package/src/renderers/electrobun/view/desktop/popover.tsx +2 -3
  257. package/src/renderers/electrobun/view/dialog-host.tsx +2 -3
  258. package/src/renderers/electrobun/view/fatal-screen.tsx +0 -1
  259. package/src/renderers/electrobun/view/host/box.tsx +0 -1
  260. package/src/renderers/electrobun/view/host/chart-surface.tsx +0 -1
  261. package/src/renderers/electrobun/view/host/input.tsx +0 -1
  262. package/src/renderers/electrobun/view/host/media-surface.tsx +0 -1
  263. package/src/renderers/electrobun/view/host/native.ts +0 -1
  264. package/src/renderers/electrobun/view/host/scroll-box.tsx +0 -1
  265. package/src/renderers/electrobun/view/host/tabs.tsx +0 -1
  266. package/src/renderers/electrobun/view/host/text.tsx +0 -1
  267. package/src/renderers/electrobun/view/http-fetch.ts +3 -11
  268. package/src/renderers/electrobun/view/input-host.tsx +10 -53
  269. package/src/renderers/electrobun/view/key-event.ts +0 -1
  270. package/src/renderers/electrobun/view/main.tsx +3 -3
  271. package/src/renderers/electrobun/view/native-renderer.ts +0 -1
  272. package/src/renderers/electrobun/view/native-stubs/backend-rpc.ts +13 -1
  273. package/src/renderers/electrobun/view/remote/asset-data-client.ts +6 -44
  274. package/src/renderers/electrobun/view/remote/persistence.ts +6 -2
  275. package/src/renderers/electrobun/view/remote/quote-subscription-registry.test.ts +86 -0
  276. package/src/renderers/electrobun/view/remote/quote-subscription-registry.ts +72 -0
  277. package/src/renderers/electrobun/view/remote/ticker-repository.ts +2 -2
  278. package/src/renderers/electrobun/view/toast-host.tsx +0 -1
  279. package/src/renderers/electrobun/view/ui-host.tsx +1 -2
  280. package/src/renderers/electrobun/view/update-host.ts +1 -1
  281. package/src/renderers/opentui/chart-surface.tsx +2 -1
  282. package/src/renderers/opentui/dialog-host.tsx +20 -4
  283. package/src/renderers/opentui/image/loader.ts +12 -2
  284. package/src/renderers/opentui/input-host.tsx +7 -49
  285. package/src/renderers/opentui/start.tsx +2 -0
  286. package/src/sources/gloomberb-cloud/history-quality.ts +44 -0
  287. package/src/sources/gloomberb-cloud/index.test.ts +173 -0
  288. package/src/sources/gloomberb-cloud/index.ts +42 -20
  289. package/src/sources/gloomberb-cloud/normalizers.ts +9 -3
  290. package/src/sources/provider-router/cache.ts +1 -1
  291. package/src/sources/provider-router/document-routes.ts +10 -5
  292. package/src/sources/provider-router/financials.test.ts +14 -0
  293. package/src/sources/provider-router/financials.ts +6 -1
  294. package/src/sources/provider-router/history.test.ts +163 -1
  295. package/src/sources/provider-router/history.ts +278 -347
  296. package/src/sources/provider-router/index.ts +2 -2
  297. package/src/sources/provider-router/routing.test.ts +39 -0
  298. package/src/sources/provider-router/routing.ts +24 -11
  299. package/src/sources/provider-router/supplemental.ts +37 -17
  300. package/src/sources/sec-edgar/content.ts +2 -12
  301. package/src/sources/sec-edgar.ts +2 -13
  302. package/src/sources/yahoo-finance/history.ts +2 -2
  303. package/src/sources/yahoo-finance/options.ts +5 -0
  304. package/src/sources/yahoo-finance.ts +2 -2
  305. package/src/state/app/bootstrap.ts +2 -2
  306. package/src/state/app/context/index.tsx +2 -2
  307. package/src/state/hooks/quote-streaming.ts +66 -1
  308. package/src/state/hooks/session-snapshot.ts +2 -2
  309. package/src/state/selectors.ts +0 -9
  310. package/src/sync/controller.test.ts +98 -0
  311. package/src/sync/controller.ts +29 -5
  312. package/src/sync/react.ts +2 -2
  313. package/src/sync/types.ts +2 -2
  314. package/src/theme/colors.test.ts +10 -0
  315. package/src/theme/colors.ts +75 -78
  316. package/src/theme/theme-context.test.tsx +67 -0
  317. package/src/theme/theme-context.tsx +9 -5
  318. package/src/tickers/open-target.ts +2 -2
  319. package/src/tickers/search/upsert.ts +2 -2
  320. package/src/time-series/alignment.ts +0 -7
  321. package/src/time-series/chart-data.ts +145 -0
  322. package/src/time-series/date-window.ts +44 -0
  323. package/src/time-series/history-window.ts +21 -0
  324. package/src/time-series/hooks.test.ts +59 -0
  325. package/src/time-series/hooks.ts +33 -12
  326. package/src/time-series/market-resolution.ts +2 -2
  327. package/src/time-series/market.ts +1 -1
  328. package/src/time-series/range.ts +9 -0
  329. package/src/{components/chart/core → time-series}/resolution.ts +3 -102
  330. package/src/time-series/resolve.test.ts +203 -1
  331. package/src/time-series/resolve.ts +96 -18
  332. package/src/time-series/spec-studies.test.ts +15 -0
  333. package/src/time-series/spec.ts +2 -2
  334. package/src/time-series/studies.ts +5 -2
  335. package/src/time-series/types.ts +3 -1
  336. package/src/time-series/use-chart-resolution.test.tsx +358 -0
  337. package/src/time-series/use-chart-resolution.ts +46 -4
  338. package/src/types/broker.ts +2 -2
  339. package/src/types/data-provider.ts +3 -3
  340. package/src/types/financials.ts +12 -0
  341. package/src/types/plugin.ts +2 -2
  342. package/src/ui/context-menu.tsx +3 -2
  343. package/src/ui/dialog.tsx +2 -2
  344. package/src/utils/html-entities.ts +3 -5
  345. package/src/utils/main-thread-monitor.ts +1 -1
  346. package/src/utils/price-history.ts +5 -5
  347. package/src/version.ts +3 -1
  348. package/src/components/chart/core/viewport.test.ts +0 -8
  349. package/src/components/chart/core/viewport.ts +0 -24
  350. package/src/time-series/index.ts +0 -14
  351. /package/src/{components/chart/core → time-series}/resolution.test.ts +0 -0
@@ -1,8 +1,8 @@
1
- import { appendLiveQuotePoint } from "../components/chart/core/data";
1
+ import { appendLiveQuotePoint } from "./chart-data";
2
2
  import {
3
3
  getTimeRangeForDateWindow,
4
4
  subtractTimeRange,
5
- } from "../components/chart/core/date-window";
5
+ } from "./date-window";
6
6
  import {
7
7
  CHART_RESOLUTION_STEP_MS,
8
8
  clampTimeRangeToMaxRange,
@@ -14,8 +14,8 @@ import {
14
14
  TIME_RANGE_ORDER,
15
15
  type ChartResolutionSupport,
16
16
  type ManualChartResolution,
17
- } from "../components/chart/core/resolution";
18
- import type { TimeRange } from "../components/chart/core/types";
17
+ } from "./resolution";
18
+ import type { TimeRange } from "./range";
19
19
  import type { DataProvider, MarketDataRequestContext } from "../types/data-provider";
20
20
  import type { Quote, TickerFinancials } from "../types/financials";
21
21
  import type { FredSeriesLoadResult, FredSeriesRequest } from "../data/fred-series";
@@ -75,8 +75,10 @@ export interface ChartResolveSources {
75
75
  }
76
76
 
77
77
  export interface ChartResolveOptions {
78
- /** Runtime interaction window used only while the authored resolution is Auto. */
78
+ /** Runtime zoom window used only to choose an adaptive Auto resolution. */
79
79
  autoViewport?: { start: Date; end: Date } | null;
80
+ /** Runtime interaction window used to load history around the visible chart. */
81
+ requestViewport?: { start: Date; end: Date } | null;
80
82
  /** Approximate number of horizontal observations the current surface can use. */
81
83
  targetPointCount?: number;
82
84
  }
@@ -122,6 +124,24 @@ function instrumentLabel(spec: Extract<ChartSeriesSpec["source"], { kind: "secur
122
124
  return publicTickerKey(spec.instrument.symbol, spec.instrument.exchange);
123
125
  }
124
126
 
127
+ function withQuoteExchange(
128
+ source: Extract<ChartSeriesSpec["source"], { kind: "security" }>,
129
+ ...quotes: Array<Quote | undefined>
130
+ ): Extract<ChartSeriesSpec["source"], { kind: "security" }> {
131
+ if (source.instrument.exchange?.trim()) return source;
132
+ const exchange = quotes
133
+ .map((quote) => quote?.listingExchangeName?.trim() || quote?.exchangeName?.trim())
134
+ .find((value): value is string => !!value);
135
+ if (!exchange) return source;
136
+ return {
137
+ ...source,
138
+ instrument: {
139
+ ...source.instrument,
140
+ exchange,
141
+ },
142
+ };
143
+ }
144
+
125
145
  function finiteDate(value: string | Date | undefined): Date | null {
126
146
  if (!value) return null;
127
147
  const date = value instanceof Date ? new Date(value) : new Date(value);
@@ -170,6 +190,18 @@ function runtimeAutoBounds(options: ChartResolveOptions): DateBounds | null {
170
190
  : null;
171
191
  }
172
192
 
193
+ function runtimeRequestBounds(options: ChartResolveOptions): DateBounds | null {
194
+ const start = options.requestViewport?.start.getTime();
195
+ const end = options.requestViewport?.end.getTime();
196
+ return typeof start === "number"
197
+ && Number.isFinite(start)
198
+ && typeof end === "number"
199
+ && Number.isFinite(end)
200
+ && start <= end
201
+ ? { start, end }
202
+ : null;
203
+ }
204
+
173
205
  function sameBounds(left: DateBounds | null, right: DateBounds | null): boolean {
174
206
  return left?.start === right?.start && left?.end === right?.end;
175
207
  }
@@ -450,6 +482,7 @@ function prepareBaseSeriesForStudies(
450
482
  series: ResolvedSeries,
451
483
  bounds: DateBounds,
452
484
  clipToBounds = false,
485
+ fallbackBaselineBounds?: DateBounds,
453
486
  ): ResolvedSeries {
454
487
  const baselineTransform = series.transform === "percent" || series.transform === "index100";
455
488
  let source = series;
@@ -458,10 +491,14 @@ function prepareBaseSeriesForStudies(
458
491
  } else if (clipToBounds) {
459
492
  source = { ...series, points: filterPoints(series.points, bounds) };
460
493
  }
494
+ const baseline = baselineTransform
495
+ ? scalarBaseline(series, bounds)
496
+ ?? (fallbackBaselineBounds ? scalarBaseline(series, fallbackBaselineBounds) : null)
497
+ : null;
461
498
  return applyResolvedSeriesTransform(
462
499
  source,
463
500
  source.transform,
464
- baselineTransform ? { baseline: scalarBaseline(series, bounds) } : undefined,
501
+ baselineTransform ? { baseline } : undefined,
465
502
  );
466
503
  }
467
504
 
@@ -499,6 +536,7 @@ function applyStudyPresentationTransforms(
499
536
  studies: readonly ChartSpec["studies"][number][],
500
537
  rawSeries: readonly ResolvedSeries[],
501
538
  visibleBounds: DateBounds,
539
+ fallbackBaselineBounds?: DateBounds,
502
540
  ): ResolvedSeries[] {
503
541
  const rawById = new Map(rawSeries.map((series) => [series.id, series] as const));
504
542
  return outputs.map((output) => {
@@ -510,6 +548,7 @@ function applyStudyPresentationTransforms(
510
548
  if (!input || input.transform === "raw") return output;
511
549
  const baseline = input.transform === "percent" || input.transform === "index100"
512
550
  ? scalarBaseline(input, visibleBounds)
551
+ ?? (fallbackBaselineBounds ? scalarBaseline(input, fallbackBaselineBounds) : null)
513
552
  : undefined;
514
553
  return applyResolvedSeriesTransform(output, input.transform, { baseline });
515
554
  });
@@ -577,10 +616,23 @@ export async function resolveChartSpecData(
577
616
  }
578
617
  return pending;
579
618
  };
619
+ const sourceWithResolvedExchange = (
620
+ source: Extract<ChartSeriesSpec["source"], { kind: "security" }>,
621
+ financials: TickerFinancials | null,
622
+ ) => {
623
+ const quoteOverride = sources.quoteOverrides?.get(chartQuoteOverrideKeyForSource(source));
624
+ return withQuoteExchange(
625
+ source,
626
+ latestQuote(financials?.quote, quoteOverride),
627
+ financials?.quote,
628
+ quoteOverride,
629
+ );
630
+ };
580
631
 
581
- const runtimeBounds = spec.viewport.resolution === "auto"
632
+ const adaptiveBounds = spec.viewport.resolution === "auto"
582
633
  ? runtimeAutoBounds(options)
583
634
  : null;
635
+ const requestBounds = runtimeRequestBounds(options) ?? adaptiveBounds;
584
636
  const activeMarketSources = [...new Map(spec.series.flatMap((entry) => (
585
637
  calculationSeriesIds.has(entry.id)
586
638
  && entry.source.kind === "security"
@@ -588,9 +640,16 @@ export async function resolveChartSpecData(
588
640
  ? [[instrumentKey(entry.source), entry.source] as const]
589
641
  : []
590
642
  ))).values()];
591
- const sharedSupport = runtimeBounds && activeMarketSources.length > 0
643
+ const resolutionSupportSources = adaptiveBounds
644
+ ? await Promise.all(activeMarketSources.map(async (source) => (
645
+ source.instrument.exchange?.trim()
646
+ ? source
647
+ : sourceWithResolvedExchange(source, await loadFinancials(source))
648
+ )))
649
+ : activeMarketSources;
650
+ const sharedSupport = adaptiveBounds && activeMarketSources.length > 0
592
651
  ? intersectChartResolutionSupport(await Promise.all(
593
- activeMarketSources.map((source) => loadResolutionSupport(source)),
652
+ resolutionSupportSources.map((source) => loadResolutionSupport(source)),
594
653
  ))
595
654
  : [];
596
655
  const initialResolution = requestResolution(
@@ -600,14 +659,14 @@ export async function resolveChartSpecData(
600
659
  options,
601
660
  sharedSupport,
602
661
  );
603
- const requestVisibleBounds = runtimeBounds ?? initialVisibleBounds;
662
+ const requestVisibleBounds = requestBounds ?? initialVisibleBounds;
604
663
  const initialCalculationBounds = calculationBounds(
605
664
  spec,
606
665
  requestVisibleBounds,
607
666
  initialResolution,
608
667
  );
609
668
  const hasExplicitWindow = explicitBounds(spec) !== null
610
- || (runtimeBounds !== null && !sameBounds(runtimeBounds, initialVisibleBounds));
669
+ || (requestBounds !== null && !sameBounds(requestBounds, initialVisibleBounds));
611
670
 
612
671
  const loadHistory = async (
613
672
  source: Extract<ChartSeriesSpec["source"], { kind: "security" }>,
@@ -676,13 +735,22 @@ export async function resolveChartSpecData(
676
735
  const marketField = isMarketFieldId(source.fieldId);
677
736
  const quoteDerivedValuation = valuationSeriesUsesLiveQuote(source.fieldId);
678
737
  const needsHistory = marketField || quoteDerivedValuation;
679
- const [financials, history] = await Promise.all([
680
- loadFinancials(source),
681
- needsHistory ? loadHistory(source, quoteDerivedValuation) : Promise.resolve(null),
682
- ]);
683
738
  const quoteOverride = marketField || quoteDerivedValuation
684
739
  ? sources.quoteOverrides?.get(chartQuoteOverrideKeyForSource(source))
685
740
  : undefined;
741
+ const financialsPromise = loadFinancials(source);
742
+ let financials: TickerFinancials | null;
743
+ let history: TickerFinancials["priceHistory"] | null;
744
+ if (needsHistory && !source.instrument.exchange?.trim()) {
745
+ financials = await financialsPromise;
746
+ const historySource = sourceWithResolvedExchange(source, financials);
747
+ history = await loadHistory(historySource, quoteDerivedValuation);
748
+ } else {
749
+ [financials, history] = await Promise.all([
750
+ financialsPromise,
751
+ needsHistory ? loadHistory(source, quoteDerivedValuation) : Promise.resolve(null),
752
+ ]);
753
+ }
686
754
  const liveBarResolution = isOhlcSeriesStyle(seriesSpec.style) ? initialResolution : undefined;
687
755
  const merged = history
688
756
  ? mergeHistory(financials, history, quoteOverride, referenceNow.getTime(), liveBarResolution)
@@ -703,6 +771,9 @@ export async function resolveChartSpecData(
703
771
  }));
704
772
 
705
773
  const rawSeries = loaded.filter((entry): entry is ResolvedSeries => !!entry);
774
+ const marketTimelineSeries = primaryMarketSeries?.visible === false
775
+ ? rawSeries.filter((entry) => entry.id === primaryMarketSeries.id)
776
+ : [];
706
777
  // Preset ranges describe a window ending at the requested reference time,
707
778
  // even when the newest filing or economic observation lags that endpoint.
708
779
  // Re-anchoring to the latest observation both mislabels the range and asks
@@ -712,7 +783,7 @@ export async function resolveChartSpecData(
712
783
  const studyBounds = initialCalculationBounds;
713
784
  const baseSeries = rawSeries
714
785
  .filter((entry) => visibleSeriesIds.has(entry.id))
715
- .map((entry) => prepareBaseSeriesForStudies(entry, bounds));
786
+ .map((entry) => prepareBaseSeriesForStudies(entry, bounds, false, requestVisibleBounds));
716
787
  const calculationSeries = rawSeries.map((entry) => rawCalculationSeries(entry, studyBounds));
717
788
  let resolved = baseSeries;
718
789
 
@@ -721,7 +792,13 @@ export async function resolveChartSpecData(
721
792
  const studyResult = resolveStudies(calculationSeries, spec.studies);
722
793
  resolved = [
723
794
  ...resolved,
724
- ...applyStudyPresentationTransforms(studyResult.series, spec.studies, rawSeries, bounds),
795
+ ...applyStudyPresentationTransforms(
796
+ studyResult.series,
797
+ spec.studies,
798
+ rawSeries,
799
+ bounds,
800
+ requestVisibleBounds,
801
+ ),
725
802
  ];
726
803
  warnings.push(...studyResult.warnings);
727
804
  errors.push(...studyResult.errors);
@@ -742,7 +819,7 @@ export async function resolveChartSpecData(
742
819
  const resolvedById = new Map(resolved.map((entry) => [entry.id, entry] as const));
743
820
  const hiddenBaseSeries = rawSeries
744
821
  .filter((entry) => !visibleSeriesIds.has(entry.id))
745
- .map((entry) => prepareBaseSeriesForStudies(entry, bounds, true));
822
+ .map((entry) => prepareBaseSeriesForStudies(entry, bounds, true, requestVisibleBounds));
746
823
  const hiddenBaseById = new Map(hiddenBaseSeries.map((entry) => [entry.id, entry] as const));
747
824
  const legendSeries = [
748
825
  ...spec.series.flatMap((seriesSpec) => {
@@ -775,6 +852,7 @@ export async function resolveChartSpecData(
775
852
  series: resolved,
776
853
  legendSeries,
777
854
  ...(spec.viewport.maxPoints === undefined ? { bufferedSeries } : {}),
855
+ ...(marketTimelineSeries.length > 0 ? { timelineSeries: marketTimelineSeries } : {}),
778
856
  loading: false,
779
857
  errors,
780
858
  warnings: [...new Set([...priorityWarnings, ...warnings])],
@@ -281,6 +281,21 @@ describe("study resolution", () => {
281
281
  expect(maxStudyWarmupPoints(specs)).toBe(33);
282
282
  });
283
283
 
284
+ test("omits empty-volume noise while preserving analytical history warnings", () => {
285
+ const input = resolved("a");
286
+ input.points = input.points.map(({ volume: _volume, ...point }) => point);
287
+
288
+ const result = resolveStudies([input], [
289
+ study("volume", "volume", ["a"]),
290
+ study("sma", "sma", ["a"], { period: 100 }),
291
+ ]);
292
+
293
+ expect(result.series.find(({ id }) => id === "volume")?.points).toEqual([]);
294
+ expect(result.warnings).toEqual([
295
+ "sma: not enough valid history to calculate sma.",
296
+ ]);
297
+ });
298
+
284
299
  test("aligns pair formulas to the latest available value even when display interpolation is off", () => {
285
300
  const point = (date: string, value: number): TimeSeriesPoint => {
286
301
  const availableAt = new Date(`${date}T00:00:00Z`);
@@ -1,5 +1,5 @@
1
- import type { ChartResolution, TimeRange } from "../components/chart/core/types";
2
- import { getChartResolutionLabel } from "../components/chart/core/resolution";
1
+ import type { ChartResolution, TimeRange } from "./range";
2
+ import { getChartResolutionLabel } from "./resolution";
3
3
  import {
4
4
  canonicalTimeSeriesFieldId,
5
5
  getTimeSeriesField,
@@ -156,7 +156,7 @@ function studyPeriod(spec: ChartStudySpec, fallback: number): number {
156
156
  return positiveInteger(spec.parameters.period, fallback);
157
157
  }
158
158
 
159
- export function studyWarmupPoints(spec: ChartStudySpec): number {
159
+ function studyWarmupPoints(spec: ChartStudySpec): number {
160
160
  if (spec.kind === "sma" || spec.kind === "ema" || spec.kind === "bollinger") {
161
161
  return studyPeriod(spec, 20) - 1;
162
162
  }
@@ -554,7 +554,10 @@ export function resolveStudies(
554
554
  }
555
555
  outputs = resolvePairStudy(spec, input, pairedInput, color);
556
556
  }
557
- if (outputs.length === 0 || outputs.every((output) => output.points.length === 0)) {
557
+ if (
558
+ spec.kind !== "volume"
559
+ && (outputs.length === 0 || outputs.every((output) => output.points.length === 0))
560
+ ) {
558
561
  warnings.push(`${spec.id}: not enough valid history to calculate ${spec.kind}.`);
559
562
  }
560
563
  resolved.push(...outputs);
@@ -1,4 +1,4 @@
1
- import type { ChartResolution, TimeRange } from "../components/chart/core/types";
1
+ import type { ChartResolution, TimeRange } from "./range";
2
2
  import type { InstrumentRef } from "../market-data/request-types";
3
3
 
4
4
  export const CHART_SPEC_VERSION = 1 as const;
@@ -154,6 +154,8 @@ export interface ChartResolutionResult {
154
154
  legendSeries?: ResolvedSeries[];
155
155
  /** Loaded observations retained outside the visible window for interactive navigation. */
156
156
  bufferedSeries?: ResolvedSeries[];
157
+ /** Hidden market data retained as a deterministic session-time anchor. */
158
+ timelineSeries?: ResolvedSeries[];
157
159
  loading: boolean;
158
160
  errors: string[];
159
161
  warnings: string[];
@@ -0,0 +1,358 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { act, useState } from "react";
3
+ import { testRender } from "../renderers/opentui/test-utils";
4
+ import { createTestDataProvider } from "../test-support/data-provider";
5
+ import type { DataProvider } from "../types/data-provider";
6
+ import type { PricePoint, TickerFinancials } from "../types/financials";
7
+ import type { ChartResolveSources } from "./resolve";
8
+ import { CHART_SPEC_VERSION, type ChartSpec } from "./types";
9
+ import {
10
+ useChartResolution,
11
+ type UseChartResolutionResult,
12
+ } from "./use-chart-resolution";
13
+
14
+ type AutoViewport = NonNullable<Parameters<typeof useChartResolution>[2]["autoViewport"]>;
15
+
16
+ let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
17
+ let setAutoViewport: ((viewport: AutoViewport | null) => void) | null = null;
18
+ let setRequestViewport: ((viewport: AutoViewport | null) => void) | null = null;
19
+ let latestResult: UseChartResolutionResult | null = null;
20
+
21
+ const EMPTY_FINANCIALS: TickerFinancials = {
22
+ annualStatements: [],
23
+ quarterlyStatements: [],
24
+ priceHistory: [],
25
+ };
26
+
27
+ const INITIAL_HISTORY: PricePoint[] = [
28
+ {
29
+ date: new Date("2025-01-02T16:00:00.000Z"),
30
+ open: 100,
31
+ high: 103,
32
+ low: 99,
33
+ close: 102,
34
+ volume: 1_000,
35
+ },
36
+ {
37
+ date: new Date("2025-01-03T16:00:00.000Z"),
38
+ open: 102,
39
+ high: 105,
40
+ low: 101,
41
+ close: 104,
42
+ volume: 1_200,
43
+ },
44
+ ];
45
+
46
+ const SPEC: ChartSpec = {
47
+ version: CHART_SPEC_VERSION,
48
+ viewport: { range: "1Y", resolution: "auto" },
49
+ panels: [{ id: "main" }],
50
+ series: [{
51
+ id: "price",
52
+ source: {
53
+ kind: "security",
54
+ instrument: { symbol: "TEST", exchange: "NASDAQ" },
55
+ fieldId: "market.ohlcv",
56
+ },
57
+ style: "candles",
58
+ transform: "raw",
59
+ axis: "left",
60
+ panelId: "main",
61
+ interpolation: "none",
62
+ }],
63
+ studies: [],
64
+ };
65
+
66
+ function deferred<T>() {
67
+ let resolve!: (value: T | PromiseLike<T>) => void;
68
+ let reject!: (reason?: unknown) => void;
69
+ const promise = new Promise<T>((resolvePromise, rejectPromise) => {
70
+ resolve = resolvePromise;
71
+ reject = rejectPromise;
72
+ });
73
+ return { promise, resolve, reject };
74
+ }
75
+
76
+ function sourcesFor(dataProvider: DataProvider): ChartResolveSources {
77
+ return {
78
+ dataProvider,
79
+ now: new Date("2025-01-10T00:00:00.000Z"),
80
+ loadFredSeries: async () => {
81
+ throw new Error("FRED is unused in this test.");
82
+ },
83
+ };
84
+ }
85
+
86
+ function ResolutionHarness({
87
+ sources,
88
+ }: {
89
+ sources: ChartResolveSources;
90
+ }) {
91
+ const [autoViewport, setAutoViewportState] = useState<AutoViewport | null>(null);
92
+ const [requestViewport, setRequestViewportState] = useState<AutoViewport | null>(null);
93
+ setAutoViewport = setAutoViewportState;
94
+ setRequestViewport = setRequestViewportState;
95
+ latestResult = useChartResolution(SPEC, sources, {
96
+ autoViewport,
97
+ requestViewport,
98
+ targetPointCount: 100,
99
+ liveRefreshIntervalMs: 0,
100
+ });
101
+ const pointCount = (latestResult.bufferedSeries ?? latestResult.series)
102
+ .reduce((total, series) => total + series.points.length, 0);
103
+ return <text>{`${latestResult.loading ? "loading" : "settled"}:${pointCount}`}</text>;
104
+ }
105
+
106
+ async function flushEffects(iterations = 1): Promise<void> {
107
+ for (let iteration = 0; iteration < iterations; iteration += 1) {
108
+ await act(async () => {
109
+ await Promise.resolve();
110
+ await testSetup!.renderOnce();
111
+ });
112
+ }
113
+ }
114
+
115
+ async function waitFor(predicate: () => boolean): Promise<void> {
116
+ for (let attempt = 0; attempt < 100; attempt += 1) {
117
+ await flushEffects();
118
+ if (predicate()) return;
119
+ await new Promise((resolve) => setTimeout(resolve, 1));
120
+ }
121
+ throw new Error("Timed out waiting for chart resolution.");
122
+ }
123
+
124
+ afterEach(async () => {
125
+ if (testSetup) {
126
+ await act(async () => {
127
+ testSetup!.renderer.destroy();
128
+ });
129
+ testSetup = undefined;
130
+ }
131
+ setAutoViewport = null;
132
+ setRequestViewport = null;
133
+ latestResult = null;
134
+ });
135
+
136
+ describe("useChartResolution", () => {
137
+ test("keeps renderable data settled through empty adaptive and live refreshes", async () => {
138
+ const adaptiveHistory = deferred<PricePoint[]>();
139
+ let detailedCalls = 0;
140
+ let quoteHandler: Parameters<NonNullable<DataProvider["subscribeQuotes"]>>[1] | null = null;
141
+ let quoteTarget: Parameters<NonNullable<DataProvider["subscribeQuotes"]>>[0][number] | null = null;
142
+ const provider = createTestDataProvider({
143
+ getTickerFinancials: async () => EMPTY_FINANCIALS,
144
+ getChartResolutionSupport: () => [
145
+ { resolution: "15m", maxRange: "1M" },
146
+ { resolution: "1d", maxRange: "ALL" },
147
+ ],
148
+ getPriceHistoryForResolution: async (_symbol, _exchange, _range, resolution) => (
149
+ resolution === "1d" ? INITIAL_HISTORY : []
150
+ ),
151
+ getDetailedPriceHistory: async () => {
152
+ detailedCalls += 1;
153
+ return adaptiveHistory.promise;
154
+ },
155
+ getPriceHistory: async () => [],
156
+ subscribeQuotes: (targets, onQuote) => {
157
+ quoteTarget = targets[0] ?? null;
158
+ quoteHandler = onQuote;
159
+ return () => {};
160
+ },
161
+ });
162
+ const sources = sourcesFor(provider);
163
+ testSetup = await testRender(<ResolutionHarness sources={sources} />, {
164
+ width: 24,
165
+ height: 1,
166
+ });
167
+
168
+ await waitFor(() => latestResult?.loading === false && latestResult.series[0]?.points.length === 2);
169
+ const lastGoodSeries = latestResult!.series;
170
+ const lastGoodBufferedSeries = latestResult!.bufferedSeries;
171
+
172
+ await act(async () => {
173
+ setAutoViewport?.({
174
+ start: new Date("2025-01-02T00:00:00.000Z"),
175
+ end: new Date("2025-01-04T00:00:00.000Z"),
176
+ });
177
+ await testSetup!.renderOnce();
178
+ });
179
+ await waitFor(() => detailedCalls === 1);
180
+
181
+ expect(latestResult?.loading).toBe(false);
182
+ expect(latestResult?.series).toBe(lastGoodSeries);
183
+ expect(latestResult?.bufferedSeries).toBe(lastGoodBufferedSeries);
184
+
185
+ adaptiveHistory.resolve([]);
186
+ await flushEffects(3);
187
+
188
+ expect(latestResult?.loading).toBe(false);
189
+ expect(latestResult?.series).toBe(lastGoodSeries);
190
+ expect(latestResult?.bufferedSeries).toBe(lastGoodBufferedSeries);
191
+ expect(latestResult?.errors).toEqual([]);
192
+
193
+ await act(async () => {
194
+ quoteHandler!(quoteTarget!, {
195
+ symbol: "TEST",
196
+ price: 105,
197
+ currency: "USD",
198
+ change: 1,
199
+ changePercent: 0.96,
200
+ lastUpdated: Date.parse("2025-01-03T20:00:00.000Z"),
201
+ });
202
+ await new Promise((resolve) => setTimeout(resolve, 10));
203
+ await testSetup!.renderOnce();
204
+ });
205
+ await flushEffects(3);
206
+
207
+ expect(latestResult?.loading).toBe(false);
208
+ expect(latestResult?.series).toBe(lastGoodSeries);
209
+ expect(latestResult?.bufferedSeries).toBe(lastGoodBufferedSeries);
210
+ expect(latestResult?.errors).toEqual([]);
211
+ });
212
+
213
+ test("keeps renderable data through an empty panned-history live refresh", async () => {
214
+ let detailedCalls = 0;
215
+ let fixedHistoryCalls = 0;
216
+ let quoteHandler: Parameters<NonNullable<DataProvider["subscribeQuotes"]>>[1] | null = null;
217
+ let quoteTarget: Parameters<NonNullable<DataProvider["subscribeQuotes"]>>[0][number] | null = null;
218
+ const provider = createTestDataProvider({
219
+ getTickerFinancials: async () => EMPTY_FINANCIALS,
220
+ getPriceHistoryForResolution: async () => {
221
+ fixedHistoryCalls += 1;
222
+ return fixedHistoryCalls === 1 ? INITIAL_HISTORY : [];
223
+ },
224
+ getDetailedPriceHistory: async () => {
225
+ detailedCalls += 1;
226
+ return [];
227
+ },
228
+ getPriceHistory: async () => [],
229
+ subscribeQuotes: (targets, onQuote) => {
230
+ quoteTarget = targets[0] ?? null;
231
+ quoteHandler = onQuote;
232
+ return () => {};
233
+ },
234
+ });
235
+ const sources = sourcesFor(provider);
236
+ testSetup = await testRender(<ResolutionHarness sources={sources} />, {
237
+ width: 24,
238
+ height: 1,
239
+ });
240
+
241
+ await waitFor(() => latestResult?.loading === false && latestResult.series[0]?.points.length === 2);
242
+ const lastGoodSeries = latestResult!.series;
243
+ const lastGoodBufferedSeries = latestResult!.bufferedSeries;
244
+
245
+ await act(async () => {
246
+ setRequestViewport?.({
247
+ start: new Date("2024-12-20T00:00:00.000Z"),
248
+ end: new Date("2024-12-27T00:00:00.000Z"),
249
+ });
250
+ await testSetup!.renderOnce();
251
+ });
252
+ await waitFor(() => detailedCalls === 1);
253
+ await flushEffects(3);
254
+
255
+ expect(latestResult?.series).toBe(lastGoodSeries);
256
+ expect(latestResult?.bufferedSeries).toBe(lastGoodBufferedSeries);
257
+
258
+ await act(async () => {
259
+ quoteHandler!(quoteTarget!, {
260
+ symbol: "TEST",
261
+ price: 105,
262
+ currency: "USD",
263
+ change: 1,
264
+ changePercent: 0.96,
265
+ lastUpdated: Date.parse("2025-01-03T20:00:00.000Z"),
266
+ });
267
+ await new Promise((resolve) => setTimeout(resolve, 10));
268
+ await testSetup!.renderOnce();
269
+ });
270
+ await flushEffects(3);
271
+
272
+ expect(latestResult?.loading).toBe(false);
273
+ expect(latestResult?.series).toBe(lastGoodSeries);
274
+ expect(latestResult?.bufferedSeries).toBe(lastGoodBufferedSeries);
275
+ expect(latestResult?.errors).toEqual([]);
276
+ });
277
+
278
+ test("keeps renderable data when an adaptive viewport request rejects", async () => {
279
+ const provider = createTestDataProvider({
280
+ getTickerFinancials: async () => EMPTY_FINANCIALS,
281
+ getPriceHistoryForResolution: async () => INITIAL_HISTORY,
282
+ });
283
+ const sources = sourcesFor(provider);
284
+ testSetup = await testRender(<ResolutionHarness sources={sources} />, {
285
+ width: 24,
286
+ height: 1,
287
+ });
288
+
289
+ await waitFor(() => latestResult?.loading === false && latestResult.series[0]?.points.length === 2);
290
+ const lastGoodSeries = latestResult!.series;
291
+ const lastGoodBufferedSeries = latestResult!.bufferedSeries;
292
+ provider.getChartResolutionSupport = () => {
293
+ throw new Error("adaptive history failed");
294
+ };
295
+
296
+ await act(async () => {
297
+ setAutoViewport?.({
298
+ start: new Date("2025-01-02T00:00:00.000Z"),
299
+ end: new Date("2025-01-04T00:00:00.000Z"),
300
+ });
301
+ await testSetup!.renderOnce();
302
+ });
303
+ await flushEffects(3);
304
+
305
+ expect(latestResult?.loading).toBe(false);
306
+ expect(latestResult?.series).toBe(lastGoodSeries);
307
+ expect(latestResult?.bufferedSeries).toBe(lastGoodBufferedSeries);
308
+ expect(latestResult?.errors).toEqual([]);
309
+ });
310
+
311
+ test("keeps initial and explicit reloads blocking", async () => {
312
+ const initialHistory = deferred<PricePoint[]>();
313
+ const reloadHistory = deferred<PricePoint[]>();
314
+ let historyCalls = 0;
315
+ const provider = createTestDataProvider({
316
+ getTickerFinancials: async () => EMPTY_FINANCIALS,
317
+ getPriceHistoryForResolution: async () => {
318
+ historyCalls += 1;
319
+ return historyCalls === 1 ? initialHistory.promise : reloadHistory.promise;
320
+ },
321
+ getPriceHistory: async () => [],
322
+ });
323
+ const sources = sourcesFor(provider);
324
+ testSetup = await testRender(<ResolutionHarness sources={sources} />, {
325
+ width: 24,
326
+ height: 1,
327
+ });
328
+
329
+ await waitFor(() => historyCalls === 1);
330
+ expect(latestResult?.loading).toBe(true);
331
+ expect(latestResult?.series).toEqual([]);
332
+
333
+ await act(async () => {
334
+ initialHistory.resolve(INITIAL_HISTORY);
335
+ await testSetup!.renderOnce();
336
+ });
337
+ await waitFor(() => latestResult?.loading === false && latestResult.series[0]?.points.length === 2);
338
+ await act(async () => {
339
+ latestResult!.reload();
340
+ await testSetup!.renderOnce();
341
+ });
342
+ await waitFor(() => historyCalls === 2);
343
+
344
+ expect(latestResult?.loading).toBe(true);
345
+
346
+ await act(async () => {
347
+ reloadHistory.resolve([]);
348
+ await testSetup!.renderOnce();
349
+ });
350
+ await waitFor(() => (
351
+ latestResult?.loading === false
352
+ && latestResult.series.every((series) => series.points.length === 0)
353
+ ));
354
+
355
+ expect(latestResult?.series.every((series) => series.points.length === 0)).toBe(true);
356
+ expect(latestResult?.errors).toEqual([]);
357
+ });
358
+ });