From 08798d15c3a10ff67b5ef0dfdc41136aeee6e9fa Mon Sep 17 00:00:00 2001 From: MuslemRahimi Date: Sat, 22 Feb 2025 22:06:55 +0100 Subject: [PATCH] bugfixing --- src/lib/components/EarningsSurprise.svelte | 153 +++++++------ src/routes/etf/[tickerID]/+layout.server.ts | 227 +++++++++++++------- src/routes/stocks/[tickerID]/+layout.svelte | 4 +- 3 files changed, 226 insertions(+), 158 deletions(-) diff --git a/src/lib/components/EarningsSurprise.svelte b/src/lib/components/EarningsSurprise.svelte index eb3957db..6731a515 100644 --- a/src/lib/components/EarningsSurprise.svelte +++ b/src/lib/components/EarningsSurprise.svelte @@ -1,6 +1,6 @@ -{#if Object?.keys(rawData)?.length !== 0} -
- -
-
-
- -
-

- Earnings Surprise -

- -
+
+ +
+
+
+ +
+

+ Earnings Surprise +

+
- -
-
- {$displayCompanyName} has released their quartely earnings on {new Date( - rawData?.date, - )?.toLocaleString("en-US", { - month: "short", - day: "numeric", - year: "numeric", - daySuffix: "2-digit", - })}: -
- -
  • - Revenue of {abbreviateNumber(rawData?.revenue, true)} - {rawData?.revenueSurprise > 0 ? "exceeds" : "misses"} estimates by {abbreviateNumber( - Math.abs(rawData?.revenueSurprise), - true, - )}, with - {revenueRatio}% - YoY {revenueRatio < 0 ? "decline" : "growth"}. -
  • -
  • - EPS of {rawData?.eps} - {rawData?.epsSurprise > 0 ? "exceeds" : "misses"} estimates by {rawData?.epsSurprise?.toFixed( - 2, - )}, with - - {epsRatio === null ? "n/a" : `${epsRatio}%`} - - YoY {epsRatio === null ? "" : epsRatio < 0 ? "decline" : "growth"}. -
  • -
    +
    + +
    +
    + {removeCompanyStrings($displayCompanyName)} has released their quartely earnings + on {new Date(rawData?.date)?.toLocaleString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + daySuffix: "2-digit", + })}: +
    + +
  • + Revenue of {abbreviateNumber(rawData?.revenue, true)} + {rawData?.revenueSurprise > 0 ? "exceeds" : "misses"} estimates by {abbreviateNumber( + Math.abs(rawData?.revenueSurprise), + true, + )}, with + {revenueRatio}% + YoY {revenueRatio < 0 ? "decline" : "growth"}. +
  • +
  • + EPS of {rawData?.eps} + {rawData?.epsSurprise > 0 ? "exceeds" : "misses"} estimates by {rawData?.epsSurprise?.toFixed( + 2, + )}, with + + {epsRatio === null ? "n/a" : `${epsRatio}%`} + + YoY {epsRatio === null ? "" : epsRatio < 0 ? "decline" : "growth"}. +
  • -{/if} +
    diff --git a/src/routes/etf/[tickerID]/+layout.server.ts b/src/routes/etf/[tickerID]/+layout.server.ts index f72914cc..bf464861 100644 --- a/src/routes/etf/[tickerID]/+layout.server.ts +++ b/src/routes/etf/[tickerID]/+layout.server.ts @@ -1,5 +1,6 @@ -const cleanString = (input) => { - const substringsToRemove = [ +// Pre-compile regex pattern and substrings for cleaning +const REMOVE_PATTERNS = { + pattern: new RegExp(`\\b(${[ "Depositary", "Inc.", "Incorporated", @@ -11,73 +12,154 @@ const cleanString = (input) => { "Oyj", "Company", "The", - "plc", - ]; - const pattern = new RegExp(`\\b(${substringsToRemove.join("|")})\\b|,`, "gi"); - return input?.replace(pattern, "").trim(); + "plc" + ].join("|")})\\b|,`, "gi") }; -const fetchData = async (apiURL, apiKey, endpoint, ticker) => { - try { - const response = await fetch(`${apiURL}${endpoint}`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-API-KEY": apiKey, - }, - body: JSON.stringify({ ticker }), - }); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); +// Memoized string cleaning function +const cleanString = (() => { + const cache = new Map(); + return (input) => { + if (!input) return ''; + if (cache.has(input)) return cache.get(input); + const cleaned = input.replace(REMOVE_PATTERNS.pattern, '').trim(); + cache.set(input, cleaned); + return cleaned; + }; +})(); + +// Constants +const CACHE_DURATION = 5 * 60 * 1000; +const REQUEST_TIMEOUT = 5000; +const ENDPOINTS = Object.freeze([ + "/etf-profile", + "/etf-holdings", + "/etf-sector-weighting", + "/stock-dividend", + "/stock-quote", + "/pre-post-quote", + "/wiim", + "/one-day-price", + "/stock-news" +]); + +// LRU Cache implementation with automatic cleanup +class LRUCache { + constructor(maxSize = 100) { + this.cache = new Map(); + this.maxSize = maxSize; + } + + get(key) { + const item = this.cache.get(key); + if (!item) return null; + if (Date.now() - item.timestamp >= CACHE_DURATION) { + this.cache.delete(key); + return null; } - - const data = await response.json(); + return item.data; + } + + set(key, data) { + if (this.cache.size >= this.maxSize) { + const oldestKey = this.cache.keys().next().value; + this.cache.delete(oldestKey); + } + this.cache.set(key, { data, timestamp: Date.now() }); + } +} + +const dataCache = new LRUCache(); + +// Optimized fetch function with AbortController and timeout +const fetchWithTimeout = async (url, options, timeout) => { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeout); + + try { + const response = await fetch(url, { + ...options, + signal: controller.signal + }); + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); + return await response.json(); + } finally { + clearTimeout(timeoutId); + } +}; + +// Main data fetching function +const fetchData = async (apiURL, apiKey, endpoint, ticker) => { + const cacheKey = `${endpoint}-${ticker}`; + const cachedData = dataCache.get(cacheKey); + if (cachedData) return cachedData; + + const options = { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-API-KEY": apiKey + }, + body: JSON.stringify({ ticker, endpoints: ENDPOINTS }) + }; + + try { + const data = await fetchWithTimeout( + `${apiURL}${endpoint}`, + options, + REQUEST_TIMEOUT + ); + dataCache.set(cacheKey, data); return data; } catch (error) { - console.error(`Error fetching ${endpoint}:`, error); + if (error.name === 'AbortError') { + throw new Error(`Request timeout for ${endpoint}`); + } return []; } }; +// Optimized watchlist fetching with error boundary const fetchWatchlist = async (pb, userId) => { - let output; + if (!userId) return []; try { - output = await pb.collection("watchlist").getFullList({ - filter: `user="${userId}"`, + return await pb.collection("watchlist").getFullList({ + filter: `user="${userId}"` }); - } catch (e) { - //console.log(e) - output = []; + } catch { + return []; } - return output; }; +// Main load function with parallel fetching export const load = async ({ params, locals }) => { const { apiURL, apiKey, pb, user } = locals; const { tickerID } = params; + if (!tickerID) { + return getDefaultResponse(tickerID); + } + try { - const endpoints = [ - "/etf-profile", - "/etf-holdings", - "/etf-sector-weighting", - "/stock-dividend", - "/stock-quote", - "/pre-post-quote", - "/wiim", - "/one-day-price", - "/stock-news", - ]; + const [bulkData, userWatchlist] = await Promise.all([ + fetchData(apiURL, apiKey, "/bulk-data", tickerID), + fetchWatchlist(pb, user?.id) + ]); - const promises = [ - ...endpoints.map((endpoint) => - fetchData(apiURL, apiKey, endpoint, tickerID), - ), - fetchWatchlist(pb, user?.id), - ]; + // Destructure with default empty arrays to prevent undefined errors + const { + '/etf-profile': getETFProfile = [], + '/etf-holdings': getETFHoldings = [], + '/etf-sector-weighting': getETFSectorWeighting = [], + '/stock-dividend': getStockDividend = [], + '/stock-quote': getStockQuote = [], + '/pre-post-quote': getPrePostQuote = [], + '/wiim': getWhyPriceMoved = [], + '/one-day-price': getOneDayPrice = [], + '/stock-news': getNews = [] + } = bulkData; - const [ + return { getETFProfile, getETFHoldings, getETFSectorWeighting, @@ -87,38 +169,27 @@ export const load = async ({ params, locals }) => { getWhyPriceMoved, getOneDayPrice, getNews, - getUserWatchlist, - ] = await Promise.all(promises); - - return { - getETFProfile: getETFProfile || [], - getETFHoldings: getETFHoldings || [], - getETFSectorWeighting: getETFSectorWeighting || [], - getStockDividend: getStockDividend || [], - getStockQuote: getStockQuote || [], - getPrePostQuote: getPrePostQuote || [], - getWhyPriceMoved: getWhyPriceMoved || [], - getOneDayPrice: getOneDayPrice || [], - getNews: getNews || [], - getUserWatchlist: getUserWatchlist || [], + getUserWatchlist: userWatchlist, companyName: cleanString(getETFProfile?.at(0)?.name), - getParams: params.tickerID, + getParams: tickerID }; } catch (error) { - console.error('Error in load function:', error); - return { - getETFProfile: [], - getETFHoldings: [], - getETFSectorWeighting: [], - getStockDividend: [], - getStockQuote: [], - getPrePostQuote: [], - getWhyPriceMoved: [], - getOneDayPrice: [], - getNews: [], - getUserWatchlist: [], - companyName: '', - getParams: params.tickerID, - }; + return getDefaultResponse(tickerID); } -}; \ No newline at end of file +}; + +// Helper function to generate default response +const getDefaultResponse = (tickerID) => ({ + getETFProfile: [], + getETFHoldings: [], + getETFSectorWeighting: [], + getStockDividend: [], + getStockQuote: [], + getPrePostQuote: [], + getWhyPriceMoved: [], + getOneDayPrice: [], + getNews: [], + getUserWatchlist: [], + companyName: '', + getParams: tickerID +}); \ No newline at end of file diff --git a/src/routes/stocks/[tickerID]/+layout.svelte b/src/routes/stocks/[tickerID]/+layout.svelte index dc878c3b..ad8e7c07 100644 --- a/src/routes/stocks/[tickerID]/+layout.svelte +++ b/src/routes/stocks/[tickerID]/+layout.svelte @@ -380,11 +380,11 @@ class="bg-default w-full max-w-screen sm:max-w-[1250px] min-h-screen overflow-hidden" > -
    +
    -
    +