This commit is contained in:
MuslemRahimi 2025-02-22 22:14:06 +01:00
parent 08798d15c3
commit 1411cde332
3 changed files with 291 additions and 106 deletions

View File

@ -2,28 +2,103 @@ import { error, fail, redirect } from "@sveltejs/kit";
import { validateData } from "$lib/utils"; import { validateData } from "$lib/utils";
import { loginUserSchema, registerUserSchema } from "$lib/schemas"; import { loginUserSchema, registerUserSchema } from "$lib/schemas";
export const load = async ({ locals }) => {
const { apiKey, apiURL } = locals;
const getDashboard = async () => { // Constants
const response = await fetch(apiURL + "/dashboard-info", { const CACHE_DURATION = 60 * 1000; // 1 minute in milliseconds
const REQUEST_TIMEOUT = 5000;
// 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 dashboardCache = 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);
}
};
// Dashboard data fetching function with caching
const getDashboard = async (apiURL, apiKey) => {
const cacheKey = 'dashboard-info';
const cachedData = dashboardCache.get(cacheKey);
if (cachedData) return cachedData;
const options = {
method: "GET", method: "GET",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"X-API-KEY": apiKey, "X-API-KEY": apiKey
}, }
});
const output = await response?.json();
return output;
}; };
// Make sure to return a promise try {
const data = await fetchWithTimeout(
`${apiURL}/dashboard-info`,
options,
REQUEST_TIMEOUT
);
dashboardCache.set(cacheKey, data);
return data;
} catch (error) {
if (error.name === 'AbortError') {
throw new Error('Dashboard request timeout');
}
console.error('Error fetching dashboard:', error);
return {};
}
};
// Main load function
export const load = async ({ locals }) => {
const { apiKey, apiURL } = locals;
try {
return { return {
getDashboard: await getDashboard(), getDashboard: await getDashboard(apiURL, apiKey)
}; };
} catch (error) {
console.error('Error in dashboard load:', error);
return {
getDashboard: {}
}; };
}
};
async function checkDisposableEmail(email) { async function checkDisposableEmail(email) {
const url = `https://disposable.debounce.io/?email=${encodeURIComponent(email)}`; const url = `https://disposable.debounce.io/?email=${encodeURIComponent(email)}`;

View File

@ -1,5 +1,6 @@
const cleanString = (input) => { // Pre-compile regex pattern and substrings for cleaning
const substringsToRemove = [ const REMOVE_PATTERNS = {
pattern: new RegExp(`\\b(${[
"Depositary", "Depositary",
"Inc.", "Inc.",
"Incorporated", "Incorporated",
@ -11,54 +12,14 @@ const cleanString = (input) => {
"Oyj", "Oyj",
"Company", "Company",
"The", "The",
"plc", "plc"
]; ].join("|")})\\b|,`, "gi")
const pattern = new RegExp(`\\b(${substringsToRemove.join("|")})\\b|,`, "gi");
return input?.replace(pattern, "").trim();
}; };
const fetchData = async (apiURL, apiKey, endpoint, ticker) => { // Constants
try { const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
const response = await fetch(`${apiURL}${endpoint}`, { const REQUEST_TIMEOUT = 5000; // 5 seconds
method: "POST", const ENDPOINTS = Object.freeze([
headers: {
"Content-Type": "application/json",
"X-API-KEY": apiKey,
},
body: JSON.stringify({ ticker }),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error(`Error fetching ${endpoint}:`, error);
return [];
}
};
const fetchWatchlist = async (pb, userId) => {
let output;
try {
output = await pb.collection("watchlist").getFullList({
filter: `user="${userId}"`,
});
} catch (e) {
//console.log(e)
output = [];
}
return output;
};
export const load = async ({ params, locals }) => {
const { apiURL, apiKey, pb, user } = locals;
const { tickerID } = params;
try {
const endpoints = [
"/index-profile", "/index-profile",
"/etf-holdings", "/etf-holdings",
"/etf-sector-weighting", "/etf-sector-weighting",
@ -66,17 +27,151 @@ export const load = async ({ params, locals }) => {
"/pre-post-quote", "/pre-post-quote",
"/wiim", "/wiim",
"/one-day-price", "/one-day-price",
"/stock-news", "/stock-news"
]; ]);
const promises = endpoints.map((endpoint) => { const SPY_PROXY_ENDPOINTS = Object.freeze([
// Use SPY for specific endpoints when tickerID is ^SPC or ^spc "/etf-holdings",
const useSpyTicker = tickerID?.toLowerCase() === "^spx" && "/etf-sector-weighting",
["/etf-holdings", "/etf-sector-weighting", "/wiim", "/stock-news"]?.includes(endpoint); "/wiim",
return fetchData(apiURL, apiKey, endpoint, useSpyTicker ? "SPY" : tickerID); "/stock-news"
]);
// 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;
};
})();
// 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 with SPX/SPY handling
const fetchData = async (apiURL, apiKey, endpoint, ticker) => {
const useSpyTicker = ticker?.toLowerCase() === "^spx" &&
SPY_PROXY_ENDPOINTS.includes(endpoint);
const effectiveTicker = useSpyTicker ? "SPY" : ticker;
const cacheKey = `${endpoint}-${effectiveTicker}`;
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: effectiveTicker })
};
try {
const data = await fetchWithTimeout(
`${apiURL}${endpoint}`,
options,
REQUEST_TIMEOUT
);
dataCache.set(cacheKey, data);
return data;
} catch (error) {
if (error.name === 'AbortError') {
console.error(`Request timeout for ${endpoint}`);
} else {
console.error(`Error fetching ${endpoint}:`, error);
}
return [];
}
};
// 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 (error) {
console.error('Error fetching watchlist:', error);
return [];
}
};
// Helper function to generate default response
const getDefaultResponse = (tickerID) => ({
getIndexProfile: [],
getIndexHolding: [],
getIndexSectorWeighting: [],
getStockQuote: [],
getPrePostQuote: [],
getWhyPriceMoved: [],
getOneDayPrice: [],
getNews: [],
getUserWatchlist: [],
companyName: '',
getParams: tickerID
}); });
// Add watchlist promise // 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 promises = ENDPOINTS.map(endpoint =>
fetchData(apiURL, apiKey, endpoint, tickerID)
);
promises.push(fetchWatchlist(pb, user?.id)); promises.push(fetchWatchlist(pb, user?.id));
const [ const [
@ -102,22 +197,10 @@ export const load = async ({ params, locals }) => {
getNews: getNews || [], getNews: getNews || [],
getUserWatchlist: getUserWatchlist || [], getUserWatchlist: getUserWatchlist || [],
companyName: cleanString(getIndexProfile?.at(0)?.name), companyName: cleanString(getIndexProfile?.at(0)?.name),
getParams: params.tickerID, getParams: tickerID
}; };
} catch (error) { } catch (error) {
console.error('Error in load function:', error); console.error('Error in load function:', error);
return { return getDefaultResponse(tickerID);
getIndexProfile: [],
getIndexHolding: [],
getIndexSectorWeighting: [],
getStockQuote: [],
getPrePostQuote: [],
getWhyPriceMoved: [],
getOneDayPrice: [],
getNews: [],
getUserWatchlist: [],
companyName: '',
getParams: params.tickerID,
};
} }
}; };

View File

@ -32,14 +32,20 @@ const cleanString = (() => {
const CACHE_DURATION = 5 * 60 * 1000; const CACHE_DURATION = 5 * 60 * 1000;
const REQUEST_TIMEOUT = 5000; const REQUEST_TIMEOUT = 5000;
const ENDPOINTS = Object.freeze([ const ENDPOINTS = Object.freeze([
"/stockdeck", "/index-profile",
"/analyst-summary-rating", "/etf-holdings",
"/etf-sector-weighting",
"/stock-quote", "/stock-quote",
"/pre-post-quote", "/pre-post-quote",
"/wiim", "/wiim",
"/one-day-price", "/one-day-price",
"/next-earnings", "/stock-news"
"/earnings-surprise", ]);
const SPY_PROXY_ENDPOINTS = Object.freeze([
"/etf-holdings",
"/etf-sector-weighting",
"/wiim",
"/stock-news" "/stock-news"
]); ]);
@ -88,9 +94,13 @@ const fetchWithTimeout = async (url, options, timeout) => {
} }
}; };
// Main data fetching function // Main data fetching function with SPX/SPY handling
const fetchData = async (apiURL, apiKey, endpoint, ticker) => { const fetchData = async (apiURL, apiKey, endpoint, ticker) => {
const cacheKey = `${endpoint}-${ticker}`; const useSpyTicker = ticker?.toLowerCase() === "^spx" &&
SPY_PROXY_ENDPOINTS.some(proxyEndpoint => ENDPOINTS.includes(proxyEndpoint));
const effectiveTicker = useSpyTicker ? "SPY" : ticker;
const cacheKey = `${endpoint}-${effectiveTicker}`;
const cachedData = dataCache.get(cacheKey); const cachedData = dataCache.get(cacheKey);
if (cachedData) return cachedData; if (cachedData) return cachedData;
@ -100,7 +110,10 @@ const fetchData = async (apiURL, apiKey, endpoint, ticker) => {
"Content-Type": "application/json", "Content-Type": "application/json",
"X-API-KEY": apiKey "X-API-KEY": apiKey
}, },
body: JSON.stringify({ ticker, endpoints: ENDPOINTS }) body: JSON.stringify({
ticker: effectiveTicker,
endpoints: ENDPOINTS
})
}; };
try { try {
@ -131,50 +144,64 @@ const fetchWatchlist = async (pb, userId) => {
} }
}; };
// Helper function to generate default response
const getDefaultResponse = (tickerID) => ({
getIndexProfile: [],
getIndexHolding: [],
getIndexSectorWeighting: [],
getStockQuote: [],
getPrePostQuote: [],
getWhyPriceMoved: [],
getOneDayPrice: [],
getNews: [],
getUserWatchlist: [],
companyName: '',
getParams: tickerID
});
// Main load function with parallel fetching // Main load function with parallel fetching
export const load = async ({ params, locals }) => { export const load = async ({ params, locals }) => {
const { apiURL, apiKey, pb, user } = locals; const { apiURL, apiKey, pb, user } = locals;
const { tickerID } = params; const { tickerID } = params;
if (!tickerID) { if (!tickerID) {
return { error: 'Invalid ticker ID' }; return getDefaultResponse(tickerID);
} }
try { try {
// Fetch data in parallel // Fetch data in parallel
const [stockData, userWatchlist] = await Promise.all([ const [indexData, userWatchlist] = await Promise.all([
fetchData(apiURL, apiKey, "/bulk-data", tickerID), fetchData(apiURL, apiKey, "/bulk-data", tickerID),
fetchWatchlist(pb, user?.id) fetchWatchlist(pb, user?.id)
]); ]);
// Destructure with default empty object to prevent undefined errors // Destructure with default empty arrays to prevent undefined errors
const { const {
'/stockdeck': getStockDeck = {}, '/index-profile': getIndexProfile = [],
'/analyst-summary-rating': getAnalystSummary = {}, '/etf-holdings': getIndexHolding = [],
'/stock-quote': getStockQuote = {}, '/etf-sector-weighting': getIndexSectorWeighting = [],
'/pre-post-quote': getPrePostQuote = {}, '/stock-quote': getStockQuote = [],
'/wiim': getWhyPriceMoved = {}, '/pre-post-quote': getPrePostQuote = [],
'/one-day-price': getOneDayPrice = {}, '/wiim': getWhyPriceMoved = [],
'/next-earnings': getNextEarnings = {}, '/one-day-price': getOneDayPrice = [],
'/earnings-surprise': getEarningsSurprise = {}, '/stock-news': getNews = []
'/stock-news': getNews = {} } = indexData;
} = stockData;
return { return {
getStockDeck, getIndexProfile,
getAnalystSummary, getIndexHolding,
getIndexSectorWeighting,
getStockQuote, getStockQuote,
getPrePostQuote, getPrePostQuote,
getWhyPriceMoved, getWhyPriceMoved,
getOneDayPrice, getOneDayPrice,
getNextEarnings,
getEarningsSurprise,
getNews, getNews,
getUserWatchlist: userWatchlist, getUserWatchlist: userWatchlist,
companyName: cleanString(getStockDeck?.companyName), companyName: cleanString(getIndexProfile?.at(0)?.name),
getParams: tickerID getParams: tickerID
}; };
} catch (error) { } catch (error) {
return { error: 'Failed to load stock data' }; console.error('Error in load function:', error);
return getDefaultResponse(tickerID);
} }
}; };