speed up
This commit is contained in:
parent
08798d15c3
commit
1411cde332
@ -2,29 +2,104 @@ import { error, fail, redirect } from "@sveltejs/kit";
|
||||
import { validateData } from "$lib/utils";
|
||||
import { loginUserSchema, registerUserSchema } from "$lib/schemas";
|
||||
|
||||
|
||||
// Constants
|
||||
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",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-API-KEY": apiKey
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
const getDashboard = async () => {
|
||||
const response = await fetch(apiURL + "/dashboard-info", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-API-KEY": apiKey,
|
||||
},
|
||||
});
|
||||
|
||||
const output = await response?.json();
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
// Make sure to return a promise
|
||||
return {
|
||||
getDashboard: await getDashboard(),
|
||||
};
|
||||
try {
|
||||
return {
|
||||
getDashboard: await getDashboard(apiURL, apiKey)
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error in dashboard load:', error);
|
||||
return {
|
||||
getDashboard: {}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
async function checkDisposableEmail(email) {
|
||||
const url = `https://disposable.debounce.io/?email=${encodeURIComponent(email)}`;
|
||||
const response = await fetch(url, {
|
||||
|
||||
@ -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,72 +12,166 @@ 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}`);
|
||||
// Constants
|
||||
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
|
||||
const REQUEST_TIMEOUT = 5000; // 5 seconds
|
||||
const ENDPOINTS = Object.freeze([
|
||||
"/index-profile",
|
||||
"/etf-holdings",
|
||||
"/etf-sector-weighting",
|
||||
"/stock-quote",
|
||||
"/pre-post-quote",
|
||||
"/wiim",
|
||||
"/one-day-price",
|
||||
"/stock-news"
|
||||
]);
|
||||
|
||||
const SPY_PROXY_ENDPOINTS = Object.freeze([
|
||||
"/etf-holdings",
|
||||
"/etf-sector-weighting",
|
||||
"/wiim",
|
||||
"/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;
|
||||
}
|
||||
|
||||
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 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) {
|
||||
console.error(`Error fetching ${endpoint}:`, 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) => {
|
||||
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 (error) {
|
||||
console.error('Error fetching watchlist:', error);
|
||||
return [];
|
||||
}
|
||||
return output;
|
||||
};
|
||||
|
||||
// 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
|
||||
export const load = async ({ params, locals }) => {
|
||||
const { apiURL, apiKey, pb, user } = locals;
|
||||
const { tickerID } = params;
|
||||
|
||||
if (!tickerID) {
|
||||
return getDefaultResponse(tickerID);
|
||||
}
|
||||
|
||||
try {
|
||||
const endpoints = [
|
||||
"/index-profile",
|
||||
"/etf-holdings",
|
||||
"/etf-sector-weighting",
|
||||
"/stock-quote",
|
||||
"/pre-post-quote",
|
||||
"/wiim",
|
||||
"/one-day-price",
|
||||
"/stock-news",
|
||||
];
|
||||
|
||||
const promises = endpoints.map((endpoint) => {
|
||||
// Use SPY for specific endpoints when tickerID is ^SPC or ^spc
|
||||
const useSpyTicker = tickerID?.toLowerCase() === "^spx" &&
|
||||
["/etf-holdings", "/etf-sector-weighting", "/wiim", "/stock-news"]?.includes(endpoint);
|
||||
return fetchData(apiURL, apiKey, endpoint, useSpyTicker ? "SPY" : tickerID);
|
||||
});
|
||||
|
||||
// Add watchlist promise
|
||||
const promises = ENDPOINTS.map(endpoint =>
|
||||
fetchData(apiURL, apiKey, endpoint, tickerID)
|
||||
);
|
||||
promises.push(fetchWatchlist(pb, user?.id));
|
||||
|
||||
const [
|
||||
@ -102,22 +197,10 @@ export const load = async ({ params, locals }) => {
|
||||
getNews: getNews || [],
|
||||
getUserWatchlist: getUserWatchlist || [],
|
||||
companyName: cleanString(getIndexProfile?.at(0)?.name),
|
||||
getParams: params.tickerID,
|
||||
getParams: tickerID
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error in load function:', error);
|
||||
return {
|
||||
getIndexProfile: [],
|
||||
getIndexHolding: [],
|
||||
getIndexSectorWeighting: [],
|
||||
getStockQuote: [],
|
||||
getPrePostQuote: [],
|
||||
getWhyPriceMoved: [],
|
||||
getOneDayPrice: [],
|
||||
getNews: [],
|
||||
getUserWatchlist: [],
|
||||
companyName: '',
|
||||
getParams: params.tickerID,
|
||||
};
|
||||
return getDefaultResponse(tickerID);
|
||||
}
|
||||
};
|
||||
@ -32,14 +32,20 @@ const cleanString = (() => {
|
||||
const CACHE_DURATION = 5 * 60 * 1000;
|
||||
const REQUEST_TIMEOUT = 5000;
|
||||
const ENDPOINTS = Object.freeze([
|
||||
"/stockdeck",
|
||||
"/analyst-summary-rating",
|
||||
"/index-profile",
|
||||
"/etf-holdings",
|
||||
"/etf-sector-weighting",
|
||||
"/stock-quote",
|
||||
"/pre-post-quote",
|
||||
"/wiim",
|
||||
"/one-day-price",
|
||||
"/next-earnings",
|
||||
"/earnings-surprise",
|
||||
"/stock-news"
|
||||
]);
|
||||
|
||||
const SPY_PROXY_ENDPOINTS = Object.freeze([
|
||||
"/etf-holdings",
|
||||
"/etf-sector-weighting",
|
||||
"/wiim",
|
||||
"/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 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);
|
||||
if (cachedData) return cachedData;
|
||||
|
||||
@ -100,7 +110,10 @@ const fetchData = async (apiURL, apiKey, endpoint, ticker) => {
|
||||
"Content-Type": "application/json",
|
||||
"X-API-KEY": apiKey
|
||||
},
|
||||
body: JSON.stringify({ ticker, endpoints: ENDPOINTS })
|
||||
body: JSON.stringify({
|
||||
ticker: effectiveTicker,
|
||||
endpoints: ENDPOINTS
|
||||
})
|
||||
};
|
||||
|
||||
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
|
||||
export const load = async ({ params, locals }) => {
|
||||
const { apiURL, apiKey, pb, user } = locals;
|
||||
const { tickerID } = params;
|
||||
|
||||
if (!tickerID) {
|
||||
return { error: 'Invalid ticker ID' };
|
||||
return getDefaultResponse(tickerID);
|
||||
}
|
||||
|
||||
try {
|
||||
// Fetch data in parallel
|
||||
const [stockData, userWatchlist] = await Promise.all([
|
||||
const [indexData, userWatchlist] = await Promise.all([
|
||||
fetchData(apiURL, apiKey, "/bulk-data", tickerID),
|
||||
fetchWatchlist(pb, user?.id)
|
||||
]);
|
||||
|
||||
// Destructure with default empty object to prevent undefined errors
|
||||
// Destructure with default empty arrays 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;
|
||||
'/index-profile': getIndexProfile = [],
|
||||
'/etf-holdings': getIndexHolding = [],
|
||||
'/etf-sector-weighting': getIndexSectorWeighting = [],
|
||||
'/stock-quote': getStockQuote = [],
|
||||
'/pre-post-quote': getPrePostQuote = [],
|
||||
'/wiim': getWhyPriceMoved = [],
|
||||
'/one-day-price': getOneDayPrice = [],
|
||||
'/stock-news': getNews = []
|
||||
} = indexData;
|
||||
|
||||
return {
|
||||
getStockDeck,
|
||||
getAnalystSummary,
|
||||
getIndexProfile,
|
||||
getIndexHolding,
|
||||
getIndexSectorWeighting,
|
||||
getStockQuote,
|
||||
getPrePostQuote,
|
||||
getWhyPriceMoved,
|
||||
getOneDayPrice,
|
||||
getNextEarnings,
|
||||
getEarningsSurprise,
|
||||
getNews,
|
||||
getUserWatchlist: userWatchlist,
|
||||
companyName: cleanString(getStockDeck?.companyName),
|
||||
companyName: cleanString(getIndexProfile?.at(0)?.name),
|
||||
getParams: tickerID
|
||||
};
|
||||
} catch (error) {
|
||||
return { error: 'Failed to load stock data' };
|
||||
console.error('Error in load function:', error);
|
||||
return getDefaultResponse(tickerID);
|
||||
}
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user