gloomberb 0.4.1 → 0.5.1
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 +2 -2
- package/package.json +1 -1
- package/src/app.tsx +168 -65
- package/src/cli/commands/ticker.ts +45 -15
- package/src/cli/helpers.ts +4 -2
- package/src/cli/registry.test.ts +42 -35
- package/src/cli.test.ts +4 -2
- package/src/components/chart/chart-control-hint.tsx +39 -0
- package/src/components/chart/chart-controller.test.ts +110 -0
- package/src/components/chart/chart-controller.ts +364 -2
- package/src/components/chart/chart-data.ts +177 -29
- package/src/components/chart/chart-indicator-selector.tsx +53 -0
- package/src/components/chart/chart-pane-settings.ts +20 -0
- package/src/components/chart/chart-renderer.ts +220 -30
- package/src/components/chart/chart-resolution.test.ts +67 -0
- package/src/components/chart/chart-resolution.ts +74 -4
- package/src/components/chart/chart-scroll.test.ts +69 -0
- package/src/components/chart/chart-scroll.ts +104 -0
- package/src/components/chart/chart-types.ts +12 -0
- package/src/components/chart/chart-viewport.test.ts +5 -1
- package/src/components/chart/chart-viewport.ts +1 -1
- package/src/components/chart/chart.test.ts +249 -10
- package/src/components/chart/comparison-chart-renderer.test.ts +71 -1
- package/src/components/chart/comparison-chart-renderer.ts +40 -3
- package/src/components/chart/comparison-stock-chart.tsx +218 -123
- package/src/components/chart/indicators/bands.test.ts +50 -0
- package/src/components/chart/indicators/bands.ts +29 -0
- package/src/components/chart/indicators/moving-averages.test.ts +54 -0
- package/src/components/chart/indicators/moving-averages.ts +42 -0
- package/src/components/chart/indicators/options.test.ts +32 -0
- package/src/components/chart/indicators/options.ts +107 -0
- package/src/components/chart/indicators/oscillators.test.ts +79 -0
- package/src/components/chart/indicators/oscillators.ts +92 -0
- package/src/components/chart/indicators/types.ts +36 -0
- package/src/components/chart/native/chart-rasterizer.ts +51 -0
- package/src/components/chart/native/kitty.test.ts +68 -0
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +740 -9
- package/src/components/chart/stock-chart.test.ts +239 -0
- package/src/components/chart/stock-chart.tsx +2257 -372
- package/src/components/command-bar/command-bar.test.tsx +337 -5
- package/src/components/command-bar/command-bar.tsx +292 -120
- package/src/components/command-bar/helpers.ts +1 -1
- package/src/components/command-bar/root-shortcuts.ts +33 -4
- package/src/components/command-bar/view-model.test.ts +3 -3
- package/src/components/command-bar/view-model.ts +4 -4
- package/src/components/command-bar/workflow-ops.ts +10 -0
- package/src/components/layout/floating-pane.tsx +3 -2
- package/src/components/layout/header.tsx +2 -1
- package/src/components/layout/pane-sizing.ts +10 -0
- package/src/components/layout/pane.tsx +9 -2
- package/src/components/layout/shell.test.tsx +282 -28
- package/src/components/layout/shell.tsx +10 -7
- package/src/components/layout/status-bar.test.tsx +16 -7
- package/src/components/layout/status-bar.tsx +4 -1
- package/src/components/markdown-editor.tsx +1 -0
- package/src/components/pane-settings-dialog.tsx +13 -152
- package/src/components/pane-template-wizard.tsx +1 -0
- package/src/components/price-selector-dialog.tsx +12 -7
- package/src/components/ticker-badge-text.test.tsx +50 -0
- package/src/components/ticker-badge-text.tsx +17 -2
- package/src/components/ticker-badge.tsx +2 -2
- package/src/components/ticker-list-table.tsx +54 -1
- package/src/components/toggle-list.tsx +15 -2
- package/src/components/ui/data-table.tsx +60 -3
- package/src/components/ui/external-link.tsx +48 -11
- package/src/components/ui/fields.tsx +2 -1
- package/src/components/ui/index.ts +8 -1
- package/src/components/ui/multi-select-chips.tsx +69 -0
- package/src/components/ui/multi-select-dialog.tsx +267 -0
- package/src/components/ui/multi-select.ts +78 -0
- package/src/components/ui/ui.test.tsx +343 -1
- package/src/data/plugin-state-store.ts +30 -19
- package/src/data/resource-store.ts +133 -103
- package/src/data/session-store.ts +19 -12
- package/src/data/sqlite-cache.test.ts +37 -2
- package/src/data/sqlite-database.ts +8 -2
- package/src/data/sqlite-retry.test.ts +63 -0
- package/src/data/sqlite-retry.ts +66 -0
- package/src/data/ticker-store.ts +29 -18
- package/src/index.tsx +6 -1
- package/src/market-data/coordinator.test.ts +114 -5
- package/src/market-data/coordinator.ts +70 -1
- package/src/market-data/hooks.test.tsx +73 -2
- package/src/market-data/hooks.tsx +16 -4
- package/src/notifications/app-notifier.test.ts +177 -0
- package/src/notifications/app-notifier.ts +241 -0
- package/src/pane-settings.test.ts +2 -0
- package/src/plugins/builtin/ai/index.test.tsx +1 -1
- package/src/plugins/builtin/ai/screener-pane.test.tsx +2 -0
- package/src/plugins/builtin/ai/screener-pane.tsx +1 -0
- package/src/plugins/builtin/ai/ticker-context.ts +3 -2
- package/src/plugins/builtin/alerts/alert-engine.test.ts +90 -0
- package/src/plugins/builtin/alerts/alert-engine.ts +55 -0
- package/src/plugins/builtin/alerts/index.test.tsx +357 -0
- package/src/plugins/builtin/alerts/index.tsx +519 -0
- package/src/plugins/builtin/alerts/types.ts +14 -0
- package/src/plugins/builtin/analytics/index.test.tsx +300 -0
- package/src/plugins/builtin/analytics/index.tsx +775 -0
- package/src/plugins/builtin/analytics/metrics.test.ts +125 -0
- package/src/plugins/builtin/analytics/metrics.ts +155 -0
- package/src/plugins/builtin/ask-ai.test.tsx +2 -0
- package/src/plugins/builtin/chat-controller.test.ts +83 -18
- package/src/plugins/builtin/chat-controller.ts +26 -14
- package/src/plugins/builtin/chat.test.tsx +715 -4
- package/src/plugins/builtin/chat.tsx +424 -120
- package/src/plugins/builtin/comparison-chart.test.tsx +97 -57
- package/src/plugins/builtin/comparison-chart.tsx +1 -2
- package/src/plugins/builtin/correlation/compute.test.ts +147 -0
- package/src/plugins/builtin/correlation/compute.ts +115 -0
- package/src/plugins/builtin/correlation/index.tsx +385 -0
- package/src/plugins/builtin/correlation/plugin.test.ts +44 -0
- package/src/plugins/builtin/correlation/settings.test.ts +34 -0
- package/src/plugins/builtin/correlation/settings.ts +95 -0
- package/src/plugins/builtin/debug.tsx +36 -50
- package/src/plugins/builtin/earnings/earnings-cache.test.ts +181 -0
- package/src/plugins/builtin/earnings/earnings-cache.ts +136 -0
- package/src/plugins/builtin/earnings/earnings-data.test.ts +59 -0
- package/src/plugins/builtin/earnings/earnings-data.ts +50 -0
- package/src/plugins/builtin/earnings/index.tsx +337 -0
- package/src/plugins/builtin/econ/calendar-source.test.ts +219 -0
- package/src/plugins/builtin/econ/calendar-source.ts +121 -0
- package/src/plugins/builtin/econ/fred-client.test.ts +63 -0
- package/src/plugins/builtin/econ/fred-client.ts +79 -0
- package/src/plugins/builtin/econ/fred-series-map.ts +88 -0
- package/src/plugins/builtin/econ/index.tsx +999 -0
- package/src/plugins/builtin/econ/types.ts +13 -0
- package/src/plugins/builtin/fred-settings.ts +10 -0
- package/src/plugins/builtin/fx-matrix/index.tsx +178 -0
- package/src/plugins/builtin/fx-matrix/pairs.ts +30 -0
- package/src/plugins/builtin/help.test.tsx +1 -5
- package/src/plugins/builtin/help.tsx +1 -16
- package/src/plugins/builtin/insider/index.tsx +417 -0
- package/src/plugins/builtin/insider/insider-data.test.ts +55 -0
- package/src/plugins/builtin/insider/insider-data.ts +49 -0
- package/src/plugins/builtin/layout-manager.tsx +24 -16
- package/src/plugins/builtin/market-movers/index.tsx +573 -0
- package/src/plugins/builtin/market-movers/screener.test.ts +143 -0
- package/src/plugins/builtin/market-movers/screener.ts +105 -0
- package/src/plugins/builtin/options.tsx +4 -3
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +11 -6
- package/src/plugins/builtin/portfolio-list/command-bar.ts +6 -3
- package/src/plugins/builtin/portfolio-list/header.tsx +3 -2
- package/src/plugins/builtin/portfolio-list/metrics.ts +22 -11
- package/src/plugins/builtin/portfolio-list/pane.tsx +3 -9
- package/src/plugins/builtin/portfolio-list/settings.ts +9 -0
- package/src/plugins/builtin/portfolio-list/table.tsx +3 -0
- package/src/plugins/builtin/portfolio-list.test.tsx +3 -1
- package/src/plugins/builtin/portfolio-metrics.test.ts +13 -1
- package/src/plugins/builtin/sec.tsx +114 -7
- package/src/plugins/builtin/sectors/index.tsx +359 -0
- package/src/plugins/builtin/sectors/sector-data.ts +18 -0
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +75 -13
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +316 -80
- package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +4 -6
- package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +17 -12
- package/src/plugins/builtin/ticker-detail.test.tsx +3 -3
- package/src/plugins/builtin/world-indices/index.tsx +410 -0
- package/src/plugins/builtin/world-indices/indices.ts +52 -0
- package/src/plugins/builtin/yield-curve/index.tsx +242 -0
- package/src/plugins/builtin/yield-curve/treasury-data.test.ts +212 -0
- package/src/plugins/builtin/yield-curve/treasury-data.ts +107 -0
- package/src/plugins/catalog.ts +22 -0
- package/src/plugins/gridlock-notification.ts +15 -0
- package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +5 -5
- package/src/plugins/ibkr/index.test.tsx +1 -1
- package/src/plugins/ibkr/index.tsx +1 -1
- package/src/plugins/ibkr/trade-tab.tsx +23 -5
- package/src/plugins/ibkr/trade-utils.test.ts +1 -1
- package/src/plugins/ibkr/trade-utils.ts +7 -6
- package/src/plugins/ibkr/trading-pane.tsx +8 -7
- package/src/plugins/plugin-runtime.test.tsx +2 -0
- package/src/plugins/plugin-runtime.tsx +10 -0
- package/src/plugins/prediction-markets/controller-data.ts +8 -0
- package/src/plugins/prediction-markets/detail/shared.tsx +7 -6
- package/src/plugins/prediction-markets/plugin.test.tsx +48 -1
- package/src/plugins/prediction-markets/services/fetch.ts +13 -7
- package/src/plugins/prediction-markets/services/polymarket-adapter.ts +29 -11
- package/src/plugins/prediction-markets/services/polymarket-ws.ts +20 -0
- package/src/plugins/prediction-markets/test-helpers.tsx +2 -0
- package/src/plugins/registry.ts +22 -13
- package/src/sources/provider-router.test.ts +3 -2
- package/src/sources/sec-edgar.ts +13 -1
- package/src/sources/yahoo-finance.ts +75 -1
- package/src/state/app-context-selector.test.tsx +17 -1
- package/src/state/app-context.test.tsx +24 -0
- package/src/state/app-context.tsx +29 -6
- package/src/theme/colors.ts +5 -0
- package/src/theme/themes.ts +25 -0
- package/src/types/config.test.ts +107 -0
- package/src/types/config.ts +30 -1
- package/src/types/data-provider.ts +13 -0
- package/src/types/plugin.ts +35 -1
- package/src/utils/api-client.test.ts +90 -0
- package/src/utils/api-client.ts +56 -16
- package/src/utils/format.test.ts +11 -1
- package/src/utils/format.ts +86 -8
- package/src/utils/inline-content-tokenizer.test.ts +22 -0
- package/src/utils/inline-content-tokenizer.ts +76 -0
- package/src/utils/link-tokenizer.test.ts +27 -0
- package/src/utils/link-tokenizer.ts +93 -0
- package/src/utils/market-format.test.ts +109 -0
- package/src/utils/market-format.ts +299 -0
- package/src/utils/market-status.ts +3 -2
- package/src/utils/selection-clipboard.test.ts +43 -0
- package/src/utils/selection-clipboard.ts +88 -0
- package/src/utils/terminal-input-reset.test.ts +50 -0
- package/src/utils/terminal-input-reset.ts +24 -0
- package/src/utils/throttled-fetch.test.ts +145 -0
- package/src/utils/throttled-fetch.ts +212 -0
- package/src/utils/ticker-tokenizer.test.ts +31 -0
- package/src/utils/ticker-tokenizer.ts +7 -42
- package/src/version.ts +1 -1
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate-limited fetch with per-host throttling, retry with backoff, and request deduplication.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* const client = createThrottledFetch({ requestsPerMinute: 30 });
|
|
6
|
+
* const data = await client.fetch("https://api.example.com/data");
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const DEFAULT_REQUESTS_PER_MINUTE = 30;
|
|
10
|
+
const DEFAULT_MAX_RETRIES = 2;
|
|
11
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
12
|
+
const BACKOFF_BASE_MS = 1_000;
|
|
13
|
+
|
|
14
|
+
export interface ThrottledFetchOptions {
|
|
15
|
+
/** Max requests per minute per host. Default: 30 */
|
|
16
|
+
requestsPerMinute?: number;
|
|
17
|
+
/** Max retries on 429 or 5xx. Default: 2 */
|
|
18
|
+
maxRetries?: number;
|
|
19
|
+
/** Request timeout in ms. Default: 10000 */
|
|
20
|
+
timeoutMs?: number;
|
|
21
|
+
/** Initial retry backoff in ms. Default: 1000 */
|
|
22
|
+
backoffBaseMs?: number;
|
|
23
|
+
/** Default headers applied to every request */
|
|
24
|
+
defaultHeaders?: Record<string, string>;
|
|
25
|
+
/** Share concurrent GET requests to the same URL. Default: true */
|
|
26
|
+
dedupeGetRequests?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface QueueEntry {
|
|
30
|
+
resolve: (value: Response) => void;
|
|
31
|
+
reject: (reason: unknown) => void;
|
|
32
|
+
url: string;
|
|
33
|
+
init: RequestInit | undefined;
|
|
34
|
+
retries: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ThrottledFetchClient {
|
|
38
|
+
fetch(url: string, init?: RequestInit): Promise<Response>;
|
|
39
|
+
fetchJson<T = unknown>(url: string, init?: RequestInit): Promise<T>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function createThrottledFetch(
|
|
43
|
+
options: ThrottledFetchOptions = {},
|
|
44
|
+
): ThrottledFetchClient {
|
|
45
|
+
const requestsPerMinute =
|
|
46
|
+
options.requestsPerMinute ?? DEFAULT_REQUESTS_PER_MINUTE;
|
|
47
|
+
const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
48
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
49
|
+
const backoffBaseMs = options.backoffBaseMs ?? BACKOFF_BASE_MS;
|
|
50
|
+
const defaultHeaders = options.defaultHeaders ?? {};
|
|
51
|
+
const dedupeGetRequests = options.dedupeGetRequests ?? true;
|
|
52
|
+
|
|
53
|
+
// Sliding window: track timestamps of recent requests per host
|
|
54
|
+
const hostTimestamps = new Map<string, number[]>();
|
|
55
|
+
|
|
56
|
+
// Deduplication: in-flight requests by URL
|
|
57
|
+
const inflight = new Map<string, Promise<Response>>();
|
|
58
|
+
|
|
59
|
+
function getHost(url: string): string {
|
|
60
|
+
try {
|
|
61
|
+
return new URL(url).host;
|
|
62
|
+
} catch {
|
|
63
|
+
return "unknown";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getDelayMs(host: string): number {
|
|
68
|
+
const timestamps = hostTimestamps.get(host);
|
|
69
|
+
if (!timestamps || timestamps.length === 0) return 0;
|
|
70
|
+
|
|
71
|
+
const windowStart = Date.now() - 60_000;
|
|
72
|
+
// Prune old entries
|
|
73
|
+
while (timestamps.length > 0 && timestamps[0]! < windowStart) {
|
|
74
|
+
timestamps.shift();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (timestamps.length < requestsPerMinute) return 0;
|
|
78
|
+
|
|
79
|
+
// Delay until the oldest request in the window falls outside
|
|
80
|
+
const oldest = timestamps[0]!;
|
|
81
|
+
return Math.max(0, oldest + 60_000 - Date.now() + 50); // +50ms buffer
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function recordRequest(host: string): void {
|
|
85
|
+
let timestamps = hostTimestamps.get(host);
|
|
86
|
+
if (!timestamps) {
|
|
87
|
+
timestamps = [];
|
|
88
|
+
hostTimestamps.set(host, timestamps);
|
|
89
|
+
}
|
|
90
|
+
timestamps.push(Date.now());
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function getBackoffMs(retriesLeft: number, retryAfter?: string | null): number {
|
|
94
|
+
const attempt = maxRetries - retriesLeft + 1;
|
|
95
|
+
const backoff = backoffBaseMs * 2 ** (attempt - 1);
|
|
96
|
+
const retryAfterMs = retryAfter
|
|
97
|
+
? (parseInt(retryAfter, 10) || 0) * 1000
|
|
98
|
+
: 0;
|
|
99
|
+
return Math.max(retryAfterMs, backoff);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function isRetryableFetchError(
|
|
103
|
+
error: unknown,
|
|
104
|
+
retryAbortError: boolean,
|
|
105
|
+
): boolean {
|
|
106
|
+
const detail =
|
|
107
|
+
error instanceof Error
|
|
108
|
+
? `${error.name} ${error.message} ${String((error as { code?: unknown }).code ?? "")}`
|
|
109
|
+
: String(error);
|
|
110
|
+
return (
|
|
111
|
+
/TimeoutError|ECONNRESET|ECONNREFUSED|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|socket connection|socket hang up|fetch failed|network connection|connection closed/i.test(
|
|
112
|
+
detail,
|
|
113
|
+
) ||
|
|
114
|
+
(retryAbortError && /AbortError/i.test(detail))
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function executeRequest(
|
|
119
|
+
url: string,
|
|
120
|
+
init: RequestInit | undefined,
|
|
121
|
+
retriesLeft: number,
|
|
122
|
+
): Promise<Response> {
|
|
123
|
+
const host = getHost(url);
|
|
124
|
+
|
|
125
|
+
// Wait for rate limit window
|
|
126
|
+
const delay = getDelayMs(host);
|
|
127
|
+
if (delay > 0) {
|
|
128
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
recordRequest(host);
|
|
132
|
+
|
|
133
|
+
const mergedInit: RequestInit = {
|
|
134
|
+
...init,
|
|
135
|
+
headers: {
|
|
136
|
+
...defaultHeaders,
|
|
137
|
+
...((init?.headers as Record<string, string>) ?? {}),
|
|
138
|
+
},
|
|
139
|
+
signal: init?.signal ?? AbortSignal.timeout(timeoutMs),
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
let resp: Response;
|
|
143
|
+
try {
|
|
144
|
+
resp = await fetch(url, mergedInit);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
if (retriesLeft > 0 && isRetryableFetchError(error, !init?.signal)) {
|
|
147
|
+
await new Promise((resolve) =>
|
|
148
|
+
setTimeout(resolve, getBackoffMs(retriesLeft)),
|
|
149
|
+
);
|
|
150
|
+
return executeRequest(url, init, retriesLeft - 1);
|
|
151
|
+
}
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Retry on 429 or 5xx
|
|
156
|
+
if ((resp.status === 429 || resp.status >= 500) && retriesLeft > 0) {
|
|
157
|
+
await new Promise((resolve) =>
|
|
158
|
+
setTimeout(
|
|
159
|
+
resolve,
|
|
160
|
+
getBackoffMs(retriesLeft, resp.headers.get("retry-after")),
|
|
161
|
+
),
|
|
162
|
+
);
|
|
163
|
+
return executeRequest(url, init, retriesLeft - 1);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return resp;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function throttledFetch(url: string, init?: RequestInit): Promise<Response> {
|
|
170
|
+
// Deduplicate concurrent GET requests to the same URL
|
|
171
|
+
const method = (init?.method ?? "GET").toUpperCase();
|
|
172
|
+
if (dedupeGetRequests && method === "GET") {
|
|
173
|
+
const existing = inflight.get(url);
|
|
174
|
+
if (existing) {
|
|
175
|
+
return existing.then(
|
|
176
|
+
(response): Response => response.clone() as unknown as Response,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const promise = executeRequest(url, init, maxRetries).finally(() => {
|
|
182
|
+
inflight.delete(url);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
if (dedupeGetRequests && method === "GET") {
|
|
186
|
+
inflight.set(url, promise);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return dedupeGetRequests && method === "GET"
|
|
190
|
+
? promise.then(
|
|
191
|
+
(response): Response => response.clone() as unknown as Response,
|
|
192
|
+
)
|
|
193
|
+
: promise;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function fetchJson<T = unknown>(
|
|
197
|
+
url: string,
|
|
198
|
+
init?: RequestInit,
|
|
199
|
+
): Promise<T> {
|
|
200
|
+
const resp = await throttledFetch(url, init);
|
|
201
|
+
if (!resp.ok) {
|
|
202
|
+
throw new Error(
|
|
203
|
+
resp.status === 429
|
|
204
|
+
? `Rate limited (${getHost(url)}) — try again later`
|
|
205
|
+
: `HTTP ${resp.status} from ${getHost(url)}`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
return resp.json() as Promise<T>;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return { fetch: throttledFetch, fetchJson };
|
|
212
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { collectUniqueTickerSymbols, tokenizeTickerText } from "./ticker-tokenizer";
|
|
3
|
+
|
|
4
|
+
describe("tokenizeTickerText", () => {
|
|
5
|
+
test("finds normal inline ticker symbols", () => {
|
|
6
|
+
expect(tokenizeTickerText("Watching $TSLA and $NVDA")).toEqual([
|
|
7
|
+
{ kind: "text", value: "Watching " },
|
|
8
|
+
{ kind: "ticker", value: "$TSLA", symbol: "TSLA" },
|
|
9
|
+
{ kind: "text", value: " and " },
|
|
10
|
+
{ kind: "ticker", value: "$NVDA", symbol: "NVDA" },
|
|
11
|
+
]);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("does not tokenize ticker-like text inside links", () => {
|
|
15
|
+
expect(tokenizeTickerText("Read https://example.com/$TSLA before buying $NVDA")).toEqual([
|
|
16
|
+
{ kind: "text", value: "Read " },
|
|
17
|
+
{ kind: "text", value: "https://example.com/$TSLA" },
|
|
18
|
+
{ kind: "text", value: " before buying " },
|
|
19
|
+
{ kind: "ticker", value: "$NVDA", symbol: "NVDA" },
|
|
20
|
+
]);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("collectUniqueTickerSymbols", () => {
|
|
25
|
+
test("ignores ticker-like text inside links", () => {
|
|
26
|
+
expect(collectUniqueTickerSymbols([
|
|
27
|
+
"Read https://example.com/$TSLA",
|
|
28
|
+
"Watching $NVDA",
|
|
29
|
+
])).toEqual(["NVDA"]);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { tokenizeInlineContent } from "./inline-content-tokenizer";
|
|
2
|
+
|
|
1
3
|
export interface InlineTickerTextToken {
|
|
2
4
|
kind: "text";
|
|
3
5
|
value: string;
|
|
@@ -11,49 +13,12 @@ export interface InlineTickerSymbolToken {
|
|
|
11
13
|
|
|
12
14
|
export type InlineTickerToken = InlineTickerTextToken | InlineTickerSymbolToken;
|
|
13
15
|
|
|
14
|
-
const TICKER_TOKEN_RE = /\$[A-Z][A-Z0-9.-]{0,9}/g;
|
|
15
|
-
const SYMBOL_CHAR_RE = /[A-Za-z0-9.-]/;
|
|
16
|
-
|
|
17
|
-
function trimTrailingPunctuation(value: string): string {
|
|
18
|
-
let trimmed = value;
|
|
19
|
-
while (trimmed.length > 2 && /[.-]$/.test(trimmed)) {
|
|
20
|
-
trimmed = trimmed.slice(0, -1);
|
|
21
|
-
}
|
|
22
|
-
return trimmed;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function isValidBoundary(text: string, start: number, end: number): boolean {
|
|
26
|
-
const prev = start > 0 ? text[start - 1] : "";
|
|
27
|
-
const next = end < text.length ? text[end] : "";
|
|
28
|
-
return !SYMBOL_CHAR_RE.test(prev) && !SYMBOL_CHAR_RE.test(next);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
16
|
export function tokenizeTickerText(text: string): InlineTickerToken[] {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
while ((match = TICKER_TOKEN_RE.exec(text)) !== null) {
|
|
38
|
-
const rawValue = match[0];
|
|
39
|
-
const start = match.index;
|
|
40
|
-
const rawEnd = start + rawValue.length;
|
|
41
|
-
const value = trimTrailingPunctuation(rawValue);
|
|
42
|
-
const end = start + value.length;
|
|
43
|
-
if (!isValidBoundary(text, start, rawEnd)) continue;
|
|
44
|
-
|
|
45
|
-
if (start > cursor) {
|
|
46
|
-
tokens.push({ kind: "text", value: text.slice(cursor, start) });
|
|
47
|
-
}
|
|
48
|
-
tokens.push({ kind: "ticker", value, symbol: value.slice(1) });
|
|
49
|
-
cursor = end;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (cursor < text.length) {
|
|
53
|
-
tokens.push({ kind: "text", value: text.slice(cursor) });
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return tokens;
|
|
17
|
+
return tokenizeInlineContent(text).map((token) => (
|
|
18
|
+
token.kind === "ticker"
|
|
19
|
+
? token
|
|
20
|
+
: { kind: "text", value: token.value }
|
|
21
|
+
));
|
|
57
22
|
}
|
|
58
23
|
|
|
59
24
|
export function collectUniqueTickerSymbols(texts: readonly string[]): string[] {
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.
|
|
1
|
+
export const VERSION = "0.5.1";
|