fix tv
This commit is contained in:
@@ -50,10 +50,18 @@ function hasValidPricePayload(data?: Price | null): boolean {
|
||||
export function markPriceSourceReceived() {
|
||||
lastPriceSourceAt = Date.now()
|
||||
consecutivePriceFetchFailures = 0
|
||||
void import('@/utils/priceSocket').then(({ clearPriceNetworkAlert }) => {
|
||||
clearPriceNetworkAlert()
|
||||
})
|
||||
}
|
||||
|
||||
export function markPriceSourceFetchFailed() {
|
||||
consecutivePriceFetchFailures += 1
|
||||
if (consecutivePriceFetchFailures >= 2) {
|
||||
void import('@/utils/priceSocket').then(({ notifyPriceLastFetchFailed }) => {
|
||||
notifyPriceLastFetchFailed()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function hasRecentPriceFromSource(maxAgeMs = 45000): boolean {
|
||||
|
||||
+111
-37
@@ -24,15 +24,99 @@ export {
|
||||
|
||||
const RECONNECT_BASE_MS = 3000
|
||||
const RECONNECT_MAX_MS = 30000
|
||||
const FAILURES_BEFORE_ALERT = 4
|
||||
/** چند بار خطای پشتسرهم /api/price/last قبل از نمایش مدال */
|
||||
const LAST_FETCH_FAILURES_BEFORE_ALERT = 2
|
||||
const ALERT_COOLDOWN_MS = 5 * 60 * 1000
|
||||
const FALLBACK_POLL_INTERVAL_MS = 10000
|
||||
const PRICE_SOURCE_STALE_MS_MOBILE = 50000
|
||||
const PRICE_SOURCE_STALE_MS_DESKTOP = 35000
|
||||
const STALE_STREAM_MS = 120000
|
||||
const FALLBACK_POLL_DELAY_MS = 2500
|
||||
const STREAM_CONNECTING = 0
|
||||
const STREAM_OPEN = 1
|
||||
const STREAM_CLOSED = 2
|
||||
|
||||
let eventSource: EventSource | null = null
|
||||
class AuthPriceStream extends EventTarget {
|
||||
readyState = STREAM_CONNECTING
|
||||
onopen: (() => void) | null = null
|
||||
onerror: (() => void) | null = null
|
||||
onmessage: (() => void) | null = null
|
||||
private abort = new AbortController()
|
||||
|
||||
constructor(url: string) {
|
||||
super()
|
||||
void this.start(url)
|
||||
}
|
||||
|
||||
close() {
|
||||
this.readyState = STREAM_CLOSED
|
||||
this.abort.abort()
|
||||
}
|
||||
|
||||
private async start(url: string) {
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
headers: { Accept: 'text/event-stream' },
|
||||
cache: 'no-store',
|
||||
signal: this.abort.signal,
|
||||
})
|
||||
|
||||
if (!response.ok || !response.body) {
|
||||
this.readyState = STREAM_CLOSED
|
||||
this.onerror?.()
|
||||
return
|
||||
}
|
||||
|
||||
this.readyState = STREAM_OPEN
|
||||
this.onopen?.()
|
||||
|
||||
const reader = response.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let buffer = ''
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) {
|
||||
break
|
||||
}
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
buffer = this.flushSse(buffer)
|
||||
}
|
||||
|
||||
this.readyState = STREAM_CLOSED
|
||||
this.onerror?.()
|
||||
} catch {
|
||||
if (this.abort.signal.aborted) {
|
||||
return
|
||||
}
|
||||
this.readyState = STREAM_CLOSED
|
||||
this.onerror?.()
|
||||
}
|
||||
}
|
||||
|
||||
private flushSse(buffer: string): string {
|
||||
const blocks = buffer.split('\n\n')
|
||||
const rest = blocks.pop() ?? ''
|
||||
|
||||
for (const block of blocks) {
|
||||
let event = 'message'
|
||||
const data: string[] = []
|
||||
for (const line of block.split('\n')) {
|
||||
if (line.startsWith('event:')) {
|
||||
event = line.slice(6).trim()
|
||||
} else if (line.startsWith('data:')) {
|
||||
data.push(line.slice(5).trimStart())
|
||||
}
|
||||
}
|
||||
this.dispatchEvent(new MessageEvent(event, { data: data.join('\n') }))
|
||||
this.onmessage?.()
|
||||
}
|
||||
|
||||
return rest
|
||||
}
|
||||
}
|
||||
|
||||
let eventSource: AuthPriceStream | null = null
|
||||
let subscribers = 0
|
||||
let stopped = false
|
||||
let getSetting: ((key: string) => unknown) | null = null
|
||||
@@ -131,7 +215,7 @@ function startStaleCheck() {
|
||||
stopped
|
||||
|| subscribers <= 0
|
||||
|| !eventSource
|
||||
|| eventSource.readyState !== EventSource.OPEN
|
||||
|| eventSource.readyState !== STREAM_OPEN
|
||||
) {
|
||||
return
|
||||
}
|
||||
@@ -184,31 +268,40 @@ function scheduleFallbackPoll() {
|
||||
}, FALLBACK_POLL_DELAY_MS)
|
||||
}
|
||||
|
||||
export function clearPriceNetworkAlert() {
|
||||
networkAlertOpen = false
|
||||
pendingPriceAlert = false
|
||||
}
|
||||
|
||||
/** وقتی /api/price/last خطا میدهد (از polling عادی یا fallback) */
|
||||
export function notifyPriceLastFetchFailed() {
|
||||
void maybeShowNetworkAlert()
|
||||
}
|
||||
|
||||
async function runFallbackPoll() {
|
||||
if (!priceWssEnabled.value || priceSocketConnected.value) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const { getLastPrices, hasRecentPriceFromSource, markPriceSourceFetchFailed } = await import(
|
||||
'@/utils/price'
|
||||
)
|
||||
const { getLastPrices, hasRecentPriceFromSource } = await import('@/utils/price')
|
||||
const { refreshProductsPageData } = await import(
|
||||
'@/views/base/products/composables/useProductsPageData'
|
||||
)
|
||||
|
||||
const [lastResult] = await Promise.allSettled([
|
||||
const [priceResult] = await Promise.allSettled([
|
||||
getLastPrices(),
|
||||
refreshProductsPageData(),
|
||||
])
|
||||
|
||||
const priceOk =
|
||||
lastResult.status === 'fulfilled'
|
||||
&& lastResult.value === true
|
||||
priceResult.status === 'fulfilled'
|
||||
&& priceResult.value === true
|
||||
&& hasRecentPriceFromSource(getPriceSourceStaleMs())
|
||||
|
||||
if (!priceOk) {
|
||||
markPriceSourceFetchFailed()
|
||||
if (priceOk) {
|
||||
clearPriceNetworkAlert()
|
||||
return
|
||||
}
|
||||
|
||||
void maybeShowNetworkAlert()
|
||||
@@ -237,25 +330,7 @@ async function isRealPriceOutage(): Promise<boolean> {
|
||||
return false
|
||||
}
|
||||
|
||||
const fetchFailures = consecutivePriceSourceFailures()
|
||||
|
||||
if (fetchFailures >= 2) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!priceSocketConnected.value && fetchFailures >= 1 && reconnectAttempt >= 2) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!isMobileDevice() && !navigator.onLine && fetchFailures >= 1) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!isMobileDevice() && reconnectAttempt >= FAILURES_BEFORE_ALERT) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
return consecutivePriceSourceFailures() >= LAST_FETCH_FAILURES_BEFORE_ALERT
|
||||
}
|
||||
|
||||
function showNetworkAlert() {
|
||||
@@ -316,7 +391,6 @@ function scheduleReconnect() {
|
||||
reconnectTimer = setTimeout(() => {
|
||||
reconnectAttempt += 1
|
||||
connect()
|
||||
maybeShowNetworkAlert()
|
||||
}, delay)
|
||||
}
|
||||
|
||||
@@ -384,7 +458,7 @@ async function applyUnreadCountFromStreamPayload(payload: Record<string, unknown
|
||||
}
|
||||
}
|
||||
|
||||
function bindStreamEvents(source: EventSource) {
|
||||
function bindStreamEvents(source: AuthPriceStream) {
|
||||
source.addEventListener('last', (event) => {
|
||||
try {
|
||||
handleLastPayload(JSON.parse(event.data))
|
||||
@@ -475,7 +549,7 @@ function connect() {
|
||||
|
||||
if (eventSource) {
|
||||
const state = eventSource.readyState
|
||||
if (state === EventSource.OPEN || state === EventSource.CONNECTING) {
|
||||
if (state === STREAM_OPEN || state === STREAM_CONNECTING) {
|
||||
return
|
||||
}
|
||||
disconnectSourceOnly()
|
||||
@@ -484,7 +558,7 @@ function connect() {
|
||||
markConnectingLifecycle()
|
||||
|
||||
try {
|
||||
eventSource = new EventSource(url)
|
||||
eventSource = new AuthPriceStream(url)
|
||||
} catch (error) {
|
||||
console.warn('Price SSE connect failed', error)
|
||||
handleStreamLost()
|
||||
@@ -507,7 +581,7 @@ function connect() {
|
||||
|
||||
eventSource.onerror = () => {
|
||||
const state = eventSource?.readyState
|
||||
if (state === EventSource.OPEN || state === EventSource.CONNECTING) {
|
||||
if (state === STREAM_OPEN || state === STREAM_CONNECTING) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -621,7 +695,7 @@ function onVisibilityChange() {
|
||||
void maybeShowNetworkAlert()
|
||||
}
|
||||
|
||||
if (!eventSource || eventSource.readyState !== EventSource.OPEN) {
|
||||
if (!eventSource || eventSource.readyState !== STREAM_OPEN) {
|
||||
forceReconnect()
|
||||
}
|
||||
}
|
||||
@@ -631,7 +705,7 @@ function onPageShow(event: PageTransitionEvent) {
|
||||
return
|
||||
}
|
||||
|
||||
if (event.persisted || !eventSource || eventSource.readyState !== EventSource.OPEN) {
|
||||
if (event.persisted || !eventSource || eventSource.readyState !== STREAM_OPEN) {
|
||||
reconnectAttempt = 0
|
||||
networkAlertOpen = false
|
||||
forceReconnect()
|
||||
|
||||
Reference in New Issue
Block a user