Files
boursecu_user/src/utils/price.ts
T
mohmdsdgandCursor 5ffd246f61 Improve SSE reconnect on mobile and show price alert only on real outage.
Track price source freshness so the modal appears when SSE and API both fail, not on transient mobile reconnects.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-27 16:54:16 +03:30

192 lines
4.7 KiB
TypeScript

import { getLastPrice, getPrice } from '@/api/home'
import type { Price, PriceUp } from '@/models/home'
import { ref, watch } from 'vue'
import {
priceSocketConnected,
priceStreamLifecycle,
priceWssEnabled,
shouldUsePricePolling,
} from '@/utils/priceSocketState'
const LAST_INTERVAL_MS = 10000
let lastInterval: ReturnType<typeof setInterval> | undefined
let lastSubscribers = 0
/** @deprecated از lastPrice استفاده کنید — برای سازگاری با کد قدیمی sync می‌شود */
export const price = ref<Price>()
export const lastPrice = ref<Price>()
export const olderPrice = ref<number | undefined>(0)
let lastPriceSourceAt = 0
let consecutivePriceFetchFailures = 0
function hasValidPricePayload(data?: Price | null): boolean {
if (!data) {
return false
}
return (
getTradeBuyPrice(data) > 0
|| getTradeSellPrice(data) > 0
|| Number(data.price_last ?? data.price ?? 0) > 0
)
}
export function markPriceSourceReceived() {
lastPriceSourceAt = Date.now()
consecutivePriceFetchFailures = 0
}
export function markPriceSourceFetchFailed() {
consecutivePriceFetchFailures += 1
}
export function hasRecentPriceFromSource(maxAgeMs = 45000): boolean {
return lastPriceSourceAt > 0 && Date.now() - lastPriceSourceAt < maxAgeMs
}
export function consecutivePriceSourceFailures(): number {
return consecutivePriceFetchFailures
}
/** buy/sell نمایش و معامله — همان buy/sell/price_last از /api/price/last */
export function getTradeBuyPrice(p?: Price | null) {
return p?.buy ?? p?.price_last ?? p?.up?.buy ?? 0
}
export function getTradeSellPrice(p?: Price | null) {
return p?.sell ?? p?.up?.sell ?? 0
}
/** up جدا نگه داشته می‌شود؛ buy/sell/price_last از lastPrice سرور دست‌نخورده می‌ماند */
export function normalizePriceResponse(data: Price): Price {
return data
}
function startPollingInterval() {
if (lastInterval || !shouldUsePricePolling()) {
return
}
lastInterval = setInterval(() => {
if (shouldUsePricePolling()) {
void getLastPrices()
}
}, LAST_INTERVAL_MS)
}
function stopPollingInterval() {
if (lastInterval) {
clearInterval(lastInterval)
lastInterval = undefined
}
}
function syncLastPricePolling() {
const usePolling = shouldUsePricePolling()
if (usePolling) {
if (lastSubscribers > 0) {
startPollingInterval()
}
return
}
stopPollingInterval()
}
watch(
() => [priceWssEnabled.value, priceSocketConnected.value, priceStreamLifecycle.value] as const,
() => {
syncLastPricePolling()
},
)
/** polling /api/price/last — وقتی wss خاموش است یا ws قطع شده */
export function startLastPricePolling() {
lastSubscribers++
syncLastPricePolling()
}
/** @deprecated از startLastPricePolling استفاده کنید */
export async function setIntervalGetAllPrice() {
startLastPricePolling()
}
/** @deprecated از startLastPricePolling استفاده کنید */
export async function setIntervalGetLastPrice() {
startLastPricePolling()
}
export function stopLastPricePolling() {
lastSubscribers = Math.max(0, lastSubscribers - 1)
if (lastSubscribers === 0) {
stopPollingInterval()
}
}
/** @deprecated از stopLastPricePolling استفاده کنید */
export function stopPriceInterval() {
stopLastPricePolling()
}
/** @deprecated فقط getLastPrices را صدا بزنید */
export async function getUpPrice() {
await getLastPrices()
}
async function fetchUpPrices(): Promise<PriceUp | undefined> {
try {
const up = await getPrice()
if (up?.buy != null && up?.sell != null) {
return { buy: up.buy, sell: up.sell }
}
} catch (e) {
console.log(e)
}
}
export async function getLastPrices(): Promise<boolean> {
try {
let dataLast = await getLastPrice()
if (!dataLast?.up) {
const up = await fetchUpPrices()
if (up) {
dataLast = { ...dataLast, up }
}
}
applyLastPricePayload(dataLast)
return hasValidPricePayload(dataLast)
} catch (e) {
console.log(e)
markPriceSourceFetchFailed()
return false
}
}
export function applyLastPricePayload(dataLast: Price) {
const normalized = normalizePriceResponse(dataLast)
if (lastPrice.value?.price_last != normalized.price_last) {
olderPrice.value = lastPrice.value?.price_last
}
lastPrice.value = normalized
price.value = {
...normalized,
buy: getTradeBuyPrice(normalized),
sell: getTradeSellPrice(normalized),
}
if (hasValidPricePayload(normalized)) {
markPriceSourceReceived()
}
}
export function percentPriceChanged() {
if (!olderPrice.value) {
return 0
}
return ((lastPrice.value?.price_last! - olderPrice.value) * 100) / olderPrice.value
}