frontend/src/routes/stocks/[tickerID]/+layout.server.ts
MuslemRahimi f602c1a1d0 bugfixing
2025-02-22 22:17:32 +01:00

180 lines
4.4 KiB
TypeScript

// Pre-compile regex pattern and substrings for cleaning
const REMOVE_PATTERNS = {
pattern: new RegExp(`\\b(${[
"Depositary",
"Inc.",
"Incorporated",
"Holdings",
"Corporations",
"LLC",
"Holdings plc American Depositary Shares",
"Holding Corporation",
"Oyj",
"Company",
"The",
"plc"
].join("|")})\\b|,`, "gi")
};
// 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([
"/stockdeck",
"/analyst-summary-rating",
"/stock-quote",
"/pre-post-quote",
"/wiim",
"/one-day-price",
"/next-earnings",
"/earnings-surprise",
"/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;
}
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) {
if (error.name === 'AbortError') {
throw new Error(`Request timeout for ${endpoint}`);
}
throw error;
}
};
// Optimized watchlist fetching with error boundary
const fetchWatchlist = async (pb, userId) => {
if (!userId) return [];
try {
return await pb.collection("watchlist").getFullList({
filter: `user="${userId}"`
});
} catch {
return [];
}
};
// Main load function with parallel fetching
export const load = async ({ params, locals }) => {
const { apiURL, apiKey, pb, user } = locals;
const { tickerID } = params;
if (!tickerID) {
return { error: 'Invalid ticker ID' };
}
try {
// Fetch data in parallel
const [stockData, userWatchlist] = await Promise.all([
fetchData(apiURL, apiKey, "/bulk-data", tickerID),
fetchWatchlist(pb, user?.id)
]);
// Destructure with default empty object to prevent undefined errors
const {
'/stockdeck': getStockDeck = {},
'/analyst-summary-rating': getAnalystSummary = {},
'/stock-quote': getStockQuote = {},
'/pre-post-quote': getPrePostQuote = {},
'/wiim': getWhyPriceMoved = {},
'/one-day-price': getOneDayPrice = {},
'/next-earnings': getNextEarnings = {},
'/earnings-surprise': getEarningsSurprise = {},
'/stock-news': getNews = {}
} = stockData;
return {
getStockDeck,
getAnalystSummary,
getStockQuote,
getPrePostQuote,
getWhyPriceMoved,
getOneDayPrice,
getNextEarnings,
getEarningsSurprise,
getNews,
getUserWatchlist: userWatchlist,
companyName: cleanString(getStockDeck?.companyName),
getParams: tickerID
};
} catch (error) {
return { error: 'Failed to load stock data' };
}
};