7import { Hono } from "npm:hono";
8import { html } from "npm:hono/html";
9import { fetch } from "npm:node-fetch";
10import { z } from "npm:zod";
11// --- Constants ---
70
71/**
72 * Gets data from cache or fetches it if expired/missing.
73 * @param cacheKey - Unique key for the cached data.
74 * @param fetchFn - Async function to fetch data if cache is invalid.
75 * @param apiKey - API key, used to determine if live fetch is possible.
76 * @param steps - Log array.
77 * @returns The fetched or cached data.
78 */
79async function getCachedData<T>(
80 cacheKey: string,
81 fetchFn: () => Promise<T>,
82 apiKey: string | undefined | null,
83 steps: string[],
113
114 if (!apiKey) {
115 steps.push(`API key missing for ${cacheKey}, cannot fetch fresh data.`);
116 if (cachedData) {
117 steps.push(`Returning expired cached data for ${cacheKey} as fallback.`);
123
124 steps.push(`Workspaceing fresh data for ${cacheKey}`);
125 // TODO: error handling: Handle potential errors during the actual fetchFn execution
126 try {
127 const freshData = await fetchFn();
128 const newTimestamp = Date.now();
129 // TODO: error handling: Handle potential SQLite errors during INSERT/REPLACE
133 args: [cacheKey, JSON.stringify(freshData), newTimestamp],
134 });
135 steps.push(`Successfully fetched and cached data for ${cacheKey}`);
136 } catch (e: any) {
137 steps.push(`Error writing to cache for ${cacheKey}: ${e.message}`);
139 }
140 return freshData;
141 } catch (fetchError: any) {
142 steps.push(`Error fetching fresh data for ${cacheKey}: ${fetchError.message}`);
143 if (cachedData) {
144 steps.push(`Returning expired cached data for ${cacheKey} due to fetch error.`);
145 return cachedData; // Return stale data if fetch fails
146 }
147 throw new Error(`Failed to fetch data for ${cacheKey} and no cache available: ${fetchError.message}`);
148 }
149}
150
151/**
152 * Fetches historical stock data from Alpha Vantage.
153 * @param ticker - Stock ticker symbol.
154 * @param apiKey - Alpha Vantage API key.
156 * @returns Parsed Alpha Vantage time series data or null.
157 */
158async function fetchAlphaVantageData(
159 ticker: string,
160 apiKey: string | undefined | null,
162): Promise<AlphaVantageTimeSeriesData | null> {
163 const cacheKey = `alphavantage_${ticker}`;
164 const fetchFn = async () => {
165 steps.push(`Workspaceing Alpha Vantage data for ${ticker}`);
166 // TODO: error handling: Handle potential fetch errors (network, non-200 status)
167 const url =
168 `https://www.alphavantage.co/query?function=TIME_SERIES_DAILY_ADJUSTED&symbol=${ticker}&outputsize=full&apikey=${apiKey}`;
169 const response = await fetch(url);
170 if (!response.ok) {
171 throw new Error(`Alpha Vantage API error: ${response.statusText} (Status: ${response.status})`);
185 };
186
187 return getCachedData(cacheKey, fetchFn, apiKey, steps);
188}
189
190/**
191 * Fetches previous day's close price from Polygon.io.
192 * @param ticker - Stock ticker symbol.
193 * @param apiKey - Polygon.io API key.
195 * @returns Parsed Polygon previous close data or null.
196 */
197async function fetchPolygonData(
198 ticker: string,
199 apiKey: string | undefined | null,
201): Promise<PolygonPreviousCloseData | null> {
202 const cacheKey = `polygon_${ticker}`;
203 const fetchFn = async () => {
204 steps.push(`Workspaceing Polygon.io data for ${ticker}`);
205 // TODO: error handling: Handle potential fetch errors (network, non-200 status)
206 const url = `https://api.polygon.io/v2/aggs/ticker/${ticker}/prev?adjusted=true&apiKey=${apiKey}`;
207 const response = await fetch(url);
208 if (!response.ok) {
209 let errorBody = await response.text();
225 };
226
227 return getCachedData(cacheKey, fetchFn, apiKey, steps);
228}
229
390 const { ticker, alphaVantageKey, polygonKey } = validatedInput;
391
392 // 2. Fetch Data
393 steps.push("Fetching data");
394 // TODO: error handling: These fetches might return null or throw errors
395 let alphaVantageData: AlphaVantageTimeSeriesData | null = null;
396 let polygonData: PolygonPreviousCloseData | null = null;
397 let fetchError: Error | null = null;
398
399 try {
400 [alphaVantageData, polygonData] = await Promise.all([
401 fetchAlphaVantageData(ticker, alphaVantageKey || null, steps),
402 fetchPolygonData(ticker, polygonKey || null, steps),
403 ]);
404 } catch (error: any) {
405 steps.push(`Error during data fetching: ${error.message}`);
406 fetchError = error;
407 // Attempt to continue if some data was fetched (e.g., from cache fallback)
408 if (!alphaVantageData)
409 alphaVantageData = await fetchAlphaVantageData(ticker, alphaVantageKey || null, steps).catch(() => null); // Try again individually, catch error
410 if (!polygonData) polygonData = await fetchPolygonData(ticker, polygonKey || null, steps).catch(() => null); // Try again individually, catch error
411 }
412
413 if (!alphaVantageData && !polygonData && fetchError) {
414 // If both fetches failed critically and no cache fallback was possible, rethrow
415 throw fetchError;
416 }
417
500 const data = Object.fromEntries(formData.entries());
501
502 // Use the current URL for the fetch endpoint
503 const url = new URL(window.location.href);
504 url.pathname += '/analyze'; // Append /analyze to the current path
505
506 try {
507 const response = await fetch(url.toString(), {
508 method: 'POST',
509 headers: {
559 };
560
561 // Mock fetch to avoid actual API calls and simulate responses
562 const originalFetch = global.fetch;
563 global.fetch = (url: string, options?: any): Promise<any> => {
564 console.log(`Mock fetch called for: ${url}`);
565 let responseData: any = {};
566 let status = 200;
603 statusText: status === 200 ? "OK" : "Error",
604 json: () => Promise.resolve(responseData),
605 text: () => Promise.resolve(JSON.stringify(responseData)), // For error handling in polygon fetch
606 });
607 };
629 console.error("Self-test FAILED with error:", error);
630 } finally {
631 // Restore original fetch
632 global.fetch = originalFetch;
633 console.log("Self-test finished.");
634 }