gloomberb 0.18.4 → 0.18.5
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/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/package.json +1 -1
- package/patches/electrobun@1.18.1.patch +32 -1
- package/src/account/auth-model.ts +7 -3
- package/src/api-client/auth.ts +9 -4
- package/src/api-client/catalysts.ts +2 -1
- package/src/api-client/data.ts +14 -0
- package/src/api-client/index.ts +160 -11
- package/src/api-client/live-scan.ts +91 -0
- package/src/api-client/members.ts +12 -0
- package/src/api-client/request.ts +222 -41
- package/src/api-client/research-activity.ts +3 -10
- package/src/api-client/types.ts +6 -0
- package/src/app/keybindings/index.ts +3 -0
- package/src/app/keybindings/labels.ts +18 -1
- package/src/app/keybindings/resolve.ts +14 -0
- package/src/app/runtime/broker-import.ts +43 -26
- package/src/architecture/core-builtin-allowlist.json +1 -1
- package/src/cli/commands/fred-series.ts +5 -0
- package/src/cli/commands/keybindings.ts +23 -0
- package/src/cli/commands/market.ts +68 -22
- package/src/cli/commands/options-chain.ts +59 -3
- package/src/cli/commands/overview.ts +119 -31
- package/src/cli/commands/search.ts +18 -2
- package/src/cli/commands/ticker.ts +2 -1
- package/src/cli/errors.ts +2 -2
- package/src/cli/export-source.ts +29 -1
- package/src/cli/glossary.ts +49 -0
- package/src/cli/history-rows.ts +30 -7
- package/src/cli/known-gaps.ts +5 -2
- package/src/cli/listing-arg.ts +16 -5
- package/src/cli/pane-functions/freshness.ts +25 -13
- package/src/cli/pane-functions/function-help.ts +10 -3
- package/src/cli/pane-functions/headless.ts +39 -5
- package/src/cli/pane-functions/index.ts +10 -3
- package/src/cli/pane-functions/market-session.ts +15 -5
- package/src/cli/pane-functions/report.ts +5 -1
- package/src/cli/pane-functions/resolver.ts +18 -2
- package/src/cli/quote-answer.ts +10 -4
- package/src/cli/report-tables.ts +18 -7
- package/src/cli/restore-plugins.ts +13 -1
- package/src/cli/result.ts +57 -5
- package/src/components/chart/composite/composite-chart.tsx +22 -4
- package/src/components/chart/composite/format.ts +27 -0
- package/src/components/chart/composite/levels.ts +2 -0
- package/src/components/chart/composite/panel-series.ts +1 -0
- package/src/components/chart/composite/panel-surface.tsx +9 -2
- package/src/components/chart/composite/range-statistics.ts +62 -0
- package/src/components/chart/composite/scene.ts +7 -3
- package/src/components/chart/composite/series-axis-tags.tsx +16 -9
- package/src/components/chart/composite/types.ts +4 -0
- package/src/components/command-bar/panel/prompt-binding.ts +0 -8
- package/src/components/command-bar/workflow/alert.ts +1 -1
- package/src/components/layout/header.tsx +26 -40
- package/src/components/layout/pane/footer/index.tsx +1 -0
- package/src/components/layout/pane/footer/model.ts +27 -0
- package/src/components/onboarding/onboarding-frame.tsx +1 -1
- package/src/components/onboarding/portfolio-step/positions-panel.tsx +2 -2
- package/src/components/ui/display.tsx +5 -1
- package/src/core/state/app/layout.ts +2 -0
- package/src/core/state/app/state.ts +9 -0
- package/src/core/state/app/types.ts +3 -0
- package/src/data/saved-copies.ts +55 -16
- package/src/docs/snapshot.json +361 -84
- package/src/i18n/es.ts +1 -1
- package/src/i18n/ja.ts +1 -1
- package/src/i18n/ko.ts +1 -1
- package/src/i18n/zh-cn.ts +1 -1
- package/src/i18n/zh-tw.ts +1 -1
- package/src/market-data/coordinator/financials.ts +10 -2
- package/src/market-data/coordinator/index.ts +3 -1
- package/src/market-data/coordinator/quotes.ts +4 -2
- package/src/market-data/market/change-basis.ts +33 -5
- package/src/market-data/market/dated-previous-close.ts +203 -0
- package/src/market-data/market/futures-week.ts +68 -0
- package/src/market-data/market/status.ts +16 -4
- package/src/market-data/options-alternatives.ts +60 -0
- package/src/market-data/quotes/contributions.ts +20 -2
- package/src/market-data/quotes/resolution.ts +3 -0
- package/src/market-data/selectors.ts +3 -0
- package/src/notifications/notification-log.ts +6 -4
- package/src/plugins/builtin/alerts/alert-engine.ts +5 -1
- package/src/plugins/builtin/alerts/calendar-events.ts +57 -6
- package/src/plugins/builtin/alerts/cli.ts +59 -20
- package/src/plugins/builtin/alerts/command.ts +7 -0
- package/src/plugins/builtin/alerts/constants.ts +1 -1
- package/src/plugins/builtin/alerts/fired-log.ts +45 -4
- package/src/plugins/builtin/alerts/format.ts +8 -2
- package/src/plugins/builtin/alerts/headless.ts +10 -5
- package/src/plugins/builtin/alerts/history-rows.ts +5 -2
- package/src/plugins/builtin/alerts/history.ts +2 -0
- package/src/plugins/builtin/alerts/index.tsx +66 -16
- package/src/plugins/builtin/alerts/live.ts +3 -3
- package/src/plugins/builtin/alerts/metric-command.ts +61 -0
- package/src/plugins/builtin/alerts/metric-data.ts +70 -0
- package/src/plugins/builtin/alerts/metric-model.ts +175 -0
- package/src/plugins/builtin/alerts/metric-parity-fixture.json +725 -0
- package/src/plugins/builtin/alerts/pane.tsx +42 -29
- package/src/plugins/builtin/alerts/storage.ts +33 -10
- package/src/plugins/builtin/alerts/types.ts +3 -0
- package/src/plugins/builtin/backtest/headless.ts +10 -5
- package/src/plugins/builtin/backtest/pane.tsx +9 -4
- package/src/plugins/builtin/backtest/presets.ts +29 -3
- package/src/plugins/builtin/broker-manager/index.tsx +3 -2
- package/src/plugins/builtin/broker-manager/model.ts +6 -1
- package/src/plugins/builtin/broker-trading/controller.ts +22 -12
- package/src/plugins/builtin/broker-trading/index.tsx +13 -1
- package/src/plugins/builtin/broker-trading/order-feed.ts +9 -4
- package/src/plugins/builtin/broker-trading/pane.tsx +1 -1
- package/src/plugins/builtin/builtin-plugin-meta.ts +1 -1
- package/src/plugins/builtin/catalysts/model.ts +2 -1
- package/src/plugins/builtin/chart-composer/catalog-inventory.ts +3 -1
- package/src/plugins/builtin/chart-composer/chart-spec-edit.ts +1 -1
- package/src/plugins/builtin/chart-composer/cli-options.ts +5 -5
- package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +5 -2
- package/src/plugins/builtin/chart-composer/editor/controller.ts +2 -0
- package/src/plugins/builtin/chart-composer/editor/model.ts +2 -0
- package/src/plugins/builtin/chart-composer/editor.tsx +7 -5
- package/src/plugins/builtin/chart-composer/formula.ts +71 -18
- package/src/plugins/builtin/chart-composer/headless-schema.ts +14 -3
- package/src/plugins/builtin/chart-composer/headless.ts +13 -2
- package/src/plugins/builtin/chart-composer/pane.tsx +17 -2
- package/src/plugins/builtin/chart-composer/presets.ts +50 -15
- package/src/plugins/builtin/chart-composer/quick-add.tsx +40 -25
- package/src/plugins/builtin/chart-composer/session-history.ts +18 -9
- package/src/plugins/builtin/chart-composer/use-series-catalog.ts +40 -26
- package/src/plugins/builtin/chat/content/command-suggestions.ts +92 -0
- package/src/plugins/builtin/chat/content/composer.tsx +13 -6
- package/src/plugins/builtin/chat/content/footer.tsx +6 -3
- package/src/plugins/builtin/chat/content/index.tsx +50 -21
- package/src/plugins/builtin/chat/content/open-targets.ts +14 -3
- package/src/plugins/builtin/chat/controller/session-runtime.ts +4 -2
- package/src/plugins/builtin/chat/index.tsx +23 -0
- package/src/plugins/builtin/chat/message/desktop.tsx +1 -0
- package/src/plugins/builtin/chat/message/inline-tokens.tsx +23 -0
- package/src/plugins/builtin/chat/message/pane-chip.tsx +50 -20
- package/src/plugins/builtin/chat/pane-links/commands.ts +265 -0
- package/src/plugins/builtin/chat/pane-links/model.ts +12 -3
- package/src/plugins/builtin/chat/pane-links/open.ts +8 -1
- package/src/plugins/builtin/chat/unread-inbox-pane.tsx +10 -1
- package/src/plugins/builtin/cloud/plugin.tsx +1 -1
- package/src/plugins/builtin/econ/calendar-model.ts +14 -9
- package/src/plugins/builtin/econ/figures.ts +72 -0
- package/src/plugins/builtin/econ/index.tsx +33 -3
- package/src/plugins/builtin/econ/types.ts +2 -0
- package/src/plugins/builtin/econ-statistics/headless.ts +79 -31
- package/src/plugins/builtin/econ-statistics/stats.ts +1 -0
- package/src/plugins/builtin/econ-statistics/view.ts +3 -0
- package/src/plugins/builtin/equity-screener/pane.tsx +2 -2
- package/src/plugins/builtin/exposure/charts.tsx +2 -2
- package/src/plugins/builtin/exposure/model.ts +13 -1
- package/src/plugins/builtin/exposure/pane.tsx +4 -4
- package/src/plugins/builtin/futures/index.tsx +1 -0
- package/src/plugins/builtin/futures/model.ts +2 -1
- package/src/plugins/builtin/futures/table.tsx +2 -2
- package/src/plugins/builtin/fx-matrix/client.ts +6 -1
- package/src/plugins/builtin/fx-matrix/headless.ts +5 -2
- package/src/plugins/builtin/fx-matrix/index.tsx +13 -3
- package/src/plugins/builtin/fx-matrix/live-legs.ts +12 -0
- package/src/plugins/builtin/help/keybindings-editor.tsx +18 -14
- package/src/plugins/builtin/iv-history/headless.ts +3 -0
- package/src/plugins/builtin/macro-day/pane.tsx +7 -5
- package/src/plugins/builtin/market-movers/footer.ts +2 -2
- package/src/plugins/builtin/members/breadth-pane.tsx +81 -0
- package/src/plugins/builtin/members/client.ts +19 -4
- package/src/plugins/builtin/members/headless.ts +20 -7
- package/src/plugins/builtin/members/index.tsx +9 -9
- package/src/plugins/builtin/members/model.ts +23 -4
- package/src/plugins/builtin/members/pane.tsx +25 -15
- package/src/plugins/builtin/options/analytics.ts +26 -2
- package/src/plugins/builtin/options/model-spot.ts +5 -2
- package/src/plugins/builtin/options/strike-window.ts +2 -1
- package/src/plugins/builtin/options/table.ts +1 -7
- package/src/plugins/builtin/options/view.tsx +25 -8
- package/src/plugins/builtin/options-positioning/client.ts +19 -0
- package/src/plugins/builtin/options-positioning/headless.ts +54 -14
- package/src/plugins/builtin/options-positioning/index.tsx +3 -3
- package/src/plugins/builtin/options-positioning/model.ts +47 -8
- package/src/plugins/builtin/options-positioning/pane.tsx +68 -38
- package/src/plugins/builtin/perps/freshness.ts +12 -10
- package/src/plugins/builtin/perps/live-rows.ts +10 -3
- package/src/plugins/builtin/perps/pane.tsx +15 -11
- package/src/plugins/builtin/portfolio-list/cli/render.ts +5 -2
- package/src/plugins/builtin/portfolio-list/headless.ts +1 -1
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +11 -8
- package/src/plugins/builtin/rate-path/pane.tsx +2 -2
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +4 -4
- package/src/plugins/builtin/scanner/feed.ts +2 -2
- package/src/plugins/builtin/scanner/index.ts +24 -1
- package/src/plugins/builtin/scanner/live-condition-editor.tsx +222 -0
- package/src/plugins/builtin/scanner/live-feed.ts +168 -0
- package/src/plugins/builtin/scanner/live-headless.ts +114 -0
- package/src/plugins/builtin/scanner/live-model.ts +351 -0
- package/src/plugins/builtin/scanner/live-pane.tsx +677 -0
- package/src/plugins/builtin/scanner/live-track.ts +186 -0
- package/src/plugins/builtin/series-inventory.ts +1 -0
- package/src/plugins/builtin/shared/macro-releases.ts +65 -0
- package/src/plugins/builtin/shared/scanner-capture.ts +3 -0
- package/src/plugins/builtin/shared/use-quote-board.ts +8 -2
- package/src/plugins/builtin/shared/volatility/pricing.ts +7 -1
- package/src/plugins/builtin/themes/headless.ts +6 -4
- package/src/plugins/builtin/themes/model.ts +1 -1
- package/src/plugins/builtin/themes/pane.tsx +3 -3
- package/src/plugins/builtin/ticker-detail/quote-footer.ts +6 -1
- package/src/plugins/builtin/vol-surface/charts.tsx +9 -4
- package/src/plugins/builtin/vol-surface/client.ts +6 -4
- package/src/plugins/builtin/vol-surface/headless.ts +62 -13
- package/src/plugins/builtin/vol-surface/index.tsx +2 -2
- package/src/plugins/builtin/vol-surface/model.ts +22 -7
- package/src/plugins/builtin/vol-surface/pane.tsx +44 -7
- package/src/plugins/builtin/vol-surface/tables.ts +31 -3
- package/src/plugins/builtin/volatility/dvol.ts +46 -0
- package/src/plugins/builtin/volatility/pane.tsx +2 -2
- package/src/plugins/builtin/volatility/series.ts +26 -0
- package/src/plugins/builtin/world-indices/footer.ts +1 -1
- package/src/plugins/catalog-browser.ts +13 -8
- package/src/portfolio/column-values.ts +5 -5
- package/src/public/__snapshots__/public-api.test.ts.snap +1 -0
- package/src/renderers/browser/app-services.ts +1 -7
- package/src/renderers/browser/config-host.ts +2 -55
- package/src/renderers/browser/main.tsx +7 -57
- package/src/renderers/electrobun/bun/desktop/app-name.ts +222 -0
- package/src/renderers/electrobun/bun/desktop/backend.ts +17 -7
- package/src/renderers/electrobun/bun/desktop/relaunch.ts +74 -25
- package/src/renderers/electrobun/bun/desktop/state-broadcaster.ts +1 -0
- package/src/renderers/electrobun/bun/desktop/update.ts +70 -3
- package/src/renderers/electrobun/bun/desktop/workspace/index.ts +2 -0
- package/src/renderers/electrobun/bun/index.ts +25 -1
- package/src/renderers/electrobun/shared/app-name.ts +17 -0
- package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +3 -3
- package/src/renderers/electrobun/view/desktop/window/snapshot.ts +1 -0
- package/src/renderers/opentui/toast-host.tsx +71 -43
- package/src/sources/gloomberb-cloud/normalizers.ts +2 -0
- package/src/sources/provider-router/batches.ts +10 -2
- package/src/sources/provider-router/financial-routes.ts +3 -3
- package/src/sources/provider-router/history.ts +28 -7
- package/src/sources/provider-router/index.ts +146 -10
- package/src/sources/provider-router/search.ts +13 -2
- package/src/state/app/context/index.tsx +3 -0
- package/src/sync/auth-transition.ts +19 -9
- package/src/sync/baseline.ts +10 -4
- package/src/sync/controller.ts +66 -16
- package/src/sync/react.ts +4 -1
- package/src/sync/types.ts +3 -0
- package/src/telemetry/usage-counts.ts +0 -18
- package/src/theme/colors.ts +8 -0
- package/src/tickers/company-name.ts +59 -0
- package/src/tickers/listing-venues.ts +23 -0
- package/src/tickers/search/bare-symbol.ts +36 -0
- package/src/tickers/search/index.ts +36 -7
- package/src/tickers/search/ranking.ts +10 -1
- package/src/tickers/search/types.ts +5 -0
- package/src/time-series/base-series.ts +13 -5
- package/src/time-series/daily-valuation.ts +62 -0
- package/src/time-series/field-catalog.ts +1 -3
- package/src/time-series/forward-valuation.ts +32 -0
- package/src/time-series/fundamentals.ts +155 -7
- package/src/time-series/hooks.ts +1 -0
- package/src/time-series/price-units.ts +72 -2
- package/src/time-series/resolve-context.ts +5 -1
- package/src/time-series/resolve.ts +61 -12
- package/src/time-series/series-presentation.ts +40 -6
- package/src/time-series/spec.ts +2 -1
- package/src/time-series/studies.ts +270 -72
- package/src/time-series/transforms.ts +1 -0
- package/src/time-series/types.ts +5 -0
- package/src/time-series/valuation-releases.ts +39 -0
- package/src/types/data-provider.ts +9 -1
- package/src/types/desktop-window.ts +2 -0
- package/src/types/financials.ts +41 -1
- package/src/types/headless.ts +42 -4
- package/src/types/instrument.ts +3 -0
- package/src/types/plugin.ts +1 -1
- package/src/updater/command-names.ts +12 -7
- package/src/utils/financial-statements.ts +6 -1
- package/src/utils/network-failure.ts +9 -6
- package/src/utils/option-expiry.ts +48 -1
- package/src/utils/options.ts +10 -0
- package/src/utils/plugin-proxy-hosts.json +1 -0
- package/src/utils/price-history.ts +5 -0
- package/src/renderers/browser/first-screen.ts +0 -193
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { logFailure } from "../utils/log-failure";
|
|
2
2
|
import { httpFetch, isHttpFetchStreaming } from "../utils/http-transport";
|
|
3
|
-
import {
|
|
3
|
+
import { OperationTimeoutError } from "../utils/async-deadline";
|
|
4
4
|
import {
|
|
5
5
|
ApiRequestError,
|
|
6
6
|
parseApiErrorCode,
|
|
@@ -16,17 +16,59 @@ import {
|
|
|
16
16
|
type ConnectionHealthRegistry,
|
|
17
17
|
} from "../core/connection-health";
|
|
18
18
|
import { SESSION_COOKIE_NAMES } from "./session-cookie";
|
|
19
|
-
import { asDataServiceFailure } from "../utils/network-failure";
|
|
19
|
+
import { asDataServiceFailure, DataServiceUnreachableError } from "../utils/network-failure";
|
|
20
20
|
|
|
21
21
|
/** The public Gloom Cloud API, which the web app's same-origin `/api` path forwards to. */
|
|
22
22
|
export const DEFAULT_API_URL = "https://api.gloom.sh";
|
|
23
|
+
/**
|
|
24
|
+
* How long a request waits for the service to answer at all. Nothing back by
|
|
25
|
+
* then, and nothing else answered meanwhile, is a hung connection: the request
|
|
26
|
+
* fails as the data service being unreachable, as a refused one does.
|
|
27
|
+
*/
|
|
23
28
|
const DEFAULT_MARKET_REQUEST_TIMEOUT_MS = 10_000;
|
|
29
|
+
/**
|
|
30
|
+
* A search, which a command and a keystroke wait on, gives up on a connection
|
|
31
|
+
* that sends nothing back sooner. A slow search on a service that answers
|
|
32
|
+
* (some take five seconds) still gets the full market deadline.
|
|
33
|
+
*/
|
|
34
|
+
const SEARCH_ANSWER_TIMEOUT_MS = 4_000;
|
|
35
|
+
/**
|
|
36
|
+
* Once a request went unanswered, later ones wait this long for an answer,
|
|
37
|
+
* until any answer comes back: a hung connection is not waited out in full by
|
|
38
|
+
* every request a command or a pane makes after it.
|
|
39
|
+
*/
|
|
40
|
+
const UNREACHABLE_REQUEST_TIMEOUT_MS = 3_000;
|
|
41
|
+
/**
|
|
42
|
+
* Right after a request hung with nothing back, the requests that follow it
|
|
43
|
+
* (a fallback, the next step of a command) fail at once rather than each
|
|
44
|
+
* waiting out a connection that just proved dead. Past this they try again.
|
|
45
|
+
*/
|
|
46
|
+
const HUNG_FAIL_FAST_MS = 5_000;
|
|
24
47
|
const DEFAULT_RESEARCH_REQUEST_TIMEOUT_MS = 45_000;
|
|
48
|
+
/** Requests outside market reads (saves, uploads, drafts) may take the server this long once it is known to answer. */
|
|
49
|
+
const DEFAULT_SERVICE_REQUEST_LIMIT_MS = 120_000;
|
|
50
|
+
const UPLOAD_REQUEST_LIMIT_MS = 300_000;
|
|
51
|
+
/**
|
|
52
|
+
* How long the check that the service answers at all waits. It runs this long
|
|
53
|
+
* before a request's answer deadline, unless something answered meanwhile, so
|
|
54
|
+
* one slow request on a service that answers is waited for and a dead
|
|
55
|
+
* connection is not.
|
|
56
|
+
*/
|
|
57
|
+
const SERVICE_PROBE_TIMEOUT_MS = 3_000;
|
|
58
|
+
const SERVICE_PROBE_PATH = "/health";
|
|
25
59
|
// Research may fill missing sections after a primary read; quote/history reads remain latency bounded.
|
|
26
60
|
const RESEARCH_MARKET_PATHS = new Set([
|
|
27
61
|
"/market/dividends", "/market/short-interest", "/market/movers", "/market/trending",
|
|
28
62
|
"/market/heatmap", "/market/earnings-calendar",
|
|
29
63
|
]);
|
|
64
|
+
|
|
65
|
+
/** When a request gives up: without any answer from the service, and in all. */
|
|
66
|
+
interface RequestDeadline {
|
|
67
|
+
answerMs: number;
|
|
68
|
+
limitMs: number;
|
|
69
|
+
/** When to check that the service answers at all, if nothing has by then; null when the deadline is too short for it. */
|
|
70
|
+
probeAtMs: number | null;
|
|
71
|
+
}
|
|
30
72
|
/** Local status for "this runtime cannot stream", never returned by the server. */
|
|
31
73
|
export const STREAMING_UNSUPPORTED_STATUS = 0;
|
|
32
74
|
|
|
@@ -74,6 +116,8 @@ type CloudApiUploadTransport = (request: CloudApiUploadRequest) => Promise<Cloud
|
|
|
74
116
|
let cloudApiFetchTransport: CloudApiFetchTransport = httpFetch;
|
|
75
117
|
let cloudApiUploadTransport: CloudApiUploadTransport | null = null;
|
|
76
118
|
let cloudApiTransportInstalled = false;
|
|
119
|
+
/** Counts installed transports, so what a client learned of one connection is not applied to the next. */
|
|
120
|
+
let cloudApiTransportGeneration = 0;
|
|
77
121
|
let cloudApiFetchStreaming = true;
|
|
78
122
|
let cloudApiStreamFetch: CloudApiStreamFetch | null = null;
|
|
79
123
|
|
|
@@ -83,6 +127,7 @@ export function setCloudApiFetchTransport(
|
|
|
83
127
|
): void {
|
|
84
128
|
cloudApiFetchTransport = transport ?? httpFetch;
|
|
85
129
|
cloudApiTransportInstalled = !!transport;
|
|
130
|
+
cloudApiTransportGeneration += 1;
|
|
86
131
|
cloudApiFetchStreaming = transport ? (options.streaming ?? false) : true;
|
|
87
132
|
cloudApiStreamFetch = transport ? options.streamFetch ?? null : null;
|
|
88
133
|
}
|
|
@@ -163,22 +208,6 @@ function throwIfRequestAborted(signal: AbortSignal | null | undefined): void {
|
|
|
163
208
|
);
|
|
164
209
|
}
|
|
165
210
|
|
|
166
|
-
/**
|
|
167
|
-
* Sends one request, saying the same thing whichever fetch failed to connect
|
|
168
|
-
* (Bun's "Unable to connect", a browser's "Failed to fetch", a refused proxy):
|
|
169
|
-
* every command, pane and status line then reads "Gloom couldn't reach its
|
|
170
|
-
* data service", and code that falls back to a saved copy can tell it apart.
|
|
171
|
-
* A caller's abort stays an abort.
|
|
172
|
-
*/
|
|
173
|
-
async function reachService<R>(fetch: () => Promise<R>, signal: AbortSignal | null | undefined): Promise<R> {
|
|
174
|
-
try {
|
|
175
|
-
return await fetch();
|
|
176
|
-
} catch (error) {
|
|
177
|
-
if (signal?.aborted) throw error;
|
|
178
|
-
throw asDataServiceFailure(error);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
211
|
export class CloudApiRequestTransport {
|
|
183
212
|
private sessionToken: string | null = null;
|
|
184
213
|
private sessionCookieName: SessionCookieName | null = null;
|
|
@@ -188,6 +217,17 @@ export class CloudApiRequestTransport {
|
|
|
188
217
|
private readonly marketRequestTimeoutMs: number;
|
|
189
218
|
private readonly researchRequestTimeoutMs: number;
|
|
190
219
|
private readonly connectionHealth: ConnectionHealthRegistry;
|
|
220
|
+
/** Answers received from the service, by any request: a request that sees this grow knows the connection works. */
|
|
221
|
+
private answers = 0;
|
|
222
|
+
/** The last request went unanswered (refused or hung) and nothing has answered since. */
|
|
223
|
+
private unreachable = false;
|
|
224
|
+
/** When a request last hung with nothing back, while nothing has answered since. */
|
|
225
|
+
private hungAt: number | null = null;
|
|
226
|
+
/** The installed transport the two above were learned on. */
|
|
227
|
+
private connectionGeneration = cloudApiTransportGeneration;
|
|
228
|
+
/** Requests waiting under a deadline: the check that the service answers runs only while one is. */
|
|
229
|
+
private pending = 0;
|
|
230
|
+
private probe: AbortController | null = null;
|
|
191
231
|
|
|
192
232
|
readonly baseUrl = getCloudApiBaseUrl();
|
|
193
233
|
|
|
@@ -280,7 +320,7 @@ export class CloudApiRequestTransport {
|
|
|
280
320
|
GLOOM_CLOUD_HTTP_CONNECTION_ID,
|
|
281
321
|
operation,
|
|
282
322
|
async () => {
|
|
283
|
-
const response = await reachService(() => streamFetch(`${this.baseUrl}${path}`, {
|
|
323
|
+
const response = await this.reachService(() => streamFetch(`${this.baseUrl}${path}`, {
|
|
284
324
|
...options,
|
|
285
325
|
headers,
|
|
286
326
|
credentials: "include",
|
|
@@ -301,37 +341,178 @@ export class CloudApiRequestTransport {
|
|
|
301
341
|
}
|
|
302
342
|
|
|
303
343
|
async request<T>(path: string, options?: RequestInit, canApplySession: () => boolean = () => true): Promise<T> {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
344
|
+
return this.withDeadline(path, this.deadlineFor(path), options?.signal, (signal) => this.performRequest<T>(path, {
|
|
345
|
+
...options,
|
|
346
|
+
signal,
|
|
347
|
+
}, canApplySession));
|
|
348
|
+
}
|
|
307
349
|
|
|
308
|
-
|
|
309
|
-
|
|
350
|
+
/**
|
|
351
|
+
* Every request's deadline. A market read or a search waits about ten
|
|
352
|
+
* seconds, research and requests outside market data longer. With nothing
|
|
353
|
+
* back from the service, a request gives up at ten seconds, a search at
|
|
354
|
+
* four, and any request at three after one went unanswered, until something
|
|
355
|
+
* answers again. Three seconds before that, a request still unanswered
|
|
356
|
+
* checks that the service answers at all, so one slow request on a working
|
|
357
|
+
* connection is waited for and a dead connection is not.
|
|
358
|
+
*/
|
|
359
|
+
private deadlineFor(path: string, limitMs?: number): RequestDeadline {
|
|
360
|
+
this.forgetOtherTransport();
|
|
361
|
+
const pathname = path.split("?")[0]!;
|
|
362
|
+
const market = pathname.startsWith("/market/");
|
|
363
|
+
const limit = limitMs ?? (!market ? DEFAULT_SERVICE_REQUEST_LIMIT_MS
|
|
364
|
+
: RESEARCH_MARKET_PATHS.has(pathname) ? this.researchRequestTimeoutMs : this.marketRequestTimeoutMs);
|
|
365
|
+
let answerMs = pathname === "/market/search"
|
|
366
|
+
? Math.min(this.marketRequestTimeoutMs, SEARCH_ANSWER_TIMEOUT_MS)
|
|
367
|
+
: this.marketRequestTimeoutMs;
|
|
368
|
+
if (this.unreachable) answerMs = Math.min(answerMs, UNREACHABLE_REQUEST_TIMEOUT_MS);
|
|
369
|
+
return {
|
|
370
|
+
answerMs,
|
|
371
|
+
limitMs: Math.max(limit, answerMs),
|
|
372
|
+
probeAtMs: answerMs >= SERVICE_PROBE_TIMEOUT_MS ? answerMs - SERVICE_PROBE_TIMEOUT_MS : null,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Runs one request under its deadline. Without an answer from the service by
|
|
378
|
+
* `answerMs` (from this request, any other, or the check that the service
|
|
379
|
+
* answers at all), the connection is taken as hung: the request is aborted
|
|
380
|
+
* and fails as the data service being unreachable, as a refused connection
|
|
381
|
+
* does. A service that answers but is slow gets until `limitMs`, then the
|
|
382
|
+
* request times out. A caller's abort stays an abort.
|
|
383
|
+
*/
|
|
384
|
+
private async withDeadline<T>(
|
|
385
|
+
path: string,
|
|
386
|
+
deadline: RequestDeadline,
|
|
387
|
+
callerSignal: AbortSignal | null | undefined,
|
|
388
|
+
run: (signal: AbortSignal) => Promise<T>,
|
|
389
|
+
): Promise<T> {
|
|
310
390
|
const controller = new AbortController();
|
|
311
|
-
const callerSignal = options?.signal;
|
|
312
391
|
const abortFromCaller = () => controller.abort(callerSignal?.reason);
|
|
313
392
|
if (callerSignal?.aborted) {
|
|
314
393
|
abortFromCaller();
|
|
315
394
|
} else {
|
|
316
395
|
callerSignal?.addEventListener("abort", abortFromCaller, { once: true });
|
|
317
396
|
}
|
|
318
|
-
|
|
397
|
+
const answersAtStart = this.answers;
|
|
398
|
+
const answered = () => this.answers > answersAtStart;
|
|
399
|
+
const generation = cloudApiTransportGeneration;
|
|
400
|
+
const what = path.startsWith("/market/") ? "Cloud market request" : "Cloud request";
|
|
401
|
+
const timedOut = (ms: number) => new OperationTimeoutError(`${what} timed out after ${ms}ms: ${path.split("?")[0]}`);
|
|
402
|
+
const timers: Array<ReturnType<typeof setTimeout>> = [];
|
|
403
|
+
if (this.hungAt != null && Date.now() - this.hungAt < HUNG_FAIL_FAST_MS && !controller.signal.aborted) {
|
|
404
|
+
callerSignal?.removeEventListener("abort", abortFromCaller);
|
|
405
|
+
throw new DataServiceUnreachableError(new OperationTimeoutError(`${what} not sent: the last one got no answer: ${path.split("?")[0]}`));
|
|
406
|
+
}
|
|
407
|
+
this.pending += 1;
|
|
319
408
|
try {
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
409
|
+
const work = run(controller.signal);
|
|
410
|
+
return await new Promise<T>((resolve, reject) => {
|
|
411
|
+
const fail = (error: Error) => {
|
|
412
|
+
reject(error);
|
|
413
|
+
controller.abort(error);
|
|
414
|
+
};
|
|
415
|
+
work.then(resolve, reject);
|
|
416
|
+
if (deadline.probeAtMs != null) {
|
|
417
|
+
timers.push(setTimeout(() => {
|
|
418
|
+
if (!answered()) this.checkServiceAnswers();
|
|
419
|
+
}, deadline.probeAtMs));
|
|
420
|
+
}
|
|
421
|
+
timers.push(setTimeout(() => {
|
|
422
|
+
// The caller gave up first: its abort settles the request, and says nothing of the connection.
|
|
423
|
+
if (callerSignal?.aborted) return;
|
|
424
|
+
if (!answered()) {
|
|
425
|
+
this.recordNoAnswer(generation, true);
|
|
426
|
+
fail(new DataServiceUnreachableError(timedOut(deadline.answerMs)));
|
|
427
|
+
} else if (deadline.limitMs > deadline.answerMs) {
|
|
428
|
+
timers.push(setTimeout(() => fail(timedOut(deadline.limitMs)), deadline.limitMs - deadline.answerMs));
|
|
429
|
+
} else {
|
|
430
|
+
fail(timedOut(deadline.answerMs));
|
|
431
|
+
}
|
|
432
|
+
}, deadline.answerMs));
|
|
433
|
+
});
|
|
330
434
|
} finally {
|
|
435
|
+
for (const timer of timers) clearTimeout(timer);
|
|
331
436
|
callerSignal?.removeEventListener("abort", abortFromCaller);
|
|
437
|
+
this.pending -= 1;
|
|
438
|
+
// Nothing waits on the check any more: it must not hold a command's process open.
|
|
439
|
+
if (this.pending === 0) this.probe?.abort();
|
|
332
440
|
}
|
|
333
441
|
}
|
|
334
442
|
|
|
443
|
+
/**
|
|
444
|
+
* Sends one request, saying the same thing whichever fetch failed to connect
|
|
445
|
+
* (Bun's "Unable to connect", a browser's "Failed to fetch", a refused proxy):
|
|
446
|
+
* every command, pane and status line then reads "Gloom couldn't reach its
|
|
447
|
+
* data service", and code that falls back to a saved copy can tell it apart.
|
|
448
|
+
* A caller's abort stays an abort. An answer of any status says the
|
|
449
|
+
* connection works, for the deadlines of the requests beside it.
|
|
450
|
+
*/
|
|
451
|
+
private async reachService<R>(fetch: () => Promise<R>, signal: AbortSignal | null | undefined): Promise<R> {
|
|
452
|
+
const generation = cloudApiTransportGeneration;
|
|
453
|
+
let response: R;
|
|
454
|
+
try {
|
|
455
|
+
response = await fetch();
|
|
456
|
+
} catch (error) {
|
|
457
|
+
if (signal?.aborted) throw error;
|
|
458
|
+
const failure = asDataServiceFailure(error);
|
|
459
|
+
if (failure instanceof DataServiceUnreachableError) this.recordNoAnswer(generation, false);
|
|
460
|
+
throw failure;
|
|
461
|
+
}
|
|
462
|
+
this.recordAnswer();
|
|
463
|
+
return response;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** A new transport installed is a new connection: what failed on the last one says nothing about it. */
|
|
467
|
+
private forgetOtherTransport(): void {
|
|
468
|
+
if (this.fetchTransport || this.connectionGeneration === cloudApiTransportGeneration) return;
|
|
469
|
+
this.connectionGeneration = cloudApiTransportGeneration;
|
|
470
|
+
this.unreachable = false;
|
|
471
|
+
this.hungAt = null;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/** Something came back from the service: the connection works. */
|
|
475
|
+
private recordAnswer(): void {
|
|
476
|
+
this.answers += 1;
|
|
477
|
+
this.unreachable = false;
|
|
478
|
+
this.hungAt = null;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* A request sent on the transport installed as `generation` got no answer:
|
|
483
|
+
* refused, or `hung` with nothing back by its deadline. Once another
|
|
484
|
+
* transport is installed, what one sent on the last says nothing of it.
|
|
485
|
+
*/
|
|
486
|
+
private recordNoAnswer(generation: number, hung: boolean): void {
|
|
487
|
+
if (!this.fetchTransport && generation !== cloudApiTransportGeneration) return;
|
|
488
|
+
this.unreachable = true;
|
|
489
|
+
if (hung) this.hungAt = Date.now();
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Asks the service for anything at all, once at a time, so a request that
|
|
494
|
+
* is slow can tell a slow server (which answers this) from a dead
|
|
495
|
+
* connection (which does not). Any status counts as an answer. It stops
|
|
496
|
+
* when nothing waits on it any more.
|
|
497
|
+
*/
|
|
498
|
+
private checkServiceAnswers(): void {
|
|
499
|
+
if (this.probe) return;
|
|
500
|
+
const controller = new AbortController();
|
|
501
|
+
const timer = setTimeout(() => controller.abort(), SERVICE_PROBE_TIMEOUT_MS);
|
|
502
|
+
const done = () => {
|
|
503
|
+
clearTimeout(timer);
|
|
504
|
+
if (this.probe === controller) this.probe = null;
|
|
505
|
+
};
|
|
506
|
+
// Done once given up on, even through a transport that ignores the abort.
|
|
507
|
+
controller.signal.addEventListener("abort", done, { once: true });
|
|
508
|
+
this.probe = controller;
|
|
509
|
+
(this.fetchTransport ?? cloudApiFetchTransport)(`${this.baseUrl}${SERVICE_PROBE_PATH}`, { signal: controller.signal })
|
|
510
|
+
.then(() => this.recordAnswer(), () => {
|
|
511
|
+
// Intentional ignore: an unanswered check only leaves the request it guards to its own deadline.
|
|
512
|
+
})
|
|
513
|
+
.finally(done);
|
|
514
|
+
}
|
|
515
|
+
|
|
335
516
|
/**
|
|
336
517
|
* POSTs raw bytes, such as an image, and parses the JSON answer like
|
|
337
518
|
* `request`. Progress is reported where the installed upload transport can
|
|
@@ -355,17 +536,17 @@ export class CloudApiRequestTransport {
|
|
|
355
536
|
url,
|
|
356
537
|
body,
|
|
357
538
|
headers: new Headers(init?.headers),
|
|
358
|
-
signal: options.signal,
|
|
539
|
+
signal: init?.signal ?? options.signal,
|
|
359
540
|
onProgress: options.onProgress,
|
|
360
541
|
})
|
|
361
542
|
: undefined;
|
|
362
|
-
return this.performRequest<T>(path, {
|
|
543
|
+
return this.withDeadline(path, this.deadlineFor(path, UPLOAD_REQUEST_LIMIT_MS), options.signal, (signal) => this.performRequest<T>(path, {
|
|
363
544
|
method: "POST",
|
|
364
545
|
headers,
|
|
365
546
|
// Typed arrays are valid fetch bodies; RequestInit's DOM typing lags behind.
|
|
366
547
|
body: body as BodyInit,
|
|
367
|
-
signal
|
|
368
|
-
}, () => true, send);
|
|
548
|
+
signal,
|
|
549
|
+
}, () => true, send));
|
|
369
550
|
}
|
|
370
551
|
|
|
371
552
|
private async performRequest<T>(
|
|
@@ -389,7 +570,7 @@ export class CloudApiRequestTransport {
|
|
|
389
570
|
|
|
390
571
|
const operation = `${options?.method ?? "GET"} ${path.split("?")[0]}`;
|
|
391
572
|
const request = async () => {
|
|
392
|
-
const res = await reachService(() => (send ?? this.fetchTransport ?? cloudApiFetchTransport)(
|
|
573
|
+
const res = await this.reachService(() => (send ?? this.fetchTransport ?? cloudApiFetchTransport)(
|
|
393
574
|
`${this.baseUrl}${path}`,
|
|
394
575
|
{
|
|
395
576
|
...options,
|
|
@@ -29,10 +29,7 @@ export type ResearchActivity =
|
|
|
29
29
|
| "onboarding_signed_in"
|
|
30
30
|
| "onboarding_pro_viewed"
|
|
31
31
|
| "onboarding_completed"
|
|
32
|
-
| "onboarding_skipped"
|
|
33
|
-
// The first action of a web visitor counted in the first-screen experiment,
|
|
34
|
-
// in that first session. Keep in sync with the server's list.
|
|
35
|
-
| "first_screen_engaged";
|
|
32
|
+
| "onboarding_skipped";
|
|
36
33
|
export type ResearchFeature =
|
|
37
34
|
| "overview"
|
|
38
35
|
| "chart"
|
|
@@ -461,10 +458,7 @@ function webExperimentAttribution(): { experiments?: string } {
|
|
|
461
458
|
* visitors on term.gloom.sh with an analytics id (no Do Not Track or Global
|
|
462
459
|
* Privacy Control) and not driven by automation are asked. See web-experiments.ts.
|
|
463
460
|
*/
|
|
464
|
-
export function exposeWebExperiment(
|
|
465
|
-
experiment: string,
|
|
466
|
-
{ placement, signal }: { placement?: string; signal?: AbortSignal } = {},
|
|
467
|
-
): Promise<string | null> {
|
|
461
|
+
export function exposeWebExperiment(experiment: string): Promise<string | null> {
|
|
468
462
|
if (!researchTelemetryAllowed()) return Promise.resolve(null);
|
|
469
463
|
if (getCurrentPluginTarget() !== "web") return Promise.resolve(null);
|
|
470
464
|
try {
|
|
@@ -481,8 +475,7 @@ export function exposeWebExperiment(
|
|
|
481
475
|
attribution: attributionPayload(),
|
|
482
476
|
experiment: key,
|
|
483
477
|
variant,
|
|
484
|
-
|
|
485
|
-
}, signal),
|
|
478
|
+
}),
|
|
486
479
|
});
|
|
487
480
|
} catch {
|
|
488
481
|
return Promise.resolve(null);
|
package/src/api-client/types.ts
CHANGED
|
@@ -516,6 +516,12 @@ export interface CloudEconEventPayload {
|
|
|
516
516
|
referencePeriod?: string | null;
|
|
517
517
|
/** Who publishes the release (Freddie Mac for the 30-year mortgage rate), and its page when one is named. */
|
|
518
518
|
source?: { name: string; url?: string };
|
|
519
|
+
/**
|
|
520
|
+
* The indicator this release is of, the same from one release to the next
|
|
521
|
+
* (US CPI m/m on every month's row), where `id` names the one row. Absent
|
|
522
|
+
* until Gloom Cloud names indicators; exports leave the column empty.
|
|
523
|
+
*/
|
|
524
|
+
indicatorId?: string | null;
|
|
519
525
|
}
|
|
520
526
|
|
|
521
527
|
/** One currency's short-term rate, as GET /cloud/econ/short-rates serves it. */
|
|
@@ -14,6 +14,7 @@ export {
|
|
|
14
14
|
type KeyChordEventLike,
|
|
15
15
|
} from "./chord";
|
|
16
16
|
export {
|
|
17
|
+
coreActionOnChord,
|
|
17
18
|
describeKeybindingIssue,
|
|
18
19
|
isPaneKeybindingAction,
|
|
19
20
|
keyChordsOverlap,
|
|
@@ -32,6 +33,8 @@ export {
|
|
|
32
33
|
formatChordForHost,
|
|
33
34
|
menuAcceleratorFor,
|
|
34
35
|
primaryModifierFor,
|
|
36
|
+
shortActionLabel,
|
|
37
|
+
takenChordMessage,
|
|
35
38
|
} from "./labels";
|
|
36
39
|
export { KeybindingsProvider, getDefaultKeybindings, useKeybindings, useResolvedKeybindings } from "./react";
|
|
37
40
|
export {
|
|
@@ -3,8 +3,9 @@ import {
|
|
|
3
3
|
type ShortcutDisplayMode,
|
|
4
4
|
type ShortcutPlatform,
|
|
5
5
|
} from "../../utils/shortcut-labels";
|
|
6
|
+
import { t, tf } from "../../i18n";
|
|
6
7
|
import { formatKeyChord, serializeKeyChord, type KeyChord, type PrimaryModifier } from "./chord";
|
|
7
|
-
import type { ResolvedKeybindings } from "./resolve";
|
|
8
|
+
import type { ResolvedKeybindingAction, ResolvedKeybindings } from "./resolve";
|
|
8
9
|
|
|
9
10
|
/** What `CmdOrCtrl` is on this host: Control in a terminal, Command on a Mac desktop. */
|
|
10
11
|
export function primaryModifierFor(
|
|
@@ -85,3 +86,19 @@ export function menuAcceleratorFor(resolved: ResolvedKeybindings, actionId: stri
|
|
|
85
86
|
const chord = resolved.actionsById.get(actionId)?.chords.find((entry) => entry.primary || entry.cmd);
|
|
86
87
|
return chord ? serializeKeyChord(chord) : undefined;
|
|
87
88
|
}
|
|
89
|
+
|
|
90
|
+
/** The first clause of a description, for messages that name an action mid-sentence. */
|
|
91
|
+
export function shortActionLabel(description: string): string {
|
|
92
|
+
const clause = description.split(/[,.](?:\s|$)/)[0] ?? description;
|
|
93
|
+
return clause.trim() || description.trim();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Why a chord a built-in action uses (coreActionOnChord) is refused, the
|
|
98
|
+
* same in the shortcuts editor and `config set keybindings`: "Ctrl+P is
|
|
99
|
+
* taken: Open the command bar. Press another key."
|
|
100
|
+
*/
|
|
101
|
+
export function takenChordMessage(chord: string, action: ResolvedKeybindingAction): string {
|
|
102
|
+
const name = action.def ? t(action.def.description) : action.pluginShortcut?.description ?? action.id;
|
|
103
|
+
return tf("{chord} is taken: {target}. Press another key.", { chord, target: shortActionLabel(name) });
|
|
104
|
+
}
|
|
@@ -249,6 +249,20 @@ export function matchKeybinding(resolved: ResolvedKeybindings, event: KeyChordEv
|
|
|
249
249
|
return null;
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
+
/**
|
|
253
|
+
* The built-in action already on a chord, on this host, other than `exceptId`:
|
|
254
|
+
* one whose chord fires on the same keypress, a digit under the layout
|
|
255
|
+
* switcher's 1-9 included. Capture refuses such a chord, so a new binding never
|
|
256
|
+
* leaves a core key (the command bar, close pane, quit) dead. Null when no
|
|
257
|
+
* built-in action has it; a plugin shortcut or a bound command does not count.
|
|
258
|
+
*/
|
|
259
|
+
export function coreActionOnChord(resolved: ResolvedKeybindings, chord: KeyChord, exceptId?: string): ResolvedKeybindingAction | null {
|
|
260
|
+
return resolved.actions.find((action) => action.def && action.id !== exceptId
|
|
261
|
+
&& action.chords.some((existing) => keyChordsOverlap(existing, chord)
|
|
262
|
+
|| (existing.key === "digit" && /^[1-9]$/.test(chord.key) && keyChordsOverlap({ ...existing, key: chord.key }, chord))))
|
|
263
|
+
?? null;
|
|
264
|
+
}
|
|
265
|
+
|
|
252
266
|
export function matchesKeybindingAction(resolved: ResolvedKeybindings, id: string, event: KeyChordEventLike): boolean {
|
|
253
267
|
const action = resolved.actionsById.get(id);
|
|
254
268
|
return !!action && action.chords.some((chord) => matchesKeyChord(chord, event));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, type Dispatch } from "react";
|
|
1
|
+
import { useCallback, useRef, type Dispatch } from "react";
|
|
2
2
|
import {
|
|
3
3
|
rebaseBrokerSyncConfig,
|
|
4
4
|
restoreBrokerPortfoliosFromTickerPositions,
|
|
@@ -53,6 +53,7 @@ export function useBrokerImportRuntime({
|
|
|
53
53
|
stateRef: { current: AppState };
|
|
54
54
|
tickerRepository: AppTickerRepositoryPort;
|
|
55
55
|
}): AppBrokerImportRuntime {
|
|
56
|
+
const syncRequests = useRef(new Map<string, object>());
|
|
56
57
|
/**
|
|
57
58
|
* Commits a sync: carried onto the app as it is now (a pull or an edit while
|
|
58
59
|
* the broker answered stays) and handed to the app before anything is
|
|
@@ -104,36 +105,52 @@ export function useBrokerImportRuntime({
|
|
|
104
105
|
tickerMap?: Map<string, TickerRecord>,
|
|
105
106
|
options?: BrokerImportOptions,
|
|
106
107
|
) => {
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
persistResolvedBrokerConfig: options?.persistResolvedBrokerConfig,
|
|
116
|
-
signal: options?.signal,
|
|
117
|
-
deferPersistence: true,
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
// A profile removed while its broker was answering stays removed, positions included.
|
|
121
|
-
if (!options?.config && !getBrokerInstance(stateRef.current.config.brokerInstances, instanceId)) return result;
|
|
122
|
-
let commitStarted = false;
|
|
108
|
+
const request = {};
|
|
109
|
+
syncRequests.current.set(instanceId, request);
|
|
110
|
+
const report = (error: string | null) => {
|
|
111
|
+
// A late read must not replace the outcome of a newer manual retry.
|
|
112
|
+
if (syncRequests.current.get(instanceId) === request && !options?.signal?.aborted) {
|
|
113
|
+
dispatch({ type: "SET_BROKER_SYNC_ERROR", instanceId, error });
|
|
114
|
+
}
|
|
115
|
+
};
|
|
123
116
|
try {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
117
|
+
const baseConfig = options?.config ?? stateRef.current.config;
|
|
118
|
+
const result = await syncBrokerInstance({
|
|
119
|
+
config: baseConfig,
|
|
120
|
+
instanceId,
|
|
121
|
+
brokers: pluginRegistry.brokers,
|
|
122
|
+
tickerRepository,
|
|
123
|
+
existingTickers: tickerMap ?? new Map(stateRef.current.tickers),
|
|
124
|
+
resources: pluginRegistry.persistence.resources,
|
|
125
|
+
persistResolvedBrokerConfig: options?.persistResolvedBrokerConfig,
|
|
126
|
+
signal: options?.signal,
|
|
127
|
+
deferPersistence: true,
|
|
130
128
|
});
|
|
131
129
|
|
|
132
|
-
|
|
130
|
+
// A profile removed while its broker was answering stays removed, positions included.
|
|
131
|
+
if (!options?.config && !getBrokerInstance(stateRef.current.config.brokerInstances, instanceId)) return result;
|
|
132
|
+
let commitStarted = false;
|
|
133
|
+
try {
|
|
134
|
+
throwIfBrokerImportCancelled(options?.signal);
|
|
135
|
+
options?.onCommitStart?.();
|
|
136
|
+
commitStarted = true;
|
|
137
|
+
await applyBrokerImportResult(instanceId, result, baseConfig, {
|
|
138
|
+
...options,
|
|
139
|
+
callerOwnsConfig: options?.config !== undefined,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
report(null);
|
|
143
|
+
return result;
|
|
144
|
+
} finally {
|
|
145
|
+
if (commitStarted) options?.onCommitEnd?.();
|
|
146
|
+
}
|
|
147
|
+
} catch (error) {
|
|
148
|
+
report("Positions could not be synced. Use Sync to retry.");
|
|
149
|
+
throw error;
|
|
133
150
|
} finally {
|
|
134
|
-
if (
|
|
151
|
+
if (syncRequests.current.get(instanceId) === request) syncRequests.current.delete(instanceId);
|
|
135
152
|
}
|
|
136
|
-
}, [applyBrokerImportResult, pluginRegistry.brokers, pluginRegistry.persistence.resources, stateRef, tickerRepository]);
|
|
153
|
+
}, [applyBrokerImportResult, dispatch, pluginRegistry.brokers, pluginRegistry.persistence.resources, stateRef, tickerRepository]);
|
|
137
154
|
|
|
138
155
|
const autoImportBrokerPositions = useCallback(async (tickerMap: Map<string, TickerRecord>) => {
|
|
139
156
|
// Once per install, manual portfolios held in one other currency take it (see adoptHeldPortfolioCurrencies).
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"src/components/layout/shell/index.tsx": ["cloud"],
|
|
28
28
|
"src/components/onboarding/use-onboarding-upgrade.ts": ["chat"],
|
|
29
29
|
"src/layout/desks.ts": ["chart-composer"],
|
|
30
|
-
"src/plugins/catalog-browser.ts": ["alerts","analytics","attention","awards","backtest","bond-calculator","catalysts","cds","central-bank-rates","changelog","chart-composer","clinical-trials","cloud","comment-letters","commodities-energy","commodity-balances","company-attention","company-kpis","connections","correlation","cot","cpi","credit-boards","credit-conditions","credit-documents","crypto-board","crypto-etf-flows","debt-maturities","dividend-yield","docs","doe","earnings-calls","earnings-ripple","econ","econ-statistics","equity-screener","estimate-revisions","executives","exposure","fear-greed","filing-events","fixed-income","futures","futures-curve","fx-matrix","gpu","help","insider","ipo-calendar","iv-history","jobs","jodi","kelly-sizer","layout-manager","macro-day","market-halts","market-heatmap","market-valuation","members","mna","money-markets","news","openfda","options","options-calculator","options-positioning","options-scenario","pe-band","perps","portfolio-list","power","primary-dealers","rate-path","realized-vol","relative-rotation","research","research-search","revenue-breakdown","reverse-dcf","risk-factors","scanner","seasonality","sec","sectors","short-interest","short-volume","social-mentions","supply-chain","ticker-detail","time-sales","trace-bonds","treasury-auctions","treasury-daily","vol-surface","volatility","world-indices","world-venue-map","yield-curve"],
|
|
30
|
+
"src/plugins/catalog-browser.ts": ["alerts","analytics","attention","awards","backtest","bond-calculator","catalysts","cds","central-bank-rates","changelog","chart-composer","clinical-trials","cloud","comment-letters","commodities-energy","commodity-balances","company-attention","company-kpis","connections","correlation","cot","cpi","credit-boards","credit-conditions","credit-documents","crypto-board","crypto-etf-flows","debt-maturities","dividend-yield","docs","doe","earnings","earnings-calls","earnings-ripple","econ","econ-statistics","equity-screener","estimate-revisions","executives","exposure","fear-greed","filing-events","fixed-income","futures","futures-curve","fx-matrix","gpu","help","holders","insider","ipo-calendar","iv-history","jobs","jodi","kelly-sizer","layout-manager","macro-day","market-halts","market-heatmap","market-valuation","members","mna","money-markets","news","openfda","options","options-calculator","options-positioning","options-scenario","pe-band","perps","portfolio-list","power","primary-dealers","rate-path","realized-vol","relative-rotation","research","research-search","revenue-breakdown","reverse-dcf","risk-factors","scanner","seasonality","sec","sectors","short-interest","short-volume","social-mentions","supply-chain","thirteenf","ticker-detail","time-sales","trace-bonds","treasury-auctions","treasury-daily","vol-surface","volatility","world-indices","world-venue-map","yield-curve"],
|
|
31
31
|
"src/plugins/catalog-ui.ts": ["alerts","clinical-trials","cloud","comment-letters","custom-view","fear-greed","ipo-calendar","market-halts","market-heatmap","members","news","notes","openfda","research-search"],
|
|
32
32
|
"src/plugins/catalog.ts": ["debug"],
|
|
33
33
|
"src/remote/controller.ts": ["custom-view"],
|
|
@@ -44,6 +44,11 @@ export function fredInterval(frequency: string | null | undefined): string | nul
|
|
|
44
44
|
return word || null;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/** Whether a series' dates are days it was observed on (FRED's `Daily`, `Weekly, Ending Friday`), not the start of a period; false when it does not say. */
|
|
48
|
+
export function fredObservesDays(frequency: string | null | undefined): boolean {
|
|
49
|
+
return /^(?:daily|weekly)\b/i.test(frequency?.trim() ?? "");
|
|
50
|
+
}
|
|
51
|
+
|
|
47
52
|
/**
|
|
48
53
|
* The observations from the series' first one on: Gloom Cloud answers a
|
|
49
54
|
* `--start` before a series begins with a blank row for every day in between
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
applyActionBinding,
|
|
3
|
+
coreActionOnChord,
|
|
3
4
|
describeKeybindingIssue,
|
|
5
|
+
formatChordForHost,
|
|
4
6
|
isCoreKeybindingActionId,
|
|
5
7
|
isTypingChord,
|
|
6
8
|
parseKeyChord,
|
|
@@ -9,7 +11,9 @@ import {
|
|
|
9
11
|
resolveKeybindings,
|
|
10
12
|
serializeKeyChord,
|
|
11
13
|
setCommandBinding,
|
|
14
|
+
takenChordMessage,
|
|
12
15
|
updateKeybindingsConfig,
|
|
16
|
+
type KeyChord,
|
|
13
17
|
} from "../../app/keybindings";
|
|
14
18
|
import type { AppConfig } from "../../types/config";
|
|
15
19
|
|
|
@@ -34,6 +38,21 @@ export function describeKeybindingsForCli(config: AppConfig): Record<string, unk
|
|
|
34
38
|
};
|
|
35
39
|
}
|
|
36
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Why the first of `chords` a built-in action other than `exceptId` already
|
|
43
|
+
* uses is refused, as the shortcuts editor says it; null when none is. The
|
|
44
|
+
* table is the terminal's and the desktop's at once, so an action either has
|
|
45
|
+
* keeps its key.
|
|
46
|
+
*/
|
|
47
|
+
function takenByCoreAction(config: AppConfig, chords: readonly KeyChord[], exceptId?: string): string | null {
|
|
48
|
+
const resolved = resolveKeybindings(config.keybindings);
|
|
49
|
+
for (const chord of chords) {
|
|
50
|
+
const core = coreActionOnChord(resolved, chord, exceptId);
|
|
51
|
+
if (core) return takenChordMessage(formatChordForHost(chord, "terminal"), core);
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
37
56
|
export type KeybindingCliSetResult =
|
|
38
57
|
| { ok: true; config: AppConfig; value: unknown }
|
|
39
58
|
| { ok: false; message: string };
|
|
@@ -71,6 +90,8 @@ export function applyKeybindingCliSet(config: AppConfig, key: string, rawValue:
|
|
|
71
90
|
if (!chord) return { ok: false, message: `"${text}" is not a key chord. Examples: Ctrl+T, CmdOrCtrl+Shift+F, Alt+1, F5, \`` };
|
|
72
91
|
chords.push(chord);
|
|
73
92
|
}
|
|
93
|
+
const taken = takenByCoreAction(config, chords, target);
|
|
94
|
+
if (taken) return { ok: false, message: taken };
|
|
74
95
|
return {
|
|
75
96
|
ok: true,
|
|
76
97
|
config: updateKeybindingsConfig(config, applyActionBinding(config.keybindings, target, chords)),
|
|
@@ -91,6 +112,8 @@ export function applyKeybindingCliSet(config: AppConfig, key: string, rawValue:
|
|
|
91
112
|
if (isTypingChord(chord)) {
|
|
92
113
|
return { ok: false, message: `"${target}" would fire while typing; command bindings need Ctrl, Cmd, Alt or a function key.` };
|
|
93
114
|
}
|
|
115
|
+
const taken = takenByCoreAction(config, [chord]);
|
|
116
|
+
if (taken) return { ok: false, message: taken };
|
|
94
117
|
return {
|
|
95
118
|
ok: true,
|
|
96
119
|
config: updateKeybindingsConfig(config, setCommandBinding(config.keybindings, chord, value)),
|