gloomberb 0.7.0 → 0.7.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.
- package/package.json +1 -1
- package/src/app.tsx +149 -51
- package/src/capabilities/factories.ts +3 -0
- package/src/components/chart/chart-renderer.ts +24 -12
- package/src/components/chart/chart.test.ts +43 -0
- package/src/components/chart/comparison-chart-renderer.test.ts +26 -0
- package/src/components/chart/comparison-chart-renderer.ts +13 -6
- package/src/components/chart/comparison-stock-chart.tsx +15 -12
- package/src/components/chart/price-axis-labels.tsx +114 -0
- package/src/components/chart/static-chart-surface.tsx +2 -0
- package/src/components/chart/stock-chart.tsx +31 -20
- package/src/components/chart/time-axis-label.tsx +78 -1
- package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +0 -28
- package/src/components/command-bar/command-bar.test.tsx +114 -31
- package/src/components/command-bar/command-bar.tsx +587 -335
- package/src/components/command-bar/command-registry.ts +0 -10
- package/src/components/command-bar/helpers.ts +2 -5
- package/src/components/command-bar/list-model.ts +1 -2
- package/src/components/command-bar/theme-picker.tsx +254 -0
- package/src/components/command-bar/view-model.test.ts +4 -4
- package/src/components/command-bar/view-model.ts +1 -1
- package/src/components/command-bar/workflow-types.ts +1 -2
- package/src/components/layout/detached-pane-shell.tsx +29 -10
- package/src/components/layout/floating-pane.tsx +5 -2
- package/src/components/layout/header.tsx +2 -0
- package/src/components/layout/pane-content.test.tsx +64 -0
- package/src/components/layout/pane-content.tsx +2 -0
- package/src/components/layout/pane-sizing.ts +9 -2
- package/src/components/layout/pane-title.ts +4 -1
- package/src/components/layout/pane.tsx +11 -5
- package/src/components/layout/shell.test.tsx +198 -0
- package/src/components/layout/shell.tsx +33 -2
- package/src/components/layout/status-bar.tsx +2 -0
- package/src/components/speedometer-gauge.tsx +29 -25
- package/src/components/table-view-shared.tsx +85 -3
- package/src/components/ticker-list-table-view.tsx +16 -2
- package/src/components/ticker-list-table.test.tsx +82 -1
- package/src/components/ticker-list-table.tsx +22 -9
- package/src/components/ui/data-table.tsx +12 -9
- package/src/components/ui/frame.tsx +2 -0
- package/src/components/ui/table-layout.test.ts +27 -1
- package/src/components/ui/table-layout.ts +39 -1
- package/src/components/ui/tabs.tsx +34 -1
- package/src/components/ui/ui.test.tsx +68 -1
- package/src/core/state/app-state.ts +4 -14
- package/src/market-data/coordinator.test.ts +75 -0
- package/src/market-data/coordinator.ts +218 -48
- package/src/news/aggregator.test.ts +49 -1
- package/src/news/aggregator.ts +48 -0
- package/src/news/hooks.ts +6 -2
- package/src/plugins/builtin/ai/screener-pane.tsx +3 -2
- package/src/plugins/builtin/analytics/index.tsx +1 -18
- package/src/plugins/builtin/chat-controller.test.ts +83 -1
- package/src/plugins/builtin/chat-controller.ts +491 -243
- package/src/plugins/builtin/chat.test.tsx +425 -7
- package/src/plugins/builtin/chat.tsx +1109 -211
- package/src/plugins/builtin/cloud-tweets.tsx +1 -0
- package/src/plugins/builtin/fear-greed/index.tsx +148 -39
- package/src/plugins/builtin/holders/index.test.ts +13 -1
- package/src/plugins/builtin/holders/index.tsx +54 -14
- package/src/plugins/builtin/market-movers/index.tsx +12 -18
- package/src/plugins/builtin/market-movers/screener.test.ts +39 -1
- package/src/plugins/builtin/market-movers/screener.ts +70 -17
- package/src/plugins/builtin/news-wire/breaking-pane.tsx +3 -2
- package/src/plugins/builtin/news-wire/industry-pane.tsx +4 -18
- package/src/plugins/builtin/news-wire/news-detail-view.tsx +46 -4
- package/src/plugins/builtin/news-wire/news-preset-pane.tsx +3 -2
- package/src/plugins/builtin/notes.tsx +1 -0
- package/src/plugins/builtin/options.tsx +2 -16
- package/src/plugins/builtin/portfolio-list/pane.tsx +16 -39
- package/src/plugins/builtin/research/index.test.ts +94 -0
- package/src/plugins/builtin/research/index.tsx +159 -12
- package/src/plugins/builtin/ticker-detail/pane.tsx +28 -22
- package/src/plugins/builtin/ticker-detail.test.tsx +38 -0
- package/src/plugins/plugin-runtime.test.tsx +34 -0
- package/src/plugins/plugin-runtime.tsx +7 -2
- package/src/plugins/prediction-markets/controller.ts +0 -15
- package/src/plugins/prediction-markets/detail/pane.tsx +1 -0
- package/src/plugins/registry.ts +3 -3
- package/src/plugins/ticker-navigation.test.ts +66 -0
- package/src/plugins/ticker-navigation.ts +44 -0
- package/src/react/input.ts +1 -1
- package/src/renderers/electrobun/bun/index.ts +32 -11
- package/src/renderers/electrobun/bun/window-focus.ts +37 -0
- package/src/renderers/electrobun/shared/protocol.ts +7 -1
- package/src/renderers/electrobun/view/app-services.ts +88 -31
- package/src/renderers/electrobun/view/backend-rpc.ts +15 -0
- package/src/renderers/electrobun/view/data-table.tsx +111 -96
- package/src/renderers/electrobun/view/desktop-controls.tsx +8 -0
- package/src/renderers/electrobun/view/desktop-window-bridge.ts +12 -1
- package/src/renderers/electrobun/view/dialog-host.tsx +2 -0
- package/src/renderers/electrobun/view/input-host.tsx +1 -1
- package/src/renderers/electrobun/view/main.tsx +18 -1
- package/src/renderers/electrobun/view/styles.css +6 -8
- package/src/renderers/electrobun/view/toast-host.tsx +29 -3
- package/src/renderers/electrobun/view/ui-host.tsx +101 -5
- package/src/renderers/opentui/input-host.tsx +1 -1
- package/src/sources/gloomberb-cloud.test.ts +86 -1
- package/src/sources/gloomberb-cloud.ts +117 -21
- package/src/sources/provider-router.test.ts +122 -0
- package/src/sources/provider-router.ts +198 -11
- package/src/sources/yahoo-finance.test.ts +44 -0
- package/src/sources/yahoo-finance.ts +6 -0
- package/src/state/app-bootstrap.ts +20 -7
- package/src/state/app-context-selector.test.tsx +189 -4
- package/src/state/app-context.test.ts +20 -0
- package/src/state/app-context.tsx +48 -16
- package/src/state/use-inline-tickers.ts +13 -10
- package/src/theme/colors.test.ts +75 -0
- package/src/theme/colors.ts +64 -3
- package/src/theme/theme-context.tsx +26 -0
- package/src/types/capability-route-source.ts +1 -0
- package/src/types/data-provider.ts +15 -1
- package/src/types/desktop-window.ts +6 -0
- package/src/types/financials.ts +2 -0
- package/src/types/plugin.ts +1 -1
- package/src/ui/host.tsx +14 -2
- package/src/ui/index.tsx +1 -0
- package/src/utils/api-client.test.ts +32 -0
- package/src/utils/api-client.ts +119 -18
- package/src/utils/double-escape-close.test.ts +33 -0
- package/src/utils/double-escape-close.ts +40 -0
- package/src/utils/throttled-fetch.test.ts +22 -0
- package/src/utils/throttled-fetch.ts +3 -1
- package/src/utils/ticker-search.ts +53 -9
- package/src/version.ts +1 -1
|
@@ -13,7 +13,7 @@ import * as nodeConfigStoreHost from "../../../data/config-store-node";
|
|
|
13
13
|
import { cloneLayout, findPaneInstance, type AppConfig } from "../../../types/config";
|
|
14
14
|
import type { AppSessionSnapshot } from "../../../core/state/session-persistence";
|
|
15
15
|
import type { PaneRuntimeState } from "../../../core/state/app-state";
|
|
16
|
-
import type { DesktopDockPreviewState, DesktopSharedStateSnapshot } from "../../../types/desktop-window";
|
|
16
|
+
import type { DesktopDockPreviewState, DesktopSharedStateSnapshot, DesktopThemePreviewState } from "../../../types/desktop-window";
|
|
17
17
|
import type { ReleaseInfo, UpdateCheckResult, UpdateProgress } from "../../../updater";
|
|
18
18
|
import { buildSoundCommand } from "../../../notifications/app-notifier";
|
|
19
19
|
import { isPaneDetached } from "../../../plugins/pane-manager";
|
|
@@ -37,8 +37,12 @@ import {
|
|
|
37
37
|
type WindowFrame,
|
|
38
38
|
type WindowMinimumSize,
|
|
39
39
|
} from "./window-frame";
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
import {
|
|
41
|
+
detachedRpcKey,
|
|
42
|
+
focusWindowForRpcKey,
|
|
43
|
+
MAIN_WINDOW_RPC_KEY,
|
|
44
|
+
paneIdFromDetachedRpcKey,
|
|
45
|
+
} from "./window-focus";
|
|
42
46
|
|
|
43
47
|
type DesktopRpc = ReturnType<typeof BrowserView.defineRPC<ElectrobunDesktopRpcSchema>>;
|
|
44
48
|
type WindowMoveEvent = { data?: { x?: number; y?: number } };
|
|
@@ -61,6 +65,7 @@ let services: AppServices | null = null;
|
|
|
61
65
|
let mainWindow: BrowserWindow | null = null;
|
|
62
66
|
let desktopWorkspace: DesktopWorkspace | null = null;
|
|
63
67
|
let currentDockPreview: DesktopDockPreviewState = { paneId: null, edge: null };
|
|
68
|
+
let currentThemePreview: DesktopThemePreviewState = { theme: null };
|
|
64
69
|
let desktopUpdateInProgress = false;
|
|
65
70
|
|
|
66
71
|
const detachedWindows = new Map<string, BrowserWindow>();
|
|
@@ -101,10 +106,6 @@ function requireDesktopWorkspace(): DesktopWorkspace {
|
|
|
101
106
|
return desktopWorkspace;
|
|
102
107
|
}
|
|
103
108
|
|
|
104
|
-
function detachedRpcKey(instanceId: string): string {
|
|
105
|
-
return `detached:${instanceId}`;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
109
|
function registerWindowRpc(key: string, rpc: DesktopRpc): void {
|
|
109
110
|
windowRpcs.set(key, rpc);
|
|
110
111
|
rpcWindowKeys.set(rpc, key);
|
|
@@ -156,10 +157,11 @@ function normalizeInitWindowTarget(
|
|
|
156
157
|
if (rpcKey === MAIN_WINDOW_RPC_KEY) {
|
|
157
158
|
return { kind: "main" };
|
|
158
159
|
}
|
|
159
|
-
|
|
160
|
+
const detachedPaneId = paneIdFromDetachedRpcKey(rpcKey);
|
|
161
|
+
if (detachedPaneId) {
|
|
160
162
|
return {
|
|
161
163
|
kind: "detached",
|
|
162
|
-
paneId:
|
|
164
|
+
paneId: detachedPaneId,
|
|
163
165
|
};
|
|
164
166
|
}
|
|
165
167
|
const kind = payload.kind === "detached" ? "detached" : "main";
|
|
@@ -560,6 +562,17 @@ function sendDockPreview(preview: DesktopDockPreviewState): void {
|
|
|
560
562
|
});
|
|
561
563
|
}
|
|
562
564
|
|
|
565
|
+
function sendThemePreview(preview: DesktopThemePreviewState): void {
|
|
566
|
+
if (currentThemePreview.theme === preview.theme) return;
|
|
567
|
+
currentThemePreview = preview;
|
|
568
|
+
const encodedPreview = encodeRpcValue(preview) as DesktopThemePreviewState;
|
|
569
|
+
forEachReadyWindowRpc((rpc) => {
|
|
570
|
+
rpc.send["desktop.themePreview"]({
|
|
571
|
+
preview: encodedPreview,
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
|
|
563
576
|
function clearDockPreview(paneId?: string): void {
|
|
564
577
|
if (paneId && currentDockPreview.paneId && currentDockPreview.paneId !== paneId) return;
|
|
565
578
|
sendDockPreview({ paneId: null, edge: null });
|
|
@@ -848,6 +861,7 @@ async function initialize(
|
|
|
848
861
|
config: requireConfig(),
|
|
849
862
|
sessionSnapshot: getSessionSnapshot(),
|
|
850
863
|
desktopSnapshot: getDesktopSnapshot(),
|
|
864
|
+
desktopThemePreview: currentThemePreview,
|
|
851
865
|
pluginState: loadPluginState(),
|
|
852
866
|
capabilityManifests: requireServices().pluginRegistry.capabilities.manifests({ rendererOnly: true }),
|
|
853
867
|
windowKind: windowTarget.kind,
|
|
@@ -869,6 +883,9 @@ async function handleDesktop(
|
|
|
869
883
|
sendDesktopState(snapshot);
|
|
870
884
|
return null;
|
|
871
885
|
}
|
|
886
|
+
case "desktop.setThemePreview":
|
|
887
|
+
sendThemePreview((payload.preview ?? { theme: null }) as DesktopThemePreviewState);
|
|
888
|
+
return null;
|
|
872
889
|
case "desktop.replaceDetachedPaneState":
|
|
873
890
|
if (typeof payload.paneId !== "string") {
|
|
874
891
|
throw new Error("desktop.replaceDetachedPaneState requires paneId.");
|
|
@@ -881,7 +898,7 @@ async function handleDesktop(
|
|
|
881
898
|
}
|
|
882
899
|
const snapshot = workspace.popOutPane(payload.paneId, resolveDetachedWindowFrame(payload.paneId));
|
|
883
900
|
await commitDesktopSnapshot(snapshot);
|
|
884
|
-
(
|
|
901
|
+
focusWindowForRpcKey(detachedRpcKey(payload.paneId), mainWindow, detachedWindows);
|
|
885
902
|
return null;
|
|
886
903
|
}
|
|
887
904
|
case "desktop.dockDetachedPane": {
|
|
@@ -911,7 +928,7 @@ async function handleDesktop(
|
|
|
911
928
|
if (typeof payload.paneId !== "string") {
|
|
912
929
|
throw new Error("desktop.focusDetachedPane requires paneId.");
|
|
913
930
|
}
|
|
914
|
-
(
|
|
931
|
+
focusWindowForRpcKey(detachedRpcKey(payload.paneId), mainWindow, detachedWindows);
|
|
915
932
|
return null;
|
|
916
933
|
default:
|
|
917
934
|
throw new Error(`Unknown desktop method: ${method}`);
|
|
@@ -1055,6 +1072,9 @@ async function handleBackendRequest(
|
|
|
1055
1072
|
case "host.copyText":
|
|
1056
1073
|
Utils.clipboardWriteText(normalizeText(payload.text) ?? "");
|
|
1057
1074
|
return null;
|
|
1075
|
+
case "host.focusWindow":
|
|
1076
|
+
focusWindowForRpcKey(getRpcWindowKey(rpc), mainWindow, detachedWindows);
|
|
1077
|
+
return null;
|
|
1058
1078
|
case "host.copyPngImage": {
|
|
1059
1079
|
const pngBase64 = normalizeText(payload.pngBase64);
|
|
1060
1080
|
if (!pngBase64) throw new Error("host.copyPngImage requires PNG data.");
|
|
@@ -1141,6 +1161,7 @@ mainWindow = new BrowserWindow({
|
|
|
1141
1161
|
sandbox: false,
|
|
1142
1162
|
});
|
|
1143
1163
|
updateWindowFrameCache(mainWindow, DEFAULT_WINDOW_FRAME, MAIN_WINDOW_MIN_SIZE);
|
|
1164
|
+
focusWindowForRpcKey(MAIN_WINDOW_RPC_KEY, mainWindow, detachedWindows);
|
|
1144
1165
|
(mainWindow as any).on?.("move", (event: WindowMoveEvent) => {
|
|
1145
1166
|
applyWindowMoveEvent(mainWindow, event);
|
|
1146
1167
|
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export const MAIN_WINDOW_RPC_KEY = "main";
|
|
2
|
+
|
|
3
|
+
const DETACHED_WINDOW_RPC_PREFIX = "detached:";
|
|
4
|
+
|
|
5
|
+
export interface FocusableElectrobunWindow {
|
|
6
|
+
focus?: () => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function detachedRpcKey(instanceId: string): string {
|
|
10
|
+
return `${DETACHED_WINDOW_RPC_PREFIX}${instanceId}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function paneIdFromDetachedRpcKey(rpcKey: string | undefined): string | null {
|
|
14
|
+
if (!rpcKey?.startsWith(DETACHED_WINDOW_RPC_PREFIX)) return null;
|
|
15
|
+
return rpcKey.slice(DETACHED_WINDOW_RPC_PREFIX.length) || null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function resolveWindowForRpcKey<T>(
|
|
19
|
+
rpcKey: string | undefined,
|
|
20
|
+
mainWindow: T | null,
|
|
21
|
+
detachedWindows: ReadonlyMap<string, T>,
|
|
22
|
+
): T | null {
|
|
23
|
+
if (rpcKey === MAIN_WINDOW_RPC_KEY) return mainWindow;
|
|
24
|
+
const paneId = paneIdFromDetachedRpcKey(rpcKey);
|
|
25
|
+
return paneId ? detachedWindows.get(paneId) ?? null : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function focusWindowForRpcKey(
|
|
29
|
+
rpcKey: string | undefined,
|
|
30
|
+
mainWindow: FocusableElectrobunWindow | null,
|
|
31
|
+
detachedWindows: ReadonlyMap<string, FocusableElectrobunWindow>,
|
|
32
|
+
): boolean {
|
|
33
|
+
const window = resolveWindowForRpcKey(rpcKey, mainWindow, detachedWindows);
|
|
34
|
+
if (!window?.focus) return false;
|
|
35
|
+
window.focus();
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AppSessionSnapshot } from "../../../core/state/session-persistence";
|
|
2
|
-
import type { DesktopDockPreviewState, DesktopSharedStateSnapshot } from "../../../types/desktop-window";
|
|
2
|
+
import type { DesktopDockPreviewState, DesktopSharedStateSnapshot, DesktopThemePreviewState } from "../../../types/desktop-window";
|
|
3
3
|
import type { DesktopApplicationMenuCommand } from "../../../types/desktop-menu";
|
|
4
4
|
import type { AppConfig } from "../../../types/config";
|
|
5
5
|
import type { UpdateProgress } from "../../../updater";
|
|
@@ -11,6 +11,7 @@ export interface ElectrobunBackendInit {
|
|
|
11
11
|
config: AppConfig;
|
|
12
12
|
sessionSnapshot: AppSessionSnapshot | null;
|
|
13
13
|
desktopSnapshot: DesktopSharedStateSnapshot | null;
|
|
14
|
+
desktopThemePreview: DesktopThemePreviewState;
|
|
14
15
|
pluginState: Record<string, Record<string, unknown>>;
|
|
15
16
|
capabilityManifests: CapabilityManifest[];
|
|
16
17
|
windowKind: "main" | "detached";
|
|
@@ -39,6 +40,10 @@ export interface DesktopDockPreviewMessage {
|
|
|
39
40
|
preview: DesktopDockPreviewState;
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
export interface DesktopThemePreviewMessage {
|
|
44
|
+
preview: DesktopThemePreviewState;
|
|
45
|
+
}
|
|
46
|
+
|
|
42
47
|
export interface UpdateProgressMessage {
|
|
43
48
|
progress: UpdateProgress;
|
|
44
49
|
}
|
|
@@ -65,6 +70,7 @@ export interface ElectrobunDesktopRpcSchema {
|
|
|
65
70
|
"application-menu.select": ApplicationMenuSelectMessage;
|
|
66
71
|
"desktop.state": DesktopStateMessage;
|
|
67
72
|
"desktop.dockPreview": DesktopDockPreviewMessage;
|
|
73
|
+
"desktop.themePreview": DesktopThemePreviewMessage;
|
|
68
74
|
"update.progress": UpdateProgressMessage;
|
|
69
75
|
"capability.event": CapabilityEventMessage;
|
|
70
76
|
};
|
|
@@ -6,7 +6,7 @@ import { PluginRegistry } from "../../../plugins/registry";
|
|
|
6
6
|
import type { AppServices } from "../../../core/app-services";
|
|
7
7
|
import type { AppConfig } from "../../../types/config";
|
|
8
8
|
import type { DataProvider, QuoteSubscriptionTarget } from "../../../types/data-provider";
|
|
9
|
-
import type { Quote } from "../../../types/financials";
|
|
9
|
+
import type { Quote, TickerFinancials } from "../../../types/financials";
|
|
10
10
|
import type { NewsArticle, NewsQuery } from "../../../news/types";
|
|
11
11
|
import type { TickerRecord, TickerMetadata } from "../../../types/ticker";
|
|
12
12
|
import { newsProvider, type CapabilityManifest } from "../../../capabilities";
|
|
@@ -21,7 +21,6 @@ import { debugLog } from "../../../utils/debug-log";
|
|
|
21
21
|
import { measurePerf } from "../../../utils/perf-marks";
|
|
22
22
|
import { getRendererBuiltinPlugins } from "../../../plugins/catalog-ui";
|
|
23
23
|
|
|
24
|
-
const REMOTE_DATA_REQUEST_CACHE_LIMIT = 150;
|
|
25
24
|
const servicesLog = debugLog.createLogger("services");
|
|
26
25
|
const ASSET_DATA_CAPABILITY_ID = "asset-data.asset-data-router";
|
|
27
26
|
const NEWS_CAPABILITY_ID = "news.core";
|
|
@@ -58,6 +57,9 @@ type PayloadBuilder = (...args: any[]) => Record<string, unknown>;
|
|
|
58
57
|
|
|
59
58
|
const assetDataPayloads: Record<string, PayloadBuilder> = {
|
|
60
59
|
canProvide: (ticker, exchange, context) => ({ ticker, exchange, context }),
|
|
60
|
+
getCachedFinancialsForTargets: (targets, options) => ({ targets, options }),
|
|
61
|
+
getQuotesBatch: (targets, options) => ({ targets, options }),
|
|
62
|
+
getTickerFinancialsBatch: (targets, options) => ({ targets, options }),
|
|
61
63
|
getTickerFinancials: (ticker, exchange, context) => ({ ticker, exchange, context }),
|
|
62
64
|
getQuote: (ticker, exchange, context) => ({ ticker, exchange, context }),
|
|
63
65
|
getExchangeRate: (fromCurrency) => ({ fromCurrency }),
|
|
@@ -96,21 +98,16 @@ function hasRendererOperation(operations: Set<string> | null, operationId: strin
|
|
|
96
98
|
}
|
|
97
99
|
|
|
98
100
|
function createCapabilityInvoker() {
|
|
99
|
-
const
|
|
101
|
+
const inFlightRequests = new Map<string, Promise<unknown>>();
|
|
100
102
|
return function invoke<T>(capabilityId: string, operationId: string, payload: unknown): Promise<T> {
|
|
101
103
|
const requestPayload = { capabilityId, operationId, payload };
|
|
102
104
|
const key = JSON.stringify(requestPayload);
|
|
103
|
-
const
|
|
104
|
-
if (
|
|
105
|
-
const promise = backendRequest<T>("capability.invoke", requestPayload).
|
|
106
|
-
|
|
107
|
-
throw error;
|
|
105
|
+
const inFlight = inFlightRequests.get(key);
|
|
106
|
+
if (inFlight) return inFlight as Promise<T>;
|
|
107
|
+
const promise = backendRequest<T>("capability.invoke", requestPayload).finally(() => {
|
|
108
|
+
inFlightRequests.delete(key);
|
|
108
109
|
});
|
|
109
|
-
|
|
110
|
-
if (requestCache.size > REMOTE_DATA_REQUEST_CACHE_LIMIT) {
|
|
111
|
-
const oldestKey = requestCache.keys().next().value;
|
|
112
|
-
if (oldestKey) requestCache.delete(oldestKey);
|
|
113
|
-
}
|
|
110
|
+
inFlightRequests.set(key, promise);
|
|
114
111
|
return promise;
|
|
115
112
|
};
|
|
116
113
|
}
|
|
@@ -120,10 +117,69 @@ function createRemoteAssetDataClient(): RemoteAssetDataClient {
|
|
|
120
117
|
const assetDataOperations = getRendererOperationIds(ASSET_DATA_CAPABILITY_ID);
|
|
121
118
|
const newsOperations = getRendererOperationIds(NEWS_CAPABILITY_ID);
|
|
122
119
|
let nextSubscriptionId = 1;
|
|
120
|
+
const quoteListeners = new Map<string, {
|
|
121
|
+
target: QuoteSubscriptionTarget;
|
|
122
|
+
listeners: Set<(target: QuoteSubscriptionTarget, quote: Quote) => void>;
|
|
123
|
+
}>();
|
|
124
|
+
let quoteBackendSubscriptionId: string | null = null;
|
|
125
|
+
let disposeQuoteBackendMessages: (() => void) | null = null;
|
|
126
|
+
let quoteBackendFlushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
127
|
+
|
|
128
|
+
const quoteTargetKey = (target: QuoteSubscriptionTarget) =>
|
|
129
|
+
`${target.symbol}:${target.exchange ?? ""}:${target.context?.brokerId ?? ""}:${target.context?.brokerInstanceId ?? ""}:${target.context?.instrument?.conId ?? target.context?.instrument?.localSymbol ?? ""}`;
|
|
130
|
+
|
|
131
|
+
const scheduleQuoteBackendSubscriptionFlush = () => {
|
|
132
|
+
if (quoteBackendFlushTimer) return;
|
|
133
|
+
quoteBackendFlushTimer = setTimeout(() => {
|
|
134
|
+
quoteBackendFlushTimer = null;
|
|
135
|
+
flushQuoteBackendSubscription();
|
|
136
|
+
}, 25);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const flushQuoteBackendSubscription = () => {
|
|
140
|
+
if (!hasRendererOperation(assetDataOperations, "subscribeQuotes")) return;
|
|
141
|
+
const targets = [...quoteListeners.values()].map((entry) => entry.target);
|
|
142
|
+
const previousSubscriptionId = quoteBackendSubscriptionId;
|
|
143
|
+
if (previousSubscriptionId) {
|
|
144
|
+
quoteBackendSubscriptionId = null;
|
|
145
|
+
disposeQuoteBackendMessages?.();
|
|
146
|
+
disposeQuoteBackendMessages = null;
|
|
147
|
+
void backendRequest("capability.unsubscribe", { subscriptionId: previousSubscriptionId }).catch(() => {});
|
|
148
|
+
}
|
|
149
|
+
if (targets.length === 0) return;
|
|
150
|
+
|
|
151
|
+
const subscriptionId = `quote:${nextSubscriptionId++}`;
|
|
152
|
+
quoteBackendSubscriptionId = subscriptionId;
|
|
153
|
+
disposeQuoteBackendMessages = onCapabilityEvent(subscriptionId, (message) => {
|
|
154
|
+
const event = message.event as { target: QuoteSubscriptionTarget; quote: Quote };
|
|
155
|
+
const key = quoteTargetKey(event.target);
|
|
156
|
+
for (const listener of quoteListeners.get(key)?.listeners ?? []) {
|
|
157
|
+
listener(event.target, event.quote);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
void backendRequest("capability.subscribe", {
|
|
161
|
+
subscriptionId,
|
|
162
|
+
capabilityId: ASSET_DATA_CAPABILITY_ID,
|
|
163
|
+
operationId: "subscribeQuotes",
|
|
164
|
+
payload: { targets },
|
|
165
|
+
}).catch((error) => {
|
|
166
|
+
if (quoteBackendSubscriptionId === subscriptionId) {
|
|
167
|
+
quoteBackendSubscriptionId = null;
|
|
168
|
+
disposeQuoteBackendMessages?.();
|
|
169
|
+
disposeQuoteBackendMessages = null;
|
|
170
|
+
}
|
|
171
|
+
console.error("Failed to subscribe to backend quotes", error);
|
|
172
|
+
});
|
|
173
|
+
};
|
|
174
|
+
|
|
123
175
|
const base = {
|
|
124
176
|
id: "desktop-backend",
|
|
125
177
|
name: "Gloomberb Backend",
|
|
126
|
-
getCachedFinancialsForTargets: () =>
|
|
178
|
+
getCachedFinancialsForTargets: (targets, options) => (
|
|
179
|
+
hasRendererOperation(assetDataOperations, "getCachedFinancialsForTargets")
|
|
180
|
+
? invoke<Map<string, TickerFinancials>>(ASSET_DATA_CAPABILITY_ID, "getCachedFinancialsForTargets", { targets, options })
|
|
181
|
+
: new Map()
|
|
182
|
+
),
|
|
127
183
|
getNews: (query: NewsQuery) => (
|
|
128
184
|
hasRendererOperation(newsOperations, "fetchNews")
|
|
129
185
|
? invoke<NewsArticle[]>(NEWS_CAPABILITY_ID, "fetchNews", { query })
|
|
@@ -142,28 +198,29 @@ function createRemoteAssetDataClient(): RemoteAssetDataClient {
|
|
|
142
198
|
).values()];
|
|
143
199
|
if (uniqueTargets.length === 0) return () => {};
|
|
144
200
|
|
|
145
|
-
const subscriptionId = `quote:${nextSubscriptionId++}`;
|
|
146
|
-
const disposeMessages = onCapabilityEvent(subscriptionId, (message) => {
|
|
147
|
-
const event = message.event as { target: QuoteSubscriptionTarget; quote: Quote };
|
|
148
|
-
onQuote(event.target, event.quote);
|
|
149
|
-
});
|
|
150
201
|
let disposed = false;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
console.error("Failed to subscribe to backend quotes", error);
|
|
160
|
-
});
|
|
202
|
+
for (const target of uniqueTargets) {
|
|
203
|
+
const key = quoteTargetKey(target);
|
|
204
|
+
const entry = quoteListeners.get(key) ?? { target, listeners: new Set() };
|
|
205
|
+
entry.target = target;
|
|
206
|
+
entry.listeners.add(onQuote);
|
|
207
|
+
quoteListeners.set(key, entry);
|
|
208
|
+
}
|
|
209
|
+
scheduleQuoteBackendSubscriptionFlush();
|
|
161
210
|
|
|
162
211
|
return () => {
|
|
163
212
|
if (disposed) return;
|
|
164
213
|
disposed = true;
|
|
165
|
-
|
|
166
|
-
|
|
214
|
+
for (const target of uniqueTargets) {
|
|
215
|
+
const key = quoteTargetKey(target);
|
|
216
|
+
const entry = quoteListeners.get(key);
|
|
217
|
+
if (!entry) continue;
|
|
218
|
+
entry.listeners.delete(onQuote);
|
|
219
|
+
if (entry.listeners.size === 0) {
|
|
220
|
+
quoteListeners.delete(key);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
scheduleQuoteBackendSubscriptionFlush();
|
|
167
224
|
};
|
|
168
225
|
},
|
|
169
226
|
};
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type ContextMenuSelectMessage,
|
|
8
8
|
type DesktopDockPreviewMessage,
|
|
9
9
|
type DesktopStateMessage,
|
|
10
|
+
type DesktopThemePreviewMessage,
|
|
10
11
|
type ElectrobunBackendInit,
|
|
11
12
|
type ElectrobunDesktopRpcSchema,
|
|
12
13
|
type UpdateProgressMessage,
|
|
@@ -17,6 +18,7 @@ type ContextMenuSelectListener = (message: ContextMenuSelectMessage) => void;
|
|
|
17
18
|
type ApplicationMenuSelectListener = (message: ApplicationMenuSelectMessage) => void;
|
|
18
19
|
type DesktopStateListener = (message: DesktopStateMessage) => void;
|
|
19
20
|
type DesktopDockPreviewListener = (message: DesktopDockPreviewMessage) => void;
|
|
21
|
+
type DesktopThemePreviewListener = (message: DesktopThemePreviewMessage) => void;
|
|
20
22
|
type UpdateProgressListener = (message: UpdateProgressMessage) => void;
|
|
21
23
|
type CapabilityEventListener = (message: CapabilityEventMessage) => void;
|
|
22
24
|
|
|
@@ -25,6 +27,7 @@ const contextMenuSelectListeners = new Map<string, Set<ContextMenuSelectListener
|
|
|
25
27
|
const applicationMenuSelectListeners = new Set<ApplicationMenuSelectListener>();
|
|
26
28
|
const desktopStateListeners = new Set<DesktopStateListener>();
|
|
27
29
|
const desktopDockPreviewListeners = new Set<DesktopDockPreviewListener>();
|
|
30
|
+
const desktopThemePreviewListeners = new Set<DesktopThemePreviewListener>();
|
|
28
31
|
const updateProgressListeners = new Set<UpdateProgressListener>();
|
|
29
32
|
const capabilityEventListeners = new Map<string, Set<CapabilityEventListener>>();
|
|
30
33
|
|
|
@@ -81,6 +84,11 @@ const rpc = Electroview.defineRPC<ElectrobunDesktopRpcSchema>({
|
|
|
81
84
|
listener({ preview: decodeRpcValue(message.preview) });
|
|
82
85
|
}
|
|
83
86
|
},
|
|
87
|
+
"desktop.themePreview": (message) => {
|
|
88
|
+
for (const listener of desktopThemePreviewListeners) {
|
|
89
|
+
listener({ preview: decodeRpcValue(message.preview) });
|
|
90
|
+
}
|
|
91
|
+
},
|
|
84
92
|
"update.progress": (message) => {
|
|
85
93
|
const decoded = { progress: decodeRpcValue<UpdateProgressMessage["progress"]>(message.progress) };
|
|
86
94
|
for (const listener of updateProgressListeners) {
|
|
@@ -165,6 +173,13 @@ export function onDesktopDockPreview(listener: DesktopDockPreviewListener): () =
|
|
|
165
173
|
};
|
|
166
174
|
}
|
|
167
175
|
|
|
176
|
+
export function onDesktopThemePreview(listener: DesktopThemePreviewListener): () => void {
|
|
177
|
+
desktopThemePreviewListeners.add(listener);
|
|
178
|
+
return () => {
|
|
179
|
+
desktopThemePreviewListeners.delete(listener);
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
168
183
|
export function onUpdateProgress(listener: UpdateProgressListener): () => void {
|
|
169
184
|
updateProgressListeners.add(listener);
|
|
170
185
|
return () => {
|