import type { CapabilityManifest } from "../../../../capabilities"; import type { NewsArticle, NewsQuery } from "../../../../news/types"; import type { DataProvider } from "../../../../types/data-provider"; import type { TickerFinancials } from "../../../../types/financials"; import { backendRequest, getElectrobunBackendInitSnapshot, onCapabilityEvent } from "../backend-rpc"; import { setForwardedServerClockOffset } from "../../../../market-data/quotes/clock"; import { createBackendQuoteSubscription } from "./backend-quote-subscription"; import { createCapabilityInvoker } from "./capability-invoker"; import { RemoteQuoteSubscriptionRegistry } from "./quote-subscription-registry"; const ASSET_DATA_CAPABILITY_ID = "asset-data.asset-data-router"; const NEWS_CAPABILITY_ID = "news.core"; const ASSET_DATA_REQUEST_TIMEOUT_MS = 115_000; export type RemoteAssetDataClient = DataProvider & { getNews(query: NewsQuery): Promise; }; interface RemoteAssetDataClientBase { id: string; name: string; getCachedFinancialsForTargets: NonNullable; getNews(query: NewsQuery): Promise; subscribeQuotes: NonNullable; } type PayloadBuilder = (...args: unknown[]) => Record; const assetDataPayloads: Record = { canProvide: (ticker, exchange, context) => ({ ticker, exchange, context }), primaryMarketSourceName: () => ({}), getCachedFinancialsForTargets: (targets, options) => ({ targets, options }), getQuotesBatch: (targets, options) => ({ targets, options }), getTickerFinancialsBatch: (targets, options) => ({ targets, options }), getTickerFinancials: (ticker, exchange, context) => ({ ticker, exchange, context }), getQuote: (ticker, exchange, context) => ({ ticker, exchange, context }), getQuoteMetadata: (ticker, exchange, context) => ({ ticker, exchange, context }), getExchangeRate: (fromCurrency) => ({ fromCurrency }), search: (query, context) => ({ query, context }), getSecFilings: (ticker, count, exchange, context) => ({ ticker, count, exchange, context }), getHolders: (ticker, exchange, context) => ({ ticker, exchange, context }), getAnalystResearch: (ticker, exchange, context) => ({ ticker, exchange, context }), getCorporateActions: (ticker, exchange, context) => ({ ticker, exchange, context }), getSecFilingDocuments: (filing) => ({ filing }), getSecFilingContent: (filing) => ({ filing }), getEarningsCalendar: (symbols, context) => ({ symbols, context }), getArticleSummary: (url) => ({ url }), getPriceHistory: (ticker, exchange, range, context) => ({ ticker, exchange, range, context }), getPriceHistoryWithMetadata: (ticker, exchange, range, context) => ({ ticker, exchange, range, context }), getPriceHistoryForResolutionWithMetadata: (ticker, exchange, bufferRange, resolution, context) => ({ ticker, exchange, bufferRange, resolution, context }), getDetailedPriceHistoryWithMetadata: (ticker, exchange, startDate, endDate, barSize, context) => ({ ticker, exchange, startDate, endDate, barSize, context }), getPriceHistoryForResolution: (ticker, exchange, bufferRange, resolution, context) => ({ ticker, exchange, bufferRange, resolution, context }), getDetailedPriceHistory: (ticker, exchange, startDate, endDate, barSize, context) => ({ ticker, exchange, startDate, endDate, barSize, context }), getChartResolutionSupport: (ticker, exchange, context) => ({ ticker, exchange, context }), getChartResolutionCapabilities: (ticker, exchange, context) => ({ ticker, exchange, context }), getOptionsChain: (ticker, exchange, expirationDate, context) => ({ ticker, exchange, expirationDate, context }), }; function findCapabilityManifest(capabilityId: string): CapabilityManifest | null { return getElectrobunBackendInitSnapshot()?.capabilityManifests.find((manifest) => manifest.id === capabilityId) ?? null; } function getRendererOperationIds(capabilityId: string): Set | null { const manifest = findCapabilityManifest(capabilityId); if (!manifest) return null; return new Set( manifest.operations .filter((operation) => operation.rendererSafe) .map((operation) => operation.id), ); } function hasRendererOperation(operations: Set | null, operationId: string): boolean { return operations === null || operations.has(operationId); } export function createRemoteAssetDataClient(): RemoteAssetDataClient { const invoke = createCapabilityInvoker({ request: backendRequest, shouldApplyDeadline: (capabilityId) => capabilityId === ASSET_DATA_CAPABILITY_ID, timeoutMs: ASSET_DATA_REQUEST_TIMEOUT_MS, }); const assetDataOperations = getRendererOperationIds(ASSET_DATA_CAPABILITY_ID); const newsOperations = getRendererOperationIds(NEWS_CAPABILITY_ID); let quoteBackendFlushTimer: ReturnType | null = null; const scheduleQuoteBackendSubscriptionFlush = () => { if (quoteBackendFlushTimer) return; quoteBackendFlushTimer = setTimeout(() => { quoteBackendFlushTimer = null; flushQuoteBackendSubscription(); }, 25); }; const quoteSubscriptions = new RemoteQuoteSubscriptionRegistry(scheduleQuoteBackendSubscriptionFlush); const quoteBackend = createBackendQuoteSubscription({ subscribe: (subscriptionId, targets) => backendRequest("capability.subscribe", { subscriptionId, capabilityId: ASSET_DATA_CAPABILITY_ID, operationId: "subscribeQuotes", payload: { targets }, }), unsubscribe: (subscriptionId) => { void backendRequest("capability.unsubscribe", { subscriptionId }).catch(() => {}); }, onEvent: (subscriptionId, listener) => onCapabilityEvent(subscriptionId, (message) => listener(message.event)), dispatch: (target, quote) => quoteSubscriptions.dispatch(target, quote), onClockOffset: setForwardedServerClockOffset, onError: (error) => console.error("Failed to subscribe to backend quotes", error), }); const flushQuoteBackendSubscription = () => { if (!hasRendererOperation(assetDataOperations, "subscribeQuotes")) return; quoteBackend.sync(quoteSubscriptions.backendTargets()); }; const base: RemoteAssetDataClientBase = { id: "desktop-backend", name: "Gloomberb Backend", getCachedFinancialsForTargets: (targets, options) => ( hasRendererOperation(assetDataOperations, "getCachedFinancialsForTargets") ? invoke>(ASSET_DATA_CAPABILITY_ID, "getCachedFinancialsForTargets", { targets, options }) : new Map() ), getNews: (query) => ( hasRendererOperation(newsOperations, "fetchNews") ? invoke(NEWS_CAPABILITY_ID, "fetchNews", { query }) : Promise.resolve([]) ), subscribeQuotes: (targets, onQuote) => { if (!hasRendererOperation(assetDataOperations, "subscribeQuotes")) return () => {}; return quoteSubscriptions.subscribe(targets, onQuote); }, }; return new Proxy(base, { get(target, prop, receiver) { if (typeof prop !== "string" || prop in target) { return Reflect.get(target, prop, receiver); } const buildPayload = assetDataPayloads[prop]; if (!buildPayload || !hasRendererOperation(assetDataOperations, prop)) return undefined; return (...args: unknown[]) => invoke(ASSET_DATA_CAPABILITY_ID, prop, buildPayload(...args)); }, }) as RemoteAssetDataClient; }