-
-
-
- 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"
>
-