gloomberb 0.10.0 → 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 (353) 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/native-stubs/chart/surface-manager.ts +11 -0
  274. package/src/renderers/electrobun/view/native-stubs/native-stubs.test.ts +47 -0
  275. package/src/renderers/electrobun/view/remote/asset-data-client.ts +6 -44
  276. package/src/renderers/electrobun/view/remote/persistence.ts +6 -2
  277. package/src/renderers/electrobun/view/remote/quote-subscription-registry.test.ts +86 -0
  278. package/src/renderers/electrobun/view/remote/quote-subscription-registry.ts +72 -0
  279. package/src/renderers/electrobun/view/remote/ticker-repository.ts +2 -2
  280. package/src/renderers/electrobun/view/toast-host.tsx +0 -1
  281. package/src/renderers/electrobun/view/ui-host.tsx +1 -2
  282. package/src/renderers/electrobun/view/update-host.ts +1 -1
  283. package/src/renderers/opentui/chart-surface.tsx +2 -1
  284. package/src/renderers/opentui/dialog-host.tsx +20 -4
  285. package/src/renderers/opentui/image/loader.ts +12 -2
  286. package/src/renderers/opentui/input-host.tsx +7 -49
  287. package/src/renderers/opentui/start.tsx +2 -0
  288. package/src/sources/gloomberb-cloud/history-quality.ts +44 -0
  289. package/src/sources/gloomberb-cloud/index.test.ts +173 -0
  290. package/src/sources/gloomberb-cloud/index.ts +42 -20
  291. package/src/sources/gloomberb-cloud/normalizers.ts +9 -3
  292. package/src/sources/provider-router/cache.ts +1 -1
  293. package/src/sources/provider-router/document-routes.ts +10 -5
  294. package/src/sources/provider-router/financials.test.ts +14 -0
  295. package/src/sources/provider-router/financials.ts +6 -1
  296. package/src/sources/provider-router/history.test.ts +163 -1
  297. package/src/sources/provider-router/history.ts +278 -347
  298. package/src/sources/provider-router/index.ts +2 -2
  299. package/src/sources/provider-router/routing.test.ts +39 -0
  300. package/src/sources/provider-router/routing.ts +24 -11
  301. package/src/sources/provider-router/supplemental.ts +37 -17
  302. package/src/sources/sec-edgar/content.ts +2 -12
  303. package/src/sources/sec-edgar.ts +2 -13
  304. package/src/sources/yahoo-finance/history.ts +2 -2
  305. package/src/sources/yahoo-finance/options.ts +5 -0
  306. package/src/sources/yahoo-finance.ts +2 -2
  307. package/src/state/app/bootstrap.ts +2 -2
  308. package/src/state/app/context/index.tsx +2 -2
  309. package/src/state/hooks/quote-streaming.ts +66 -1
  310. package/src/state/hooks/session-snapshot.ts +2 -2
  311. package/src/state/selectors.ts +0 -9
  312. package/src/sync/controller.test.ts +98 -0
  313. package/src/sync/controller.ts +29 -5
  314. package/src/sync/react.ts +2 -2
  315. package/src/sync/types.ts +2 -2
  316. package/src/theme/colors.test.ts +10 -0
  317. package/src/theme/colors.ts +75 -78
  318. package/src/theme/theme-context.test.tsx +67 -0
  319. package/src/theme/theme-context.tsx +9 -5
  320. package/src/tickers/open-target.ts +2 -2
  321. package/src/tickers/search/upsert.ts +2 -2
  322. package/src/time-series/alignment.ts +0 -7
  323. package/src/time-series/chart-data.ts +145 -0
  324. package/src/time-series/date-window.ts +44 -0
  325. package/src/time-series/history-window.ts +21 -0
  326. package/src/time-series/hooks.test.ts +59 -0
  327. package/src/time-series/hooks.ts +33 -12
  328. package/src/time-series/market-resolution.ts +2 -2
  329. package/src/time-series/market.ts +1 -1
  330. package/src/time-series/range.ts +9 -0
  331. package/src/{components/chart/core → time-series}/resolution.ts +3 -102
  332. package/src/time-series/resolve.test.ts +203 -1
  333. package/src/time-series/resolve.ts +96 -18
  334. package/src/time-series/spec-studies.test.ts +15 -0
  335. package/src/time-series/spec.ts +2 -2
  336. package/src/time-series/studies.ts +5 -2
  337. package/src/time-series/types.ts +3 -1
  338. package/src/time-series/use-chart-resolution.test.tsx +358 -0
  339. package/src/time-series/use-chart-resolution.ts +46 -4
  340. package/src/types/broker.ts +2 -2
  341. package/src/types/data-provider.ts +3 -3
  342. package/src/types/financials.ts +12 -0
  343. package/src/types/plugin.ts +2 -2
  344. package/src/ui/context-menu.tsx +3 -2
  345. package/src/ui/dialog.tsx +2 -2
  346. package/src/utils/html-entities.ts +3 -5
  347. package/src/utils/main-thread-monitor.ts +1 -1
  348. package/src/utils/price-history.ts +5 -5
  349. package/src/version.ts +3 -1
  350. package/src/components/chart/core/viewport.test.ts +0 -8
  351. package/src/components/chart/core/viewport.ts +0 -24
  352. package/src/time-series/index.ts +0 -14
  353. /package/src/{components/chart/core → time-series}/resolution.test.ts +0 -0
@@ -11,6 +11,7 @@ import {
11
11
  } from "./normalizers";
12
12
  import { debugLog } from "../utils/debug-log";
13
13
  import { canonicalExchange, normalizeSymbol } from "../utils/exchanges";
14
+ import { mergeQuoteSubscriptionTargets } from "../market-data/quote-subscription-target";
14
15
 
15
16
  const QUOTE_SUBSCRIPTION_FLUSH_MS = 25;
16
17
  const cloudApiLog = debugLog.createLogger("cloud-api");
@@ -19,6 +20,18 @@ type ChannelListener = (message: ChatMessage) => void;
19
20
  type ChatNotificationListener = (notification: ChatNotification) => void;
20
21
  type ChatPresenceListener = (onlineCount: number) => void;
21
22
  type QuoteListener = (target: QuoteStreamTarget, quote: CloudQuotePayload) => void;
23
+ type QuoteSubscription = {
24
+ target: QuoteStreamTarget;
25
+ listener: QuoteListener;
26
+ };
27
+
28
+ function mergeQuoteStreamSubscriptions(
29
+ subscriptions: Iterable<QuoteSubscription>,
30
+ ): QuoteStreamTarget | null {
31
+ return mergeQuoteSubscriptionTargets(
32
+ [...subscriptions].map((subscription) => subscription.target),
33
+ );
34
+ }
22
35
 
23
36
  type CloudApiSocketDelegate = {
24
37
  getBaseUrl: () => string;
@@ -49,7 +62,8 @@ export class CloudApiSocket {
49
62
  private readonly channelListeners = new Map<string, Set<ChannelListener>>();
50
63
  private readonly chatNotificationListeners = new Set<ChatNotificationListener>();
51
64
  private readonly chatPresenceListeners = new Set<ChatPresenceListener>();
52
- private readonly quoteListeners = new Map<string, Set<QuoteListener>>();
65
+ private nextQuoteSubscriptionId = 1;
66
+ private readonly quoteSubscriptions = new Map<string, Map<number, QuoteSubscription>>();
53
67
  private readonly quoteTargets = new Map<string, QuoteStreamTarget>();
54
68
  private readonly pendingQuoteSubscribes = new Map<string, QuoteStreamTarget>();
55
69
  private readonly pendingQuoteUnsubscribes = new Map<string, QuoteStreamTarget>();
@@ -57,11 +71,14 @@ export class CloudApiSocket {
57
71
 
58
72
  constructor(private readonly delegate: CloudApiSocketDelegate) {}
59
73
 
60
- syncAuthState(): void {
74
+ syncAuthState(options: { reconnect?: boolean } = {}): void {
61
75
  if (!this.shouldKeepSocketOpen()) {
62
76
  this.teardown();
63
77
  return;
64
78
  }
79
+ if (options.reconnect) {
80
+ this.teardown();
81
+ }
65
82
  this.ensureSocket();
66
83
  }
67
84
 
@@ -150,6 +167,7 @@ export class CloudApiSocket {
150
167
  targets: QuoteStreamTarget[],
151
168
  onQuote: (target: QuoteStreamTarget, quote: CloudQuotePayload) => void,
152
169
  ): () => void {
170
+ const subscriptionId = this.nextQuoteSubscriptionId++;
153
171
  const uniqueTargets = [...new Map(
154
172
  targets
155
173
  .filter((target) => typeof target.symbol === "string" && target.symbol.trim().length > 0)
@@ -170,18 +188,17 @@ export class CloudApiSocket {
170
188
  const updatedSubscriptions: QuoteStreamTarget[] = [];
171
189
  for (const target of uniqueTargets) {
172
190
  const key = marketKey(target.symbol, target.exchange);
173
- const listeners = this.quoteListeners.get(key) ?? new Set<QuoteListener>();
174
- if (listeners.size === 0) {
191
+ const subscriptions = this.quoteSubscriptions.get(key) ?? new Map<number, QuoteSubscription>();
192
+ const previousTarget = this.quoteTargets.get(key);
193
+ subscriptions.set(subscriptionId, { target, listener: onQuote });
194
+ this.quoteSubscriptions.set(key, subscriptions);
195
+ const mergedTarget = mergeQuoteStreamSubscriptions(subscriptions.values()) ?? target;
196
+ if (!previousTarget) {
175
197
  newSubscriptions.push(target);
176
- } else {
177
- const existing = this.quoteTargets.get(key);
178
- if (JSON.stringify(this.serializeQuoteStreamTarget(existing ?? target)) !== JSON.stringify(this.serializeQuoteStreamTarget(target))) {
179
- updatedSubscriptions.push(target);
180
- }
198
+ } else if (!this.areQuoteStreamTargetsEquivalent(previousTarget, mergedTarget)) {
199
+ updatedSubscriptions.push(mergedTarget);
181
200
  }
182
- listeners.add(onQuote);
183
- this.quoteListeners.set(key, listeners);
184
- this.quoteTargets.set(key, target);
201
+ this.quoteTargets.set(key, mergedTarget);
185
202
  }
186
203
 
187
204
  this.ensureSocket();
@@ -196,20 +213,29 @@ export class CloudApiSocket {
196
213
 
197
214
  return () => {
198
215
  const removedTargets: QuoteStreamTarget[] = [];
216
+ const updatedTargets: QuoteStreamTarget[] = [];
199
217
 
200
218
  for (const target of uniqueTargets) {
201
219
  const key = marketKey(target.symbol, target.exchange);
202
- const listeners = this.quoteListeners.get(key);
203
- if (!listeners) continue;
204
- listeners.delete(onQuote);
205
- if (listeners.size === 0) {
206
- this.quoteListeners.delete(key);
207
- const storedTarget = this.quoteTargets.get(key);
208
- if (storedTarget) {
209
- removedTargets.push(storedTarget);
210
- }
220
+ const subscriptions = this.quoteSubscriptions.get(key);
221
+ if (!subscriptions || !subscriptions.delete(subscriptionId)) continue;
222
+ const previousTarget = this.quoteTargets.get(key);
223
+ if (subscriptions.size === 0) {
224
+ this.quoteSubscriptions.delete(key);
225
+ if (previousTarget) removedTargets.push(previousTarget);
211
226
  this.quoteTargets.delete(key);
227
+ continue;
212
228
  }
229
+ const mergedTarget = mergeQuoteStreamSubscriptions(subscriptions.values());
230
+ if (!mergedTarget) continue;
231
+ this.quoteTargets.set(key, mergedTarget);
232
+ if (previousTarget && !this.areQuoteStreamTargetsEquivalent(previousTarget, mergedTarget)) {
233
+ updatedTargets.push(mergedTarget);
234
+ }
235
+ }
236
+
237
+ if (updatedTargets.length > 0) {
238
+ this.queueQuoteSubscribes(updatedTargets);
213
239
  }
214
240
 
215
241
  if (removedTargets.length > 0) {
@@ -234,7 +260,7 @@ export class CloudApiSocket {
234
260
  this.channelListeners.clear();
235
261
  this.chatNotificationListeners.clear();
236
262
  this.chatPresenceListeners.clear();
237
- this.quoteListeners.clear();
263
+ this.quoteSubscriptions.clear();
238
264
  this.quoteTargets.clear();
239
265
  this.pendingQuoteSubscribes.clear();
240
266
  this.pendingQuoteUnsubscribes.clear();
@@ -302,12 +328,15 @@ export class CloudApiSocket {
302
328
 
303
329
  if (parsed?.type === "market.quote" && parsed.quote && typeof parsed.symbol === "string") {
304
330
  const key = marketKey(parsed.symbol, parsed.exchange);
305
- const target = this.quoteTargets.get(key) ?? {
306
- symbol: normalizeSymbol(parsed.symbol),
307
- exchange: canonicalExchange(parsed.exchange),
331
+ const quote: CloudQuotePayload = {
332
+ ...(parsed.quote as CloudQuotePayload),
333
+ ...(parsed.delivery === "stream" || parsed.delivery === "poll"
334
+ ? { delivery: parsed.delivery }
335
+ : {}),
336
+ ...(typeof parsed.stale === "boolean" ? { stale: parsed.stale } : {}),
308
337
  };
309
- for (const listener of this.quoteListeners.get(key) ?? []) {
310
- listener(target, parsed.quote as CloudQuotePayload);
338
+ for (const subscription of this.quoteSubscriptions.get(key)?.values() ?? []) {
339
+ subscription.listener(subscription.target, quote);
311
340
  }
312
341
  }
313
342
  }
@@ -366,7 +395,8 @@ export class CloudApiSocket {
366
395
  reason: closeEvent?.reason,
367
396
  tokenSource: usingWebSocketToken ? "websocket" : "session",
368
397
  });
369
- if (activeSocket && usingWebSocketToken && this.delegate.clearWebSocketTokenForFallback()) {
398
+ if (!activeSocket) return;
399
+ if (usingWebSocketToken && this.delegate.clearWebSocketTokenForFallback()) {
370
400
  this.reconnectDelayMs = 1000;
371
401
  cloudApiLog.warn("cleared websocket token after socket close; falling back to session token");
372
402
  }
@@ -440,7 +470,7 @@ export class CloudApiSocket {
440
470
  private queueQuoteUnsubscribes(targets: QuoteStreamTarget[]): void {
441
471
  for (const target of targets) {
442
472
  const key = marketKey(target.symbol, target.exchange);
443
- if (this.pendingQuoteSubscribes.delete(key)) continue;
473
+ this.pendingQuoteSubscribes.delete(key);
444
474
  this.pendingQuoteUnsubscribes.set(key, target);
445
475
  }
446
476
  this.scheduleQuoteSubscriptionFlush();
@@ -456,18 +486,18 @@ export class CloudApiSocket {
456
486
  const unsubscribes = [...this.pendingQuoteUnsubscribes.values()];
457
487
  this.pendingQuoteSubscribes.clear();
458
488
  this.pendingQuoteUnsubscribes.clear();
459
- if (subscribes.length > 0) {
460
- this.sendSocketMessage({
461
- type: "market.subscribe",
462
- symbols: subscribes.map((target) => this.serializeQuoteStreamTarget(target)),
463
- });
464
- }
465
489
  if (unsubscribes.length > 0) {
466
490
  this.sendSocketMessage({
467
491
  type: "market.unsubscribe",
468
492
  symbols: unsubscribes.map((target) => this.serializeQuoteStreamTarget(target)),
469
493
  });
470
494
  }
495
+ if (subscribes.length > 0) {
496
+ this.sendSocketMessage({
497
+ type: "market.subscribe",
498
+ symbols: subscribes.map((target) => this.serializeQuoteStreamTarget(target)),
499
+ });
500
+ }
471
501
  }
472
502
 
473
503
  private serializeQuoteStreamTarget(target: QuoteStreamTarget): QuoteStreamTarget {
@@ -480,4 +510,9 @@ export class CloudApiSocket {
480
510
  ...(Number.isFinite(target.weight) ? { weight: target.weight } : {}),
481
511
  };
482
512
  }
513
+
514
+ private areQuoteStreamTargetsEquivalent(left: QuoteStreamTarget, right: QuoteStreamTarget): boolean {
515
+ return JSON.stringify(this.serializeQuoteStreamTarget(left))
516
+ === JSON.stringify(this.serializeQuoteStreamTarget(right));
517
+ }
483
518
  }
@@ -530,7 +530,7 @@ export interface CloudVerificationResponse {
530
530
  export interface QuoteStreamTarget {
531
531
  symbol: string;
532
532
  exchange?: string;
533
- surface?: "portfolio" | "watchlist" | "detail" | "monitor" | "inline" | "screener" | "unknown";
533
+ surface?: "portfolio" | "watchlist" | "detail" | "monitor" | "inline" | "options" | "screener" | "unknown";
534
534
  visible?: boolean;
535
535
  selected?: boolean;
536
536
  weight?: number;
@@ -0,0 +1,82 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { act, type Dispatch } from "react";
3
+ import { getLanguage, setLanguage, t } from "../i18n";
4
+ import { useAppLanguage } from "../i18n/react";
5
+ import { testRender } from "../renderers/opentui/test-utils";
6
+ import {
7
+ AppProvider,
8
+ useAppDispatch,
9
+ type AppAction,
10
+ } from "../state/app/context";
11
+ import { createDefaultConfig } from "../types/config";
12
+ import { AppLanguageConfigObserver } from "./language-observer";
13
+
14
+ let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
15
+ let dispatch: Dispatch<AppAction> | null = null;
16
+
17
+ function DispatchCapture() {
18
+ dispatch = useAppDispatch();
19
+ return null;
20
+ }
21
+
22
+ function LanguageProbe() {
23
+ useAppLanguage();
24
+ return <text>{t("Done")}</text>;
25
+ }
26
+
27
+ afterEach(async () => {
28
+ if (testSetup) {
29
+ await act(async () => testSetup?.renderer.destroy());
30
+ }
31
+ testSetup = undefined;
32
+ dispatch = null;
33
+ setLanguage("en");
34
+ });
35
+
36
+ async function renderSettled() {
37
+ await act(async () => {
38
+ await testSetup?.renderOnce();
39
+ await testSetup?.renderOnce();
40
+ });
41
+ }
42
+
43
+ describe("AppLanguageConfigObserver", () => {
44
+ test("tracks language changes from config replacement and desktop hydration", async () => {
45
+ const config = { ...createDefaultConfig("/tmp/gloomberb-language-observer"), language: "en" as const };
46
+ testSetup = await testRender(
47
+ <AppProvider config={config}>
48
+ <AppLanguageConfigObserver />
49
+ <DispatchCapture />
50
+ <LanguageProbe />
51
+ </AppProvider>,
52
+ { width: 20, height: 2 },
53
+ );
54
+
55
+ await renderSettled();
56
+ expect(getLanguage()).toBe("en");
57
+ expect(testSetup.captureCharFrame()).toContain("Done");
58
+
59
+ act(() => {
60
+ dispatch?.({ type: "SET_CONFIG", config: { ...config, language: "zh-CN" } });
61
+ });
62
+ await renderSettled();
63
+ expect(getLanguage()).toBe("zh-CN");
64
+ expect(testSetup.captureCharFrame()).toContain("完成");
65
+
66
+ act(() => {
67
+ dispatch?.({
68
+ type: "HYDRATE_DESKTOP_SNAPSHOT",
69
+ snapshot: {
70
+ config: { ...config, language: "ko" },
71
+ paneState: {},
72
+ focusedPaneId: null,
73
+ activePanel: "left",
74
+ statusBarVisible: true,
75
+ },
76
+ });
77
+ });
78
+ await renderSettled();
79
+ expect(getLanguage()).toBe("ko");
80
+ expect(testSetup.captureCharFrame()).toContain("완료");
81
+ });
82
+ });
@@ -0,0 +1,13 @@
1
+ import { useEffect } from "react";
2
+ import { applyLanguageFromConfig } from "../i18n";
3
+ import { useAppSelector } from "../state/app/context";
4
+
5
+ export function AppLanguageConfigObserver() {
6
+ const language = useAppSelector((state) => state.config.language);
7
+
8
+ useEffect(() => {
9
+ applyLanguageFromConfig({ language });
10
+ }, [language]);
11
+
12
+ return null;
13
+ }
@@ -1,5 +1,5 @@
1
1
  import { useCallback, type Dispatch } from "react";
2
- import type { TickerRepository } from "../../data/ticker-repository";
2
+ import type { AppTickerRepositoryPort } from "../../core/app-service-ports";
3
3
  import {
4
4
  addPaneFloating,
5
5
  addPaneToLayout,
@@ -54,7 +54,7 @@ interface AppPaneRuntimeArgs {
54
54
  pluginRegistry: PluginRegistry;
55
55
  state: AppState;
56
56
  stateRef: { current: AppState };
57
- tickerRepository: TickerRepository;
57
+ tickerRepository: AppTickerRepositoryPort;
58
58
  }
59
59
 
60
60
  export function useAppPaneRuntime({
@@ -3,7 +3,7 @@ import {
3
3
  applyPaneSettingFieldValue as applyPaneSettingFieldValueShared,
4
4
  } from "../../components/command-bar/workflow/ops";
5
5
  import { PaneSettingsDialogContent } from "../../components/pane-settings-dialog";
6
- import type { TickerRepository } from "../../data/ticker-repository";
6
+ import type { AppTickerRepositoryPort } from "../../core/app-service-ports";
7
7
  import type { PluginRegistry } from "../../plugins/registry";
8
8
  import type { AppAction, AppState } from "../../state/app/context";
9
9
  import type { LayoutConfig } from "../../types/config";
@@ -19,7 +19,7 @@ interface UseAppPaneSettingsRuntimeOptions {
19
19
  pluginRegistry: PluginRegistry;
20
20
  resolvePaneTarget: (paneId: string, layout?: LayoutConfig) => string | null;
21
21
  stateRef: { current: AppState };
22
- tickerRepository: TickerRepository;
22
+ tickerRepository: AppTickerRepositoryPort;
23
23
  }
24
24
 
25
25
  export function useAppPaneSettingsRuntime({
@@ -1,7 +1,7 @@
1
1
  import { useCallback, type Dispatch } from "react";
2
2
  import { getPaneTemplateDisplayLabel } from "../../components/command-bar/pane-templates/items";
3
3
  import { createPaneTemplateOrThrow } from "../../components/command-bar/workflow/ops";
4
- import type { TickerRepository } from "../../data/ticker-repository";
4
+ import type { AppTickerRepositoryPort } from "../../core/app-service-ports";
5
5
  import type { PluginRegistry } from "../../plugins/registry";
6
6
  import type { AppAction, AppState } from "../../state/app/context";
7
7
  import type { PaneBinding, PaneInstanceConfig } from "../../types/config";
@@ -34,7 +34,7 @@ interface UseAppPaneTemplateRuntimeOptions {
34
34
  ) => void;
35
35
  pluginRegistry: PluginRegistry;
36
36
  stateRef: { current: AppState };
37
- tickerRepository: TickerRepository;
37
+ tickerRepository: AppTickerRepositoryPort;
38
38
  }
39
39
 
40
40
  export function useAppPaneTemplateRuntime({
@@ -3,7 +3,7 @@ import {
3
3
  applyPaneSettingFieldValue as applyPaneSettingFieldValueShared,
4
4
  createPaneTemplateOrThrow,
5
5
  } from "../../components/command-bar/workflow/ops";
6
- import type { TickerRepository } from "../../data/ticker-repository";
6
+ import type { AppTickerRepositoryPort } from "../../core/app-service-ports";
7
7
  import { setLayoutManagerDispatch } from "../../plugins/builtin/layout-manager";
8
8
  import {
9
9
  isPaneInLayout,
@@ -65,7 +65,7 @@ interface BindAppPanePluginRegistryOptions {
65
65
  state: AppState;
66
66
  stateRef: { current: AppState };
67
67
  switchTickerResearchTab: (tabId: string, preferredPaneId?: string | null) => void;
68
- tickerRepository: TickerRepository;
68
+ tickerRepository: AppTickerRepositoryPort;
69
69
  }
70
70
 
71
71
  export function bindAppPanePluginRegistry({
@@ -1,5 +1,5 @@
1
1
  import { useCallback, type Dispatch } from "react";
2
- import type { TickerRepository } from "../../data/ticker-repository";
2
+ import type { AppTickerRepositoryPort } from "../../core/app-service-ports";
3
3
  import {
4
4
  addPaneFloating,
5
5
  addPaneToLayout,
@@ -37,7 +37,7 @@ interface UseAppTickerOpenRuntimeOptions {
37
37
  persistLayout: (layout: LayoutConfig, options?: { pushHistory?: boolean }) => void;
38
38
  pluginRegistry: PluginRegistry;
39
39
  stateRef: { current: AppState };
40
- tickerRepository: TickerRepository;
40
+ tickerRepository: AppTickerRepositoryPort;
41
41
  }
42
42
 
43
43
  export function useAppTickerOpenRuntime({
@@ -1,7 +1,7 @@
1
1
  import { useCallback, type Dispatch } from "react";
2
2
  import { restoreBrokerPortfoliosFromTickerPositions, syncBrokerInstance, syncBrokerInstances } from "../../brokers/sync-broker-instance";
3
3
  import type { SyncBrokerInstanceResult } from "../../brokers/sync-broker-instance";
4
- import type { TickerRepository } from "../../data/ticker-repository";
4
+ import type { AppTickerRepositoryPort } from "../../core/app-service-ports";
5
5
  import type { PluginRegistry } from "../../plugins/registry";
6
6
  import { saveConfigImmediately } from "../../state/config-save-scheduler";
7
7
  import type { AppAction, AppState } from "../../state/app/context";
@@ -28,7 +28,7 @@ export function useBrokerImportRuntime({
28
28
  pluginRegistry: PluginRegistry;
29
29
  refreshQuote: (symbol: string, exchange?: string, tickerOverride?: TickerRecord | null, priority?: number) => void;
30
30
  stateRef: { current: AppState };
31
- tickerRepository: TickerRepository;
31
+ tickerRepository: AppTickerRepositoryPort;
32
32
  }): AppBrokerImportRuntime {
33
33
  const applyBrokerImportResult = useCallback(async (
34
34
  instanceId: string,
@@ -3,7 +3,7 @@ import {
3
3
  clearPersistedBrokerAccounts,
4
4
  getBrokerAccountCacheSourceKey,
5
5
  } from "../../brokers/account-cache";
6
- import type { TickerRepository } from "../../data/ticker-repository";
6
+ import type { AppTickerRepositoryPort } from "../../core/app-service-ports";
7
7
  import type { MarketDataCoordinator } from "../../market-data/coordinator";
8
8
  import { instrumentFromTicker } from "../../market-data/request-types";
9
9
  import type { PluginRegistry } from "../../plugins/registry";
@@ -32,7 +32,7 @@ export function bindPluginRegistryRuntimeAccess({
32
32
  marketData: MarketDataCoordinator;
33
33
  pluginRegistry: PluginRegistry;
34
34
  state: AppState;
35
- tickerRepository: TickerRepository;
35
+ tickerRepository: AppTickerRepositoryPort;
36
36
  }) {
37
37
  pluginRegistry.getTickerFn = (symbol) => state.tickers.get(symbol) ?? null;
38
38
  pluginRegistry.getDataFn = (symbol) => {
@@ -1,7 +1,7 @@
1
1
  import { useEffect, type Dispatch } from "react";
2
2
  import { loadPersistedBrokerAccountMap } from "../../brokers/account-cache";
3
3
  import type { AppSessionSnapshot } from "../../core/state/session-persistence";
4
- import type { TickerRepository } from "../../data/ticker-repository";
4
+ import type { AppTickerRepositoryPort } from "../../core/app-service-ports";
5
5
  import type { MarketDataCoordinator } from "../../market-data/coordinator";
6
6
  import { instrumentFromTicker } from "../../market-data/request-types";
7
7
  import { chatController } from "../../plugins/builtin/chat/controller";
@@ -36,7 +36,7 @@ interface UseAppStartupRuntimeOptions {
36
36
  refreshTickersBatch: InitializeAppStateArgs["refreshTickersBatch"];
37
37
  sessionSnapshot?: AppSessionSnapshot | null;
38
38
  state: AppState;
39
- tickerRepository: TickerRepository;
39
+ tickerRepository: AppTickerRepositoryPort;
40
40
  }
41
41
 
42
42
  export function useAppStartupRuntime({
package/src/app.tsx CHANGED
@@ -19,7 +19,8 @@ import { CommandBar } from "./components/command-bar/surface";
19
19
  import { OnboardingWizard } from "./components/onboarding/onboarding-wizard";
20
20
  import { useDialog } from "./ui/dialog";
21
21
  import { PluginRegistry } from "./plugins/registry";
22
- import type { TickerRepository } from "./data/ticker-repository";
22
+ import type { LoadedExternalPlugin } from "./plugins/loader";
23
+ import type { AppServicesFactory, AppTickerRepositoryPort } from "./core/app-service-ports";
23
24
  import { ThemeProvider, useThemeColors } from "./theme/theme-context";
24
25
  import type { AppConfig } from "./types/config";
25
26
  import type { DesktopDeepLinkBridge } from "./types/desktop-deeplink";
@@ -31,7 +32,6 @@ import type { LayoutBounds } from "./plugins/pane-manager";
31
32
  import type { AppSessionSnapshot } from "./core/state/session-persistence";
32
33
  import type { MarketDataCoordinator } from "./market-data/coordinator";
33
34
  import { createAppNotifier } from "./notifications/app-notifier";
34
- import { createAppServices } from "./core/app-services";
35
35
  import { getLoadablePlugins } from "./plugins/catalog";
36
36
  import { useBrokerImportRuntime } from "./app/runtime/broker-import";
37
37
  import { useDesktopDeepLinkRuntime } from "./app/runtime/desktop-deeplink";
@@ -53,10 +53,14 @@ import {
53
53
  } from "./app/app-bootstrap-state";
54
54
  import { scheduleConfigSave } from "./state/config-save-scheduler";
55
55
  import { measurePerf } from "./utils/perf-marks";
56
+ import { useAppLanguage } from "./i18n/react";
57
+ import { AppLanguageConfigObserver } from "./app/language-observer";
58
+
59
+ const EMPTY_EXTERNAL_PLUGINS: LoadedExternalPlugin[] = [];
56
60
 
57
61
  interface AppInnerProps {
58
62
  pluginRegistry: PluginRegistry;
59
- tickerRepository: TickerRepository;
63
+ tickerRepository: AppTickerRepositoryPort;
60
64
  dataProvider: DataProvider;
61
65
  marketData: MarketDataCoordinator;
62
66
  sessionSnapshot?: AppSessionSnapshot | null;
@@ -384,7 +388,8 @@ function AppInner({
384
388
 
385
389
  interface AppProps {
386
390
  config: AppConfig;
387
- externalPlugins?: import("./plugins/loader").LoadedExternalPlugin[];
391
+ servicesFactory: AppServicesFactory;
392
+ externalPlugins?: LoadedExternalPlugin[];
388
393
  cliLaunchRequest?: CliLaunchRequest | null;
389
394
  desktopWindowBridge?: DesktopWindowBridge;
390
395
  desktopApplicationMenuBridge?: DesktopApplicationMenuBridge;
@@ -396,7 +401,8 @@ interface AppProps {
396
401
 
397
402
  export function App({
398
403
  config: initialConfig,
399
- externalPlugins = [],
404
+ servicesFactory,
405
+ externalPlugins: providedExternalPlugins,
400
406
  cliLaunchRequest = null,
401
407
  desktopWindowBridge,
402
408
  desktopApplicationMenuBridge,
@@ -405,6 +411,8 @@ export function App({
405
411
  desktopThemePreview = null,
406
412
  remoteControlAdapter,
407
413
  }: AppProps) {
414
+ useAppLanguage();
415
+ const externalPlugins = providedExternalPlugins ?? EMPTY_EXTERNAL_PLUGINS;
408
416
  const renderer = useNativeRenderer();
409
417
  const effectiveInitialConfig = useMemo(() => {
410
418
  return resolveInitialAppConfig({
@@ -432,7 +440,7 @@ export function App({
432
440
 
433
441
  const services = useMemo(() => {
434
442
  return measurePerf("startup.app.create-services", () => (
435
- createAppServices({
443
+ servicesFactory({
436
444
  config,
437
445
  plugins: getLoadablePlugins(externalPlugins),
438
446
  })
@@ -441,7 +449,7 @@ export function App({
441
449
  disabledPluginCount: config.disabledPlugins.length,
442
450
  brokerInstanceCount: config.brokerInstances.length,
443
451
  });
444
- }, [config.dataDir, externalPlugins]);
452
+ }, [config.dataDir, externalPlugins, servicesFactory]);
445
453
 
446
454
  useEffect(() => {
447
455
  return () => services.destroy();
@@ -483,6 +491,7 @@ export function App({
483
491
  desktopSnapshot={desktopSnapshot}
484
492
  initialThemePreview={desktopThemePreview}
485
493
  >
494
+ <AppLanguageConfigObserver />
486
495
  <AppInner
487
496
  pluginRegistry={services.pluginRegistry}
488
497
  tickerRepository={services.tickerRepository}
@@ -1,4 +1,4 @@
1
- import type { ResourceStore } from "../data/resource-store";
1
+ import type { AppResourceStorePort } from "../core/app-service-ports";
2
2
  import type { BrokerAdapter } from "../types/broker";
3
3
  import type { BrokerInstanceConfig } from "../types/config";
4
4
  import type { CachePolicy } from "../types/persistence";
@@ -42,7 +42,7 @@ function getBrokerAccountCachePolicy(
42
42
  }
43
43
 
44
44
  function pruneMismatchedSnapshots(
45
- resources: ResourceStore,
45
+ resources: AppResourceStorePort,
46
46
  instance: BrokerInstanceConfig,
47
47
  sourceKey: string,
48
48
  ): void {
@@ -67,7 +67,7 @@ function pruneMismatchedSnapshots(
67
67
  }
68
68
 
69
69
  export function loadPersistedBrokerAccounts(
70
- resources: ResourceStore,
70
+ resources: AppResourceStorePort,
71
71
  instance: BrokerInstanceConfig,
72
72
  broker?: BrokerAdapter | null,
73
73
  ): BrokerAccount[] | null {
@@ -85,7 +85,7 @@ export function loadPersistedBrokerAccounts(
85
85
  }
86
86
 
87
87
  export function persistBrokerAccounts(
88
- resources: ResourceStore,
88
+ resources: AppResourceStorePort,
89
89
  instance: BrokerInstanceConfig,
90
90
  broker: BrokerAdapter,
91
91
  accounts: BrokerAccount[],
@@ -108,7 +108,7 @@ export function persistBrokerAccounts(
108
108
  }
109
109
 
110
110
  export function loadPersistedBrokerAccountMap(
111
- resources: ResourceStore,
111
+ resources: AppResourceStorePort,
112
112
  brokerInstances: BrokerInstanceConfig[],
113
113
  brokers: ReadonlyMap<string, BrokerAdapter>,
114
114
  ): Record<string, BrokerAccount[]> {
@@ -126,7 +126,7 @@ export function loadPersistedBrokerAccountMap(
126
126
  }
127
127
 
128
128
  export function clearPersistedBrokerAccounts(
129
- resources: ResourceStore,
129
+ resources: AppResourceStorePort,
130
130
  instance: BrokerInstanceConfig,
131
131
  ): void {
132
132
  const records = resources.list<PersistedBrokerAccountSnapshot>({
@@ -1,11 +1,11 @@
1
- import type { TickerRepository } from "../data/ticker-repository";
1
+ import type { AppTickerRepositoryPort } from "../core/app-service-ports";
2
2
  import type { BrokerPosition } from "../types/broker";
3
3
  import type { BrokerInstanceConfig } from "../types/config";
4
4
  import type { BrokerContractRef } from "../types/instrument";
5
5
  import type { TickerMetadata, TickerPosition, TickerRecord } from "../types/ticker";
6
6
 
7
7
  export async function loadTickerMap(
8
- tickerRepository: TickerRepository,
8
+ tickerRepository: AppTickerRepositoryPort,
9
9
  existingTickers?: Map<string, TickerRecord>,
10
10
  ): Promise<Map<string, TickerRecord>> {
11
11
  if (existingTickers) {
@@ -114,7 +114,7 @@ export async function upsertBrokerPositionTicker({
114
114
  portfolioId,
115
115
  position,
116
116
  }: {
117
- tickerRepository: TickerRepository;
117
+ tickerRepository: AppTickerRepositoryPort;
118
118
  tickers: Map<string, TickerRecord>;
119
119
  instance: BrokerInstanceConfig;
120
120
  portfolioId: string;
@@ -1,5 +1,5 @@
1
- import type { TimeRange } from "../components/chart/core/types";
2
- import type { ManualChartResolution } from "../components/chart/core/resolution";
1
+ import type { TimeRange } from "../time-series/range";
2
+ import type { ManualChartResolution } from "../time-series/resolution";
3
3
  import type { BrokerAdapter, BrokerConnectionStatus } from "../types/broker";
4
4
  import type { BrokerInstanceConfig } from "../types/config";
5
5
  import type { QuoteSubscriptionTarget } from "../types/data-provider";
@@ -1,5 +1,4 @@
1
- import type { ResourceStore } from "../data/resource-store";
2
- import type { TickerRepository } from "../data/ticker-repository";
1
+ import type { AppResourceStorePort, AppTickerRepositoryPort } from "../core/app-service-ports";
3
2
  import { persistBrokerAccounts } from "./account-cache";
4
3
  import type { BrokerAdapter, BrokerPosition } from "../types/broker";
5
4
  import type { AppConfig, BrokerInstanceConfig } from "../types/config";
@@ -23,9 +22,9 @@ export interface SyncBrokerInstanceArgs {
23
22
  config: AppConfig;
24
23
  instanceId: string;
25
24
  brokers: ReadonlyMap<string, BrokerAdapter>;
26
- tickerRepository: TickerRepository;
25
+ tickerRepository: AppTickerRepositoryPort;
27
26
  existingTickers?: Map<string, TickerRecord>;
28
- resources?: ResourceStore;
27
+ resources?: AppResourceStorePort;
29
28
  persistResolvedBrokerConfig?: boolean;
30
29
  }
31
30
 
@@ -42,9 +41,9 @@ export interface SyncBrokerInstanceResult {
42
41
  export interface SyncBrokerInstancesArgs {
43
42
  config: AppConfig;
44
43
  brokers: ReadonlyMap<string, BrokerAdapter>;
45
- tickerRepository: TickerRepository;
44
+ tickerRepository: AppTickerRepositoryPort;
46
45
  existingTickers: Map<string, TickerRecord>;
47
- resources?: ResourceStore;
46
+ resources?: AppResourceStorePort;
48
47
  persistResolvedBrokerConfig?: boolean;
49
48
  onResult?: (result: SyncBrokerInstanceResult, instance: BrokerInstanceConfig, previousConfig: AppConfig) => void | Promise<void>;
50
49
  }