gloomberb 0.17.2 → 0.17.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/api-client/futures-curve.ts +16 -0
- package/src/api-client/index.ts +18 -8
- package/src/api-client/types.ts +3 -1
- package/src/cli/commands/market.ts +60 -9
- package/src/cli/commands/search.ts +43 -2
- package/src/cli/commands/ticker.ts +100 -21
- package/src/cli/desktop-pane-shot.ts +34 -9
- package/src/cli/fx-pair.ts +17 -0
- package/src/cli/glossary.ts +134 -6
- package/src/cli/history-rows.ts +16 -4
- package/src/cli/index.ts +44 -12
- package/src/cli/known-gaps.ts +21 -2
- package/src/cli/listing-arg.ts +19 -4
- package/src/cli/listing-suggestion.ts +33 -2
- package/src/cli/pane-functions/dom.ts +39 -6
- package/src/cli/pane-functions/freshness.ts +48 -5
- package/src/cli/pane-functions/function-help.ts +1 -1
- package/src/cli/pane-functions/headless.ts +85 -19
- package/src/cli/pane-functions/index.ts +60 -4
- package/src/cli/pane-functions/market-session.ts +69 -6
- package/src/cli/pane-functions/resolver.ts +4 -2
- package/src/cli/pane-functions/screenshot.ts +14 -1
- package/src/cli/scroll-truncation.ts +5 -1
- package/src/cli/symbol-args.ts +41 -3
- package/src/cli/symbol-list.ts +19 -7
- package/src/cli/unknown-command.ts +70 -0
- package/src/components/chart/native/surface/visibility.ts +37 -7
- package/src/components/layout/header.tsx +348 -102
- package/src/components/markdown-text.tsx +40 -66
- package/src/components/market-board/index.tsx +12 -6
- package/src/components/reader/detail-reader.tsx +28 -6
- package/src/components/ticker/badge/index.tsx +2 -0
- package/src/components/ui/display.tsx +6 -1
- package/src/components/ui/external-link.tsx +10 -2
- package/src/data/memory-resource-store.ts +1 -1
- package/src/data/resource-store.ts +13 -1
- package/src/docs/snapshot.json +13 -13
- package/src/i18n/es.ts +1 -0
- package/src/i18n/ja.ts +1 -0
- package/src/i18n/ko.ts +1 -0
- package/src/i18n/zh-cn.ts +1 -0
- package/src/i18n/zh-tw.ts +1 -0
- package/src/market-data/futures-contract.ts +10 -0
- package/src/market-data/market/format.ts +7 -2
- package/src/market-data/market/freshness.ts +25 -1
- package/src/market-data/quotes/regular-session-valuation.ts +32 -1
- package/src/market-data/quotes/resolution.ts +2 -0
- package/src/plugins/builtin/account-management/pane.tsx +17 -13
- package/src/plugins/builtin/attention/headless.ts +19 -10
- package/src/plugins/builtin/attention/model.ts +22 -0
- package/src/plugins/builtin/bond-calculator/pane.tsx +2 -1
- package/src/plugins/builtin/changelog/index.tsx +11 -8
- package/src/plugins/builtin/chart-composer/editor/desktop-view.tsx +2 -0
- package/src/plugins/builtin/chat/attachments/desktop.tsx +2 -1
- package/src/plugins/builtin/chat/attachments/model.ts +5 -3
- package/src/plugins/builtin/chat/content/footer.tsx +6 -0
- package/src/plugins/builtin/chat/content/open-targets.ts +13 -4
- package/src/plugins/builtin/chat/content/transcript.tsx +2 -0
- package/src/plugins/builtin/chat/content/whole-message-copy.ts +70 -0
- package/src/plugins/builtin/chat/layout.ts +91 -31
- package/src/plugins/builtin/chat/markdown.ts +94 -0
- package/src/plugins/builtin/chat/message/desktop.tsx +3 -2
- package/src/plugins/builtin/chat/message/inline-tokens.tsx +197 -80
- package/src/plugins/builtin/chat/message/render-state.ts +3 -0
- package/src/plugins/builtin/chat/message/terminal.tsx +4 -4
- package/src/plugins/builtin/cloud/thesis/exposure.ts +18 -2
- package/src/plugins/builtin/company-attention/headless.ts +8 -5
- package/src/plugins/builtin/company-attention/model.ts +53 -2
- package/src/plugins/builtin/company-attention/pane.tsx +1 -1
- package/src/plugins/builtin/congress-trades/detail.tsx +7 -4
- package/src/plugins/builtin/congress-trades/pane.tsx +3 -1
- package/src/plugins/builtin/correlation/relationship/pane.tsx +3 -7
- package/src/plugins/builtin/cot/client.ts +12 -4
- package/src/plugins/builtin/cot/pane.tsx +2 -2
- package/src/plugins/builtin/crypto-board/model.ts +7 -3
- package/src/plugins/builtin/debt-maturities/pane.tsx +1 -6
- package/src/plugins/builtin/debug/index.tsx +15 -15
- package/src/plugins/builtin/dividend-yield/client.ts +8 -1
- package/src/plugins/builtin/dividend-yield/headless.ts +4 -1
- package/src/plugins/builtin/dividend-yield/pane.tsx +6 -5
- package/src/plugins/builtin/docs/index.tsx +5 -3
- package/src/plugins/builtin/docs/pane.tsx +18 -1
- package/src/plugins/builtin/docs/targets.ts +5 -3
- package/src/plugins/builtin/earnings-calls/data.ts +22 -0
- package/src/plugins/builtin/earnings-calls/pane.tsx +24 -14
- package/src/plugins/builtin/equity-screener/headless.ts +4 -3
- package/src/plugins/builtin/equity-screener/live.ts +27 -1
- package/src/plugins/builtin/equity-screener/pane.tsx +21 -2
- package/src/plugins/builtin/fear-greed/charts.tsx +5 -1
- package/src/plugins/builtin/fear-greed/pane.tsx +1 -1
- package/src/plugins/builtin/futures/index.tsx +32 -4
- package/src/plugins/builtin/futures/model.ts +65 -3
- package/src/plugins/builtin/futures/table.tsx +31 -11
- package/src/plugins/builtin/futures/use-board-terms.ts +89 -0
- package/src/plugins/builtin/holders/pane.tsx +6 -1
- package/src/plugins/builtin/mna/client.ts +12 -2
- package/src/plugins/builtin/mna/detail.tsx +2 -2
- package/src/plugins/builtin/mna/pane.tsx +2 -2
- package/src/plugins/builtin/notes/owner.tsx +5 -4
- package/src/plugins/builtin/notes/ticker-notes-tab.tsx +2 -3
- package/src/plugins/builtin/options-calculator/pane.tsx +2 -3
- package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -5
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -3
- package/src/plugins/builtin/portfolio-list/cli/shared.ts +9 -0
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +6 -3
- package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +2 -9
- package/src/plugins/builtin/research/event-model.ts +14 -0
- package/src/plugins/builtin/research/headless-schema.ts +31 -1
- package/src/plugins/builtin/research/relative-valuation-columns.ts +289 -0
- package/src/plugins/builtin/research/relative-valuation-currency.ts +164 -0
- package/src/plugins/builtin/research/relative-valuation-headless.ts +94 -11
- package/src/plugins/builtin/research/relative-valuation-model.ts +4 -1
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +13 -4
- package/src/plugins/builtin/research/relative-valuation-peers.ts +302 -0
- package/src/plugins/builtin/research-search/data.ts +11 -0
- package/src/plugins/builtin/research-search/pane.tsx +13 -2
- package/src/plugins/builtin/scanner/hilo-pane.tsx +7 -17
- package/src/plugins/builtin/seasonality/client.ts +23 -0
- package/src/plugins/builtin/seasonality/pane.tsx +4 -2
- package/src/plugins/builtin/sectors/index.tsx +7 -3
- package/src/plugins/builtin/shared/report-freshness.ts +6 -2
- package/src/plugins/builtin/short-interest/headless.ts +13 -3
- package/src/plugins/builtin/short-interest/index.tsx +1 -1
- package/src/plugins/builtin/short-interest/model.ts +62 -3
- package/src/plugins/builtin/short-interest/pane.tsx +2 -2
- package/src/plugins/builtin/short-interest/schedule.ts +107 -0
- package/src/plugins/builtin/short-interest/watch-client.ts +54 -8
- package/src/plugins/builtin/short-interest/watch-headless.ts +8 -3
- package/src/plugins/builtin/short-interest/watch-model.ts +52 -23
- package/src/plugins/builtin/short-interest/watch-pane.tsx +1 -1
- package/src/plugins/builtin/supply-chain/flow.tsx +14 -4
- package/src/plugins/builtin/themes/headless.ts +1 -1
- package/src/plugins/builtin/thirteenf/overlap-pane.tsx +23 -27
- package/src/plugins/builtin/ticker-detail/index.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/overview/model.ts +4 -3
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +4 -4
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +9 -4
- package/src/plugins/builtin/vol-surface/pane.tsx +16 -1
- package/src/plugins/builtin/volatility/chart-model.ts +1 -1
- package/src/plugins/builtin/volatility/charts.tsx +1 -1
- package/src/renderers/dom/styles.css +6 -0
- package/src/sources/gloomberb-cloud/normalizers.ts +1 -0
- package/src/sources/provider-router/batches.ts +2 -1
- package/src/sources/provider-router/cache.ts +3 -0
- package/src/sync/baseline.ts +19 -14
- package/src/sync/controller.ts +140 -40
- package/src/sync/core-contributors.ts +256 -32
- package/src/sync/types.ts +44 -0
- package/src/time-series/resolve.ts +19 -2
- package/src/types/data-provider.ts +8 -0
- package/src/types/financials.ts +40 -4
- package/src/types/headless.ts +43 -0
- package/src/types/plugin.ts +7 -0
- package/src/utils/declared-dividends.ts +39 -0
- package/src/utils/forward-dividend.ts +34 -11
- package/src/utils/fundamentals.ts +13 -2
- package/src/utils/markdown.ts +180 -0
- package/src/utils/market-capitalization.ts +22 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { scheduleConfigSave } from "../state/config-save-scheduler";
|
|
2
2
|
import { debugLog } from "../utils/debug-log";
|
|
3
|
-
import { stableStringify } from "../utils/hash";
|
|
3
|
+
import { fnv1aHex, stableStringify } from "../utils/hash";
|
|
4
4
|
import type { AppConfig, BrokerInstanceConfig, SavedLayout } from "../types/config";
|
|
5
5
|
import type { PricePoint, TickerFinancials } from "../types/financials";
|
|
6
6
|
import type { Portfolio, TickerMetadata, TickerPosition, TickerRecord, Watchlist } from "../types/ticker";
|
|
@@ -40,6 +40,9 @@ import { withoutMissingWatchlists } from "../plugins/builtin/portfolio-list/muta
|
|
|
40
40
|
*/
|
|
41
41
|
const SESSION_SAVED_LAYOUT_KEYS = ["paneState", "focusedPaneId", "activePanel"] as const;
|
|
42
42
|
|
|
43
|
+
/** Together these are the workspace: the tabs, the one showing, and its arrangement. */
|
|
44
|
+
const LAYOUT_SYNC_KEYS = ["layout", "layouts", "activeLayoutIndex"] as const;
|
|
45
|
+
|
|
43
46
|
const SENSITIVE_KEY_PATTERN = /(token|secret|password|credential|private|api[_-]?key|access[_-]?key|refresh[_-]?key|session|cookie|dataDir|path|directory|localPath)/i;
|
|
44
47
|
|
|
45
48
|
const log = debugLog.createLogger("sync");
|
|
@@ -351,6 +354,14 @@ function sanitizeTickerMetadata(metadata: TickerMetadata, financials?: TickerFin
|
|
|
351
354
|
};
|
|
352
355
|
}
|
|
353
356
|
|
|
357
|
+
function collectLayoutPayload(config: Pick<AppConfig, typeof LAYOUT_SYNC_KEYS[number]>) {
|
|
358
|
+
return sanitizeUnknown({
|
|
359
|
+
layout: config.layout,
|
|
360
|
+
layouts: config.layouts.map(withoutSessionLayoutState),
|
|
361
|
+
activeLayoutIndex: config.activeLayoutIndex,
|
|
362
|
+
}) as Record<string, unknown>;
|
|
363
|
+
}
|
|
364
|
+
|
|
354
365
|
function collectCoreConfigPayload(config: AppConfig) {
|
|
355
366
|
return sanitizeUnknown({
|
|
356
367
|
configVersion: config.configVersion,
|
|
@@ -358,9 +369,7 @@ function collectCoreConfigPayload(config: AppConfig) {
|
|
|
358
369
|
refreshIntervalMinutes: config.refreshIntervalMinutes,
|
|
359
370
|
portfolios: config.portfolios.map(sanitizePortfolio),
|
|
360
371
|
watchlists: config.watchlists.map(sanitizeWatchlist),
|
|
361
|
-
|
|
362
|
-
layouts: config.layouts.map(withoutSessionLayoutState),
|
|
363
|
-
activeLayoutIndex: config.activeLayoutIndex,
|
|
372
|
+
...collectLayoutPayload(config),
|
|
364
373
|
brokerInstances: config.brokerInstances.map(sanitizeBrokerInstance),
|
|
365
374
|
// This build merges profiles one by one before it pushes, so a profile
|
|
366
375
|
// missing from its list was removed, not one it never pulled.
|
|
@@ -622,11 +631,204 @@ function withLocalSessionLayoutState(
|
|
|
622
631
|
});
|
|
623
632
|
}
|
|
624
633
|
|
|
634
|
+
/**
|
|
635
|
+
* A layout as the user arranged it. Stacking order follows whichever floating
|
|
636
|
+
* pane was focused last, and a pane in its own desktop window is one a
|
|
637
|
+
* terminal draws inline as a floating pane, so neither tells two copies apart.
|
|
638
|
+
*/
|
|
639
|
+
function arrangedLayout(layout: unknown): unknown {
|
|
640
|
+
if (!isRecord(layout)) return layout;
|
|
641
|
+
const windows = [
|
|
642
|
+
...(Array.isArray(layout.floating) ? layout.floating : []),
|
|
643
|
+
...(Array.isArray(layout.detached) ? layout.detached : []),
|
|
644
|
+
].map((entry) => {
|
|
645
|
+
if (!isRecord(entry)) return entry;
|
|
646
|
+
const { zIndex: _zIndex, ...rest } = entry;
|
|
647
|
+
return rest;
|
|
648
|
+
});
|
|
649
|
+
const instanceId = (entry: unknown) => (isRecord(entry) ? String(entry.instanceId) : "");
|
|
650
|
+
windows.sort((left, right) => instanceId(left).localeCompare(instanceId(right)));
|
|
651
|
+
return { ...layout, floating: windows, detached: [] };
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Pulled and last synced payloads are compared on every sync and never change.
|
|
655
|
+
const layoutIntents = new WeakMap<object, string | null>();
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* A key for the workspace a payload describes, equal for two copies that only
|
|
659
|
+
* differ in what reflects one device, so a real edit can be told from them.
|
|
660
|
+
* Null when the payload holds no complete workspace.
|
|
661
|
+
*/
|
|
662
|
+
function layoutIntent(payload: unknown): string | null {
|
|
663
|
+
if (!isRecord(payload) || !Array.isArray(payload.layouts)) return null;
|
|
664
|
+
if (!LAYOUT_SYNC_KEYS.every((key) => key in payload)) return null;
|
|
665
|
+
const cached = layoutIntents.get(payload);
|
|
666
|
+
if (cached !== undefined) return cached;
|
|
667
|
+
const intent = stableStringify(sanitizeUnknown({
|
|
668
|
+
layout: arrangedLayout(payload.layout),
|
|
669
|
+
layouts: payload.layouts.map((entry) => (
|
|
670
|
+
isRecord(entry)
|
|
671
|
+
? { ...withoutSessionLayoutState(entry as unknown as SavedLayout), layout: arrangedLayout(entry.layout) }
|
|
672
|
+
: entry
|
|
673
|
+
)),
|
|
674
|
+
activeLayoutIndex: payload.activeLayoutIndex,
|
|
675
|
+
}));
|
|
676
|
+
const key = `${fnv1aHex(intent)}:${intent.length}`;
|
|
677
|
+
layoutIntents.set(payload, key);
|
|
678
|
+
return key;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/** The workspace keys of a payload, with the time its layout was last edited when it says. */
|
|
682
|
+
function pickLayoutPayload(payload: Record<string, unknown>): Record<string, unknown> {
|
|
683
|
+
const picked = Object.fromEntries([...LAYOUT_SYNC_KEYS, "layoutEditedAt"].flatMap((key) => (
|
|
684
|
+
key in payload ? [[key, payload[key]]] : []
|
|
685
|
+
)));
|
|
686
|
+
// An older build still sent the view it was looking at with each layout.
|
|
687
|
+
if (Array.isArray(picked.layouts)) {
|
|
688
|
+
picked.layouts = picked.layouts.map((entry) => (isRecord(entry) ? withoutSessionLayoutState(entry as unknown as SavedLayout) : entry));
|
|
689
|
+
}
|
|
690
|
+
return picked;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* What this device remembers about its workspace between syncs: when it last
|
|
695
|
+
* edited it (null when that happened while the app was closed, so the time is
|
|
696
|
+
* unknown), or, when it took the workspace from the cloud, that copy as synced.
|
|
697
|
+
*/
|
|
698
|
+
interface LayoutMemo {
|
|
699
|
+
intent: string;
|
|
700
|
+
editedAt: number | null;
|
|
701
|
+
synced?: Record<string, unknown>;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function readLayoutMemo(value: unknown): LayoutMemo | null {
|
|
705
|
+
if (!isRecord(value) || typeof value.intent !== "string") return null;
|
|
706
|
+
return {
|
|
707
|
+
intent: value.intent,
|
|
708
|
+
editedAt: typeof value.editedAt === "number" && Number.isFinite(value.editedAt) ? value.editedAt : null,
|
|
709
|
+
...(isRecord(value.synced) ? { synced: value.synced } : {}),
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* When a pulled workspace was last edited. A copy from a build that did not
|
|
715
|
+
* date edits counts as edited when it was pushed, which is never earlier.
|
|
716
|
+
*/
|
|
717
|
+
function pulledLayoutEditedAt(payload: Record<string, unknown>, pushedAt: string | undefined): number {
|
|
718
|
+
if (typeof payload.layoutEditedAt === "number" && Number.isFinite(payload.layoutEditedAt)) return payload.layoutEditedAt;
|
|
719
|
+
const pushed = pushedAt ? Date.parse(pushedAt) : Number.NaN;
|
|
720
|
+
return Number.isFinite(pushed) ? pushed : Number.POSITIVE_INFINITY;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Whether a pulled workspace replaces this device's, three ways: against the
|
|
725
|
+
* copy this device last synced, the one it had when the pull started, and the
|
|
726
|
+
* pulled one. Only a real edit here keeps the local copy; the pane state,
|
|
727
|
+
* focus and window stacking that change on every launch never do.
|
|
728
|
+
*/
|
|
729
|
+
function shouldAdoptPulledLayout({
|
|
730
|
+
local,
|
|
731
|
+
atPullStart,
|
|
732
|
+
lastSynced,
|
|
733
|
+
pulled,
|
|
734
|
+
memo,
|
|
735
|
+
pulledEditedAt,
|
|
736
|
+
}: {
|
|
737
|
+
local: string | null;
|
|
738
|
+
atPullStart: string | null;
|
|
739
|
+
lastSynced: string | null;
|
|
740
|
+
pulled: string | null;
|
|
741
|
+
memo: LayoutMemo | null;
|
|
742
|
+
pulledEditedAt: number;
|
|
743
|
+
}): boolean {
|
|
744
|
+
if (pulled === null || pulled === local) return false;
|
|
745
|
+
// Edited while the pull was in flight: newer than anything it carries.
|
|
746
|
+
if (local !== atPullStart) return false;
|
|
747
|
+
const takenFromCloud = !!memo?.synced && memo.intent === local;
|
|
748
|
+
const editedHere = lastSynced !== null && local !== lastSynced && !takenFromCloud;
|
|
749
|
+
if (!editedHere) return true;
|
|
750
|
+
// The cloud still holds what this device last synced: only this side changed.
|
|
751
|
+
if (pulled === lastSynced) return false;
|
|
752
|
+
// Both sides changed since: the later edit wins, and an undated local one loses.
|
|
753
|
+
const localEditedAt = memo?.intent === local ? memo.editedAt : null;
|
|
754
|
+
return localEditedAt === null || localEditedAt <= pulledEditedAt;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* The workspace as the cloud already has it unless this device really edited
|
|
759
|
+
* it, so what only reflects this device never replaces another device's copy,
|
|
760
|
+
* and a real edit goes out dated.
|
|
761
|
+
*/
|
|
762
|
+
function withLayoutProvenance(
|
|
763
|
+
payload: Record<string, unknown>,
|
|
764
|
+
lastSyncedPayload: unknown,
|
|
765
|
+
memoValue: unknown,
|
|
766
|
+
): Record<string, unknown> {
|
|
767
|
+
const local = layoutIntent(payload);
|
|
768
|
+
if (local === null) return payload;
|
|
769
|
+
const memo = readLayoutMemo(memoValue);
|
|
770
|
+
if (memo?.synced && memo.intent === local) return { ...payload, ...memo.synced };
|
|
771
|
+
if (isRecord(lastSyncedPayload) && layoutIntent(lastSyncedPayload) === local) {
|
|
772
|
+
return { ...payload, ...pickLayoutPayload(lastSyncedPayload) };
|
|
773
|
+
}
|
|
774
|
+
return {
|
|
775
|
+
...payload,
|
|
776
|
+
layoutEditedAt: memo?.intent === local && memo.editedAt !== null ? memo.editedAt : Date.now(),
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Dates a workspace edit when it happens, online or not, so a later conflict
|
|
782
|
+
* with another device's edit goes to the newer one.
|
|
783
|
+
*/
|
|
784
|
+
function noteLayoutEdit(config: AppConfig, lastSyncedPayload: unknown, memoValue: unknown, launch: boolean): LayoutMemo | null {
|
|
785
|
+
const lastSynced = layoutIntent(lastSyncedPayload);
|
|
786
|
+
if (lastSynced === null) return null;
|
|
787
|
+
const local = layoutIntent(collectLayoutPayload(config));
|
|
788
|
+
if (local === null || local === lastSynced) return null;
|
|
789
|
+
if (readLayoutMemo(memoValue)?.intent === local) return null;
|
|
790
|
+
// Seen at launch, it was made while the app was closed or just before it
|
|
791
|
+
// quit; when is unknown, so it gives way to a dated edit elsewhere.
|
|
792
|
+
return { intent: local, editedAt: launch ? null : Date.now() };
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Once this device holds the cloud's workspace, by taking it or because the
|
|
797
|
+
* two already matched, it remembers that copy as synced, keyed by how it
|
|
798
|
+
* looks here once applied. The next push then sends the cloud's own copy back
|
|
799
|
+
* rather than this device's rendering of it.
|
|
800
|
+
*/
|
|
801
|
+
function rememberCloudLayout(
|
|
802
|
+
payload: Record<string, unknown>,
|
|
803
|
+
pushedAt: string | undefined,
|
|
804
|
+
applied: AppConfig,
|
|
805
|
+
memo: { get(): unknown; set(value: unknown): void },
|
|
806
|
+
adopted: boolean,
|
|
807
|
+
): void {
|
|
808
|
+
const local = layoutIntent(collectLayoutPayload(applied));
|
|
809
|
+
if (local === null || (!adopted && local !== layoutIntent(payload))) return;
|
|
810
|
+
const current = readLayoutMemo(memo.get());
|
|
811
|
+
if (!adopted && current?.synced && current.intent === local) return;
|
|
812
|
+
const editedAt = pulledLayoutEditedAt(payload, pushedAt);
|
|
813
|
+
memo.set({
|
|
814
|
+
intent: local,
|
|
815
|
+
editedAt: Number.isFinite(editedAt) ? editedAt : null,
|
|
816
|
+
synced: pickLayoutPayload(payload),
|
|
817
|
+
} satisfies LayoutMemo);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
interface LayoutMergeContext {
|
|
821
|
+
memo?: unknown;
|
|
822
|
+
/** When the pulled copy was pushed, for one from a build that did not date layout edits. */
|
|
823
|
+
pushedAt?: string;
|
|
824
|
+
}
|
|
825
|
+
|
|
625
826
|
function mergeConfigPayload(
|
|
626
827
|
config: AppConfig,
|
|
627
828
|
payload: unknown,
|
|
628
829
|
baselineConfig: AppConfig = config,
|
|
629
830
|
lastSyncedPayload?: unknown,
|
|
831
|
+
layoutContext: LayoutMergeContext = {},
|
|
630
832
|
): AppConfig | null {
|
|
631
833
|
if (!isRecord(payload)) return null;
|
|
632
834
|
const next: AppConfig = { ...config };
|
|
@@ -634,11 +836,9 @@ function mergeConfigPayload(
|
|
|
634
836
|
// was in flight; lastSyncedPayload catches edits made while the app was not
|
|
635
837
|
// running at all (CLI writes, offline edits), which otherwise look pristine.
|
|
636
838
|
const lastSynced = isRecord(lastSyncedPayload) ? lastSyncedPayload : null;
|
|
637
|
-
const localPayload =
|
|
638
|
-
? collectCoreConfigPayload(config) as Record<string, unknown>
|
|
639
|
-
: null;
|
|
839
|
+
const localPayload = collectCoreConfigPayload(config) as Record<string, unknown>;
|
|
640
840
|
const matchesLastSynced = (key: string) => (
|
|
641
|
-
!lastSynced || valuesEqual(localPayload
|
|
841
|
+
!lastSynced || valuesEqual(localPayload[key], lastSynced[key])
|
|
642
842
|
);
|
|
643
843
|
const canApply = <K extends keyof AppConfig>(key: K) => (
|
|
644
844
|
valuesEqual(config[key], baselineConfig[key]) && matchesLastSynced(key as string)
|
|
@@ -689,15 +889,20 @@ function mergeConfigPayload(
|
|
|
689
889
|
);
|
|
690
890
|
}
|
|
691
891
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
892
|
+
// Compared by content, not identity: the active layout mirrors pane state,
|
|
893
|
+
// so a cursor settling at launch replaced it mid-pull, the pulled workspace
|
|
894
|
+
// was refused, and the push that followed wrote this device's older one
|
|
895
|
+
// over it on every other device.
|
|
896
|
+
const localIntent = layoutIntent(localPayload);
|
|
897
|
+
const adoptLayout = shouldAdoptPulledLayout({
|
|
898
|
+
local: localIntent,
|
|
899
|
+
atPullStart: baselineConfig === config ? localIntent : layoutIntent(collectLayoutPayload(baselineConfig)),
|
|
900
|
+
lastSynced: layoutIntent(lastSynced),
|
|
901
|
+
pulled: layoutIntent(payload),
|
|
902
|
+
memo: readLayoutMemo(layoutContext.memo),
|
|
903
|
+
pulledEditedAt: pulledLayoutEditedAt(payload, layoutContext.pushedAt),
|
|
904
|
+
});
|
|
905
|
+
if (adoptLayout) {
|
|
701
906
|
next.layout = payload.layout as unknown as AppConfig["layout"];
|
|
702
907
|
next.layouts = withLocalSessionLayoutState(payload.layouts as AppConfig["layouts"], config.layouts);
|
|
703
908
|
next.activeLayoutIndex = payload.activeLayoutIndex as number;
|
|
@@ -872,24 +1077,43 @@ function tickerChangedSinceLastSync(
|
|
|
872
1077
|
return !valuesEqual(withoutQuote(local), withoutQuote(baseline));
|
|
873
1078
|
}
|
|
874
1079
|
|
|
1080
|
+
const CORE_CONFIG_CONTRIBUTOR_ID = "core.config";
|
|
1081
|
+
|
|
875
1082
|
export const coreConfigSyncContributor: SyncContributor = {
|
|
876
|
-
id:
|
|
1083
|
+
id: CORE_CONFIG_CONTRIBUTOR_ID,
|
|
877
1084
|
schemaVersion: 1,
|
|
878
|
-
collect: ({ state }) =>
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
if (
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
1085
|
+
collect: ({ state, baselinePayload, memo }) => withLayoutProvenance(
|
|
1086
|
+
collectCoreConfigPayload(state.config) as Record<string, unknown>,
|
|
1087
|
+
baselinePayload,
|
|
1088
|
+
memo?.get(),
|
|
1089
|
+
),
|
|
1090
|
+
observe: ({ state, baselinePayload, memo, launch }) => {
|
|
1091
|
+
const edit = noteLayoutEdit(state.config, baselinePayload, memo.get(), launch);
|
|
1092
|
+
if (edit) memo.set(edit);
|
|
1093
|
+
},
|
|
1094
|
+
apply: async (payload, { snapshot, baselinePayload, baselineState, state, getState, isCurrent, dispatch, tickerRepository, memo }) => {
|
|
1095
|
+
const pushedAt = snapshot?.contributors[CORE_CONFIG_CONTRIBUTOR_ID]?.updatedAt;
|
|
1096
|
+
const nextConfig = mergeConfigPayload(state.config, payload, baselineState.config, baselinePayload, {
|
|
1097
|
+
memo: memo?.get(),
|
|
1098
|
+
pushedAt,
|
|
1099
|
+
});
|
|
1100
|
+
if (!nextConfig) return;
|
|
1101
|
+
const configChanged = !valuesEqual(nextConfig, state.config);
|
|
1102
|
+
if (configChanged) {
|
|
1103
|
+
// Adopting another device's workspace rearranges the screen under the
|
|
1104
|
+
// user, so it is worth a line in the log when a report says "my panes
|
|
1105
|
+
// moved on their own".
|
|
1106
|
+
if (nextConfig.layout !== state.config.layout) {
|
|
1107
|
+
log.info("config.layout.adopted", {
|
|
1108
|
+
panes: nextConfig.layout.instances.length,
|
|
1109
|
+
activeLayoutIndex: nextConfig.activeLayoutIndex,
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
dispatch({ type: "SET_CONFIG", config: nextConfig });
|
|
1113
|
+
scheduleConfigSave(nextConfig);
|
|
890
1114
|
}
|
|
891
|
-
|
|
892
|
-
|
|
1115
|
+
if (memo && isRecord(payload)) rememberCloudLayout(payload, pushedAt, getState().config, memo, nextConfig.layouts !== state.config.layouts);
|
|
1116
|
+
if (!configChanged) return;
|
|
893
1117
|
|
|
894
1118
|
// A profile removed on another device takes its positions with it here
|
|
895
1119
|
// too; kept, this device would push them back and recreate the tickers
|
package/src/sync/types.ts
CHANGED
|
@@ -19,8 +19,29 @@ export interface SyncSnapshot {
|
|
|
19
19
|
contributors: Record<string, SyncContributorPayload>;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* What this device remembers about its own copy of one contributor's data
|
|
24
|
+
* between syncs, kept across launches: when it last edited it, or which cloud
|
|
25
|
+
* copy it took. Never sent anywhere.
|
|
26
|
+
*/
|
|
27
|
+
interface SyncContributorMemo {
|
|
28
|
+
get(): unknown;
|
|
29
|
+
set(value: unknown): void;
|
|
30
|
+
}
|
|
31
|
+
|
|
22
32
|
interface SyncCollectContext {
|
|
23
33
|
state: AppState;
|
|
34
|
+
/** The payload this device last pushed for this contributor, or null when it never synced. */
|
|
35
|
+
baselinePayload?: unknown;
|
|
36
|
+
memo?: SyncContributorMemo;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface SyncObserveContext {
|
|
40
|
+
state: AppState;
|
|
41
|
+
baselinePayload: unknown;
|
|
42
|
+
memo: SyncContributorMemo;
|
|
43
|
+
/** True for the first look after the app started: a change seen then was made while it was closed. */
|
|
44
|
+
launch: boolean;
|
|
24
45
|
}
|
|
25
46
|
|
|
26
47
|
interface SyncApplyContext {
|
|
@@ -37,6 +58,7 @@ interface SyncApplyContext {
|
|
|
37
58
|
isCurrent: () => boolean;
|
|
38
59
|
dispatch: Dispatch<AppAction>;
|
|
39
60
|
tickerRepository: AppTickerRepositoryPort;
|
|
61
|
+
memo?: SyncContributorMemo;
|
|
40
62
|
}
|
|
41
63
|
|
|
42
64
|
export interface SyncContributor {
|
|
@@ -44,6 +66,12 @@ export interface SyncContributor {
|
|
|
44
66
|
schemaVersion: number;
|
|
45
67
|
collect(context: SyncCollectContext): unknown | Promise<unknown>;
|
|
46
68
|
apply?(payload: unknown, context: SyncApplyContext): void | Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* Called before every sync attempt and shortly after local changes, online
|
|
71
|
+
* or not, so an edit can be dated when it happens rather than when the
|
|
72
|
+
* network is next reachable.
|
|
73
|
+
*/
|
|
74
|
+
observe?(context: SyncObserveContext): void;
|
|
47
75
|
}
|
|
48
76
|
|
|
49
77
|
export interface SyncSnapshotResponse {
|
|
@@ -63,9 +91,22 @@ export interface SyncTransport {
|
|
|
63
91
|
id: string;
|
|
64
92
|
isAvailable(): boolean;
|
|
65
93
|
pullSnapshot(): Promise<SyncSnapshotResponse>;
|
|
94
|
+
/**
|
|
95
|
+
* `baseRevision` is the stored revision the snapshot was merged with (0 when
|
|
96
|
+
* there was none). A transport that can refuse a write made against an older
|
|
97
|
+
* revision throws `SyncRevisionConflictError`, and the controller pulls again.
|
|
98
|
+
*/
|
|
66
99
|
pushSnapshot(snapshot: SyncSnapshot, options?: { baseRevision?: number | null }): Promise<SyncPushResult>;
|
|
67
100
|
}
|
|
68
101
|
|
|
102
|
+
/** Another device wrote since this one pulled; its push would have replaced that write. */
|
|
103
|
+
export class SyncRevisionConflictError extends Error {
|
|
104
|
+
constructor(message = "The synced copy changed since it was pulled.") {
|
|
105
|
+
super(message);
|
|
106
|
+
this.name = "SyncRevisionConflictError";
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
69
110
|
export interface SyncSettings {
|
|
70
111
|
syncEnabled: boolean;
|
|
71
112
|
weeklyRoundupEnabled: boolean;
|
|
@@ -79,6 +120,9 @@ export interface SyncSettings {
|
|
|
79
120
|
export interface SyncBaselineStore {
|
|
80
121
|
load(): Record<string, unknown> | null;
|
|
81
122
|
save(payloads: Record<string, unknown>): void;
|
|
123
|
+
/** Contributor memos by contributor id; see `SyncContributorMemo`. */
|
|
124
|
+
loadMemos?(): Record<string, unknown> | null;
|
|
125
|
+
saveMemos?(memos: Record<string, unknown>): void;
|
|
82
126
|
}
|
|
83
127
|
|
|
84
128
|
export interface RegisteredSyncContributor {
|
|
@@ -1235,6 +1235,21 @@ function baseSecuritySeries(
|
|
|
1235
1235
|
};
|
|
1236
1236
|
}
|
|
1237
1237
|
|
|
1238
|
+
const FRED_CATALOG_UNITS: Readonly<Record<string, string>> = {
|
|
1239
|
+
"percent": "%",
|
|
1240
|
+
"millions of u.s. dollars": "million USD",
|
|
1241
|
+
"millions of dollars": "million USD",
|
|
1242
|
+
"billions of dollars": "billion USD",
|
|
1243
|
+
"billions of u.s. dollars": "billion USD",
|
|
1244
|
+
"billions of us dollars": "billion USD",
|
|
1245
|
+
"dollars per barrel": "USD/barrel",
|
|
1246
|
+
"thousands of persons": "thousand people",
|
|
1247
|
+
};
|
|
1248
|
+
|
|
1249
|
+
function fredCatalogUnit(units: string): string {
|
|
1250
|
+
return FRED_CATALOG_UNITS[units.toLowerCase()] ?? units;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1238
1253
|
function baseEconomicSeries(
|
|
1239
1254
|
spec: ChartSeriesSpec,
|
|
1240
1255
|
loaded: FredSeriesLoadResult,
|
|
@@ -1243,13 +1258,15 @@ function baseEconomicSeries(
|
|
|
1243
1258
|
if (spec.source.kind !== "economic") return null;
|
|
1244
1259
|
const { data } = loaded;
|
|
1245
1260
|
const units = data.info?.units?.trim() || "value";
|
|
1261
|
+
const catalogUnit = fredCatalogUnit(units);
|
|
1262
|
+
// "Percent Change from Year Ago" and "Percent per Annum" are percent too.
|
|
1246
1263
|
const isPercent = units.toLowerCase().includes("percent");
|
|
1247
1264
|
return {
|
|
1248
1265
|
id: spec.id,
|
|
1249
1266
|
label: spec.label?.trim() || data.info?.title?.trim() || spec.source.seriesId,
|
|
1250
1267
|
color: spec.color ?? SERIES_COLORS[index % SERIES_COLORS.length]!,
|
|
1251
|
-
unit: isPercent ? "%" :
|
|
1252
|
-
unitGroup: isPercent ? "percent" : `economic:${
|
|
1268
|
+
unit: isPercent ? "%" : catalogUnit,
|
|
1269
|
+
unitGroup: isPercent ? "percent" : `economic:${catalogUnit.toLowerCase()}`,
|
|
1253
1270
|
nativeFrequency: "auto",
|
|
1254
1271
|
timestampMode: "period-end",
|
|
1255
1272
|
dataShape: "scalar",
|
|
@@ -180,6 +180,14 @@ export interface QuoteBatchOptions {
|
|
|
180
180
|
* provider with no cache answers every target with no quote.
|
|
181
181
|
*/
|
|
182
182
|
cacheOnly?: boolean;
|
|
183
|
+
/**
|
|
184
|
+
* Ask the sources even for a target whose cached quote is still fresh; that
|
|
185
|
+
* copy answers only when they cannot. A command that prints quotes once
|
|
186
|
+
* shows the service's quote of the moment, so every form of one listing
|
|
187
|
+
* (BRK.B saved on NYSE, BRK-B, BRK.B:NYSE) prints the same quote, never
|
|
188
|
+
* each form's own copy from whenever it was last asked for.
|
|
189
|
+
*/
|
|
190
|
+
revalidate?: boolean;
|
|
183
191
|
/** The caller stopped waiting; a transport that can abandon the request does. */
|
|
184
192
|
signal?: AbortSignal;
|
|
185
193
|
}
|
package/src/types/financials.ts
CHANGED
|
@@ -5,6 +5,20 @@ export type SessionConfidence = "explicit" | "derived" | "unknown";
|
|
|
5
5
|
export type QuoteDataSource = "live" | "delayed" | "snapshot";
|
|
6
6
|
export type QuoteDelivery = "stream" | "poll";
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* The regular sessions an index quote carries, dated by its source: the
|
|
10
|
+
* calendar its publisher's name does not give (SNP, DJI, NASDAQ GIDS). Times
|
|
11
|
+
* are ms.
|
|
12
|
+
*/
|
|
13
|
+
export interface QuoteIndexSession {
|
|
14
|
+
/** The zone the sessions' dates are read in. */
|
|
15
|
+
timeZone: string;
|
|
16
|
+
/** The regular session the quote's day follows: today's, or the next one while it has not opened. */
|
|
17
|
+
current: { start: number; end: number };
|
|
18
|
+
/** The latest regular sessions, oldest first. */
|
|
19
|
+
regular: Array<{ start: number; end: number }>;
|
|
20
|
+
}
|
|
21
|
+
|
|
8
22
|
export interface QuoteFieldProvenance {
|
|
9
23
|
providerId: string;
|
|
10
24
|
dataSource?: QuoteDataSource;
|
|
@@ -100,6 +114,8 @@ export interface Quote extends DepositaryReceiptFacts {
|
|
|
100
114
|
routingExchangeFullName?: string;
|
|
101
115
|
marketState?: MarketState;
|
|
102
116
|
sessionConfidence?: SessionConfidence;
|
|
117
|
+
/** An index's own regular sessions, for the open its pre-market waits for and the close its last print was. */
|
|
118
|
+
indexSession?: QuoteIndexSession;
|
|
103
119
|
preMarketPrice?: number;
|
|
104
120
|
preMarketChange?: number;
|
|
105
121
|
preMarketChangePercent?: number;
|
|
@@ -245,15 +261,17 @@ export interface Fundamentals extends DepositaryReceiptFacts {
|
|
|
245
261
|
return3Y?: number;
|
|
246
262
|
lastQuarterGrowth?: number;
|
|
247
263
|
sharesOutstanding?: number;
|
|
248
|
-
/** Free float: shares outstanding less insider and strategic holdings
|
|
264
|
+
/** Free float: shares outstanding less insider and strategic holdings, never above `sharesOutstanding`. */
|
|
249
265
|
floatShares?: number;
|
|
250
266
|
beta?: number;
|
|
251
|
-
/** Shares
|
|
267
|
+
/** Shares short at the latest FINRA settlement, the one `SI` and `SIW` show; dated by `shortInterestDate`. */
|
|
252
268
|
sharesShort?: number;
|
|
253
|
-
/**
|
|
269
|
+
/** `sharesShort` over `floatShares`, as a fraction (0.0127 for 1.27%). */
|
|
254
270
|
shortPercentOfFloat?: number;
|
|
255
|
-
/** Days to cover: shares short over average daily volume. */
|
|
271
|
+
/** Days to cover as FINRA states it for the settlement: shares short over the period's average daily volume. */
|
|
256
272
|
shortRatio?: number;
|
|
273
|
+
/** The settlement date (YYYY-MM-DD) of `sharesShort`, `shortPercentOfFloat` and `shortRatio`. */
|
|
274
|
+
shortInterestDate?: string;
|
|
257
275
|
/** Fractions of shares outstanding. */
|
|
258
276
|
insiderPercentHeld?: number;
|
|
259
277
|
institutionPercentHeld?: number;
|
|
@@ -366,6 +384,20 @@ export interface DividendAction {
|
|
|
366
384
|
amount: number;
|
|
367
385
|
}
|
|
368
386
|
|
|
387
|
+
/**
|
|
388
|
+
* A distribution the issuer has declared and not yet paid, as the source
|
|
389
|
+
* states it. Kept apart from the paid `dividends`: no history, yield or growth
|
|
390
|
+
* figure takes it.
|
|
391
|
+
*/
|
|
392
|
+
export interface DeclaredDividendAction {
|
|
393
|
+
exDate: string;
|
|
394
|
+
/** Per share, in the listing's dividend currency (`currency`). */
|
|
395
|
+
amount: number;
|
|
396
|
+
declarationDate?: string;
|
|
397
|
+
recordDate?: string;
|
|
398
|
+
payDate?: string;
|
|
399
|
+
}
|
|
400
|
+
|
|
369
401
|
export interface SplitAction {
|
|
370
402
|
date: string;
|
|
371
403
|
description?: string;
|
|
@@ -417,6 +449,8 @@ export interface CorporateActionsData {
|
|
|
417
449
|
micCode?: string;
|
|
418
450
|
exchangeTimezone?: string;
|
|
419
451
|
dividends: DividendAction[];
|
|
452
|
+
/** Declared and unpaid, for the listings Gloom Cloud has them for; absent elsewhere and from older servers. */
|
|
453
|
+
declaredDividends?: DeclaredDividendAction[];
|
|
420
454
|
splits: SplitAction[];
|
|
421
455
|
earnings: EarningsAction[];
|
|
422
456
|
}
|
|
@@ -757,6 +791,8 @@ export interface PricePoint {
|
|
|
757
791
|
partial?: boolean;
|
|
758
792
|
/** The bar's prices were rebuilt from shorter bars where the source left it empty or too coarse. */
|
|
759
793
|
filled?: boolean;
|
|
794
|
+
/** A future's session that has closed but whose settlement is not confirmed yet: its close is the last trade. */
|
|
795
|
+
unsettled?: boolean;
|
|
760
796
|
}
|
|
761
797
|
|
|
762
798
|
export interface StatementHistoryAttempt {
|
package/src/types/headless.ts
CHANGED
|
@@ -257,6 +257,12 @@ export interface HeadlessPaneFreshness {
|
|
|
257
257
|
sources?: Array<{ source: string; asOf: string | number | Date }>;
|
|
258
258
|
/** Say how old the oldest observation is (`oldest 4 min`): rows polled apart from one source. */
|
|
259
259
|
oldestAge?: boolean;
|
|
260
|
+
/**
|
|
261
|
+
* Rows quoted on several markets (a US line beside Xetra and Paris ones)
|
|
262
|
+
* date each market's session in the status line, `US, XETRA and EPA Tue 13
|
|
263
|
+
* Oct 2026 close`, instead of only the newest row's.
|
|
264
|
+
*/
|
|
265
|
+
datesEachVenue?: boolean;
|
|
260
266
|
/**
|
|
261
267
|
* Exports also cite the oldest observation whenever the rows are dated
|
|
262
268
|
* apart, not only when it is a day older: a portfolio valued at one
|
|
@@ -265,6 +271,12 @@ export interface HeadlessPaneFreshness {
|
|
|
265
271
|
citeOldest?: boolean;
|
|
266
272
|
/** What the figures are, said after the status: `funding = current estimate, pays 08:00 UTC`. */
|
|
267
273
|
note?: string;
|
|
274
|
+
/**
|
|
275
|
+
* When the figures reach readers and when the next ones are due, said right
|
|
276
|
+
* after the as-of date: `published Fri 9 Oct, next settlement Thu 15 Oct,
|
|
277
|
+
* published Mon 26 Oct` for a settlement a regulator publishes days later.
|
|
278
|
+
*/
|
|
279
|
+
schedule?: string;
|
|
268
280
|
}
|
|
269
281
|
|
|
270
282
|
interface HeadlessPaneResultBase {
|
|
@@ -295,6 +307,12 @@ interface HeadlessPaneResultBase {
|
|
|
295
307
|
|
|
296
308
|
export interface HeadlessRowsResult extends HeadlessPaneResultBase {
|
|
297
309
|
columns?: HeadlessPaneColumn[];
|
|
310
|
+
/**
|
|
311
|
+
* The columns the text report prints in place of `columns`, when the
|
|
312
|
+
* exports carry more than a terminal table should (each cell's source and
|
|
313
|
+
* as-of beside it). CSV, NDJSON and JSON keep `columns`.
|
|
314
|
+
*/
|
|
315
|
+
textColumns?: HeadlessPaneColumn[];
|
|
298
316
|
rows: HeadlessPaneRow[];
|
|
299
317
|
/** What the report says when `rows` is empty, in place of "No data.": why there are none. */
|
|
300
318
|
empty?: string;
|
|
@@ -401,7 +419,22 @@ export interface HeadlessPaneDefinition<Shape extends HeadlessPaneShape = Headle
|
|
|
401
419
|
* points at GIP's 1-minute bars). `key` is the listing as read, RMS:ASX.
|
|
402
420
|
*/
|
|
403
421
|
usListingsOnlyHint?(key: string): string;
|
|
422
|
+
/**
|
|
423
|
+
* `fn` exits 1 when the report has no name with data: no rows, or every row
|
|
424
|
+
* is one of `unavailableSymbols`. A list that gives data for some names
|
|
425
|
+
* exits 0 and names the others in its errors.
|
|
426
|
+
*/
|
|
427
|
+
failsWithoutData?: boolean;
|
|
404
428
|
options: HeadlessPaneOptionDef[];
|
|
429
|
+
/**
|
|
430
|
+
* Widens the argument before the function runs, so its report, its capture
|
|
431
|
+
* and its pane all read the same symbols: RV's `--peers` adds a symbol's
|
|
432
|
+
* competitors. Null runs the argument as typed.
|
|
433
|
+
*/
|
|
434
|
+
expandArgument?(
|
|
435
|
+
args: HeadlessPaneLoadArgs,
|
|
436
|
+
ctx: HeadlessPaneContext,
|
|
437
|
+
): Promise<HeadlessArgumentExpansion | null>;
|
|
405
438
|
/** What is true of every load: its source, and whether it is a feed. */
|
|
406
439
|
freshness?: HeadlessPaneFreshness;
|
|
407
440
|
columns?: HeadlessPaneColumn[];
|
|
@@ -426,3 +459,13 @@ export interface HeadlessPaneDefinition<Shape extends HeadlessPaneShape = Headle
|
|
|
426
459
|
}
|
|
427
460
|
|
|
428
461
|
export type HeadlessPaneResult = HeadlessPaneResultByShape[HeadlessPaneShape];
|
|
462
|
+
|
|
463
|
+
/** What a function runs with once it has widened its argument (`expandArgument`). */
|
|
464
|
+
export interface HeadlessArgumentExpansion {
|
|
465
|
+
/** The argument as it would be typed: `DUOL, COUR, CHGG`. */
|
|
466
|
+
argument: string;
|
|
467
|
+
/** Merged into the pane's settings, which the load reads as `ctx.settings` and the pane as its own. */
|
|
468
|
+
settings?: Record<string, unknown>;
|
|
469
|
+
/** The pane's title when the widened argument needs saying (`RV DUOL + auto peers by industry: Airlines`). */
|
|
470
|
+
title?: string;
|
|
471
|
+
}
|
package/src/types/plugin.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { AppTickerRepositoryPort } from "../core/app-service-ports";
|
|
|
3
3
|
import type { HeadlessPaneDefinition, HeadlessPaneFreshness, HeadlessPaneOptionDef } from "./headless";
|
|
4
4
|
|
|
5
5
|
export type {
|
|
6
|
+
HeadlessArgumentExpansion,
|
|
6
7
|
HeadlessBundleResult,
|
|
7
8
|
HeadlessBundleSection,
|
|
8
9
|
HeadlessFreshnessStatus,
|
|
@@ -189,6 +190,12 @@ export interface PaneDef {
|
|
|
189
190
|
* the last ones. Leave it off for long or paged lists.
|
|
190
191
|
*/
|
|
191
192
|
shotFitsRows?: true;
|
|
193
|
+
/**
|
|
194
|
+
* A report of the pane grows to the whole text of the page its reader has
|
|
195
|
+
* open (a docs page), so it is not cut at the first screen. A list the reader
|
|
196
|
+
* opens from keeps its height.
|
|
197
|
+
*/
|
|
198
|
+
shotFitsReader?: true;
|
|
192
199
|
/** Add an Excel-compatible CSV action for the pane's single active DataTable. */
|
|
193
200
|
tableExport?: true;
|
|
194
201
|
settings?: PaneSettingsDef | ((context: PaneSettingsContext) => PaneSettingsDef | null);
|