/** * embed 壳:SSE 收到后台 tick 后拉 JSON 快照更新 DOM. * 同时为 standalone/embed 提供:回前台强制刷新、SSE 假连接兜底. */ (function (global) { let liveEventSource = null; let liveReconnectTimer = null; let localLiveVersion = -1; let sseConnected = false; let lastLiveEventAt = 0; let refreshTimer = null; let pendingWhileHidden = false; /** 超过该毫秒无 live 事件则视为断流,允许 interval 兜底轮询 */ const LIVE_STALE_MS = 45000; function isEmbedShell() { return document.body && document.body.getAttribute("data-embed-shell") === "1"; } function currentTab() { if (global.InstanceEmbed && typeof global.InstanceEmbed.getTab === "function") { return global.InstanceEmbed.getTab(); } return document.body.getAttribute("data-page") || "trade"; } function refreshTabData(tab, opts) { const options = opts || {}; if (typeof global.refreshAccountSnapshot === "function") { global.refreshAccountSnapshot(options); } if (typeof global.refreshPriceSnapshotConditional === "function") { global.refreshPriceSnapshotConditional(); } if (tab === "options" && global.OptionsPanelLive && typeof global.OptionsPanelLive.refreshSoft === "function") { global.OptionsPanelLive.refreshSoft(options); } } function scheduleRefresh(opts) { if (document.hidden) { pendingWhileHidden = true; return; } if (refreshTimer) return; const options = opts || {}; refreshTimer = setTimeout(function () { refreshTimer = null; if (document.hidden) { pendingWhileHidden = true; return; } pendingWhileHidden = false; refreshTabData(currentTab(), { silent: true, force: !!options.force }); }, 80); } function kickRefresh(opts) { pendingWhileHidden = false; if (refreshTimer) { clearTimeout(refreshTimer); refreshTimer = null; } refreshTabData(currentTab(), opts || { silent: true, force: true }); } function onLiveEvent(data) { lastLiveEventAt = Date.now(); const reason = data && data.reason; const ver = Number(data && data.live_version) || 0; if (!ver) return; if (reason === "connect") { localLiveVersion = ver; scheduleRefresh(); return; } if (ver === localLiveVersion) return; localLiveVersion = ver; scheduleRefresh({ force: reason === "balance" }); } function closeLiveStream() { if (liveEventSource) { try { liveEventSource.close(); } catch (_) {} liveEventSource = null; } if (liveReconnectTimer) { clearTimeout(liveReconnectTimer); liveReconnectTimer = null; } sseConnected = false; } function connectLiveStream() { if (!isEmbedShell()) return; closeLiveStream(); liveEventSource = new EventSource("/api/instance/live/stream"); liveEventSource.addEventListener("live", function (ev) { try { onLiveEvent(JSON.parse(ev.data || "{}")); } catch (_) {} }); liveEventSource.onopen = function () { sseConnected = true; lastLiveEventAt = Date.now(); }; liveEventSource.onerror = function () { sseConnected = false; closeLiveStream(); liveReconnectTimer = setTimeout(function () { connectLiveStream(); }, 8000); }; } function isConnectedFresh() { if (!sseConnected || !liveEventSource) return false; if (!lastLiveEventAt) return false; return Date.now() - lastLiveEventAt < LIVE_STALE_MS; } function startLive() { document.addEventListener("visibilitychange", function () { if (document.hidden) return; if (isEmbedShell()) { if (!isConnectedFresh()) connectLiveStream(); } kickRefresh({ silent: true, force: true }); }); window.addEventListener("pageshow", function (ev) { if (ev.persisted) kickRefresh({ silent: true, force: true }); }); if (!isEmbedShell()) return; connectLiveStream(); } global.InstanceLive = { start: startLive, refreshTabData: refreshTabData, kickRefresh: kickRefresh, isConnected: isConnectedFresh, }; if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", startLive); } else { startLive(); } })(typeof window !== "undefined" ? window : globalThis);