add websocket to Table component

This commit is contained in:
MuslemRahimi 2024-11-27 21:34:30 +01:00
parent 1ba7927711
commit d2bddf9c3c
5 changed files with 270 additions and 88 deletions

View File

@ -1,7 +1,11 @@
<script lang="ts">
import { screenWidth } from "$lib/store";
import { abbreviateNumber } from "$lib/utils";
import { onMount } from "svelte";
import { screenWidth, isOpen } from "$lib/store";
import {
abbreviateNumber,
calculateChange,
updateStockList,
} from "$lib/utils";
import { onMount, afterUpdate, onDestroy } from "svelte";
import * as DropdownMenu from "$lib/components/shadcn/dropdown-menu/index.js";
import { Button } from "$lib/components/shadcn/button/index.js";
import HoverStockChart from "$lib/components/HoverStockChart.svelte";
@ -20,7 +24,6 @@
"eps",
"marketCap",
]);
export let specificRows = [];
export let defaultList = [
@ -31,9 +34,10 @@
];
export let hideLastRow = false;
let originalData = [...rawData]; // Unaltered copy of raw data
let ruleOfList = defaultList;
let socket;
const defaultRules = defaultList?.map((item) => item?.rule);
let pagePathName = $page?.url?.pathname;
@ -318,9 +322,63 @@
stockList = [...stockList, ...filteredNewResults];
}
}
function sendMessage(message) {
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(JSON?.stringify(message));
} else {
console.error("WebSocket is not open. Unable to send message.");
}
}
async function websocketRealtimeData() {
try {
socket = new WebSocket(data?.wsURL + "/multiple-realtime-data");
socket.addEventListener("open", () => {
console.log("WebSocket connection opened");
// Send only current watchlist symbols
const tickerList = rawData?.map((item) => item?.symbol) || [];
sendMessage(tickerList);
});
socket.addEventListener("message", (event) => {
const data = event.data;
try {
const newList = JSON?.parse(data);
if (newList?.length > 0) {
//console.log("Received message:", newList);
if (originalData.some((item) => "changesPercentage" in item)) {
originalData = calculateChange(originalData, newList);
stockList = updateStockList(stockList, originalData);
setTimeout(() => {
stockList = stockList?.map((item) => ({
...item,
previous: null,
}));
}, 500);
}
}
} catch (e) {
console.error("Error parsing WebSocket message:", e);
}
});
socket.addEventListener("close", (event) => {
console.log("WebSocket connection closed:", event.reason);
});
} catch (error) {
console.error("WebSocket connection error:", error);
}
}
$: stockList = [...stockList];
onMount(async () => {
// Initialize the download worker if not already done
if ($isOpen) {
await websocketRealtimeData();
console.log("WebSocket restarted due to watchlist changes");
}
try {
const savedRules = localStorage?.getItem(pagePathName);
@ -372,6 +430,59 @@
}
});
let previousList = [];
let reconnectionTimeout;
afterUpdate(async () => {
// Compare only the symbols to detect changes
const currentSymbols = rawData?.map((item) => item?.symbol).sort();
const previousSymbols = previousList?.map((item) => item?.symbol).sort();
// Check if symbols have changed
if (
JSON.stringify(currentSymbols) !== JSON.stringify(previousSymbols) &&
typeof socket !== "undefined"
) {
// Update previous list
previousList = rawData;
try {
// Close existing socket if open
if (socket && socket.readyState !== WebSocket.CLOSED) {
socket?.close();
}
// Wait for socket to close
await new Promise((resolve) => {
socket?.addEventListener("close", resolve, { once: true });
});
// Reconnect with new symbols
if ($isOpen) {
await websocketRealtimeData();
console.log("WebSocket restarted due to watchlist changes");
}
} catch (error) {
console.error("Error restarting WebSocket:", error);
}
}
});
onDestroy(() => {
try {
// Clear any pending reconnection timeout
if (reconnectionTimeout) {
clearTimeout(reconnectionTimeout);
}
// Close the WebSocket connection
if (socket) {
socket.close(1000, "Page unloaded");
}
} catch (e) {
console.log(e);
}
});
// Function to generate columns based on keys in rawData
function generateColumns(data) {
const leftAlignKeys = new Set(["rank", "symbol", "name"]);
@ -723,7 +834,27 @@
{:else if column?.type === "decimal"}
{item[column.key]?.toLocaleString("en-US")}
{:else if column.key === "price"}
{item[column.key]?.toFixed(2)}
<div class="relative flex items-center justify-end">
{#if item?.previous !== null && item?.previous !== undefined && item?.previous !== item[column?.key]}
<span
class="absolute h-1 w-1 {item[column?.key] < 10
? 'right-[35px] sm:right-[40px]'
: item[column?.key] < 100
? 'right-[40px] sm:right-[45px]'
: 'right-[45px] sm:right-[55px]'} bottom-0 -top-0.5 sm:-top-1"
>
<span
class="inline-flex rounded-full h-1 w-1 {item?.previous >
item[column?.key]
? 'bg-[#FF2F1F]'
: 'bg-[#00FC50]'} pulse-animation"
></span>
</span>
{/if}
{item[column.key] !== null
? item[column.key]?.toFixed(2)
: "-"}
</div>
{:else if column.type === "percent"}
{item[column.key]?.toFixed(2) + "%"}
{:else if column.type === "percentSign"}
@ -785,3 +916,25 @@
</tbody>
</table>
</div>
<style>
@keyframes pulse {
0% {
transform: scale(1);
opacity: 1;
}
70% {
transform: scale(1.1); /* Adjust scale as needed for pulse effect */
opacity: 0.8;
}
100% {
transform: scale(1); /* End scale */
opacity: 0;
}
}
/* Apply the animation styles to the element */
.pulse-animation {
animation: pulse 500ms ease-out forwards; /* 300ms duration */
}
</style>

View File

@ -18,6 +18,58 @@ type FlyAndScaleParams = {
duration?: number;
};
export const calculateChange = (oldList?: any[], newList?: any[]) => {
if (!oldList?.length || !newList?.length) return [...(oldList || [])];
const newListMap = new Map(newList.map(item => [item.symbol, item]));
for (let i = 0, len = oldList.length; i < len; i++) {
const item = oldList[i];
const newItem = newListMap.get(item.symbol);
if (newItem?.ap) {
const { price, changesPercentage } = item;
const newPrice = newItem.ap;
if (price != null && changesPercentage != null) {
const baseLine = price / (1 + Number(changesPercentage) / 100);
item.changesPercentage = ((newPrice / baseLine - 1) * 100);
}
item.previous = price;
item.price = newPrice;
}
}
return oldList;
};
export function updateStockList(stockList, originalData) {
// Create a Map for O(1) lookup of original data by symbol
const originalDataMap = new Map(
originalData?.map(item => [item.symbol, item])
);
// Use .map() to create a new array with updated stocks
return stockList?.map(stock => {
// Find matching stock in originalData
const matchingStock = originalDataMap?.get(stock?.symbol);
// If a match is found, update price and changesPercentage
if (matchingStock) {
return {
...stock,
price: matchingStock?.price,
changesPercentage: matchingStock?.changesPercentage,
previous: matchingStock?.previous ?? null,
};
}
// If no match, return the original stock object unchanged
return stock;
});
}
export const flyAndScale = (
node: Element,
params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 0 },

View File

@ -29,6 +29,10 @@
loginData,
numberOfUnreadNotification,
clientSideCache,
isOpen,
isAfterMarketClose,
isBeforeMarketOpen,
isWeekend,
} from "$lib/store";
import { Button } from "$lib/components/shadcn/button/index.ts";
@ -127,6 +131,7 @@
onMount(async () => {
//await fallbackWorker();
await checkMarketHour();
await loadWorker();
//await pushNotification()
@ -175,6 +180,50 @@
$clientSideCache[$etfTicker] = {};
}
}
const checkMarketHour = async () => {
const holidays = [
"2024-01-01",
"2024-01-15",
"2024-02-19",
"2024-03-29",
"2024-05-27",
"2024-06-19",
"2024-07-04",
"2024-09-02",
"2024-11-28",
"2024-12-25",
];
const currentDate = new Date().toISOString().split("T")[0];
// Get the current time in the ET time zone
const etTimeZone = "America/New_York";
const currentTime = new Date().toLocaleString("en-US", {
timeZone: etTimeZone,
});
// Determine if the NYSE is currently open or closed
const currentHour = new Date(currentTime).getHours();
const isWeekendValue =
new Date(currentTime).getDay() === 6 ||
new Date(currentTime).getDay() === 0;
const isBeforeMarketOpenValue =
currentHour < 9 ||
(currentHour === 9 && new Date(currentTime).getMinutes() < 30);
const isAfterMarketCloseValue = currentHour >= 16;
isOpen.set(
!(
isWeekendValue ||
isBeforeMarketOpenValue ||
isAfterMarketCloseValue ||
holidays?.includes(currentDate)
),
);
isWeekend.set(isWeekendValue);
isBeforeMarketOpen.set(isBeforeMarketOpenValue);
isAfterMarketClose.set(isAfterMarketCloseValue);
};
</script>
<svelte:window bind:innerWidth={$screenWidth} />

View File

@ -1,54 +0,0 @@
import {
isOpen,
isAfterMarketClose,
isBeforeMarketOpen,
isWeekend,
} from "$lib/store";
const checkMarketHour = async () => {
const holidays = [
"2024-01-01",
"2024-01-15",
"2024-02-19",
"2024-03-29",
"2024-05-27",
"2024-06-19",
"2024-07-04",
"2024-09-02",
"2024-11-28",
"2024-12-25",
];
const currentDate = new Date().toISOString().split("T")[0];
// Get the current time in the ET time zone
const etTimeZone = "America/New_York";
const currentTime = new Date().toLocaleString("en-US", {
timeZone: etTimeZone,
});
// Determine if the NYSE is currently open or closed
const currentHour = new Date(currentTime).getHours();
const isWeekendValue =
new Date(currentTime).getDay() === 6 ||
new Date(currentTime).getDay() === 0;
const isBeforeMarketOpenValue =
currentHour < 9 ||
(currentHour === 9 && new Date(currentTime).getMinutes() < 30);
const isAfterMarketCloseValue = currentHour >= 16;
isOpen.set(
!(
isWeekendValue ||
isBeforeMarketOpenValue ||
isAfterMarketCloseValue ||
holidays?.includes(currentDate)
),
);
isWeekend.set(isWeekendValue);
isBeforeMarketOpen.set(isBeforeMarketOpenValue);
isAfterMarketClose.set(isAfterMarketCloseValue);
};
export const load = async ({ params, data }) => {
await checkMarketHour();
};

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { screenWidth, numberOfUnreadNotification, isOpen } from "$lib/store";
import { formatDate, abbreviateNumber } from "$lib/utils";
import { formatDate, abbreviateNumber, calculateChange } from "$lib/utils";
import toast from "svelte-french-toast";
import { onMount, onDestroy, afterUpdate } from "svelte";
import Input from "$lib/components/Input.svelte";
@ -136,31 +136,6 @@
let displayWatchList;
let allList = data?.getAllWatchlist;
function calculateChange(oldList, newList) {
// Create a map for faster lookups
const newListMap = new Map(newList.map((item) => [item.symbol, item]));
// Use for loop instead of forEach for better performance
for (let i = 0; i < oldList?.length; i++) {
const item = oldList[i];
const newItem = newListMap?.get(item?.symbol);
if (newItem) {
// Calculate the new changePercentage
const baseLine = item?.price / (1 + item?.changesPercentage / 100);
const newPrice = newItem?.ap;
const newChangePercentage = (newPrice / baseLine - 1) * 100;
// Update the item directly in the oldList
item.previous = item.price;
item.price = newPrice;
item.changesPercentage = newChangePercentage;
}
}
return [...oldList];
}
const handleDownloadMessage = (event) => {
isLoaded = false;
watchList = event?.data?.watchlistData ?? [];
@ -1281,7 +1256,13 @@
>
{#if item?.previous !== null && item?.previous !== undefined && item?.previous !== item[row?.rule] && row?.rule === "price"}
<span
class="absolute h-1 w-1 right-12 sm:right-14 bottom-0 -top-1"
class="absolute h-1 w-1 {item[
row?.rule
] < 10
? 'right-[35px] sm:right-[40px]'
: item[row?.rule] < 100
? 'right-[40px] sm:right-[45px]'
: 'right-[45px] sm:right-[55px]'} bottom-0 -top-0.5 sm:-top-1"
>
<span
class="inline-flex rounded-full h-1 w-1 {item?.previous >
@ -1291,6 +1272,7 @@
></span>
</span>
{/if}
{item[row?.rule] !== null
? item[row?.rule]?.toFixed(2)
: "-"}