add websocket to Table component
This commit is contained in:
parent
1ba7927711
commit
d2bddf9c3c
@ -1,7 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { screenWidth } from "$lib/store";
|
import { screenWidth, isOpen } from "$lib/store";
|
||||||
import { abbreviateNumber } from "$lib/utils";
|
import {
|
||||||
import { onMount } from "svelte";
|
abbreviateNumber,
|
||||||
|
calculateChange,
|
||||||
|
updateStockList,
|
||||||
|
} from "$lib/utils";
|
||||||
|
import { onMount, afterUpdate, onDestroy } from "svelte";
|
||||||
import * as DropdownMenu from "$lib/components/shadcn/dropdown-menu/index.js";
|
import * as DropdownMenu from "$lib/components/shadcn/dropdown-menu/index.js";
|
||||||
import { Button } from "$lib/components/shadcn/button/index.js";
|
import { Button } from "$lib/components/shadcn/button/index.js";
|
||||||
import HoverStockChart from "$lib/components/HoverStockChart.svelte";
|
import HoverStockChart from "$lib/components/HoverStockChart.svelte";
|
||||||
@ -20,7 +24,6 @@
|
|||||||
"eps",
|
"eps",
|
||||||
"marketCap",
|
"marketCap",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export let specificRows = [];
|
export let specificRows = [];
|
||||||
|
|
||||||
export let defaultList = [
|
export let defaultList = [
|
||||||
@ -31,9 +34,10 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
export let hideLastRow = false;
|
export let hideLastRow = false;
|
||||||
|
|
||||||
let originalData = [...rawData]; // Unaltered copy of raw data
|
let originalData = [...rawData]; // Unaltered copy of raw data
|
||||||
let ruleOfList = defaultList;
|
let ruleOfList = defaultList;
|
||||||
|
let socket;
|
||||||
|
|
||||||
const defaultRules = defaultList?.map((item) => item?.rule);
|
const defaultRules = defaultList?.map((item) => item?.rule);
|
||||||
|
|
||||||
let pagePathName = $page?.url?.pathname;
|
let pagePathName = $page?.url?.pathname;
|
||||||
@ -318,9 +322,63 @@
|
|||||||
stockList = [...stockList, ...filteredNewResults];
|
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 () => {
|
onMount(async () => {
|
||||||
// Initialize the download worker if not already done
|
// Initialize the download worker if not already done
|
||||||
|
if ($isOpen) {
|
||||||
|
await websocketRealtimeData();
|
||||||
|
console.log("WebSocket restarted due to watchlist changes");
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const savedRules = localStorage?.getItem(pagePathName);
|
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 to generate columns based on keys in rawData
|
||||||
function generateColumns(data) {
|
function generateColumns(data) {
|
||||||
const leftAlignKeys = new Set(["rank", "symbol", "name"]);
|
const leftAlignKeys = new Set(["rank", "symbol", "name"]);
|
||||||
@ -723,7 +834,27 @@
|
|||||||
{:else if column?.type === "decimal"}
|
{:else if column?.type === "decimal"}
|
||||||
{item[column.key]?.toLocaleString("en-US")}
|
{item[column.key]?.toLocaleString("en-US")}
|
||||||
{:else if column.key === "price"}
|
{: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"}
|
{:else if column.type === "percent"}
|
||||||
{item[column.key]?.toFixed(2) + "%"}
|
{item[column.key]?.toFixed(2) + "%"}
|
||||||
{:else if column.type === "percentSign"}
|
{:else if column.type === "percentSign"}
|
||||||
@ -785,3 +916,25 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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>
|
||||||
|
|||||||
@ -18,6 +18,58 @@ type FlyAndScaleParams = {
|
|||||||
duration?: number;
|
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 = (
|
export const flyAndScale = (
|
||||||
node: Element,
|
node: Element,
|
||||||
params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 0 },
|
params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 0 },
|
||||||
|
|||||||
@ -29,6 +29,10 @@
|
|||||||
loginData,
|
loginData,
|
||||||
numberOfUnreadNotification,
|
numberOfUnreadNotification,
|
||||||
clientSideCache,
|
clientSideCache,
|
||||||
|
isOpen,
|
||||||
|
isAfterMarketClose,
|
||||||
|
isBeforeMarketOpen,
|
||||||
|
isWeekend,
|
||||||
} from "$lib/store";
|
} from "$lib/store";
|
||||||
|
|
||||||
import { Button } from "$lib/components/shadcn/button/index.ts";
|
import { Button } from "$lib/components/shadcn/button/index.ts";
|
||||||
@ -127,6 +131,7 @@
|
|||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
//await fallbackWorker();
|
//await fallbackWorker();
|
||||||
|
await checkMarketHour();
|
||||||
await loadWorker();
|
await loadWorker();
|
||||||
//await pushNotification()
|
//await pushNotification()
|
||||||
|
|
||||||
@ -175,6 +180,50 @@
|
|||||||
$clientSideCache[$etfTicker] = {};
|
$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>
|
</script>
|
||||||
|
|
||||||
<svelte:window bind:innerWidth={$screenWidth} />
|
<svelte:window bind:innerWidth={$screenWidth} />
|
||||||
|
|||||||
@ -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();
|
|
||||||
};
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { screenWidth, numberOfUnreadNotification, isOpen } from "$lib/store";
|
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 toast from "svelte-french-toast";
|
||||||
import { onMount, onDestroy, afterUpdate } from "svelte";
|
import { onMount, onDestroy, afterUpdate } from "svelte";
|
||||||
import Input from "$lib/components/Input.svelte";
|
import Input from "$lib/components/Input.svelte";
|
||||||
@ -136,31 +136,6 @@
|
|||||||
let displayWatchList;
|
let displayWatchList;
|
||||||
let allList = data?.getAllWatchlist;
|
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) => {
|
const handleDownloadMessage = (event) => {
|
||||||
isLoaded = false;
|
isLoaded = false;
|
||||||
watchList = event?.data?.watchlistData ?? [];
|
watchList = event?.data?.watchlistData ?? [];
|
||||||
@ -1281,7 +1256,13 @@
|
|||||||
>
|
>
|
||||||
{#if item?.previous !== null && item?.previous !== undefined && item?.previous !== item[row?.rule] && row?.rule === "price"}
|
{#if item?.previous !== null && item?.previous !== undefined && item?.previous !== item[row?.rule] && row?.rule === "price"}
|
||||||
<span
|
<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
|
<span
|
||||||
class="inline-flex rounded-full h-1 w-1 {item?.previous >
|
class="inline-flex rounded-full h-1 w-1 {item?.previous >
|
||||||
@ -1291,6 +1272,7 @@
|
|||||||
></span>
|
></span>
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{item[row?.rule] !== null
|
{item[row?.rule] !== null
|
||||||
? item[row?.rule]?.toFixed(2)
|
? item[row?.rule]?.toFixed(2)
|
||||||
: "-"}
|
: "-"}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user