gloomberb 0.9.7 → 0.9.9

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 (276) hide show
  1. package/package.json +1 -1
  2. package/src/api-client/auth.ts +20 -0
  3. package/src/api-client/index.test.ts +3 -3
  4. package/src/api-client/index.ts +48 -6
  5. package/src/api-client/request.test.ts +63 -0
  6. package/src/api-client/request.ts +53 -5
  7. package/src/api-client/types.ts +45 -2
  8. package/src/app/pane-runtime/layout-placement.ts +0 -1
  9. package/src/app/runtime/desktop-deeplink.test.ts +87 -0
  10. package/src/app/runtime/desktop-deeplink.ts +496 -0
  11. package/src/app.tsx +34 -0
  12. package/src/brokers/account-cache.ts +2 -10
  13. package/src/brokers/broker-portfolio-sync.ts +0 -1
  14. package/src/brokers/broker-ticker-sync.ts +20 -16
  15. package/src/brokers/remote-broker-adapter.ts +3 -0
  16. package/src/capabilities/registry.test.ts +25 -0
  17. package/src/capabilities/registry.ts +22 -5
  18. package/src/cli/commands/automation.ts +1 -3
  19. package/src/cli/commands/command-utils.ts +0 -7
  20. package/src/cli/commands/overview.ts +2 -2
  21. package/src/cli/commands/plugins.ts +34 -17
  22. package/src/cli/index.ts +2 -2
  23. package/src/cli.test.ts +0 -157
  24. package/src/components/chart/bar-chart-renderer.test.ts +87 -12
  25. package/src/components/chart/bar-chart-renderer.ts +111 -55
  26. package/src/components/chart/comparison/renderer.ts +0 -1
  27. package/src/components/chart/comparison-stock-chart/render-output.ts +3 -8
  28. package/src/components/chart/comparison-stock-chart/view.tsx +0 -2
  29. package/src/components/chart/core/index.test.ts +0 -24
  30. package/src/components/chart/core/renderer.ts +0 -4
  31. package/src/components/chart/core/resolution.test.ts +0 -17
  32. package/src/components/chart/core/resolution.ts +0 -26
  33. package/src/components/chart/core/scroll.test.ts +0 -6
  34. package/src/components/chart/core/scroll.ts +0 -22
  35. package/src/components/chart/core/viewport.test.ts +2 -23
  36. package/src/components/chart/core/viewport.ts +0 -27
  37. package/src/components/chart/market-session.ts +1 -1
  38. package/src/components/chart/native/surface/hooks.ts +10 -3
  39. package/src/components/chart/static/bar-chart-surface.tsx +104 -20
  40. package/src/components/chart/stock-chart/data-runtime.ts +0 -1
  41. package/src/components/chart/stock-chart/footer.test.ts +65 -0
  42. package/src/components/chart/stock-chart/footer.ts +68 -13
  43. package/src/components/chart/stock-chart/resolved.tsx +18 -4
  44. package/src/components/chart/stock-chart/viewport/effects.ts +0 -3
  45. package/src/components/command-bar/helpers.test.ts +1 -80
  46. package/src/components/command-bar/helpers.ts +1 -36
  47. package/src/components/command-bar/pane-settings.ts +1 -1
  48. package/src/components/command-bar/pane-templates/items.ts +11 -3
  49. package/src/components/command-bar/panel/runtime.ts +0 -2
  50. package/src/components/command-bar/routes/root/results.ts +1 -1
  51. package/src/components/command-bar/routes/ticker-search/results.ts +0 -11
  52. package/src/components/command-bar/surface/index.test.tsx +52 -16
  53. package/src/components/command-bar/surface/index.tsx +0 -1
  54. package/src/components/command-bar/workflow/ops.ts +81 -20
  55. package/src/components/feed-data-table/stack-view.test.tsx +36 -130
  56. package/src/components/feed-data-table/stack-view.tsx +0 -1
  57. package/src/components/index.ts +1 -0
  58. package/src/components/layout/shell/index.test.tsx +2 -113
  59. package/src/components/layout/shell/index.tsx +0 -1
  60. package/src/components/layout/shell/menu.ts +0 -1
  61. package/src/components/layout/shell/terminal-pointer-runtime.ts +0 -1
  62. package/src/components/onboarding/onboarding-wizard.tsx +3 -3
  63. package/src/components/onboarding/wizard-keyboard.ts +2 -23
  64. package/src/components/price-selector-dialog.tsx +3 -3
  65. package/src/components/ticker/badge/text.tsx +0 -1
  66. package/src/components/toggle-list.tsx +29 -73
  67. package/src/components/ui/button.tsx +4 -1
  68. package/src/components/ui/checkbox.tsx +76 -0
  69. package/src/components/ui/choice-dialog.tsx +15 -3
  70. package/src/components/ui/frame.tsx +1 -1
  71. package/src/components/ui/index.ts +2 -0
  72. package/src/components/ui/message-composer.tsx +4 -0
  73. package/src/components/ui/native-select.tsx +4 -2
  74. package/src/components/ui/page-stack-view.tsx +1 -1
  75. package/src/components/ui/ui.test.tsx +0 -78
  76. package/src/core/state/app/layout-reducer.ts +0 -1
  77. package/src/data/config/layout.ts +3 -3
  78. package/src/data/resource-store.ts +6 -0
  79. package/src/data/sqlite/cache.test.ts +5 -0
  80. package/src/data/ticker-store.ts +0 -7
  81. package/src/market-data/coordinator/auxiliary.ts +1 -44
  82. package/src/market-data/coordinator/entries.ts +0 -1
  83. package/src/market-data/coordinator/events.test.ts +49 -0
  84. package/src/market-data/coordinator/events.ts +25 -4
  85. package/src/market-data/coordinator/index.test.ts +223 -10
  86. package/src/market-data/coordinator/index.ts +41 -21
  87. package/src/market-data/coordinator/quotes.ts +13 -11
  88. package/src/market-data/coordinator/subscriptions.test.ts +1 -0
  89. package/src/market-data/hooks.tsx +17 -13
  90. package/src/market-data/market/freshness.ts +58 -1
  91. package/src/market-data/market/status.ts +19 -1
  92. package/src/market-data/query-store.ts +0 -4
  93. package/src/market-data/quotes/freshness.test.ts +49 -1
  94. package/src/market-data/quotes/freshness.ts +8 -0
  95. package/src/market-data/quotes/resolution.test.ts +185 -0
  96. package/src/market-data/quotes/resolution.ts +96 -10
  97. package/src/market-data/request-types.test.ts +61 -1
  98. package/src/market-data/request-types.ts +42 -8
  99. package/src/market-data/result-types.ts +0 -11
  100. package/src/market-data/selectors.ts +1 -5
  101. package/src/news/aggregator.ts +5 -1
  102. package/src/pane-settings.test.ts +0 -18
  103. package/src/plugins/builtin/account-management/footer.ts +2 -5
  104. package/src/plugins/builtin/account-management/form-components.tsx +175 -59
  105. package/src/plugins/builtin/account-management/keyboard.ts +32 -2
  106. package/src/plugins/builtin/account-management/model.test.ts +99 -4
  107. package/src/plugins/builtin/account-management/model.ts +259 -23
  108. package/src/plugins/builtin/account-management/pane.tsx +784 -155
  109. package/src/plugins/builtin/broker-manager/index.test.tsx +0 -32
  110. package/src/plugins/builtin/buildout/cells.tsx +1 -1
  111. package/src/plugins/builtin/buildout/detail/satellite-images.tsx +1 -1
  112. package/src/plugins/builtin/buildout/detail/ui.tsx +0 -1
  113. package/src/plugins/builtin/buildout/format.ts +2 -11
  114. package/src/plugins/builtin/chat/chat.test.tsx +127 -1
  115. package/src/plugins/builtin/chat/content/channel-navigation.ts +1 -21
  116. package/src/plugins/builtin/chat/content/composer-cursor.ts +29 -0
  117. package/src/plugins/builtin/chat/content/composer-runtime.ts +22 -24
  118. package/src/plugins/builtin/chat/content/composer.tsx +84 -1
  119. package/src/plugins/builtin/chat/content/index.tsx +153 -16
  120. package/src/plugins/builtin/chat/content/layout-metrics.ts +10 -1
  121. package/src/plugins/builtin/chat/content/mentions.test.ts +74 -0
  122. package/src/plugins/builtin/chat/content/mentions.ts +88 -0
  123. package/src/plugins/builtin/chat/content/new-dm-dialog.tsx +264 -0
  124. package/src/plugins/builtin/chat/content/shortcuts.ts +47 -1
  125. package/src/plugins/builtin/chat/content/snapshot.ts +7 -0
  126. package/src/plugins/builtin/chat/content/transcript.tsx +0 -5
  127. package/src/plugins/builtin/chat/controller/index.ts +0 -10
  128. package/src/plugins/builtin/chat/controller.test.ts +5 -5
  129. package/src/plugins/builtin/chat/message/profile-popover.test.tsx +36 -0
  130. package/src/plugins/builtin/chat/message/profile-popover.tsx +124 -20
  131. package/src/plugins/builtin/chat/sidebar.test.tsx +83 -6
  132. package/src/plugins/builtin/chat/sidebar.tsx +26 -2
  133. package/src/plugins/builtin/chat/test-harness.tsx +22 -12
  134. package/src/plugins/builtin/cloud/plugin.tsx +8 -1
  135. package/src/plugins/builtin/cloud-tweets/pane.tsx +0 -1
  136. package/src/plugins/builtin/congress-trades/model.ts +2 -7
  137. package/src/plugins/builtin/congress-trades/pane.tsx +0 -1
  138. package/src/plugins/builtin/correlation/index.tsx +4 -1
  139. package/src/plugins/builtin/correlation/relationship/controls.tsx +8 -2
  140. package/src/plugins/builtin/correlation/relationship/model.ts +22 -2
  141. package/src/plugins/builtin/correlation/relationship/pane.tsx +9 -3
  142. package/src/plugins/builtin/earnings/index.tsx +0 -9
  143. package/src/plugins/builtin/econ/index.tsx +1 -5
  144. package/src/plugins/builtin/help/index.tsx +2 -2
  145. package/src/plugins/builtin/holders/pane.tsx +0 -5
  146. package/src/plugins/builtin/insider/index.tsx +1 -8
  147. package/src/plugins/builtin/kelly-sizer/index.test.tsx +0 -26
  148. package/src/plugins/builtin/kelly-sizer/index.tsx +0 -2
  149. package/src/plugins/builtin/market-movers/index.tsx +0 -2
  150. package/src/plugins/builtin/news/index.tsx +0 -1
  151. package/src/plugins/builtin/news/wire/news/table.tsx +0 -11
  152. package/src/plugins/builtin/options/view.test.tsx +1 -38
  153. package/src/plugins/builtin/plugin-groups.ts +17 -3
  154. package/src/plugins/builtin/portfolio-list/column-values.ts +15 -8
  155. package/src/plugins/builtin/portfolio-list/index.test.ts +52 -25
  156. package/src/plugins/builtin/portfolio-list/metrics.test.ts +44 -7
  157. package/src/plugins/builtin/portfolio-list/pane/data.test.ts +54 -0
  158. package/src/plugins/builtin/portfolio-list/pane/data.ts +50 -3
  159. package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +314 -16
  160. package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -4
  161. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +83 -24
  162. package/src/plugins/builtin/sec/filing-documents.ts +0 -11
  163. package/src/plugins/builtin/sec/index.tsx +0 -2
  164. package/src/plugins/builtin/substack/article-detail.tsx +1 -1
  165. package/src/plugins/builtin/thirteenf/api.ts +0 -15
  166. package/src/plugins/builtin/thirteenf/pane.tsx +1 -3
  167. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +42 -6
  168. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +415 -64
  169. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/settings.ts +23 -2
  170. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/types.ts +8 -1
  171. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +3 -1
  172. package/src/plugins/builtin/ticker-detail/data-panes.test.ts +95 -1
  173. package/src/plugins/builtin/ticker-detail/financials/tab.tsx +0 -7
  174. package/src/plugins/builtin/ticker-detail/index.tsx +2 -0
  175. package/src/plugins/builtin/ticker-detail/quote-monitor/index.test.tsx +5 -34
  176. package/src/plugins/builtin/world-indices/index.tsx +0 -4
  177. package/src/plugins/builtin/world-indices/model.ts +0 -7
  178. package/src/plugins/ibkr/account-cache.ts +1 -1
  179. package/src/plugins/ibkr/gateway/market-data.ts +12 -5
  180. package/src/plugins/ibkr/gateway/service/index.test.ts +31 -0
  181. package/src/plugins/ibkr/portfolio-performance.ts +1 -1
  182. package/src/plugins/ibkr/trade/utils.ts +1 -7
  183. package/src/plugins/loader.ts +2 -1
  184. package/src/plugins/prediction-markets/detail.test.tsx +0 -34
  185. package/src/plugins/registry/context.ts +15 -0
  186. package/src/plugins/registry/contributions.ts +22 -11
  187. package/src/plugins/registry/index.test.ts +44 -0
  188. package/src/plugins/registry/index.ts +75 -26
  189. package/src/remote/controller.ts +1 -7
  190. package/src/remote/json-patch.ts +15 -1
  191. package/src/remote/server.ts +16 -6
  192. package/src/renderers/electrobun/bun/application-menu/index.ts +1 -1
  193. package/src/renderers/electrobun/bun/core-capabilities.ts +2 -0
  194. package/src/renderers/electrobun/bun/desktop/http-fetch.test.ts +14 -0
  195. package/src/renderers/electrobun/bun/desktop/http-fetch.ts +8 -0
  196. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -1
  197. package/src/renderers/electrobun/bun/index.ts +42 -0
  198. package/src/renderers/electrobun/shared/protocol.ts +5 -0
  199. package/src/renderers/electrobun/view/backend-rpc.ts +29 -0
  200. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -0
  201. package/src/renderers/electrobun/view/data-table/dom.ts +0 -1
  202. package/src/renderers/electrobun/view/desktop/controls.tsx +135 -4
  203. package/src/renderers/electrobun/view/desktop/list-view.tsx +38 -25
  204. package/src/renderers/electrobun/view/desktop-deeplink-bridge.ts +10 -0
  205. package/src/renderers/electrobun/view/host/box.tsx +0 -2
  206. package/src/renderers/electrobun/view/host/mouse.ts +0 -11
  207. package/src/renderers/electrobun/view/host/scroll-box.tsx +1 -2
  208. package/src/renderers/electrobun/view/host/text.tsx +1 -2
  209. package/src/renderers/electrobun/view/http-fetch.ts +15 -2
  210. package/src/renderers/electrobun/view/main.tsx +3 -0
  211. package/src/renderers/electrobun/view/remote/asset-data-client.ts +9 -18
  212. package/src/renderers/electrobun/view/remote/capability-invoker.test.ts +50 -0
  213. package/src/renderers/electrobun/view/remote/capability-invoker.ts +36 -0
  214. package/src/renderers/electrobun/view/ui-host.tsx +2 -0
  215. package/src/renderers/opentui/host.tsx +11 -3
  216. package/src/renderers/opentui/input-host.tsx +5 -1
  217. package/src/sources/gloomberb-cloud/index.test.ts +0 -72
  218. package/src/sources/gloomberb-cloud/index.ts +1 -30
  219. package/src/sources/gloomberb-cloud/news.ts +0 -12
  220. package/src/sources/provider-router/batches.ts +11 -3
  221. package/src/sources/provider-router/cached-financials.test.ts +123 -0
  222. package/src/sources/provider-router/financial-routes.ts +77 -5
  223. package/src/sources/provider-router/financials.test.ts +49 -0
  224. package/src/sources/provider-router/financials.ts +40 -3
  225. package/src/sources/provider-router/index.test.ts +314 -0
  226. package/src/sources/provider-router/primary.ts +17 -3
  227. package/src/sources/provider-router/search.ts +5 -0
  228. package/src/sources/provider-router/streaming.ts +6 -1
  229. package/src/sources/provider-router/supplemental.ts +4 -3
  230. package/src/sources/sec-edgar.test.ts +42 -0
  231. package/src/sources/sec-edgar.ts +1 -2
  232. package/src/sources/yahoo-finance/history.ts +5 -1
  233. package/src/sources/yahoo-finance/mappers.test.ts +29 -0
  234. package/src/sources/yahoo-finance/mappers.ts +2 -1
  235. package/src/sources/yahoo-finance/options.ts +59 -14
  236. package/src/sources/yahoo-finance/quote-summary.ts +4 -1
  237. package/src/sources/yahoo-finance/requests.ts +2 -1
  238. package/src/sources/yahoo-finance/snapshots.ts +13 -2
  239. package/src/sources/yahoo-finance.test.ts +40 -21
  240. package/src/sources/yahoo-finance.ts +19 -10
  241. package/src/state/app/context/index.test.tsx +0 -81
  242. package/src/state/app/context/selector.test.tsx +6 -2
  243. package/src/state/hooks/quote-streaming.test.ts +14 -1
  244. package/src/state/hooks/quote-streaming.ts +5 -4
  245. package/src/sync/controller.test.ts +31 -0
  246. package/src/sync/controller.ts +300 -0
  247. package/src/sync/core-contributors.test.ts +251 -0
  248. package/src/sync/core-contributors.ts +400 -0
  249. package/src/sync/profile-analytics.ts +39 -0
  250. package/src/sync/react.ts +53 -0
  251. package/src/sync/types.ts +77 -0
  252. package/src/test-support/data-provider.ts +0 -1
  253. package/src/types/desktop-deeplink.ts +7 -0
  254. package/src/types/plugin.ts +3 -0
  255. package/src/ui/host.tsx +15 -1
  256. package/src/ui/index.tsx +2 -0
  257. package/src/updater.test.ts +0 -21
  258. package/src/updater.ts +20 -16
  259. package/src/utils/async-deadline.ts +26 -0
  260. package/src/utils/exchanges.test.ts +11 -1
  261. package/src/utils/exchanges.ts +5 -1
  262. package/src/utils/selection-clipboard.ts +2 -2
  263. package/src/version.ts +1 -1
  264. package/src/components/layout/pane/header.test.tsx +0 -39
  265. package/src/plugins/builtin/chat/channels.test.ts +0 -28
  266. package/src/plugins/builtin/chat/index.tsx +0 -4
  267. package/src/plugins/builtin/cloud-tweets/index.ts +0 -1
  268. package/src/plugins/builtin/earnings/index.test.ts +0 -12
  269. package/src/plugins/builtin/help/index.test.tsx +0 -242
  270. package/src/plugins/builtin/substack/article-detail.test.tsx +0 -10
  271. package/src/renderers/electrobun/bun/application-menu/index.test.ts +0 -21
  272. package/src/renderers/electrobun/bun/window/focus.test.ts +0 -35
  273. package/src/renderers/electrobun/view/host/mouse.test.ts +0 -37
  274. package/src/renderers/electrobun/view/host/text.test.ts +0 -22
  275. package/src/renderers/opentui/dialog-host.test.tsx +0 -44
  276. /package/src/{plugins/ibkr → utils}/hash.ts +0 -0
@@ -0,0 +1,496 @@
1
+ import { useEffect, type Dispatch } from "react";
2
+ import { getDockedPaneIds } from "../../plugins/pane-manager";
3
+ import type { PluginRegistry } from "../../plugins/registry";
4
+ import type { AppAction, AppState } from "../../state/app/context";
5
+ import {
6
+ findPaneInstance,
7
+ resolveFollowBindingInstance,
8
+ TICKER_RESEARCH_PANE_ID,
9
+ } from "../../types/config";
10
+ import type { DesktopDeepLinkBridge } from "../../types/desktop-deeplink";
11
+ import type { DesktopWindowBridge } from "../../types/desktop-window";
12
+
13
+ type CloudDeepLinkRoute = {
14
+ kind: "cloud-alerts" | "cloud-roundup";
15
+ week: string | null;
16
+ };
17
+
18
+ type CollectionDeepLinkKind = "collection" | "portfolio" | "watchlist";
19
+ type AlertDeepLinkCondition = "above" | "below" | "crosses";
20
+ type NewsDeepLinkKind = "breaking" | "feed" | "ticker" | "top";
21
+
22
+ export type DesktopDeepLinkAction =
23
+ | { type: "open-account-management"; route: CloudDeepLinkRoute; message: string }
24
+ | { type: "open-ticker"; symbol: string; tabId: string | null; message: string }
25
+ | { type: "open-collection"; kind: CollectionDeepLinkKind; collectionId: string; message: string }
26
+ | {
27
+ type: "create-alert";
28
+ values: { symbol: string; condition: AlertDeepLinkCondition; price: string };
29
+ message: string;
30
+ }
31
+ | { type: "open-chat-channel"; channelId: string; message: string }
32
+ | { type: "open-chat-dm"; participants: string; message: string }
33
+ | { type: "open-news"; kind: NewsDeepLinkKind; symbol: string | null; message: string }
34
+ | { type: "unsupported"; message: string };
35
+
36
+ interface ParsedGloomUrl {
37
+ url: URL;
38
+ host: string;
39
+ segments: string[];
40
+ }
41
+
42
+ interface DesktopDeepLinkHandlerOptions {
43
+ dispatch: Dispatch<AppAction>;
44
+ pluginRegistry: PluginRegistry;
45
+ stateRef: { current: AppState };
46
+ }
47
+
48
+ function weekSuffix(week: string | null): string {
49
+ return week ? ` for ${week}` : "";
50
+ }
51
+
52
+ function safeDecode(value: string): string {
53
+ try {
54
+ return decodeURIComponent(value);
55
+ } catch {
56
+ return value;
57
+ }
58
+ }
59
+
60
+ function splitPathSegments(pathname: string): string[] {
61
+ return pathname
62
+ .split("/")
63
+ .map((segment) => safeDecode(segment).trim())
64
+ .filter(Boolean);
65
+ }
66
+
67
+ function parseGloomUrl(rawUrl: string): ParsedGloomUrl | null {
68
+ let url: URL;
69
+ try {
70
+ url = new URL(rawUrl);
71
+ } catch {
72
+ return null;
73
+ }
74
+ if (url.protocol !== "gloomberb:") return null;
75
+
76
+ const host = url.hostname.trim().toLowerCase();
77
+ if (host) {
78
+ return { url, host, segments: splitPathSegments(url.pathname) };
79
+ }
80
+
81
+ const segments = splitPathSegments(url.pathname);
82
+ const opaqueHost = segments.shift()?.toLowerCase();
83
+ return opaqueHost ? { url, host: opaqueHost, segments } : null;
84
+ }
85
+
86
+ function param(url: URL, ...names: string[]): string | null {
87
+ for (const name of names) {
88
+ const value = url.searchParams.get(name)?.trim();
89
+ if (value) return value;
90
+ }
91
+ return null;
92
+ }
93
+
94
+ function normalizeSymbol(value: string | null | undefined): string | null {
95
+ const trimmed = value?.trim().replace(/^\$/, "");
96
+ if (!trimmed) return null;
97
+ return trimmed.toUpperCase();
98
+ }
99
+
100
+ function normalizeAlertCondition(value: string | null): AlertDeepLinkCondition | null {
101
+ switch (value?.trim().toLowerCase()) {
102
+ case ">":
103
+ case "above":
104
+ case "over":
105
+ case "gt":
106
+ return "above";
107
+ case "<":
108
+ case "below":
109
+ case "under":
110
+ case "lt":
111
+ return "below";
112
+ case "x":
113
+ case "cross":
114
+ case "crosses":
115
+ return "crosses";
116
+ default:
117
+ return null;
118
+ }
119
+ }
120
+
121
+ function normalizePrice(value: string | null): string | null {
122
+ const trimmed = value?.trim().replace(/^\$/, "");
123
+ if (!trimmed) return null;
124
+ const price = Number.parseFloat(trimmed);
125
+ return Number.isFinite(price) && price > 0 ? String(price) : null;
126
+ }
127
+
128
+ function tickerMessage(symbol: string, tabId: string | null): string {
129
+ return tabId ? `Opened ${symbol} ${tabId} tab.` : `Opened ${symbol}.`;
130
+ }
131
+
132
+ function collectionMessage(kind: CollectionDeepLinkKind, collectionId: string): string {
133
+ const label = kind === "watchlist" ? "watchlist" : kind === "portfolio" ? "portfolio" : "collection";
134
+ return `Opened ${label} ${collectionId}.`;
135
+ }
136
+
137
+ function parseCloudDeepLink(parsed: ParsedGloomUrl): DesktopDeepLinkAction {
138
+ const route = parsed.segments[0] ?? "";
139
+ const week = parsed.url.searchParams.get("week");
140
+ if (route === "roundup") {
141
+ return {
142
+ type: "open-account-management",
143
+ route: { kind: "cloud-roundup", week },
144
+ message: `Opened weekly roundup settings${weekSuffix(week)}.`,
145
+ };
146
+ }
147
+ if (route === "alerts") {
148
+ return {
149
+ type: "open-account-management",
150
+ route: { kind: "cloud-alerts", week },
151
+ message: `Opened portfolio alert settings${weekSuffix(week)}.`,
152
+ };
153
+ }
154
+ return { type: "unsupported", message: "Unsupported Gloomberb cloud link." };
155
+ }
156
+
157
+ function parseTickerDeepLink(parsed: ParsedGloomUrl): DesktopDeepLinkAction {
158
+ const symbol = normalizeSymbol(parsed.segments[0] ?? param(parsed.url, "symbol", "ticker"));
159
+ if (!symbol) return { type: "unsupported", message: "Ticker links need a symbol." };
160
+ const tabId = param(parsed.url, "tab");
161
+ return { type: "open-ticker", symbol, tabId, message: tickerMessage(symbol, tabId) };
162
+ }
163
+
164
+ function parseCollectionDeepLink(parsed: ParsedGloomUrl, kind: CollectionDeepLinkKind): DesktopDeepLinkAction {
165
+ const collectionId = parsed.segments[0] ?? param(parsed.url, "id", "collection");
166
+ if (!collectionId) return { type: "unsupported", message: "Collection links need an id." };
167
+ return {
168
+ type: "open-collection",
169
+ kind,
170
+ collectionId,
171
+ message: collectionMessage(kind, collectionId),
172
+ };
173
+ }
174
+
175
+ function parseAlertDeepLink(parsed: ParsedGloomUrl): DesktopDeepLinkAction {
176
+ if ((parsed.segments[0] ?? "new") !== "new") {
177
+ return { type: "unsupported", message: "Unsupported Gloomberb alert link." };
178
+ }
179
+ const symbol = normalizeSymbol(param(parsed.url, "symbol", "ticker"));
180
+ const condition = normalizeAlertCondition(param(parsed.url, "condition", "side", "trigger"));
181
+ const price = normalizePrice(param(parsed.url, "price", "target"));
182
+ if (!symbol || !condition || !price) {
183
+ return { type: "unsupported", message: "Alert links need symbol, condition, and price." };
184
+ }
185
+ return {
186
+ type: "create-alert",
187
+ values: { symbol, condition, price },
188
+ message: `Created ${symbol} ${condition} ${price} alert.`,
189
+ };
190
+ }
191
+
192
+ function parseChatDeepLink(parsed: ParsedGloomUrl): DesktopDeepLinkAction {
193
+ const route = parsed.segments[0] ?? "default";
194
+ if (route === "channel") {
195
+ const channelId = parsed.segments[1] ?? param(parsed.url, "id", "channel");
196
+ if (!channelId) return { type: "unsupported", message: "Chat channel links need a channel id." };
197
+ return { type: "open-chat-channel", channelId, message: `Opened chat ${channelId}.` };
198
+ }
199
+ if (route === "dm") {
200
+ const participants = param(parsed.url, "users", "participants", "user") ?? parsed.segments.slice(1).join(",");
201
+ if (!participants.trim()) return { type: "unsupported", message: "DM links need at least one username." };
202
+ return { type: "open-chat-dm", participants, message: "Opened DM." };
203
+ }
204
+ if (route === "default") {
205
+ return { type: "open-chat-channel", channelId: "", message: "Opened chat." };
206
+ }
207
+ return { type: "open-chat-channel", channelId: route, message: `Opened chat ${route}.` };
208
+ }
209
+
210
+ function parseNewsDeepLink(parsed: ParsedGloomUrl): DesktopDeepLinkAction {
211
+ const ticker = normalizeSymbol(param(parsed.url, "ticker", "symbol") ?? (parsed.segments[0] === "ticker" ? parsed.segments[1] : null));
212
+ if (ticker) return { type: "open-news", kind: "ticker", symbol: ticker, message: `Opened ${ticker} news.` };
213
+
214
+ const route = parsed.segments[0] ?? "top";
215
+ if (route === "breaking") {
216
+ return { type: "open-news", kind: "breaking", symbol: null, message: "Opened breaking news." };
217
+ }
218
+ if (route === "feed") {
219
+ return { type: "open-news", kind: "feed", symbol: null, message: "Opened news feed." };
220
+ }
221
+ if (route === "top") {
222
+ return { type: "open-news", kind: "top", symbol: null, message: "Opened top news." };
223
+ }
224
+ return { type: "unsupported", message: "Unsupported Gloomberb news link." };
225
+ }
226
+
227
+ export function resolveDesktopDeepLinkAction(rawUrl: string): DesktopDeepLinkAction {
228
+ const parsed = parseGloomUrl(rawUrl);
229
+ if (!parsed) return { type: "unsupported", message: "Unsupported Gloomberb link." };
230
+
231
+ switch (parsed.host) {
232
+ case "cloud":
233
+ return parseCloudDeepLink(parsed);
234
+ case "ticker":
235
+ return parseTickerDeepLink(parsed);
236
+ case "portfolio":
237
+ return parseCollectionDeepLink(parsed, "portfolio");
238
+ case "watchlist":
239
+ return parseCollectionDeepLink(parsed, "watchlist");
240
+ case "collection":
241
+ return parseCollectionDeepLink(parsed, "collection");
242
+ case "alert":
243
+ return parseAlertDeepLink(parsed);
244
+ case "chat":
245
+ return parseChatDeepLink(parsed);
246
+ case "news":
247
+ return parseNewsDeepLink(parsed);
248
+ default:
249
+ return { type: "unsupported", message: "Unsupported Gloomberb link." };
250
+ }
251
+ }
252
+
253
+ function notifyError(pluginRegistry: PluginRegistry, body: string): void {
254
+ pluginRegistry.notify({ body, type: "error" });
255
+ }
256
+
257
+ function notifySuccess(pluginRegistry: PluginRegistry, body: string): void {
258
+ pluginRegistry.notify({ body, type: "success" });
259
+ }
260
+
261
+ function requirePane(pluginRegistry: PluginRegistry, paneId: string, unavailableMessage: string): boolean {
262
+ if (pluginRegistry.panes.has(paneId)) return true;
263
+ notifyError(pluginRegistry, unavailableMessage);
264
+ return false;
265
+ }
266
+
267
+ function resolveCollectionRecord(
268
+ state: AppState,
269
+ action: Extract<DesktopDeepLinkAction, { type: "open-collection" }>,
270
+ ): { name?: string } | null {
271
+ const portfolio = state.config.portfolios.find((entry) => entry.id === action.collectionId);
272
+ const watchlist = state.config.watchlists.find((entry) => entry.id === action.collectionId);
273
+ if (action.kind === "portfolio") return portfolio ?? null;
274
+ if (action.kind === "watchlist") return watchlist ?? null;
275
+ return portfolio ?? watchlist ?? null;
276
+ }
277
+
278
+ function visiblePaneIds(state: AppState): Set<string> {
279
+ return new Set([
280
+ ...getDockedPaneIds(state.config.layout),
281
+ ...state.config.layout.floating.map((entry) => entry.instanceId),
282
+ ...state.config.layout.detached.map((entry) => entry.instanceId),
283
+ ]);
284
+ }
285
+
286
+ function isVisiblePane(state: AppState, paneId: string | null | undefined): paneId is string {
287
+ return !!paneId && visiblePaneIds(state).has(paneId);
288
+ }
289
+
290
+ function resolveVisibleCollectionPaneId(state: AppState): string | null {
291
+ const followedPaneId = resolveFollowBindingInstance(
292
+ state.config.layout,
293
+ state.focusedPaneId,
294
+ (instance) => instance.paneId === "portfolio-list",
295
+ )?.instanceId;
296
+ if (isVisiblePane(state, followedPaneId)) return followedPaneId;
297
+
298
+ const focusedPane = state.focusedPaneId ? findPaneInstance(state.config.layout, state.focusedPaneId) : null;
299
+ if (focusedPane?.paneId === "portfolio-list" && isVisiblePane(state, focusedPane.instanceId)) {
300
+ return focusedPane.instanceId;
301
+ }
302
+
303
+ const visibleIds = visiblePaneIds(state);
304
+ return state.config.layout.instances.find((instance) => (
305
+ instance.paneId === "portfolio-list" && visibleIds.has(instance.instanceId)
306
+ ))?.instanceId ?? null;
307
+ }
308
+
309
+ function applyCollectionSelection(
310
+ collectionId: string,
311
+ { dispatch, stateRef }: Pick<DesktopDeepLinkHandlerOptions, "dispatch" | "stateRef">,
312
+ ): boolean {
313
+ const targetPaneId = resolveVisibleCollectionPaneId(stateRef.current);
314
+ if (!targetPaneId) return false;
315
+ dispatch({ type: "UPDATE_PANE_STATE", paneId: targetPaneId, patch: { collectionId } });
316
+ dispatch({ type: "FOCUS_PANE", paneId: targetPaneId });
317
+ return true;
318
+ }
319
+
320
+ function retryCollectionSelection(
321
+ collectionId: string,
322
+ options: Pick<DesktopDeepLinkHandlerOptions, "dispatch" | "stateRef">,
323
+ ): void {
324
+ globalThis.setTimeout(() => {
325
+ applyCollectionSelection(collectionId, options);
326
+ }, 50);
327
+ }
328
+
329
+ function handleOpenTicker(
330
+ action: Extract<DesktopDeepLinkAction, { type: "open-ticker" }>,
331
+ pluginRegistry: PluginRegistry,
332
+ ): void {
333
+ if (!requirePane(pluginRegistry, TICKER_RESEARCH_PANE_ID, "Ticker research is unavailable.")) return;
334
+ if (action.tabId && !pluginRegistry.getTickerResearchTabPluginId(action.tabId)) {
335
+ notifyError(pluginRegistry, `Ticker tab "${action.tabId}" is unavailable.`);
336
+ return;
337
+ }
338
+
339
+ pluginRegistry.pinTicker(action.symbol, {
340
+ floating: true,
341
+ paneType: TICKER_RESEARCH_PANE_ID,
342
+ });
343
+
344
+ if (action.tabId) {
345
+ globalThis.setTimeout(() => {
346
+ pluginRegistry.switchTab(action.tabId!);
347
+ }, 80);
348
+ }
349
+ notifySuccess(pluginRegistry, action.message);
350
+ }
351
+
352
+ function handleOpenCollection(
353
+ action: Extract<DesktopDeepLinkAction, { type: "open-collection" }>,
354
+ options: DesktopDeepLinkHandlerOptions,
355
+ ): void {
356
+ const collection = resolveCollectionRecord(options.stateRef.current, action);
357
+ if (!collection) {
358
+ notifyError(options.pluginRegistry, `Collection "${action.collectionId}" is unavailable.`);
359
+ return;
360
+ }
361
+ if (!requirePane(options.pluginRegistry, "portfolio-list", "Collections are unavailable.")) return;
362
+
363
+ options.pluginRegistry.showPane("portfolio-list");
364
+ if (!applyCollectionSelection(action.collectionId, options)) {
365
+ retryCollectionSelection(action.collectionId, options);
366
+ }
367
+ notifySuccess(options.pluginRegistry, collection.name ? `Opened ${collection.name}.` : action.message);
368
+ }
369
+
370
+ function handleCreateAlert(
371
+ action: Extract<DesktopDeepLinkAction, { type: "create-alert" }>,
372
+ pluginRegistry: PluginRegistry,
373
+ ): void {
374
+ const command = pluginRegistry.commands.get("set-alert");
375
+ if (!command) {
376
+ notifyError(pluginRegistry, "Alerts are unavailable.");
377
+ return;
378
+ }
379
+ void Promise.resolve(command.execute(action.values)).catch((error) => {
380
+ notifyError(pluginRegistry, error instanceof Error ? error.message : "Failed to create alert.");
381
+ });
382
+ }
383
+
384
+ function handleOpenChatChannel(
385
+ action: Extract<DesktopDeepLinkAction, { type: "open-chat-channel" }>,
386
+ pluginRegistry: PluginRegistry,
387
+ ): void {
388
+ if (!pluginRegistry.paneTemplates.has("new-chat-pane")) {
389
+ notifyError(pluginRegistry, "Chat is unavailable.");
390
+ return;
391
+ }
392
+ const options = action.channelId ? { arg: action.channelId } : undefined;
393
+ void pluginRegistry.createPaneFromTemplateAsyncFn("new-chat-pane", options).then(() => {
394
+ notifySuccess(pluginRegistry, action.message);
395
+ }).catch((error) => {
396
+ notifyError(pluginRegistry, error instanceof Error ? error.message : "Failed to open chat.");
397
+ });
398
+ }
399
+
400
+ function handleOpenChatDm(
401
+ action: Extract<DesktopDeepLinkAction, { type: "open-chat-dm" }>,
402
+ pluginRegistry: PluginRegistry,
403
+ ): void {
404
+ const command = pluginRegistry.commands.get("direct-message");
405
+ if (!command) {
406
+ notifyError(pluginRegistry, "Direct messages are unavailable.");
407
+ return;
408
+ }
409
+ void Promise.resolve(command.execute({ participants: action.participants })).then(() => {
410
+ notifySuccess(pluginRegistry, action.message);
411
+ }).catch((error) => {
412
+ notifyError(pluginRegistry, error instanceof Error ? error.message : "Failed to open DM.");
413
+ });
414
+ }
415
+
416
+ function handleOpenNews(
417
+ action: Extract<DesktopDeepLinkAction, { type: "open-news" }>,
418
+ pluginRegistry: PluginRegistry,
419
+ ): void {
420
+ if (action.kind === "ticker") {
421
+ if (!action.symbol || !pluginRegistry.paneTemplates.has("ticker-news-pane")) {
422
+ notifyError(pluginRegistry, "Ticker news is unavailable.");
423
+ return;
424
+ }
425
+ void pluginRegistry.createPaneFromTemplateAsyncFn("ticker-news-pane", { symbol: action.symbol }).then(() => {
426
+ notifySuccess(pluginRegistry, action.message);
427
+ }).catch((error) => {
428
+ notifyError(pluginRegistry, error instanceof Error ? error.message : "Failed to open ticker news.");
429
+ });
430
+ return;
431
+ }
432
+
433
+ const paneId = action.kind === "breaking"
434
+ ? "news-breaking"
435
+ : action.kind === "feed"
436
+ ? "news-feed"
437
+ : "news-top";
438
+ if (!requirePane(pluginRegistry, paneId, "News is unavailable.")) return;
439
+ pluginRegistry.showPane(paneId);
440
+ notifySuccess(pluginRegistry, action.message);
441
+ }
442
+
443
+ export function handleDesktopDeepLink(rawUrl: string, options: DesktopDeepLinkHandlerOptions): void {
444
+ const action = resolveDesktopDeepLinkAction(rawUrl);
445
+ if (action.type === "unsupported") {
446
+ notifyError(options.pluginRegistry, action.message);
447
+ return;
448
+ }
449
+
450
+ switch (action.type) {
451
+ case "open-account-management":
452
+ if (!requirePane(options.pluginRegistry, "account-management", "Account management is unavailable.")) return;
453
+ options.pluginRegistry.showPane("account-management");
454
+ notifySuccess(options.pluginRegistry, action.message);
455
+ return;
456
+ case "open-ticker":
457
+ handleOpenTicker(action, options.pluginRegistry);
458
+ return;
459
+ case "open-collection":
460
+ handleOpenCollection(action, options);
461
+ return;
462
+ case "create-alert":
463
+ handleCreateAlert(action, options.pluginRegistry);
464
+ return;
465
+ case "open-chat-channel":
466
+ handleOpenChatChannel(action, options.pluginRegistry);
467
+ return;
468
+ case "open-chat-dm":
469
+ handleOpenChatDm(action, options.pluginRegistry);
470
+ return;
471
+ case "open-news":
472
+ handleOpenNews(action, options.pluginRegistry);
473
+ return;
474
+ }
475
+ }
476
+
477
+ export function useDesktopDeepLinkRuntime({
478
+ desktopDeepLinkBridge,
479
+ desktopWindowKind,
480
+ dispatch,
481
+ pluginRegistry,
482
+ stateRef,
483
+ }: {
484
+ desktopDeepLinkBridge?: DesktopDeepLinkBridge;
485
+ desktopWindowKind?: DesktopWindowBridge["kind"];
486
+ dispatch: Dispatch<AppAction>;
487
+ pluginRegistry: PluginRegistry;
488
+ stateRef: { current: AppState };
489
+ }) {
490
+ useEffect(() => {
491
+ if (desktopWindowKind !== "main" || !desktopDeepLinkBridge) return;
492
+ return desktopDeepLinkBridge.subscribe((deeplink) => {
493
+ handleDesktopDeepLink(deeplink.url, { dispatch, pluginRegistry, stateRef });
494
+ });
495
+ }, [desktopDeepLinkBridge, desktopWindowKind, dispatch, pluginRegistry, stateRef]);
496
+ }
package/src/app.tsx CHANGED
@@ -22,6 +22,7 @@ import { PluginRegistry } from "./plugins/registry";
22
22
  import type { TickerRepository } from "./data/ticker-repository";
23
23
  import { ThemeProvider, useThemeColors } from "./theme/theme-context";
24
24
  import type { AppConfig } from "./types/config";
25
+ import type { DesktopDeepLinkBridge } from "./types/desktop-deeplink";
25
26
  import type { CliLaunchRequest } from "./types/plugin";
26
27
  import type { DataProvider } from "./types/data-provider";
27
28
  import type { DesktopDockPreviewState, DesktopSharedStateSnapshot, DesktopThemePreviewState, DesktopWindowBridge } from "./types/desktop-window";
@@ -32,6 +33,7 @@ import type { MarketDataCoordinator } from "./market-data/coordinator";
32
33
  import { createAppNotifier } from "./notifications/app-notifier";
33
34
  import { createAppServices } from "./core/app-services";
34
35
  import { useBrokerImportRuntime } from "./app/runtime/broker-import";
36
+ import { useDesktopDeepLinkRuntime } from "./app/runtime/desktop-deeplink";
35
37
  import { useDesktopApplicationMenuRuntime } from "./app/runtime/desktop-menu";
36
38
  import { useAppGlobalShortcuts } from "./app/global-shortcuts";
37
39
  import { useAppPaneRuntime } from "./app/pane-runtime";
@@ -39,6 +41,8 @@ import { bindPluginRegistryRuntimeAccess } from "./app/runtime/plugin-bindings";
39
41
  import { useAppStartupRuntime } from "./app/runtime/startup";
40
42
  import { useTickerRefreshRuntime } from "./app/runtime/ticker-refresh";
41
43
  import { useAppUpdateRuntime } from "./app/runtime/update";
44
+ import { createCoreSyncContributors } from "./sync/core-contributors";
45
+ import { useCloudSyncRuntime } from "./sync/react";
42
46
  import { RemoteControlHost, type RemoteControlAdapter } from "./remote/app-host";
43
47
  import { RemoteUiRegistryProvider } from "./remote/semantic-tree";
44
48
  import {
@@ -57,6 +61,7 @@ interface AppInnerProps {
57
61
  sessionSnapshot?: AppSessionSnapshot | null;
58
62
  desktopWindowBridge?: DesktopWindowBridge;
59
63
  desktopApplicationMenuBridge?: DesktopApplicationMenuBridge;
64
+ desktopDeepLinkBridge?: DesktopDeepLinkBridge;
60
65
  remoteControlAdapter?: RemoteControlAdapter;
61
66
  }
62
67
 
@@ -86,6 +91,7 @@ function AppInner({
86
91
  sessionSnapshot = null,
87
92
  desktopWindowBridge,
88
93
  desktopApplicationMenuBridge,
94
+ desktopDeepLinkBridge,
89
95
  remoteControlAdapter,
90
96
  }: AppInnerProps) {
91
97
  const dispatch = useAppDispatch();
@@ -173,6 +179,15 @@ function AppInner({
173
179
  });
174
180
  }, [desktopWindowBridge]);
175
181
 
182
+ useEffect(() => {
183
+ const disposers = createCoreSyncContributors().map((contributor) => (
184
+ pluginRegistry.registerSyncContributorForPlugin("core", contributor)
185
+ ));
186
+ return () => {
187
+ for (const dispose of disposers) dispose();
188
+ };
189
+ }, [pluginRegistry]);
190
+
176
191
  const {
177
192
  primeCachedFinancials,
178
193
  refreshQuote,
@@ -214,6 +229,14 @@ function AppInner({
214
229
  stateRef,
215
230
  });
216
231
 
232
+ useDesktopDeepLinkRuntime({
233
+ desktopDeepLinkBridge,
234
+ desktopWindowKind: desktopWindowBridge?.kind,
235
+ dispatch,
236
+ pluginRegistry,
237
+ stateRef,
238
+ });
239
+
217
240
  const focusedTickerSymbol = getFocusedTickerSymbol(state);
218
241
  useAppStartupRuntime({
219
242
  appActive,
@@ -243,6 +266,14 @@ function AppInner({
243
266
  tickerRepository,
244
267
  });
245
268
 
269
+ useCloudSyncRuntime({
270
+ state,
271
+ dispatch,
272
+ tickerRepository,
273
+ pluginRegistry,
274
+ initialized: state.initialized && desktopWindowBridge?.kind !== "detached",
275
+ });
276
+
246
277
  useAppPaneRuntime({
247
278
  dataProvider,
248
279
  detachedPaneId,
@@ -353,6 +384,7 @@ interface AppProps {
353
384
  cliLaunchRequest?: CliLaunchRequest | null;
354
385
  desktopWindowBridge?: DesktopWindowBridge;
355
386
  desktopApplicationMenuBridge?: DesktopApplicationMenuBridge;
387
+ desktopDeepLinkBridge?: DesktopDeepLinkBridge;
356
388
  desktopSnapshot?: DesktopSharedStateSnapshot | null;
357
389
  desktopThemePreview?: DesktopThemePreviewState | null;
358
390
  remoteControlAdapter?: RemoteControlAdapter;
@@ -364,6 +396,7 @@ export function App({
364
396
  cliLaunchRequest = null,
365
397
  desktopWindowBridge,
366
398
  desktopApplicationMenuBridge,
399
+ desktopDeepLinkBridge,
367
400
  desktopSnapshot = null,
368
401
  desktopThemePreview = null,
369
402
  remoteControlAdapter,
@@ -451,6 +484,7 @@ export function App({
451
484
  sessionSnapshot={sessionSnapshot}
452
485
  desktopWindowBridge={desktopWindowBridge}
453
486
  desktopApplicationMenuBridge={desktopApplicationMenuBridge}
487
+ desktopDeepLinkBridge={desktopDeepLinkBridge}
454
488
  remoteControlAdapter={remoteControlAdapter}
455
489
  />
456
490
  </AppProvider>
@@ -3,6 +3,7 @@ import type { BrokerAdapter } from "../types/broker";
3
3
  import type { BrokerInstanceConfig } from "../types/config";
4
4
  import type { CachePolicy } from "../types/persistence";
5
5
  import type { BrokerAccount } from "../types/trading";
6
+ import { fnv1aHashString } from "../utils/hash";
6
7
 
7
8
  const BROKER_ACCOUNT_SNAPSHOT_KIND = "account-snapshot";
8
9
  const BROKER_ACCOUNT_SNAPSHOT_SCHEMA_VERSION = 1;
@@ -16,15 +17,6 @@ interface PersistedBrokerAccountSnapshot {
16
17
  brokerType?: string;
17
18
  }
18
19
 
19
- function hashString(input: string): string {
20
- let hash = 0x811c9dc5;
21
- for (let index = 0; index < input.length; index += 1) {
22
- hash ^= input.charCodeAt(index);
23
- hash = Math.imul(hash, 0x01000193);
24
- }
25
- return `fnv1a:${(hash >>> 0).toString(16).padStart(8, "0")}`;
26
- }
27
-
28
20
  function brokerAccountNamespace(instance: BrokerInstanceConfig): string {
29
21
  return `plugin:${instance.brokerType}`;
30
22
  }
@@ -36,7 +28,7 @@ export function getBrokerAccountCacheSourceKey(
36
28
  if (broker?.getAccountCacheSourceKey) {
37
29
  return broker.getAccountCacheSourceKey(instance);
38
30
  }
39
- return hashString(JSON.stringify({
31
+ return fnv1aHashString(JSON.stringify({
40
32
  brokerType: instance.brokerType,
41
33
  config: broker?.toConfigValues?.(instance) ?? instance.config,
42
34
  }));
@@ -1,5 +1,4 @@
1
1
  import type { AppConfig, BrokerInstanceConfig } from "../types/config";
2
- import type { BrokerContractRef } from "../types/instrument";
3
2
  import type { Portfolio, TickerPosition, TickerRecord } from "../types/ticker";
4
3
  import { buildBrokerPortfolioId, getBrokerInstance, isBrokerPortfolioId } from "../utils/broker-instances";
5
4
 
@@ -78,29 +78,33 @@ function updateExistingTicker(
78
78
  positionEntry: TickerPosition,
79
79
  brokerContract?: BrokerContractRef,
80
80
  ): TickerRecord {
81
- if (position.name && ticker.metadata.name === ticker.metadata.ticker) {
82
- ticker.metadata.name = position.name;
83
- }
84
- if (position.assetCategory && !ticker.metadata.assetCategory) {
85
- ticker.metadata.assetCategory = position.assetCategory;
86
- }
87
- if (position.isin && !ticker.metadata.isin) {
88
- ticker.metadata.isin = position.isin;
89
- }
90
-
91
81
  const otherPositions = ticker.metadata.positions.filter(
92
82
  (entry) => !(entry.portfolio === portfolioId && entry.broker === instance.brokerType),
93
83
  );
94
- ticker.metadata.positions = [...otherPositions, positionEntry];
95
- ticker.metadata.broker_contracts = mergeBrokerContracts(
84
+ const brokerContracts = mergeBrokerContracts(
96
85
  ticker.metadata.broker_contracts ?? [],
97
86
  brokerContract ? [brokerContract] : [],
98
87
  );
99
- if (!ticker.metadata.portfolios.includes(portfolioId)) {
100
- ticker.metadata.portfolios.push(portfolioId);
101
- }
88
+ const portfolios = ticker.metadata.portfolios.includes(portfolioId)
89
+ ? ticker.metadata.portfolios
90
+ : [...ticker.metadata.portfolios, portfolioId];
102
91
 
103
- return ticker;
92
+ return {
93
+ ...ticker,
94
+ metadata: {
95
+ ...ticker.metadata,
96
+ name: position.name && ticker.metadata.name === ticker.metadata.ticker
97
+ ? position.name
98
+ : ticker.metadata.name,
99
+ assetCategory: position.assetCategory && !ticker.metadata.assetCategory
100
+ ? position.assetCategory
101
+ : ticker.metadata.assetCategory,
102
+ isin: position.isin && !ticker.metadata.isin ? position.isin : ticker.metadata.isin,
103
+ positions: [...otherPositions, positionEntry],
104
+ broker_contracts: brokerContracts,
105
+ portfolios,
106
+ },
107
+ };
104
108
  }
105
109
 
106
110
  export async function upsertBrokerPositionTicker({