diff --git a/src/lib/components/Searchbar.svelte b/src/lib/components/Searchbar.svelte
index 03868ce2..b49594f2 100644
--- a/src/lib/components/Searchbar.svelte
+++ b/src/lib/components/Searchbar.svelte
@@ -19,6 +19,7 @@
let searchOpen = false;
let searchBarModalChecked = false; // Initialize it to false
let inputElement;
+ let isNavigating = false;
const popularList = [
{
@@ -49,12 +50,14 @@
];
async function handleSearch(symbol, assetType) {
+ if (isNavigating) return; // Prevent double calls
+ isNavigating = true;
+
// Find the matching ticker data
const newSearchItem = searchBarData?.find(
(item) => item?.symbol === symbol?.toUpperCase(),
);
if (newSearchItem) {
- // Ensure `upperState` matches the case of `item.symbol`
updatedSearchHistory = [
newSearchItem,
...(searchHistory?.filter(
@@ -63,28 +66,39 @@
].slice(0, 5);
}
- searchBarTicker(symbol);
+ // Close search modal
+ searchOpen = false;
+ if ($screenWidth < 640) {
+ const closePopup = document.getElementById("searchBarModal");
+ closePopup?.dispatchEvent(new MouseEvent("click"));
+ }
- nextPage = true;
- goto(
+ await goto(
`/${assetType === "ETF" ? "etf" : assetType === "Index" ? "index" : "stocks"}/${symbol}`,
);
+
+ // Reset the flag after navigation
+ setTimeout(() => {
+ isNavigating = false;
+ }, 100);
}
async function popularTicker(state) {
+ if (isNavigating) return;
+ isNavigating = true;
+
searchOpen = false;
- if (!state) return;
+ if (!state) {
+ isNavigating = false;
+ return;
+ }
- // Convert state to uppercase once
const upperState = state.toUpperCase();
-
- // Find the matching ticker data
const newSearchItem = searchBarData?.find(
({ symbol }) => symbol === upperState,
);
if (newSearchItem) {
- // Ensure `upperState` matches the case of `item.symbol`
updatedSearchHistory = [
newSearchItem,
...(searchHistory?.filter(
@@ -93,14 +107,19 @@
].slice(0, 5);
}
- // Close search modal
const closePopup = document.getElementById("searchBarModal");
closePopup?.dispatchEvent(new MouseEvent("click"));
+
+ // Reset the flag after a short delay
+ setTimeout(() => {
+ isNavigating = false;
+ }, 100);
}
function searchBarTicker(state) {
+ if (isNavigating) return;
showSuggestions = false;
- // Early return if state is empty or ticker not found
+
if (
!state ||
!searchBarData?.find((item) => item?.symbol === state?.toUpperCase())
@@ -109,29 +128,23 @@
const closePopup = document.getElementById("searchBarModal");
closePopup?.dispatchEvent(new MouseEvent("click"));
}
-
return;
}
- // Convert state to uppercase once
const upperState = state.toUpperCase();
-
- // Find the matching ticker data
const newSearchItem = searchBarData?.find(
({ symbol }) => symbol === upperState,
);
if (newSearchItem) {
- // Ensure `upperState` matches the case of `item.symbol`
updatedSearchHistory = [
newSearchItem,
...(searchHistory?.filter(
(item) => item?.symbol?.toUpperCase() !== upperState,
) || []),
- ]?.slice(0, 5);
+ ].slice(0, 5);
}
- // Close search modal
searchOpen = false;
if ($screenWidth < 640) {
const closePopup = document.getElementById("searchBarModal");
@@ -160,6 +173,8 @@
}
function handleKeyDown(symbol) {
+ if (isNavigating) return;
+
const list = Array.from(
new Map(
[...searchHistory, ...searchBarData, ...popularList].map((item) => [
@@ -172,8 +187,11 @@
if (!list?.length) return;
const newData = list.find((item) => item?.symbol === symbol);
- handleSearch(newData?.symbol, newData?.type);
+ if (newData) {
+ handleSearch(newData?.symbol, newData?.type);
+ }
}
+
const handleControlK = async (event) => {
if (event.ctrlKey && event.key === "k") {
const keyboardSearch = document.getElementById("combobox-input");
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 052ef740..0be97bd4 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -428,7 +428,7 @@
{item?.ticker}
@@ -441,7 +441,7 @@
{item?.ticker}
@@ -489,14 +489,16 @@
According to {analystReport?.numOfAnalyst} analyst ratings, the
average rating for
-
{analystReport?.symbol}
stock is "{analystReport?.consensusRating}" The 12-month stock
price forecast is ${analystReport?.highPriceTarget}, which is
an {analystReport?.highPriceChange > 0
? "increase"
- : "decreas"} of {analystReport?.highPriceChange}% from the
+ : "decrease"} of {analystReport?.highPriceChange}% from the
latest price.
{
- const substringsToRemove = [
+// Pre-compile regex pattern and substrings for cleaning
+const REMOVE_PATTERNS = {
+ pattern: new RegExp(`\\b(${[
"Depositary",
"Inc.",
"Incorporated",
@@ -11,81 +12,126 @@ 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 CACHE_DURATION = 5 * 60 * 1000; // 5 minutes in milliseconds
-const REQUEST_TIMEOUT = 5000; // 5 seconds
-const cache = new Map();
+// 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;
+ };
+})();
-const fetchData = async (apiURL, apiKey, endpoint, ticker) => {
- const cacheKey = `${endpoint}-${ticker}`;
- const cached = cache.get(cacheKey);
-
- if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
- return cached.data;
+// 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(), REQUEST_TIMEOUT);
-
- const endpoints = [
- "/stockdeck",
- "/analyst-summary-rating",
- "/stock-quote",
- "/pre-post-quote",
- "/wiim",
- "/one-day-price",
- "/next-earnings",
- "/earnings-surprise",
- "/stock-news",
- ]
-
-
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
+
try {
- const response = await fetch(`${apiURL}${endpoint}`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- "X-API-KEY": apiKey,
- },
- body: JSON.stringify({ticker, endpoints}),
+ 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);
+ }
+};
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
+// 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 data = await response.json();
- cache.set(cacheKey, { data, timestamp: Date.now() });
+ 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;
- } finally {
- clearTimeout(timeoutId);
}
};
+// 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;
@@ -95,24 +141,24 @@ export const load = async ({ params, locals }) => {
}
try {
- // Fetch combined stock data from the '/stock-data' endpoint
- const getStockData = await fetchData(apiURL, apiKey, "/bulk-data", tickerID);
+ // Fetch data in parallel
+ const [stockData, userWatchlist] = await Promise.all([
+ fetchData(apiURL, apiKey, "/bulk-data", tickerID),
+ fetchWatchlist(pb, user?.id)
+ ]);
- // Destructure the returned object to assign friendly names
+ // 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,
- } = getStockData;
-
- // Optionally, you can still fetch additional data like the watchlist:
- const getUserWatchlist = await fetchWatchlist(pb, user?.id).catch(() => []);
+ '/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,
@@ -124,11 +170,11 @@ export const load = async ({ params, locals }) => {
getNextEarnings,
getEarningsSurprise,
getNews,
- getUserWatchlist,
+ getUserWatchlist: userWatchlist,
companyName: cleanString(getStockDeck?.companyName),
- getParams: tickerID,
+ getParams: tickerID
};
} catch (error) {
return { error: 'Failed to load stock data' };
}
-};
+};
\ No newline at end of file