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>
192 lines
4.7 KiB
TypeScript
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
|
|
}
|