Open the price stream on the TV board with the TV key header.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-27 22:27:46 +03:30
co-authored by Cursor
parent 8083c4f06d
commit bc64ff47b2
4 changed files with 44 additions and 8 deletions
+1
View File
@@ -15,6 +15,7 @@ export type TvServerTime = {
unix: number
iso: string
timezone: string
stream_key?: string
}
export const getTvServerTime = async (): Promise<TvServerTime | null> => {
+24 -5
View File
@@ -44,9 +44,9 @@ class AuthPriceStream extends EventTarget {
onmessage: (() => void) | null = null
private abort = new AbortController()
constructor(url: string) {
constructor(url: string, tvKey = '') {
super()
void this.start(url)
void this.start(url, tvKey)
}
close() {
@@ -54,10 +54,12 @@ class AuthPriceStream extends EventTarget {
this.abort.abort()
}
private async start(url: string) {
private async start(url: string, tvKey = '') {
try {
const headers: Record<string, string> = { Accept: 'text/event-stream' }
if (tvKey) headers['X-Tv-Key'] = tvKey
const response = await fetch(url, {
headers: { Accept: 'text/event-stream' },
headers,
cache: 'no-store',
signal: this.abort.signal,
})
@@ -119,6 +121,7 @@ class AuthPriceStream extends EventTarget {
let eventSource: AuthPriceStream | null = null
let subscribers = 0
let tvStreamKey: string | null = null
let stopped = false
let getSetting: ((key: string) => unknown) | null = null
let reconnectTimer: ReturnType<typeof setTimeout> | undefined
@@ -655,7 +658,7 @@ function connect() {
markConnectingLifecycle()
try {
eventSource = new AuthPriceStream(url)
eventSource = new AuthPriceStream(url, tvStreamKey || '')
} catch (error) {
console.warn('Price SSE connect failed', error)
handleStreamLost()
@@ -740,6 +743,22 @@ function unbindBrowserNetworkListeners() {
window.removeEventListener('offline', onBrowserOffline)
}
export function startTvPriceStream(
settingGetter: (key: string) => unknown,
key: string,
) {
const secret = key.trim()
if (!secret) return
tvStreamKey = secret
startPriceSocket(settingGetter)
}
export function stopTvPriceStream() {
if (!tvStreamKey) return
tvStreamKey = null
stopPriceSocket()
}
export function startPriceSocket(settingGetter: (key: string) => unknown) {
getSetting = settingGetter
subscribers += 1
+17 -1
View File
@@ -10,6 +10,7 @@ import { usePriceWithoutType } from '@/utils/usePrice'
import { getStoreClosedMessage, isStoreClosedBySchedule, NO_QUOTE_MESSAGE } from '@/utils/storeHours'
import { isMeltedTradeGroup, isMetalCommodityGroup } from '@/utils/productCategories'
import { priceSocketConnected, priceStreamLifecycle } from '@/utils/priceSocketState'
import { startTvPriceStream, stopTvPriceStream } from '@/utils/priceSocket'
import logoFallback from '@/assets/img/logo_full_.png'
//@ts-ignore
import moment from 'moment-jalaali'
@@ -21,7 +22,7 @@ moment.loadPersian({ usePersianDigits: false })
const CURSOR_HIDE_MS = 2500
const TV_HTML_CLASS = 'app-layout--tv'
const REFRESH_MS = 3000
const REFRESH_MS = 15000
/** دقت تابلو: هزار تومان = ۱۰٬۰۰۰ ریال — باقی‌مانده به پایین حذف می‌شود */
const TV_TOMAN_STEP = 1000
@@ -121,6 +122,14 @@ export function useTvMonitorPage() {
let listTimer: ReturnType<typeof setInterval> | undefined
/** اختلاف میلی‌ثانیهٔ ساعت سرور (Asia/Tehran) با Date.now محلی */
let serverOffsetMs = 0
let tvStreamReady = false
function openTvStream(key: string) {
const secret = key.trim()
if (!secret || tvStreamReady) return
tvStreamReady = true
startTvPriceStream((settingKey) => settingsStore.getSettingByKey(settingKey), secret)
}
const brandName = computed(
() => String(getConfigApp('name_fa') || getConfigApp('name') || 'تابلو قیمت'),
@@ -150,6 +159,7 @@ export function useTvMonitorPage() {
if (!payload?.unix) return
const rttHalf = (t1 - t0) / 2
serverOffsetMs = payload.unix * 1000 + rttHalf - t1
if (payload.stream_key) openTvStream(payload.stream_key)
}
const parsianCoins = computed(() =>
@@ -246,7 +256,11 @@ export function useTvMonitorPage() {
() => priceSocketConnected.value || priceStreamLifecycle.value === 'open',
)
let productsInFlight = false
async function loadProducts() {
if (productsInFlight) return
productsInFlight = true
try {
const list = await getTvProducts()
products.value = (Array.isArray(list) ? list : []).map(normalizeTvProduct)
@@ -255,6 +269,7 @@ export function useTvMonitorPage() {
products.value = []
}
} finally {
productsInFlight = false
loading.value = false
}
}
@@ -390,6 +405,7 @@ export function useTvMonitorPage() {
document.removeEventListener('visibilitychange', onVisibilityChange)
void releaseWakeLock()
stopLastPricePolling()
stopTvPriceStream()
})
return {
+2 -2
View File
@@ -20,12 +20,12 @@
<div class="tv-monitor__quote-prices">
<div class="tv-monitor__quote-item tv-monitor__quote-item--sell">
<span>فروش</span>
<strong :class="{ 'is-empty': !mainQuoteReady }">{{ liveQuoteLabel(liveSell) }}</strong>
<strong :class="{ 'is-empty': !mainQuoteReady }">{{ liveQuoteLabel(liveBuy) }}</strong>
<em v-if="mainQuoteReady">تومان</em>
</div>
<div class="tv-monitor__quote-item tv-monitor__quote-item--buy">
<span>خرید</span>
<strong :class="{ 'is-empty': !mainQuoteReady }">{{ liveQuoteLabel(liveBuy) }}</strong>
<strong :class="{ 'is-empty': !mainQuoteReady }">{{ liveQuoteLabel(liveSell) }}</strong>
<em v-if="mainQuoteReady">تومان</em>
</div>
</div>