dbf935a570
Co-authored-by: Cursor <cursoragent@cursor.com>
152 lines
4.3 KiB
JavaScript
152 lines
4.3 KiB
JavaScript
/**
|
|
* 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);
|