fix(price): delay empty-quote UI until settle window
Avoid flashing no-quote too early while waiting for live/SSE price after first empty response. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+62
-2
@@ -19,10 +19,19 @@ export const price = ref<Price>()
|
||||
export const lastPrice = ref<Price>()
|
||||
/** حداقل یک بار تلاش برای گرفتن مظنه تمام شده (موفق یا ناموفق) */
|
||||
export const lastPriceReady = ref(false)
|
||||
/**
|
||||
* بعد از اولین پاسخ خالی/no_quote، چند لحظه صبر میکنیم تا تیک SSE/زنده برسد
|
||||
* و «مظنه در دسترس نیست» زودهنگام نشان داده نشود.
|
||||
*/
|
||||
export const lastPriceSettleGeneration = ref(0)
|
||||
export const olderPrice = ref<number | null>(null)
|
||||
|
||||
const QUOTE_ABSENCE_SETTLE_MS = 2500
|
||||
|
||||
let lastPriceSourceAt = 0
|
||||
let consecutivePriceFetchFailures = 0
|
||||
let lastPriceReadyAt = 0
|
||||
let quoteSettleTimer: ReturnType<typeof setTimeout> | undefined
|
||||
|
||||
/** buy/sell نمایش و معامله — همان buy/sell/price_last از /api/price/last */
|
||||
export function getTradeBuyPrice(p?: Price | null) {
|
||||
@@ -50,6 +59,49 @@ function hasValidPricePayload(data?: Price | null): boolean {
|
||||
)
|
||||
}
|
||||
|
||||
function clearQuoteSettleTimer() {
|
||||
if (quoteSettleTimer) {
|
||||
clearTimeout(quoteSettleTimer)
|
||||
quoteSettleTimer = undefined
|
||||
}
|
||||
}
|
||||
|
||||
function bumpQuoteSettle() {
|
||||
lastPriceSettleGeneration.value += 1
|
||||
}
|
||||
|
||||
function scheduleQuoteAbsenceSettle() {
|
||||
clearQuoteSettleTimer()
|
||||
if (hasValidPricePayload(lastPrice.value) || lastPrice.value?.status === 'closed') {
|
||||
bumpQuoteSettle()
|
||||
return
|
||||
}
|
||||
const elapsed = lastPriceReadyAt > 0 ? Date.now() - lastPriceReadyAt : 0
|
||||
const wait = Math.max(0, QUOTE_ABSENCE_SETTLE_MS - elapsed)
|
||||
quoteSettleTimer = setTimeout(() => {
|
||||
quoteSettleTimer = undefined
|
||||
bumpQuoteSettle()
|
||||
}, wait)
|
||||
}
|
||||
|
||||
function markLastPriceReady() {
|
||||
if (!lastPriceReady.value) {
|
||||
lastPriceReadyAt = Date.now()
|
||||
}
|
||||
lastPriceReady.value = true
|
||||
scheduleQuoteAbsenceSettle()
|
||||
}
|
||||
|
||||
/** قیمت آمده، بسته قطعی است، یا پنجرهٔ انتظارِ مظنه خالی تمام شده */
|
||||
export function isLastPriceAbsenceSettled(): boolean {
|
||||
void lastPriceSettleGeneration.value
|
||||
if (!lastPriceReady.value) return false
|
||||
if (hasValidPricePayload(lastPrice.value)) return true
|
||||
if (lastPrice.value?.status === 'closed') return true
|
||||
if (lastPriceReadyAt <= 0) return false
|
||||
return Date.now() - lastPriceReadyAt >= QUOTE_ABSENCE_SETTLE_MS
|
||||
}
|
||||
|
||||
function applyTradingHoursSettingsPatch(data: unknown) {
|
||||
if (!data || typeof data !== 'object') {
|
||||
return
|
||||
@@ -183,7 +235,7 @@ export async function getLastPrices(): Promise<boolean> {
|
||||
markPriceSourceFetchFailed()
|
||||
return false
|
||||
} finally {
|
||||
lastPriceReady.value = true
|
||||
markLastPriceReady()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,13 +244,21 @@ export function applyLastPricePayload(dataLast: Price) {
|
||||
const normalized = normalizePriceResponse(applyBoardSpecialToLastPayload(dataLast as any) as Price)
|
||||
const nextBuy = getTradeBuyPrice(normalized)
|
||||
const prevBuy = getTradeBuyPrice(lastPrice.value)
|
||||
const nextValid = hasValidPricePayload(normalized)
|
||||
const prevValid = hasValidPricePayload(lastPrice.value)
|
||||
|
||||
// قطع فید / no_quote خالی — قیمت فعلی را پاک نکن؛ فقط closed قطعی اجازهٔ صفر شدن دارد
|
||||
if (!nextValid && prevValid && normalized.status !== 'closed') {
|
||||
markLastPriceReady()
|
||||
return
|
||||
}
|
||||
|
||||
if (prevBuy > 0 && nextBuy > 0 && prevBuy !== nextBuy) {
|
||||
olderPrice.value = prevBuy
|
||||
}
|
||||
|
||||
lastPrice.value = normalized
|
||||
lastPriceReady.value = true
|
||||
markLastPriceReady()
|
||||
price.value = {
|
||||
...normalized,
|
||||
buy: nextBuy,
|
||||
|
||||
@@ -426,9 +426,14 @@ function handleLastPayload(payload: unknown) {
|
||||
|
||||
const raw = payload as Price & { settings_patch?: unknown }
|
||||
const last = applyBoardSpecialToLastPayload(raw)
|
||||
const hasQuote = last.price_last != null || last.price != null || last.buy != null
|
||||
const hasDisplayable =
|
||||
Number(last.buy ?? 0) > 0
|
||||
|| Number(last.sell ?? 0) > 0
|
||||
|| Number(last.price_last ?? 0) > 0
|
||||
|| Number(last.price ?? 0) > 0
|
||||
const hasStatus = last.status != null || typeof last.closed === 'boolean'
|
||||
if (!hasQuote && !hasStatus) {
|
||||
|
||||
if (!hasDisplayable && !hasStatus) {
|
||||
if (Array.isArray(raw.settings_patch) && raw.settings_patch.length) {
|
||||
touchStreamActivity()
|
||||
void applySettingsFromStream(raw.settings_patch)
|
||||
@@ -436,6 +441,22 @@ function handleLastPayload(payload: unknown) {
|
||||
return
|
||||
}
|
||||
|
||||
// SSE گاهی last خالی/no_quote میفرستد؛ قیمت معتبر فعلی را پاک نکن
|
||||
if (!hasDisplayable) {
|
||||
if (Array.isArray(raw.settings_patch) && raw.settings_patch.length) {
|
||||
touchStreamActivity()
|
||||
void applySettingsFromStream(raw.settings_patch)
|
||||
}
|
||||
void import('@/utils/price').then(({ getTradeBuyPrice, lastPrice }) => {
|
||||
const keepCurrent =
|
||||
getTradeBuyPrice(lastPrice.value) > 0 && last.status !== 'closed'
|
||||
touchStreamActivity()
|
||||
if (keepCurrent) return
|
||||
void applyLastFromStream(last)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
touchStreamActivity()
|
||||
void applyLastFromStream(last)
|
||||
}
|
||||
|
||||
+15
-4
@@ -1,6 +1,6 @@
|
||||
import { ref } from 'vue'
|
||||
import { useSettingsStore } from '@/store/settingsStore'
|
||||
import { lastPrice, lastPriceReady } from '@/utils/price'
|
||||
import { isLastPriceAbsenceSettled, lastPrice, lastPriceReady } from '@/utils/price'
|
||||
|
||||
export interface StoreTradingHours {
|
||||
start: string
|
||||
@@ -299,7 +299,10 @@ export function getPriceDisplayState(): PriceDisplayState {
|
||||
const p = lastPrice.value
|
||||
|
||||
const serverStatus = p?.status
|
||||
if (serverStatus === 'open' || serverStatus === 'closed' || serverStatus === 'no_quote') {
|
||||
// اگر عدد قیمت هست، no_quote لحظهای سرور را جدی نگیر (race با تیک بعدی)
|
||||
if (serverStatus === 'no_quote' && hasShowableHomePrice(p)) {
|
||||
// fall through به منطق کلاینت
|
||||
} else if (serverStatus === 'open' || serverStatus === 'closed' || serverStatus === 'no_quote') {
|
||||
return serverStatus
|
||||
}
|
||||
|
||||
@@ -310,19 +313,27 @@ export function getPriceDisplayState(): PriceDisplayState {
|
||||
|
||||
if (hasLiveQuote(p)) return 'open'
|
||||
if (shouldShowLastSavedPrice() && hasShowableHomePrice(p)) return 'open'
|
||||
if (hasShowableHomePrice(p)) return 'open'
|
||||
|
||||
return 'no_quote'
|
||||
}
|
||||
|
||||
/** اولین دریافت مظنه / تنظیمات هنوز تمام نشده — اسکلتون، نه «مظنه در دسترس نیست» */
|
||||
/**
|
||||
* اسکلتون تا وقتی قیمت قابلنمایش برسد، یا وضعیت بسته قطعی شود،
|
||||
* یا بعد از اولین پاسخ خالی پنجرهٔ settle تمام شود (مظنه واقعاً نیست).
|
||||
*/
|
||||
export function isHomeQuotePending(): boolean {
|
||||
const settings = useSettingsStore()
|
||||
void lastPriceReady.value
|
||||
void lastPrice.value
|
||||
void isLastPriceAbsenceSettled()
|
||||
void settings.loaded
|
||||
void settings.settingsRevision
|
||||
|
||||
return !settings.loaded || !lastPriceReady.value
|
||||
if (!settings.loaded || !lastPriceReady.value) return true
|
||||
if (hasShowableHomePrice(lastPrice.value)) return false
|
||||
if (lastPrice.value?.status === 'closed') return false
|
||||
return !isLastPriceAbsenceSettled()
|
||||
}
|
||||
|
||||
export function getPriceDisplayMessage(state: PriceDisplayState = getPriceDisplayState()): string {
|
||||
|
||||
Reference in New Issue
Block a user