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.
- package/package.json +8 -1
- package/src/api-client/auth.ts +1 -0
- package/src/api-client/index.test.ts +359 -108
- package/src/api-client/index.ts +24 -13
- package/src/api-client/socket.ts +70 -35
- package/src/api-client/types.ts +1 -1
- package/src/app/language-observer.test.tsx +82 -0
- package/src/app/language-observer.tsx +13 -0
- package/src/app/pane-runtime/index.tsx +2 -2
- package/src/app/pane-runtime/pane-settings-runtime.tsx +2 -2
- package/src/app/pane-runtime/pane-template-runtime.ts +2 -2
- package/src/app/pane-runtime/plugin-bindings.ts +2 -2
- package/src/app/pane-runtime/ticker-open-runtime.ts +2 -2
- package/src/app/runtime/broker-import.ts +2 -2
- package/src/app/runtime/plugin-bindings.ts +2 -2
- package/src/app/runtime/startup.ts +2 -2
- package/src/app.tsx +16 -7
- package/src/brokers/account-cache.ts +6 -6
- package/src/brokers/broker-ticker-sync.ts +3 -3
- package/src/brokers/remote-broker-adapter.ts +2 -2
- package/src/brokers/sync-broker-instance.ts +5 -6
- package/src/cli/commands/api.ts +3 -6
- package/src/cli/commands/automation.ts +7 -15
- package/src/cli/commands/market.ts +26 -61
- package/src/cli/commands/overview.ts +34 -45
- package/src/cli/commands/search.ts +25 -27
- package/src/cli/commands/system.ts +27 -57
- package/src/cli/commands/ticker.ts +62 -63
- package/src/cli/context.test.ts +63 -0
- package/src/cli/context.ts +15 -1
- package/src/cli/index.ts +1 -1
- package/src/cli/pane-functions/data.ts +2 -19
- package/src/cli/pane-functions/index.ts +26 -25
- package/src/cli/pane-functions/report.ts +1 -1
- package/src/cli/pane-functions/screenshot.ts +3 -3
- package/src/cli/scoped-context.ts +41 -0
- package/src/components/chart/composite/composite-chart.test.tsx +615 -18
- package/src/components/chart/composite/composite-chart.tsx +179 -79
- package/src/components/chart/composite/format.test.ts +20 -7
- package/src/components/chart/composite/index.ts +0 -21
- package/src/components/chart/composite/interactions.test.ts +90 -3
- package/src/components/chart/composite/interactions.ts +189 -24
- package/src/components/chart/composite/price-series.ts +0 -2
- package/src/components/chart/composite/rasterizer.ts +10 -9
- package/src/components/chart/composite/renderers.test.ts +108 -2
- package/src/components/chart/composite/scene.test.ts +36 -0
- package/src/components/chart/composite/scene.ts +5 -2
- package/src/components/chart/composite/text-renderer.ts +37 -25
- package/src/components/chart/composite/time-axis.test.ts +243 -0
- package/src/components/chart/composite/time-axis.ts +608 -0
- package/src/components/chart/composite/types.ts +12 -1
- package/src/components/chart/core/data.ts +2 -159
- package/src/components/chart/core/date-window.ts +6 -225
- package/src/components/chart/core/palette.ts +1 -12
- package/src/components/chart/core/pointer.ts +0 -142
- package/src/components/chart/core/types.ts +1 -58
- package/src/components/chart/cursor-motion.ts +2 -4
- package/src/components/chart/indicators/types.ts +0 -9
- package/src/components/chart/market-session.ts +0 -11
- package/src/components/chart/native/kitty/protocol.ts +1 -1
- package/src/components/chart/native/raster/layout.ts +3 -10
- package/src/components/chart/static/chart/axis-overlays.test.tsx +64 -0
- package/src/components/chart/static/chart/axis-overlays.tsx +59 -14
- package/src/components/chart/static/chart/surface.tsx +1 -2
- package/src/components/command-bar/action-runtime.ts +2 -2
- package/src/components/command-bar/commands/direct/index.ts +1 -1
- package/src/components/command-bar/panel/body.tsx +2 -2
- package/src/components/command-bar/panel/index.tsx +11 -9
- package/src/components/command-bar/routes/ticker-search/actions.ts +2 -2
- package/src/components/command-bar/surface/index.tsx +2 -2
- package/src/components/command-bar/workflow/body.tsx +3 -3
- package/src/components/command-bar/workflow/collection-actions.ts +2 -2
- package/src/components/command-bar/workflow/coordinator.ts +2 -2
- package/src/components/command-bar/workflow/field-row.tsx +25 -11
- package/src/components/command-bar/workflow/tickers.ts +2 -2
- package/src/components/feed-data-table/stack-view.test.tsx +28 -36
- package/src/components/feed-data-table/stack-view.tsx +3 -1
- package/src/components/layout/detached-pane-shell.tsx +6 -6
- package/src/components/layout/header.tsx +13 -9
- package/src/components/layout/pane/content.test.tsx +42 -3
- package/src/components/layout/pane/content.tsx +3 -1
- package/src/components/layout/pane/footer/index.test.tsx +45 -2
- package/src/components/layout/pane/footer/registration.tsx +3 -1
- package/src/components/layout/shell/index.tsx +1 -2
- package/src/components/layout/shell/native/surfaces.ts +1 -1
- package/src/components/layout/shell/native/window-state.ts +1 -1
- package/src/components/layout/status-bar.tsx +12 -8
- package/src/components/layout/window-controls.tsx +8 -6
- package/src/components/onboarding/onboarding-wizard.tsx +3 -1
- package/src/components/pane-settings-dialog/desktop.tsx +0 -2
- package/src/components/pane-settings-dialog.tsx +0 -2
- package/src/components/pane-template-wizard.tsx +10 -9
- package/src/components/price-performance.tsx +2 -113
- package/src/components/price-sparkline/model.ts +15 -17
- package/src/components/price-sparkline/view.tsx +8 -7
- package/src/components/speedometer-gauge/desktop.tsx +2 -1
- package/src/components/speedometer-gauge/index.tsx +0 -2
- package/src/components/speedometer-gauge/terminal.tsx +24 -5
- package/src/components/ticker/badge/index.tsx +1 -18
- package/src/components/ui/data-table/opentui/index.tsx +3 -3
- package/src/components/ui/frame.tsx +1 -2
- package/src/components/ui/page-stack-view.test.tsx +126 -0
- package/src/components/ui/page-stack-view.tsx +24 -12
- package/src/components/ui/tabs.tsx +7 -3
- package/src/components/ui/ui.test.tsx +23 -2
- package/src/components/use-external-link-footer.ts +4 -2
- package/src/core/app-service-ports.ts +46 -0
- package/src/core/app-services.test.ts +66 -0
- package/src/core/app-services.ts +17 -15
- package/src/data/config/chart-settings.ts +3 -10
- package/src/data/config/store/index.test.ts +99 -2
- package/src/data/config/store/migrations.ts +278 -0
- package/src/data/config/store/normalize.ts +44 -186
- package/src/data/resource-store.ts +73 -12
- package/src/data/sqlite/cache.test.ts +104 -15
- package/src/i18n/index.test.ts +17 -0
- package/src/i18n/index.ts +23 -6
- package/src/i18n/ja.ts +13 -0
- package/src/i18n/ko.ts +13 -0
- package/src/i18n/react.ts +6 -0
- package/src/i18n/zh-cn.ts +13 -0
- package/src/i18n/zh-tw.ts +13 -0
- package/src/market-data/coordinator/auxiliary.ts +7 -3
- package/src/market-data/coordinator/chart.ts +1 -1
- package/src/market-data/coordinator/index.test.ts +26 -0
- package/src/market-data/coordinator/index.ts +11 -5
- package/src/market-data/coordinator/quotes.ts +4 -40
- package/src/market-data/coordinator/subscriptions.test.ts +89 -1
- package/src/market-data/hooks.tsx +47 -1
- package/src/market-data/market/status.ts +6 -6
- package/src/market-data/performance.ts +1 -1
- package/src/market-data/quote-subscription-target.test.ts +37 -0
- package/src/market-data/quote-subscription-target.ts +60 -0
- package/src/market-data/quotes/freshness.ts +1 -0
- package/src/market-data/quotes/resolution.ts +4 -0
- package/src/market-data/request-types.ts +3 -3
- package/src/news/aggregator.test.ts +97 -1
- package/src/news/aggregator.ts +100 -57
- package/src/news/hooks.test.tsx +64 -0
- package/src/news/hooks.ts +18 -25
- package/src/plugins/builtin/account-management/footer.ts +4 -2
- package/src/plugins/builtin/account-management/pane.tsx +9 -4
- package/src/plugins/builtin/ai/agent-history.ts +11 -7
- package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +0 -5
- package/src/plugins/builtin/ai/ask-ai.test.tsx +3 -4
- package/src/plugins/builtin/ai/index.test.ts +2 -2
- package/src/plugins/builtin/ai/index.tsx +10 -11
- package/src/plugins/builtin/ai/pane-settings.ts +1 -1
- package/src/plugins/builtin/ai/pi/runtime.ts +6 -5
- package/src/plugins/builtin/ai/providers.ts +1 -9
- package/src/plugins/builtin/ai/runner.ts +0 -6
- package/src/plugins/builtin/ai/screener/footer.ts +4 -1
- package/src/plugins/builtin/ai/screener/pane.test.tsx +9 -9
- package/src/plugins/builtin/ai/workspace/model.ts +2 -27
- package/src/plugins/builtin/ai/workspace/pane.tsx +2 -2
- package/src/plugins/builtin/analytics/metrics.ts +1 -7
- package/src/plugins/builtin/broker-manager/detail.tsx +3 -2
- package/src/plugins/builtin/broker-manager/index.tsx +4 -2
- package/src/plugins/builtin/chart-composer/editor/controller.ts +643 -0
- package/src/plugins/builtin/chart-composer/editor/desktop-view.tsx +134 -0
- package/src/plugins/builtin/chart-composer/editor/model.test.ts +85 -0
- package/src/plugins/builtin/chart-composer/editor/model.ts +292 -0
- package/src/plugins/builtin/chart-composer/editor/opentui-view.tsx +108 -0
- package/src/plugins/builtin/chart-composer/editor.tsx +65 -947
- package/src/plugins/builtin/chart-composer/index.tsx +0 -6
- package/src/plugins/builtin/chart-composer/pane.tsx +69 -36
- package/src/plugins/builtin/chart-composer/presets.ts +1 -19
- package/src/plugins/builtin/chart-composer/settings.ts +1 -1
- package/src/plugins/builtin/chat/content/composer-runtime.ts +1 -1
- package/src/plugins/builtin/chat/content/shortcuts.ts +0 -1
- package/src/plugins/builtin/chat/controller/fetch.ts +8 -2
- package/src/plugins/builtin/chat/controller/persistence.ts +2 -0
- package/src/plugins/builtin/chat/controller/session.test.ts +25 -0
- package/src/plugins/builtin/chat/controller/session.ts +2 -0
- package/src/plugins/builtin/chat/controller/state.ts +7 -0
- package/src/plugins/builtin/chat/controller.test.ts +73 -20
- package/src/plugins/builtin/chat/message/desktop.tsx +2 -0
- package/src/plugins/builtin/cloud-tweets/footer.ts +9 -28
- package/src/plugins/builtin/cloud-tweets/pane.tsx +0 -5
- package/src/plugins/builtin/cloud-tweets/table.tsx +12 -17
- package/src/plugins/builtin/correlation/compute.ts +4 -10
- package/src/plugins/builtin/econ/index.tsx +17 -19
- package/src/plugins/builtin/holders/types.ts +0 -1
- package/src/plugins/builtin/insider/index.tsx +6 -31
- package/src/plugins/builtin/news/index.tsx +5 -7
- package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +6 -8
- package/src/plugins/builtin/news/wire/news/footer.ts +10 -11
- package/src/plugins/builtin/news/wire/read-state.test.ts +7 -0
- package/src/plugins/builtin/news/wire/read-state.ts +18 -28
- package/src/plugins/builtin/news/wire/rss/parser.ts +1 -11
- package/src/plugins/builtin/options/footer.test.ts +115 -0
- package/src/plugins/builtin/options/footer.ts +110 -0
- package/src/plugins/builtin/options/live-quotes.test.ts +238 -0
- package/src/plugins/builtin/options/live-quotes.ts +147 -0
- package/src/plugins/builtin/options/view.test.tsx +71 -1
- package/src/plugins/builtin/options/view.tsx +39 -24
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +103 -117
- package/src/plugins/builtin/portfolio-list/cli/render.ts +11 -5
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +120 -135
- package/src/plugins/builtin/portfolio-list/header.tsx +9 -4
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +6 -1
- package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +4 -2
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +24 -52
- package/src/plugins/builtin/sec/filing-content.test.ts +41 -0
- package/src/plugins/builtin/sec/filing-content.ts +94 -0
- package/src/plugins/builtin/sec/index.tsx +21 -104
- package/src/plugins/builtin/sectors/sector-model.ts +1 -7
- package/src/plugins/builtin/shared/cloud-upgrade.ts +1 -0
- package/src/plugins/builtin/shared/pane-footer.ts +82 -0
- package/src/plugins/builtin/shared/read-state.ts +64 -3
- package/src/plugins/builtin/substack/read-state.ts +16 -41
- package/src/plugins/builtin/thirteenf/pane.tsx +24 -73
- package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +10 -10
- package/src/plugins/builtin/yield-curve/index.tsx +11 -9
- package/src/plugins/ibkr/dialogs.tsx +3 -3
- package/src/plugins/ibkr/gateway/fundamentals.ts +1 -1
- package/src/plugins/ibkr/gateway/history-requests.ts +2 -2
- package/src/plugins/ibkr/gateway/history.ts +2 -2
- package/src/plugins/ibkr/gateway/selection.ts +2 -2
- package/src/plugins/ibkr/gateway/service/index.ts +2 -2
- package/src/plugins/ibkr/gateway/service/native.ts +2 -2
- package/src/plugins/plugin-persistence.ts +3 -4
- package/src/plugins/prediction-markets/controller/keyboard.ts +1 -1
- package/src/plugins/prediction-markets/controller/live-updates.ts +6 -5
- package/src/plugins/prediction-markets/detail/book.tsx +2 -2
- package/src/plugins/prediction-markets/detail/outcomes.tsx +3 -1
- package/src/plugins/prediction-markets/detail/pane.tsx +3 -1
- package/src/plugins/prediction-markets/detail/trades.tsx +3 -3
- package/src/plugins/registry/context.ts +3 -4
- package/src/plugins/registry/index.ts +5 -6
- package/src/react/input.test.ts +126 -3
- package/src/react/input.ts +120 -1
- package/src/remote/app-host.tsx +2 -13
- package/src/renderers/electrobun/bun/desktop/backend-requests.ts +44 -60
- package/src/renderers/electrobun/bun/desktop/capability-bridge.ts +15 -13
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +26 -22
- package/src/renderers/electrobun/bun/desktop/http-fetch.ts +5 -1
- package/src/renderers/electrobun/bun/desktop/initialization.ts +11 -6
- package/src/renderers/electrobun/bun/desktop/media.ts +5 -3
- package/src/renderers/electrobun/bun/desktop/plugin-state.ts +21 -22
- package/src/renderers/electrobun/bun/desktop/window-controls.ts +3 -1
- package/src/renderers/electrobun/bun/desktop/workspace/requests.ts +18 -18
- package/src/renderers/electrobun/bun/index.ts +123 -83
- package/src/renderers/electrobun/shared/protocol.ts +146 -7
- package/src/renderers/electrobun/view/app-services.ts +5 -8
- package/src/renderers/electrobun/view/backend-rpc.ts +18 -8
- package/src/renderers/electrobun/view/broker-remote-client.ts +2 -2
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +5 -22
- package/src/renderers/electrobun/view/config-host.ts +1 -1
- package/src/renderers/electrobun/view/context-menu.ts +1 -17
- package/src/renderers/electrobun/view/data-table/index.tsx +0 -1
- package/src/renderers/electrobun/view/data-table/row.tsx +0 -4
- package/src/renderers/electrobun/view/desktop/control-styles.ts +37 -34
- package/src/renderers/electrobun/view/desktop/controls.tsx +36 -33
- package/src/renderers/electrobun/view/desktop/list-view.tsx +15 -12
- package/src/renderers/electrobun/view/desktop/popover.tsx +2 -3
- package/src/renderers/electrobun/view/dialog-host.tsx +2 -3
- package/src/renderers/electrobun/view/fatal-screen.tsx +0 -1
- package/src/renderers/electrobun/view/host/box.tsx +0 -1
- package/src/renderers/electrobun/view/host/chart-surface.tsx +0 -1
- package/src/renderers/electrobun/view/host/input.tsx +0 -1
- package/src/renderers/electrobun/view/host/media-surface.tsx +0 -1
- package/src/renderers/electrobun/view/host/native.ts +0 -1
- package/src/renderers/electrobun/view/host/scroll-box.tsx +0 -1
- package/src/renderers/electrobun/view/host/tabs.tsx +0 -1
- package/src/renderers/electrobun/view/host/text.tsx +0 -1
- package/src/renderers/electrobun/view/http-fetch.ts +3 -11
- package/src/renderers/electrobun/view/input-host.tsx +10 -53
- package/src/renderers/electrobun/view/key-event.ts +0 -1
- package/src/renderers/electrobun/view/main.tsx +3 -3
- package/src/renderers/electrobun/view/native-renderer.ts +0 -1
- package/src/renderers/electrobun/view/native-stubs/backend-rpc.ts +13 -1
- package/src/renderers/electrobun/view/native-stubs/chart/surface-manager.ts +11 -0
- package/src/renderers/electrobun/view/native-stubs/native-stubs.test.ts +47 -0
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +6 -44
- package/src/renderers/electrobun/view/remote/persistence.ts +6 -2
- package/src/renderers/electrobun/view/remote/quote-subscription-registry.test.ts +86 -0
- package/src/renderers/electrobun/view/remote/quote-subscription-registry.ts +72 -0
- package/src/renderers/electrobun/view/remote/ticker-repository.ts +2 -2
- package/src/renderers/electrobun/view/toast-host.tsx +0 -1
- package/src/renderers/electrobun/view/ui-host.tsx +1 -2
- package/src/renderers/electrobun/view/update-host.ts +1 -1
- package/src/renderers/opentui/chart-surface.tsx +2 -1
- package/src/renderers/opentui/dialog-host.tsx +20 -4
- package/src/renderers/opentui/image/loader.ts +12 -2
- package/src/renderers/opentui/input-host.tsx +7 -49
- package/src/renderers/opentui/start.tsx +2 -0
- package/src/sources/gloomberb-cloud/history-quality.ts +44 -0
- package/src/sources/gloomberb-cloud/index.test.ts +173 -0
- package/src/sources/gloomberb-cloud/index.ts +42 -20
- package/src/sources/gloomberb-cloud/normalizers.ts +9 -3
- package/src/sources/provider-router/cache.ts +1 -1
- package/src/sources/provider-router/document-routes.ts +10 -5
- package/src/sources/provider-router/financials.test.ts +14 -0
- package/src/sources/provider-router/financials.ts +6 -1
- package/src/sources/provider-router/history.test.ts +163 -1
- package/src/sources/provider-router/history.ts +278 -347
- package/src/sources/provider-router/index.ts +2 -2
- package/src/sources/provider-router/routing.test.ts +39 -0
- package/src/sources/provider-router/routing.ts +24 -11
- package/src/sources/provider-router/supplemental.ts +37 -17
- package/src/sources/sec-edgar/content.ts +2 -12
- package/src/sources/sec-edgar.ts +2 -13
- package/src/sources/yahoo-finance/history.ts +2 -2
- package/src/sources/yahoo-finance/options.ts +5 -0
- package/src/sources/yahoo-finance.ts +2 -2
- package/src/state/app/bootstrap.ts +2 -2
- package/src/state/app/context/index.tsx +2 -2
- package/src/state/hooks/quote-streaming.ts +66 -1
- package/src/state/hooks/session-snapshot.ts +2 -2
- package/src/state/selectors.ts +0 -9
- package/src/sync/controller.test.ts +98 -0
- package/src/sync/controller.ts +29 -5
- package/src/sync/react.ts +2 -2
- package/src/sync/types.ts +2 -2
- package/src/theme/colors.test.ts +10 -0
- package/src/theme/colors.ts +75 -78
- package/src/theme/theme-context.test.tsx +67 -0
- package/src/theme/theme-context.tsx +9 -5
- package/src/tickers/open-target.ts +2 -2
- package/src/tickers/search/upsert.ts +2 -2
- package/src/time-series/alignment.ts +0 -7
- package/src/time-series/chart-data.ts +145 -0
- package/src/time-series/date-window.ts +44 -0
- package/src/time-series/history-window.ts +21 -0
- package/src/time-series/hooks.test.ts +59 -0
- package/src/time-series/hooks.ts +33 -12
- package/src/time-series/market-resolution.ts +2 -2
- package/src/time-series/market.ts +1 -1
- package/src/time-series/range.ts +9 -0
- package/src/{components/chart/core → time-series}/resolution.ts +3 -102
- package/src/time-series/resolve.test.ts +203 -1
- package/src/time-series/resolve.ts +96 -18
- package/src/time-series/spec-studies.test.ts +15 -0
- package/src/time-series/spec.ts +2 -2
- package/src/time-series/studies.ts +5 -2
- package/src/time-series/types.ts +3 -1
- package/src/time-series/use-chart-resolution.test.tsx +358 -0
- package/src/time-series/use-chart-resolution.ts +46 -4
- package/src/types/broker.ts +2 -2
- package/src/types/data-provider.ts +3 -3
- package/src/types/financials.ts +12 -0
- package/src/types/plugin.ts +2 -2
- package/src/ui/context-menu.tsx +3 -2
- package/src/ui/dialog.tsx +2 -2
- package/src/utils/html-entities.ts +3 -5
- package/src/utils/main-thread-monitor.ts +1 -1
- package/src/utils/price-history.ts +5 -5
- package/src/version.ts +3 -1
- package/src/components/chart/core/viewport.test.ts +0 -8
- package/src/components/chart/core/viewport.ts +0 -24
- package/src/time-series/index.ts +0 -14
- /package/src/{components/chart/core → time-series}/resolution.test.ts +0 -0
|
@@ -6,6 +6,11 @@ import { trySqliteBusyOperation, withSqliteBusyRetry } from "./sqlite/retry";
|
|
|
6
6
|
|
|
7
7
|
const RESOURCE_CACHE_SOFT_ROW_LIMIT = 25_000;
|
|
8
8
|
const RESOURCE_CACHE_SOFT_SIZE_LIMIT = 100 * 1024 * 1024;
|
|
9
|
+
const RESOURCE_CACHE_MAINTENANCE_WRITE_INTERVAL = 250;
|
|
10
|
+
const RESOURCE_CACHE_MAINTENANCE_BYTE_INTERVAL = Math.floor(
|
|
11
|
+
RESOURCE_CACHE_SOFT_SIZE_LIMIT / RESOURCE_CACHE_MAINTENANCE_WRITE_INTERVAL,
|
|
12
|
+
);
|
|
13
|
+
const RESOURCE_CACHE_MAINTENANCE_TIME_INTERVAL_MS = 5 * 60_000;
|
|
9
14
|
const DEFAULT_RESOURCE_SCHEMA_VERSION = 1;
|
|
10
15
|
const EMPTY_VARIANT_KEY = "";
|
|
11
16
|
const EMPTY_SOURCE_KEY = "";
|
|
@@ -89,6 +94,10 @@ function sortRows(rows: ResourceCacheRow[]): ResourceCacheRow[] {
|
|
|
89
94
|
}
|
|
90
95
|
|
|
91
96
|
export class ResourceStore {
|
|
97
|
+
private writesSinceMaintenance = 0;
|
|
98
|
+
private bytesWrittenSinceMaintenance = 0;
|
|
99
|
+
private lastMaintenanceAt = 0;
|
|
100
|
+
|
|
92
101
|
constructor(private readonly db: Database) {}
|
|
93
102
|
|
|
94
103
|
private toCachedResource<T>(row: ResourceCacheRow): CachedResourceRecord<T> | null {
|
|
@@ -179,17 +188,38 @@ export class ResourceStore {
|
|
|
179
188
|
const remainingSize = remaining?.total_size ?? 0;
|
|
180
189
|
if (remainingCount <= RESOURCE_CACHE_SOFT_ROW_LIMIT && remainingSize <= RESOURCE_CACHE_SOFT_SIZE_LIMIT) return;
|
|
181
190
|
|
|
182
|
-
const
|
|
191
|
+
const pruneCandidates = withSqliteBusyRetry("select cached resources to prune", () => (
|
|
183
192
|
this.db
|
|
184
|
-
.query<{
|
|
185
|
-
|
|
193
|
+
.query<{
|
|
194
|
+
namespace: string;
|
|
195
|
+
kind: string;
|
|
196
|
+
entity_key: string;
|
|
197
|
+
variant_key: string;
|
|
198
|
+
source_key: string;
|
|
199
|
+
size_bytes: number;
|
|
200
|
+
}, []>(
|
|
201
|
+
`SELECT namespace, kind, entity_key, variant_key, source_key, size_bytes
|
|
186
202
|
FROM resource_cache
|
|
187
|
-
ORDER BY last_accessed_at ASC, fetched_at ASC
|
|
188
|
-
LIMIT ?`,
|
|
203
|
+
ORDER BY last_accessed_at ASC, fetched_at ASC`,
|
|
189
204
|
)
|
|
190
|
-
.all(
|
|
205
|
+
.all()
|
|
191
206
|
));
|
|
192
207
|
|
|
208
|
+
let projectedCount = remainingCount;
|
|
209
|
+
let projectedSize = remainingSize;
|
|
210
|
+
const rowsToDelete = [] as typeof pruneCandidates;
|
|
211
|
+
for (const row of pruneCandidates) {
|
|
212
|
+
if (
|
|
213
|
+
projectedCount <= RESOURCE_CACHE_SOFT_ROW_LIMIT
|
|
214
|
+
&& projectedSize <= RESOURCE_CACHE_SOFT_SIZE_LIMIT
|
|
215
|
+
) {
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
rowsToDelete.push(row);
|
|
219
|
+
projectedCount -= 1;
|
|
220
|
+
projectedSize -= row.size_bytes;
|
|
221
|
+
}
|
|
222
|
+
|
|
193
223
|
if (rowsToDelete.length === 0) return;
|
|
194
224
|
withSqliteBusyRetry("prune least recently used cached resources", () => {
|
|
195
225
|
const remove = this.db.query(
|
|
@@ -205,6 +235,21 @@ export class ResourceStore {
|
|
|
205
235
|
});
|
|
206
236
|
}
|
|
207
237
|
|
|
238
|
+
private maintainCacheAfterWrite(now: number, sizeBytes: number): void {
|
|
239
|
+
this.writesSinceMaintenance += 1;
|
|
240
|
+
this.bytesWrittenSinceMaintenance += sizeBytes;
|
|
241
|
+
const maintenanceDue = this.lastMaintenanceAt === 0
|
|
242
|
+
|| this.writesSinceMaintenance >= RESOURCE_CACHE_MAINTENANCE_WRITE_INTERVAL
|
|
243
|
+
|| this.bytesWrittenSinceMaintenance >= RESOURCE_CACHE_MAINTENANCE_BYTE_INTERVAL
|
|
244
|
+
|| now - this.lastMaintenanceAt >= RESOURCE_CACHE_MAINTENANCE_TIME_INTERVAL_MS;
|
|
245
|
+
if (!maintenanceDue) return;
|
|
246
|
+
|
|
247
|
+
this.pruneIfNeeded();
|
|
248
|
+
this.writesSinceMaintenance = 0;
|
|
249
|
+
this.bytesWrittenSinceMaintenance = 0;
|
|
250
|
+
this.lastMaintenanceAt = now;
|
|
251
|
+
}
|
|
252
|
+
|
|
208
253
|
get<T>(key: ResourceCacheKey, options: GetResourceOptions = {}): CachedResourceRecord<T> | null {
|
|
209
254
|
const row = withSqliteBusyRetry("load cached resource", () => (
|
|
210
255
|
this.db
|
|
@@ -304,6 +349,10 @@ export class ResourceStore {
|
|
|
304
349
|
const now = options.fetchedAt ?? Date.now();
|
|
305
350
|
const payload = serializeJson(value);
|
|
306
351
|
const provenance = options.provenance == null ? null : serializeJson(options.provenance);
|
|
352
|
+
const schemaVersion = options.schemaVersion ?? DEFAULT_RESOURCE_SCHEMA_VERSION;
|
|
353
|
+
const staleAt = now + options.cachePolicy.staleMs;
|
|
354
|
+
const expiresAt = now + options.cachePolicy.expireMs;
|
|
355
|
+
const sizeBytes = Buffer.byteLength(payload, "utf8");
|
|
307
356
|
const rowKey = {
|
|
308
357
|
namespace: key.namespace,
|
|
309
358
|
kind: key.kind,
|
|
@@ -326,19 +375,31 @@ export class ResourceStore {
|
|
|
326
375
|
rowKey.entityKey,
|
|
327
376
|
rowKey.variantKey,
|
|
328
377
|
rowKey.sourceKey,
|
|
329
|
-
|
|
378
|
+
schemaVersion,
|
|
330
379
|
payload,
|
|
331
380
|
provenance,
|
|
332
381
|
now,
|
|
333
|
-
|
|
334
|
-
|
|
382
|
+
staleAt,
|
|
383
|
+
expiresAt,
|
|
335
384
|
now,
|
|
336
|
-
|
|
385
|
+
sizeBytes,
|
|
337
386
|
);
|
|
338
387
|
});
|
|
339
388
|
|
|
340
|
-
this.
|
|
341
|
-
return
|
|
389
|
+
this.maintainCacheAfterWrite(Date.now(), sizeBytes);
|
|
390
|
+
return {
|
|
391
|
+
...rowKey,
|
|
392
|
+
value: JSON.parse(payload) as T,
|
|
393
|
+
schemaVersion,
|
|
394
|
+
provenance: provenance == null ? null : JSON.parse(provenance) as JsonValue,
|
|
395
|
+
fetchedAt: now,
|
|
396
|
+
staleAt,
|
|
397
|
+
expiresAt,
|
|
398
|
+
lastAccessedAt: now,
|
|
399
|
+
sizeBytes,
|
|
400
|
+
stale: isStale(staleAt),
|
|
401
|
+
expired: isExpired(expiresAt),
|
|
402
|
+
};
|
|
342
403
|
}
|
|
343
404
|
|
|
344
405
|
delete(key: ResourceCacheKey): void {
|
|
@@ -4,6 +4,7 @@ import { existsSync, rmSync } from "fs";
|
|
|
4
4
|
import { join } from "path";
|
|
5
5
|
import { tmpdir } from "os";
|
|
6
6
|
import { AppPersistence } from "../app-persistence";
|
|
7
|
+
import { ResourceStore } from "../resource-store";
|
|
7
8
|
|
|
8
9
|
const tempPaths: string[] = [];
|
|
9
10
|
|
|
@@ -82,6 +83,109 @@ describe("AppPersistence", () => {
|
|
|
82
83
|
}
|
|
83
84
|
});
|
|
84
85
|
|
|
86
|
+
test("runs resource maintenance on the first write and at write, byte, or time intervals", () => {
|
|
87
|
+
const dbPath = createTempDbPath("resource-maintenance-frequency");
|
|
88
|
+
const persistence = new AppPersistence(dbPath);
|
|
89
|
+
const connection = persistence.database.connection;
|
|
90
|
+
let maintenanceScans = 0;
|
|
91
|
+
const observedConnection = new Proxy(connection, {
|
|
92
|
+
get(target, property) {
|
|
93
|
+
if (property === "query") {
|
|
94
|
+
return (sql: string) => {
|
|
95
|
+
if (sql.includes("SELECT COUNT(*) as count")) maintenanceScans += 1;
|
|
96
|
+
return target.query(sql);
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const value = Reflect.get(target, property, target);
|
|
100
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
101
|
+
},
|
|
102
|
+
}) as Database;
|
|
103
|
+
const resources = new ResourceStore(observedConnection);
|
|
104
|
+
const realDateNow = Date.now;
|
|
105
|
+
let now = 1_000_000;
|
|
106
|
+
Date.now = () => now;
|
|
107
|
+
const write = (entityKey: string) => resources.set({
|
|
108
|
+
namespace: "test",
|
|
109
|
+
kind: "maintenance",
|
|
110
|
+
entityKey,
|
|
111
|
+
}, { entityKey }, {
|
|
112
|
+
cachePolicy: { staleMs: 60_000, expireMs: 120_000 },
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
write("first");
|
|
117
|
+
expect(maintenanceScans).toBe(1);
|
|
118
|
+
|
|
119
|
+
for (let index = 0; index < 249; index++) write(`below-${index}`);
|
|
120
|
+
expect(maintenanceScans).toBe(1);
|
|
121
|
+
|
|
122
|
+
write("write-interval");
|
|
123
|
+
expect(maintenanceScans).toBe(2);
|
|
124
|
+
|
|
125
|
+
write("below-time-interval");
|
|
126
|
+
expect(maintenanceScans).toBe(2);
|
|
127
|
+
|
|
128
|
+
now += 5 * 60_000;
|
|
129
|
+
write("time-interval");
|
|
130
|
+
expect(maintenanceScans).toBe(3);
|
|
131
|
+
|
|
132
|
+
resources.set({
|
|
133
|
+
namespace: "test",
|
|
134
|
+
kind: "maintenance",
|
|
135
|
+
entityKey: "byte-interval",
|
|
136
|
+
}, "x".repeat(512 * 1024), {
|
|
137
|
+
cachePolicy: { staleMs: 60_000, expireMs: 120_000 },
|
|
138
|
+
});
|
|
139
|
+
expect(maintenanceScans).toBe(4);
|
|
140
|
+
} finally {
|
|
141
|
+
Date.now = realDateNow;
|
|
142
|
+
persistence.close();
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("prunes only enough least-recently-used resources to restore the byte budget", () => {
|
|
147
|
+
const dbPath = createTempDbPath("resource-byte-pruning");
|
|
148
|
+
const persistence = new AppPersistence(dbPath);
|
|
149
|
+
const now = Date.now();
|
|
150
|
+
const write = (entityKey: string, value: unknown, fetchedAt: number) => persistence.resources.set({
|
|
151
|
+
namespace: "test",
|
|
152
|
+
kind: "byte-pruning",
|
|
153
|
+
entityKey,
|
|
154
|
+
}, value, {
|
|
155
|
+
cachePolicy: { staleMs: 60_000, expireMs: 10 * 60_000 },
|
|
156
|
+
fetchedAt,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
write("oldest", { value: 1 }, now - 2);
|
|
161
|
+
write("newer", { value: 2 }, now - 1);
|
|
162
|
+
persistence.database.connection.query(
|
|
163
|
+
`UPDATE resource_cache
|
|
164
|
+
SET size_bytes = CASE entity_key
|
|
165
|
+
WHEN 'oldest' THEN ?
|
|
166
|
+
WHEN 'newer' THEN ?
|
|
167
|
+
ELSE size_bytes
|
|
168
|
+
END
|
|
169
|
+
WHERE namespace = 'test' AND kind = 'byte-pruning'`,
|
|
170
|
+
).run(60 * 1024 * 1024, 50 * 1024 * 1024);
|
|
171
|
+
|
|
172
|
+
write("trigger", "x".repeat(512 * 1024), now);
|
|
173
|
+
|
|
174
|
+
const remaining = persistence.database.connection
|
|
175
|
+
.query<{ entity_key: string }, []>(
|
|
176
|
+
`SELECT entity_key
|
|
177
|
+
FROM resource_cache
|
|
178
|
+
WHERE namespace = 'test' AND kind = 'byte-pruning'
|
|
179
|
+
ORDER BY fetched_at ASC`,
|
|
180
|
+
)
|
|
181
|
+
.all()
|
|
182
|
+
.map((row) => row.entity_key);
|
|
183
|
+
expect(remaining).toEqual(["newer", "trigger"]);
|
|
184
|
+
} finally {
|
|
185
|
+
persistence.close();
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
85
189
|
test("invalidates plugin state when schema versions differ", () => {
|
|
86
190
|
const dbPath = createTempDbPath("plugin-version");
|
|
87
191
|
const persistence = new AppPersistence(dbPath);
|
|
@@ -117,21 +221,6 @@ describe("AppPersistence", () => {
|
|
|
117
221
|
persistence.close();
|
|
118
222
|
});
|
|
119
223
|
|
|
120
|
-
test("returns stable references for unchanged plugin state snapshots", () => {
|
|
121
|
-
const dbPath = createTempDbPath("plugin-stable-snapshot");
|
|
122
|
-
const persistence = new AppPersistence(dbPath);
|
|
123
|
-
persistence.pluginState.set("prediction-markets", "resume:watchlist:v1", ["kalshi:ABC"], 1);
|
|
124
|
-
|
|
125
|
-
const first = persistence.pluginState.get<string[]>("prediction-markets", "resume:watchlist:v1", 1);
|
|
126
|
-
const second = persistence.pluginState.get<string[]>("prediction-markets", "resume:watchlist:v1", 1);
|
|
127
|
-
|
|
128
|
-
expect(first).not.toBeNull();
|
|
129
|
-
expect(second).not.toBeNull();
|
|
130
|
-
if (!first || !second) throw new Error("Expected persisted plugin state");
|
|
131
|
-
expect(second.value).toBe(first.value);
|
|
132
|
-
persistence.close();
|
|
133
|
-
});
|
|
134
|
-
|
|
135
224
|
test("stores multiple plugin state records in one batch", () => {
|
|
136
225
|
const dbPath = createTempDbPath("plugin-state-batch");
|
|
137
226
|
const persistence = new AppPersistence(dbPath);
|
package/src/i18n/index.test.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
applyLanguagePreference,
|
|
5
5
|
getLanguage,
|
|
6
6
|
setLanguage,
|
|
7
|
+
subscribeLanguage,
|
|
7
8
|
} from ".";
|
|
8
9
|
import { ja } from "./ja";
|
|
9
10
|
import { ko } from "./ko";
|
|
@@ -23,6 +24,22 @@ function placeholders(value: string): string[] {
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
describe("language selection", () => {
|
|
27
|
+
test("notifies subscribers only when the resolved language changes", () => {
|
|
28
|
+
const previousLanguage = getLanguage();
|
|
29
|
+
let notifications = 0;
|
|
30
|
+
const unsubscribe = subscribeLanguage(() => {
|
|
31
|
+
notifications += 1;
|
|
32
|
+
});
|
|
33
|
+
try {
|
|
34
|
+
setLanguage(previousLanguage === "ja" ? "ko" : "ja");
|
|
35
|
+
setLanguage(getLanguage());
|
|
36
|
+
expect(notifications).toBe(1);
|
|
37
|
+
} finally {
|
|
38
|
+
unsubscribe();
|
|
39
|
+
setLanguage(previousLanguage);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
26
43
|
test("keeps a valid environment override ahead of runtime preferences", () => {
|
|
27
44
|
const previousOverride = process.env.GLOOMBERB_LANG;
|
|
28
45
|
const previousLanguage = getLanguage();
|
package/src/i18n/index.ts
CHANGED
|
@@ -77,13 +77,25 @@ function detectLanguage(): AppLanguage {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
let currentLanguage: AppLanguage = detectLanguage();
|
|
80
|
+
const languageListeners = new Set<() => void>();
|
|
81
|
+
|
|
82
|
+
function updateLanguage(language: AppLanguage): void {
|
|
83
|
+
if (currentLanguage === language) return;
|
|
84
|
+
currentLanguage = language;
|
|
85
|
+
for (const listener of languageListeners) listener();
|
|
86
|
+
}
|
|
80
87
|
|
|
81
88
|
export function getLanguage(): AppLanguage {
|
|
82
89
|
return currentLanguage;
|
|
83
90
|
}
|
|
84
91
|
|
|
85
92
|
export function setLanguage(language: AppLanguage): void {
|
|
86
|
-
|
|
93
|
+
updateLanguage(language);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function subscribeLanguage(listener: () => void): () => void {
|
|
97
|
+
languageListeners.add(listener);
|
|
98
|
+
return () => languageListeners.delete(listener);
|
|
87
99
|
}
|
|
88
100
|
|
|
89
101
|
/**
|
|
@@ -94,13 +106,16 @@ export function setLanguage(language: AppLanguage): void {
|
|
|
94
106
|
export function applyLanguageFromConfig(config: { language?: string } | null | undefined): void {
|
|
95
107
|
const override = getEnvironmentLanguageOverride();
|
|
96
108
|
if (override) {
|
|
97
|
-
|
|
109
|
+
updateLanguage(override);
|
|
98
110
|
return;
|
|
99
111
|
}
|
|
100
112
|
const configured = config?.language;
|
|
101
|
-
if (!configured || configured === "auto")
|
|
113
|
+
if (!configured || configured === "auto") {
|
|
114
|
+
updateLanguage(detectLanguage());
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
102
117
|
const normalized = normalizeLanguageTag(configured);
|
|
103
|
-
if (normalized)
|
|
118
|
+
if (normalized) updateLanguage(normalized);
|
|
104
119
|
}
|
|
105
120
|
|
|
106
121
|
/**
|
|
@@ -108,8 +123,10 @@ export function applyLanguageFromConfig(config: { language?: string } | null | u
|
|
|
108
123
|
* command). "auto" re-runs environment detection.
|
|
109
124
|
*/
|
|
110
125
|
export function applyLanguagePreference(preference: LanguagePreference): void {
|
|
111
|
-
|
|
112
|
-
|
|
126
|
+
updateLanguage(
|
|
127
|
+
getEnvironmentLanguageOverride()
|
|
128
|
+
?? (preference === "auto" ? detectLanguage() : preference),
|
|
129
|
+
);
|
|
113
130
|
}
|
|
114
131
|
|
|
115
132
|
/**
|
package/src/i18n/ja.ts
CHANGED
|
@@ -637,8 +637,15 @@ export const ja: Record<string, string> = {
|
|
|
637
637
|
"Advanced": "詳細設定",
|
|
638
638
|
"Free": "無料",
|
|
639
639
|
"Market data": "市場データ",
|
|
640
|
+
"US equities": "米国株",
|
|
641
|
+
"Options data": "オプションデータ",
|
|
640
642
|
"Delayed": "遅延",
|
|
643
|
+
"15m delay": "15 分遅延",
|
|
641
644
|
"Real-time": "リアルタイム",
|
|
645
|
+
"{count}-minute delayed options, upgrade for real-time":
|
|
646
|
+
"オプションは {count} 分遅延、リアルタイムにはアップグレード",
|
|
647
|
+
"real-time options": "リアルタイムオプション",
|
|
648
|
+
"options delayed fallback": "オプション遅延フォールバック",
|
|
642
649
|
"News": "ニュース",
|
|
643
650
|
"12h delay": "12 時間遅延",
|
|
644
651
|
"Real-time wire": "リアルタイム速報",
|
|
@@ -827,6 +834,9 @@ export const ja: Record<string, string> = {
|
|
|
827
834
|
// ── AI ──────────────────────────────────────────────────────
|
|
828
835
|
"Select a ticker to ask AI.": "AI に質問する銘柄を選択してください。",
|
|
829
836
|
"No AI CLI tools are ready:": "AI CLI ツールが準備されていません:",
|
|
837
|
+
"No AI providers are ready.": "利用可能な AI プロバイダーがありません。",
|
|
838
|
+
"No AI providers are ready. Connect an account in pane settings.": "利用可能な AI プロバイダーがありません。ペイン設定でアカウントを接続してください。",
|
|
839
|
+
"Open any AI pane's settings to connect an account.": "いずれかの AI ペインの設定を開いて、アカウントを接続してください。",
|
|
830
840
|
"Ask questions about {ticker}. Financial data will be included as context.": "{ticker} について質問してください。財務データがコンテキストとして含まれます。",
|
|
831
841
|
"You": "あなた",
|
|
832
842
|
"thinking...": "考えています...",
|
|
@@ -844,6 +854,7 @@ export const ja: Record<string, string> = {
|
|
|
844
854
|
"No supported AI CLI tools detected. Install one to run screeners.": "サポートされている AI CLI ツールが検出されません。スクリーナーを実行するには、1 つインストールしてください。",
|
|
845
855
|
"Force Refreshing...": "強制更新中...",
|
|
846
856
|
"Refreshing...": "更新中...",
|
|
857
|
+
"Refreshing…": "更新中…",
|
|
847
858
|
"Stop": "停止",
|
|
848
859
|
"Refresh": "更新",
|
|
849
860
|
"Run Screener": "スクリーナーを実行",
|
|
@@ -860,6 +871,8 @@ export const ja: Record<string, string> = {
|
|
|
860
871
|
"force refresh armed": "強制リフレッシュ準備完了",
|
|
861
872
|
"provider": "プロバイダー",
|
|
862
873
|
"new": "新規",
|
|
874
|
+
"cancel": "キャンセル",
|
|
875
|
+
"stop": "停止",
|
|
863
876
|
"close tab": "タブを閉じる",
|
|
864
877
|
"efresh": "更新",
|
|
865
878
|
"force refresh": "強制リフレッシュ",
|
package/src/i18n/ko.ts
CHANGED
|
@@ -635,8 +635,15 @@ export const ko: Record<string, string> = {
|
|
|
635
635
|
"Advanced": "고급",
|
|
636
636
|
"Free": "무료",
|
|
637
637
|
"Market data": "시장 데이터",
|
|
638
|
+
"US equities": "미국 주식",
|
|
639
|
+
"Options data": "옵션 데이터",
|
|
638
640
|
"Delayed": "지연",
|
|
641
|
+
"15m delay": "15분 지연",
|
|
639
642
|
"Real-time": "실시간",
|
|
643
|
+
"{count}-minute delayed options, upgrade for real-time":
|
|
644
|
+
"옵션 데이터 {count}분 지연, 실시간으로 업그레이드",
|
|
645
|
+
"real-time options": "실시간 옵션",
|
|
646
|
+
"options delayed fallback": "옵션 지연 대체 데이터",
|
|
640
647
|
"News": "뉴스",
|
|
641
648
|
"12h delay": "12시간 지연",
|
|
642
649
|
"Real-time wire": "실시간 속보",
|
|
@@ -825,6 +832,9 @@ export const ko: Record<string, string> = {
|
|
|
825
832
|
// ── AI ──────────────────────────────────────────────────────
|
|
826
833
|
"Select a ticker to ask AI.": "AI에 질문할 종목을 선택하세요.",
|
|
827
834
|
"No AI CLI tools are ready:": "사용 가능한 AI CLI 도구가 없습니다:",
|
|
835
|
+
"No AI providers are ready.": "사용 가능한 AI 제공업체가 없습니다.",
|
|
836
|
+
"No AI providers are ready. Connect an account in pane settings.": "사용 가능한 AI 제공업체가 없습니다. 패널 설정에서 계정을 연결하세요.",
|
|
837
|
+
"Open any AI pane's settings to connect an account.": "AI 패널 설정을 열어 계정을 연결하세요.",
|
|
828
838
|
"Ask questions about {ticker}. Financial data will be included as context.": "{ticker}에 대해 질문하세요. 재무 데이터는 컨텍스트로 포함됩니다.",
|
|
829
839
|
"You": "나",
|
|
830
840
|
"thinking...": "생각 중...",
|
|
@@ -842,6 +852,7 @@ export const ko: Record<string, string> = {
|
|
|
842
852
|
"No supported AI CLI tools detected. Install one to run screeners.": "지원되는 AI CLI 도구가 감지되지 않았습니다. 스크리너를 실행하려면 하나를 설치하세요.",
|
|
843
853
|
"Force Refreshing...": "강제 새로고침 중...",
|
|
844
854
|
"Refreshing...": "새로고침 중...",
|
|
855
|
+
"Refreshing…": "새로고침 중…",
|
|
845
856
|
"Stop": "중지",
|
|
846
857
|
"Refresh": "새로고침",
|
|
847
858
|
"Run Screener": "스크리너 실행",
|
|
@@ -858,6 +869,8 @@ export const ko: Record<string, string> = {
|
|
|
858
869
|
"force refresh armed": "강제 새로고침 준비됨",
|
|
859
870
|
"provider": "제공업체",
|
|
860
871
|
"new": "새로 만들기",
|
|
872
|
+
"cancel": "취소",
|
|
873
|
+
"stop": "중지",
|
|
861
874
|
"close tab": "탭 닫기",
|
|
862
875
|
"efresh": "새로고침",
|
|
863
876
|
"force refresh": "강제 새로고침",
|
package/src/i18n/zh-cn.ts
CHANGED
|
@@ -637,8 +637,15 @@ export const zhCN: Record<string, string> = {
|
|
|
637
637
|
"Advanced": "高级",
|
|
638
638
|
"Free": "免费版",
|
|
639
639
|
"Market data": "行情数据",
|
|
640
|
+
"US equities": "美股",
|
|
641
|
+
"Options data": "期权数据",
|
|
640
642
|
"Delayed": "延迟",
|
|
643
|
+
"15m delay": "延迟 15 分钟",
|
|
641
644
|
"Real-time": "实时",
|
|
645
|
+
"{count}-minute delayed options, upgrade for real-time":
|
|
646
|
+
"期权行情延迟 {count} 分钟,升级以获取实时数据",
|
|
647
|
+
"real-time options": "实时期权",
|
|
648
|
+
"options delayed fallback": "期权延迟备用数据",
|
|
642
649
|
"News": "新闻",
|
|
643
650
|
"12h delay": "延迟 12 小时",
|
|
644
651
|
"Real-time wire": "实时快讯",
|
|
@@ -827,6 +834,9 @@ export const zhCN: Record<string, string> = {
|
|
|
827
834
|
// ── AI ──────────────────────────────────────────────────────
|
|
828
835
|
"Select a ticker to ask AI.": "选择一个代码向 AI 提问。",
|
|
829
836
|
"No AI CLI tools are ready:": "没有就绪的 AI 命令行工具:",
|
|
837
|
+
"No AI providers are ready.": "没有已就绪的 AI 服务商。",
|
|
838
|
+
"No AI providers are ready. Connect an account in pane settings.": "没有已就绪的 AI 服务商。请在面板设置中连接账户。",
|
|
839
|
+
"Open any AI pane's settings to connect an account.": "打开任意 AI 面板的设置以连接账户。",
|
|
830
840
|
"Ask questions about {ticker}. Financial data will be included as context.": "可以询问关于 {ticker} 的问题,金融数据会作为上下文提供。",
|
|
831
841
|
"You": "你",
|
|
832
842
|
"thinking...": "思考中...",
|
|
@@ -844,6 +854,7 @@ export const zhCN: Record<string, string> = {
|
|
|
844
854
|
"No supported AI CLI tools detected. Install one to run screeners.": "未检测到受支持的 AI 命令行工具。请安装一个后运行选股器。",
|
|
845
855
|
"Force Refreshing...": "强制刷新中...",
|
|
846
856
|
"Refreshing...": "刷新中...",
|
|
857
|
+
"Refreshing…": "刷新中…",
|
|
847
858
|
"Stop": "停止",
|
|
848
859
|
"Refresh": "刷新",
|
|
849
860
|
"Run Screener": "运行选股器",
|
|
@@ -860,6 +871,8 @@ export const zhCN: Record<string, string> = {
|
|
|
860
871
|
"force refresh armed": "强制刷新已准备",
|
|
861
872
|
"provider": "提供商",
|
|
862
873
|
"new": "新建",
|
|
874
|
+
"cancel": "取消",
|
|
875
|
+
"stop": "停止",
|
|
863
876
|
"close tab": "关闭标签页",
|
|
864
877
|
"efresh": "刷新",
|
|
865
878
|
"force refresh": "强制刷新",
|
package/src/i18n/zh-tw.ts
CHANGED
|
@@ -637,8 +637,15 @@ export const zhTW: Record<string, string> = {
|
|
|
637
637
|
"Advanced": "高階",
|
|
638
638
|
"Free": "免費版",
|
|
639
639
|
"Market data": "行情資料",
|
|
640
|
+
"US equities": "美股",
|
|
641
|
+
"Options data": "選擇權資料",
|
|
640
642
|
"Delayed": "延遲",
|
|
643
|
+
"15m delay": "延遲 15 分鐘",
|
|
641
644
|
"Real-time": "即時",
|
|
645
|
+
"{count}-minute delayed options, upgrade for real-time":
|
|
646
|
+
"選擇權行情延遲 {count} 分鐘,升級以取得即時資料",
|
|
647
|
+
"real-time options": "即時選擇權",
|
|
648
|
+
"options delayed fallback": "選擇權延遲備援資料",
|
|
642
649
|
"News": "新聞",
|
|
643
650
|
"12h delay": "延遲 12 小時",
|
|
644
651
|
"Real-time wire": "即時快訊",
|
|
@@ -827,6 +834,9 @@ export const zhTW: Record<string, string> = {
|
|
|
827
834
|
// ── AI ──────────────────────────────────────────────────────
|
|
828
835
|
"Select a ticker to ask AI.": "選擇股票代號以詢問 AI。",
|
|
829
836
|
"No AI CLI tools are ready:": "沒有就緒的 AI CLI 工具:",
|
|
837
|
+
"No AI providers are ready.": "沒有已就緒的 AI 供應商。",
|
|
838
|
+
"No AI providers are ready. Connect an account in pane settings.": "沒有已就緒的 AI 供應商。請在面板設定中連線帳戶。",
|
|
839
|
+
"Open any AI pane's settings to connect an account.": "開啟任一 AI 面板的設定以連線帳戶。",
|
|
830
840
|
"Ask questions about {ticker}. Financial data will be included as context.": "可以詢問關於 {ticker} 的問題,金融資料會作為上下文提供。",
|
|
831
841
|
"You": "你",
|
|
832
842
|
"thinking...": "思考中...",
|
|
@@ -844,6 +854,7 @@ export const zhTW: Record<string, string> = {
|
|
|
844
854
|
"No supported AI CLI tools detected. Install one to run screeners.": "未偵測到支援的 AI CLI 工具。請先安裝工具再執行選股器。",
|
|
845
855
|
"Force Refreshing...": "強制重新整理中...",
|
|
846
856
|
"Refreshing...": "重新整理中...",
|
|
857
|
+
"Refreshing…": "重新整理中…",
|
|
847
858
|
"Stop": "停止",
|
|
848
859
|
"Refresh": "重新整理",
|
|
849
860
|
"Run Screener": "執行選股器",
|
|
@@ -860,6 +871,8 @@ export const zhTW: Record<string, string> = {
|
|
|
860
871
|
"force refresh armed": "已準備強制重新整理",
|
|
861
872
|
"provider": "供應商",
|
|
862
873
|
"new": "新增",
|
|
874
|
+
"cancel": "取消",
|
|
875
|
+
"stop": "停止",
|
|
863
876
|
"close tab": "關閉分頁",
|
|
864
877
|
"efresh": "重新整理",
|
|
865
878
|
"force refresh": "強制重新整理",
|
|
@@ -32,6 +32,7 @@ type RunSingleFlight = <T>(key: string, task: () => Promise<T>) => Promise<T>;
|
|
|
32
32
|
|
|
33
33
|
interface AuxiliaryLoaderOptions<T> {
|
|
34
34
|
dataProvider: DataProvider;
|
|
35
|
+
forceRefresh?: boolean;
|
|
35
36
|
key: string;
|
|
36
37
|
store: QueryStore<T>;
|
|
37
38
|
ttlMs: number;
|
|
@@ -41,14 +42,16 @@ interface AuxiliaryLoaderOptions<T> {
|
|
|
41
42
|
|
|
42
43
|
export function loadOptionsEntry(options: {
|
|
43
44
|
dataProvider: DataProvider;
|
|
45
|
+
forceRefresh?: boolean;
|
|
44
46
|
request: OptionsRequest;
|
|
45
47
|
store: QueryStore<OptionsChain>;
|
|
46
48
|
runSingleFlight: RunSingleFlight;
|
|
47
49
|
}): Promise<QueryEntry<OptionsChain>> {
|
|
48
|
-
const { dataProvider, request, store, runSingleFlight } = options;
|
|
50
|
+
const { dataProvider, forceRefresh = false, request, store, runSingleFlight } = options;
|
|
49
51
|
const key = buildOptionsKey(request);
|
|
50
52
|
return loadAuxiliaryEntry({
|
|
51
53
|
dataProvider,
|
|
54
|
+
forceRefresh,
|
|
52
55
|
key,
|
|
53
56
|
store,
|
|
54
57
|
ttlMs: OPTIONS_CACHE_TTL_MS,
|
|
@@ -213,6 +216,7 @@ export function loadFxRateEntry(options: {
|
|
|
213
216
|
|
|
214
217
|
function loadAuxiliaryEntry<T>({
|
|
215
218
|
dataProvider,
|
|
219
|
+
forceRefresh = false,
|
|
216
220
|
key,
|
|
217
221
|
store,
|
|
218
222
|
ttlMs,
|
|
@@ -220,10 +224,10 @@ function loadAuxiliaryEntry<T>({
|
|
|
220
224
|
load,
|
|
221
225
|
}: AuxiliaryLoaderOptions<T>): Promise<QueryEntry<T>> {
|
|
222
226
|
const current = store.get(key);
|
|
223
|
-
if (hasFreshReadyEntry(current, ttlMs)) {
|
|
227
|
+
if (!forceRefresh && hasFreshReadyEntry(current, ttlMs)) {
|
|
224
228
|
return Promise.resolve(current);
|
|
225
229
|
}
|
|
226
|
-
return runSingleFlight(key, async () => {
|
|
230
|
+
return runSingleFlight(forceRefresh ? `${key}|refresh` : key, async () => {
|
|
227
231
|
store.update(key, loadingEntry);
|
|
228
232
|
const startedAt = Date.now();
|
|
229
233
|
try {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isIntradayResolution, TIME_RANGE_ORDER } from "../../
|
|
1
|
+
import { isIntradayResolution, TIME_RANGE_ORDER } from "../../time-series/resolution";
|
|
2
2
|
import type { PricePoint } from "../../types/financials";
|
|
3
3
|
import { isPriceHistoryStaleForCurrentWindow, normalizePriceHistory } from "../../utils/price-history";
|
|
4
4
|
import type { ChartRequest, InstrumentRef } from "../request-types";
|
|
@@ -178,6 +178,32 @@ describe("MarketDataCoordinator", () => {
|
|
|
178
178
|
expect(optionsCalls).toBe(1);
|
|
179
179
|
});
|
|
180
180
|
|
|
181
|
+
it("force refreshes an options chain for periodic Greeks and IV updates", async () => {
|
|
182
|
+
let optionsCalls = 0;
|
|
183
|
+
const provider = createProvider({
|
|
184
|
+
getOptionsChain: async () => {
|
|
185
|
+
optionsCalls += 1;
|
|
186
|
+
return {
|
|
187
|
+
underlyingSymbol: "AAPL",
|
|
188
|
+
expirationDates: [1_800_000_000],
|
|
189
|
+
calls: [],
|
|
190
|
+
puts: [],
|
|
191
|
+
};
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
const coordinator = new MarketDataCoordinator(provider);
|
|
195
|
+
const request = {
|
|
196
|
+
instrument: { symbol: "AAPL", exchange: "NASDAQ" },
|
|
197
|
+
expirationDate: 1_800_000_000,
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
await coordinator.loadOptions(request);
|
|
201
|
+
await coordinator.loadOptions(request);
|
|
202
|
+
await coordinator.loadOptions(request, { forceRefresh: true });
|
|
203
|
+
|
|
204
|
+
expect(optionsCalls).toBe(2);
|
|
205
|
+
});
|
|
206
|
+
|
|
181
207
|
it("uses cached chart data while a wider range request is loading", async () => {
|
|
182
208
|
const oneYearHistory = [
|
|
183
209
|
{ date: new Date("2024-01-01"), close: 100 },
|
|
@@ -303,9 +303,13 @@ export class MarketDataCoordinator {
|
|
|
303
303
|
});
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
-
async loadOptions(
|
|
306
|
+
async loadOptions(
|
|
307
|
+
request: OptionsRequest,
|
|
308
|
+
options: { forceRefresh?: boolean } = {},
|
|
309
|
+
): Promise<QueryEntry<OptionsChain>> {
|
|
307
310
|
return loadOptionsEntry({
|
|
308
311
|
dataProvider: this.dataProvider,
|
|
312
|
+
forceRefresh: options.forceRefresh,
|
|
309
313
|
request,
|
|
310
314
|
store: this.optionsStore,
|
|
311
315
|
runSingleFlight: (key, task) => this.runSingleFlight(key, task),
|
|
@@ -364,11 +368,13 @@ export class MarketDataCoordinator {
|
|
|
364
368
|
private applyStreamQuote(instrument: InstrumentRef, quote: Quote): void {
|
|
365
369
|
const key = buildQuoteKey(instrument);
|
|
366
370
|
const current = this.quoteStore.get(key);
|
|
367
|
-
const resolvedQuote = this.resolveIncomingQuote(instrument, quote);
|
|
371
|
+
const resolvedQuote = quote.stale === true ? quote : this.resolveIncomingQuote(instrument, quote);
|
|
368
372
|
if (areStreamQuotesEquivalent(current.data ?? current.lastGoodData, resolvedQuote)) return;
|
|
369
|
-
const
|
|
370
|
-
const
|
|
371
|
-
|
|
373
|
+
const startedAt = Date.now();
|
|
374
|
+
const receivedAt = resolvedQuote.stale === true ? resolvedQuote.receivedAt : startedAt;
|
|
375
|
+
const storedQuote = receivedAt == null ? resolvedQuote : { ...resolvedQuote, receivedAt };
|
|
376
|
+
const attempts = [createAttempt(resolvedQuote.providerId ?? this.dataProvider.id, startedAt, "success")];
|
|
377
|
+
this.quoteStore.set(key, readyQuoteEntry(current, storedQuote, resolvedQuote.providerId ?? this.dataProvider.id, attempts));
|
|
372
378
|
}
|
|
373
379
|
|
|
374
380
|
private resolveIncomingQuote(instrument: InstrumentRef, quote: Quote): Quote {
|