/** * Background data refresh service * Updates prices, filings, and earnings data on a schedule */ import type { BrowserWindow } from "electron"; import type { Db } from "@mosaiciq/shared/db"; import { fetchQuote, fetchFilings } from "@mosaiciq/shared/data"; import { upsertCompany } from "@mosaiciq/shared/db"; import { listFilings } from "@mosaiciq/shared/db"; export interface DataRefreshOptions { priceInterval?: number; // minutes filingInterval?: number; // minutes earningsInterval?: number; // minutes } export function startDataRefresh( db: Db, mainWindow: BrowserWindow, options: DataRefreshOptions = {} ): () => void { const { priceInterval = 5, filingInterval = 60, earningsInterval = 1440, // daily } = options; let priceTimeout: NodeJS.Timeout | null = null; let filingTimeout: NodeJS.Timeout | null = null; let earningsTimeout: NodeJS.Timeout | null = null; // Get all portfolio holdings function getPortfolioTickers(): string[] { const stmt = db.prepare(` SELECT DISTINCT ticker FROM holdings UNION SELECT DISTINCT ticker FROM companies `); const rows = stmt.all() as Array<{ ticker: string }>; return rows.map((r) => r.ticker); } // Refresh prices for all tickers async function refreshPrices() { try { const tickers = getPortfolioTickers(); if (tickers.length === 0) return; console.log(`[DataRefresh] Refreshing prices for ${tickers.length} tickers`); for (const ticker of tickers) { const quote = await fetchQuote(ticker); if (quote) { // Update company price in database const company = db.prepare("SELECT * FROM companies WHERE ticker = ?").get(ticker) as any; if (company) { db.prepare(` UPDATE companies SET price = ?, change_pct = ?, updated_at = datetime('now') WHERE ticker = ? `).run(quote.price, quote.changePercent, ticker); } } } // Notify UI of price updates mainWindow?.webContents.send("data:prices-updated", { timestamp: new Date().toISOString(), }); console.log("[DataRefresh] Price refresh complete"); } catch (error) { console.error("[DataRefresh] Error refreshing prices:", error); } // Schedule next refresh priceTimeout = setTimeout(refreshPrices, priceInterval * 60 * 1000); } // Check for new filings async function refreshFilings() { try { const tickers = getPortfolioTickers(); if (tickers.length === 0) return; console.log(`[DataRefresh] Checking for new filings for ${tickers.length} tickers`); for (const ticker of tickers) { // Get most recent filing date from database const existingFilings = listFilings(db, ticker); const since = existingFilings.length > 0 ? existingFilings[0].filedDate : undefined; const newFilings = await fetchFilings(ticker, { limit: 10, since }); for (const filing of newFilings) { // Check if filing already exists const exists = db.prepare(` SELECT id FROM filings WHERE company_id = ? AND filed_date = ? AND form_type = ? `).get(ticker, filing.filedDate, filing.formType); if (!exists) { db.prepare(` INSERT INTO filings (id, company_id, form_type, filed_date, title) VALUES (?, ?, ?, ?, ?) `).run( `${ticker}-${filing.formType}-${filing.filedDate}`, ticker, filing.formType, filing.filedDate, filing.title ); // Notify UI of new filing mainWindow?.webContents.send("alert:new-filing", { ticker, formType: filing.formType, title: filing.title, filedDate: filing.filedDate, }); } } } console.log("[DataRefresh] Filing refresh complete"); } catch (error) { console.error("[DataRefresh] Error refreshing filings:", error); } // Schedule next refresh filingTimeout = setTimeout(refreshFilings, filingInterval * 60 * 1000); } // Refresh earnings dates async function refreshEarnings() { try { const tickers = getPortfolioTickers(); if (tickers.length === 0) return; console.log(`[DataRefresh] Updating earnings dates for ${tickers.length} tickers`); // Import dynamically to avoid circular dependency const { getEarningsDate, getQuarterString } = await import("@mosaiciq/shared/data"); for (const ticker of tickers) { const earningsDate = await getEarningsDate(ticker); if (earningsDate) { // Check if earnings schedule exists const existing = db.prepare(` SELECT id FROM earnings_schedules WHERE company_id = ? AND expected_date = ? `).get(ticker, earningsDate.toISOString()); if (!existing) { const quarter = getQuarterString(earningsDate); db.prepare(` INSERT INTO earnings_schedules (id, company_id, quarter, expected_date) VALUES (?, ?, ?, ?) `).run( `earnings-${ticker}-${Date.now()}`, ticker, quarter, earningsDate.toISOString() ); } } } console.log("[DataRefresh] Earnings refresh complete"); } catch (error) { console.error("[DataRefresh] Error refreshing earnings:", error); } // Schedule next refresh earningsTimeout = setTimeout(refreshEarnings, earningsInterval * 60 * 1000); } // Start all refresh cycles console.log("[DataRefresh] Starting data refresh service"); priceTimeout = setTimeout(refreshPrices, 1000); // Start immediately filingTimeout = setTimeout(refreshFilings, 5000); // Start after 5s earningsTimeout = setTimeout(refreshEarnings, 10000); // Start after 10s // Return cleanup function return () => { if (priceTimeout) clearTimeout(priceTimeout); if (filingTimeout) clearTimeout(filingTimeout); if (earningsTimeout) clearTimeout(earningsTimeout); console.log("[DataRefresh] Stopped data refresh service"); }; }