180 lines
4.4 KiB
TypeScript
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' };
|
|
}
|
|
}; |