修复实例页长时间后不自动刷新:回前台强制拉数、SSE假连接兜底、快照请求超时防卡死。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,12 +1,17 @@
|
|||||||
/**
|
/**
|
||||||
* embed 壳:SSE 收到后台 tick 后拉 JSON 快照更新 DOM,切换 tab 不再重复请求 HTML.
|
* embed 壳:SSE 收到后台 tick 后拉 JSON 快照更新 DOM.
|
||||||
|
* 同时为 standalone/embed 提供:回前台强制刷新、SSE 假连接兜底.
|
||||||
*/
|
*/
|
||||||
(function (global) {
|
(function (global) {
|
||||||
let liveEventSource = null;
|
let liveEventSource = null;
|
||||||
let liveReconnectTimer = null;
|
let liveReconnectTimer = null;
|
||||||
let localLiveVersion = -1;
|
let localLiveVersion = -1;
|
||||||
let sseConnected = false;
|
let sseConnected = false;
|
||||||
|
let lastLiveEventAt = 0;
|
||||||
let refreshTimer = null;
|
let refreshTimer = null;
|
||||||
|
let pendingWhileHidden = false;
|
||||||
|
/** 超过该毫秒无 live 事件则视为断流,允许 interval 兜底轮询 */
|
||||||
|
const LIVE_STALE_MS = 45000;
|
||||||
|
|
||||||
function isEmbedShell() {
|
function isEmbedShell() {
|
||||||
return document.body && document.body.getAttribute("data-embed-shell") === "1";
|
return document.body && document.body.getAttribute("data-embed-shell") === "1";
|
||||||
@@ -30,20 +35,37 @@
|
|||||||
if (tab === "options" && global.OptionsPanelLive && typeof global.OptionsPanelLive.refreshSoft === "function") {
|
if (tab === "options" && global.OptionsPanelLive && typeof global.OptionsPanelLive.refreshSoft === "function") {
|
||||||
global.OptionsPanelLive.refreshSoft(options);
|
global.OptionsPanelLive.refreshSoft(options);
|
||||||
}
|
}
|
||||||
// 数据看板自有 SSE + 快照,不跟 embed live tick 重拉.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleRefresh(opts) {
|
function scheduleRefresh(opts) {
|
||||||
|
if (document.hidden) {
|
||||||
|
pendingWhileHidden = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (refreshTimer) return;
|
if (refreshTimer) return;
|
||||||
const options = opts || {};
|
const options = opts || {};
|
||||||
refreshTimer = setTimeout(function () {
|
refreshTimer = setTimeout(function () {
|
||||||
refreshTimer = null;
|
refreshTimer = null;
|
||||||
if (document.hidden) return;
|
if (document.hidden) {
|
||||||
|
pendingWhileHidden = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pendingWhileHidden = false;
|
||||||
refreshTabData(currentTab(), { silent: true, force: !!options.force });
|
refreshTabData(currentTab(), { silent: true, force: !!options.force });
|
||||||
}, 80);
|
}, 80);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function kickRefresh(opts) {
|
||||||
|
pendingWhileHidden = false;
|
||||||
|
if (refreshTimer) {
|
||||||
|
clearTimeout(refreshTimer);
|
||||||
|
refreshTimer = null;
|
||||||
|
}
|
||||||
|
refreshTabData(currentTab(), opts || { silent: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
function onLiveEvent(data) {
|
function onLiveEvent(data) {
|
||||||
|
lastLiveEventAt = Date.now();
|
||||||
const reason = data && data.reason;
|
const reason = data && data.reason;
|
||||||
const ver = Number(data && data.live_version) || 0;
|
const ver = Number(data && data.live_version) || 0;
|
||||||
if (!ver) return;
|
if (!ver) return;
|
||||||
@@ -59,7 +81,9 @@
|
|||||||
|
|
||||||
function closeLiveStream() {
|
function closeLiveStream() {
|
||||||
if (liveEventSource) {
|
if (liveEventSource) {
|
||||||
liveEventSource.close();
|
try {
|
||||||
|
liveEventSource.close();
|
||||||
|
} catch (_) {}
|
||||||
liveEventSource = null;
|
liveEventSource = null;
|
||||||
}
|
}
|
||||||
if (liveReconnectTimer) {
|
if (liveReconnectTimer) {
|
||||||
@@ -80,6 +104,7 @@
|
|||||||
});
|
});
|
||||||
liveEventSource.onopen = function () {
|
liveEventSource.onopen = function () {
|
||||||
sseConnected = true;
|
sseConnected = true;
|
||||||
|
lastLiveEventAt = Date.now();
|
||||||
};
|
};
|
||||||
liveEventSource.onerror = function () {
|
liveEventSource.onerror = function () {
|
||||||
sseConnected = false;
|
sseConnected = false;
|
||||||
@@ -90,7 +115,23 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isConnectedFresh() {
|
||||||
|
if (!sseConnected || !liveEventSource) return false;
|
||||||
|
if (!lastLiveEventAt) return false;
|
||||||
|
return Date.now() - lastLiveEventAt < LIVE_STALE_MS;
|
||||||
|
}
|
||||||
|
|
||||||
function startLive() {
|
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;
|
if (!isEmbedShell()) return;
|
||||||
connectLiveStream();
|
connectLiveStream();
|
||||||
}
|
}
|
||||||
@@ -98,9 +139,8 @@
|
|||||||
global.InstanceLive = {
|
global.InstanceLive = {
|
||||||
start: startLive,
|
start: startLive,
|
||||||
refreshTabData: refreshTabData,
|
refreshTabData: refreshTabData,
|
||||||
isConnected: function () {
|
kickRefresh: kickRefresh,
|
||||||
return sseConnected;
|
isConnected: isConnectedFresh,
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (document.readyState === "loading") {
|
if (document.readyState === "loading") {
|
||||||
|
|||||||
@@ -1173,6 +1173,17 @@ function accountSnapshotFundingMissing(data){
|
|||||||
return !hasFunding && !hasTotal && !hasTrading;
|
return !hasFunding && !hasTotal && !hasTrading;
|
||||||
}
|
}
|
||||||
let accountSnapshotRetryCount = 0;
|
let accountSnapshotRetryCount = 0;
|
||||||
|
let accountSnapshotInflight = false;
|
||||||
|
let priceSnapshotInflight = false;
|
||||||
|
|
||||||
|
function fetchJsonWithTimeout(url, timeoutMs) {
|
||||||
|
const ms = timeoutMs != null ? timeoutMs : 25000;
|
||||||
|
const ac = new AbortController();
|
||||||
|
const timer = setTimeout(function () { ac.abort(); }, ms);
|
||||||
|
return fetch(url, { signal: ac.signal, credentials: "same-origin" })
|
||||||
|
.then(function (r) { return r.json(); })
|
||||||
|
.finally(function () { clearTimeout(timer); });
|
||||||
|
}
|
||||||
function applyAccountSnapshot(data){
|
function applyAccountSnapshot(data){
|
||||||
if(!data || typeof data !== "object") return;
|
if(!data || typeof data !== "object") return;
|
||||||
if(typeof data.show_perp_funds !== "undefined"){
|
if(typeof data.show_perp_funds !== "undefined"){
|
||||||
@@ -1263,8 +1274,10 @@ function applyAccountSnapshot(data){
|
|||||||
}
|
}
|
||||||
function refreshAccountSnapshot(opts){
|
function refreshAccountSnapshot(opts){
|
||||||
const options = opts || {};
|
const options = opts || {};
|
||||||
|
if(accountSnapshotInflight && !options.force) return;
|
||||||
|
accountSnapshotInflight = true;
|
||||||
const qs = options.force ? "?force=1" : "";
|
const qs = options.force ? "?force=1" : "";
|
||||||
fetch("/api/account_snapshot" + qs).then(r=>r.json()).then(data=>{
|
fetchJsonWithTimeout("/api/account_snapshot" + qs, 25000).then(data=>{
|
||||||
applyAccountSnapshot(data);
|
applyAccountSnapshot(data);
|
||||||
if(accountSnapshotFundingMissing(data) && !options.force && accountSnapshotRetryCount < 3){
|
if(accountSnapshotFundingMissing(data) && !options.force && accountSnapshotRetryCount < 3){
|
||||||
accountSnapshotRetryCount += 1;
|
accountSnapshotRetryCount += 1;
|
||||||
@@ -1277,7 +1290,7 @@ function refreshAccountSnapshot(opts){
|
|||||||
accountSnapshotRetryCount += 1;
|
accountSnapshotRetryCount += 1;
|
||||||
setTimeout(() => refreshAccountSnapshot({ silent: true }), 1200 * accountSnapshotRetryCount);
|
setTimeout(() => refreshAccountSnapshot({ silent: true }), 1200 * accountSnapshotRetryCount);
|
||||||
}
|
}
|
||||||
});
|
}).finally(()=>{ accountSnapshotInflight = false; });
|
||||||
}
|
}
|
||||||
|
|
||||||
const orderSymbolEl = document.getElementById("order-symbol");
|
const orderSymbolEl = document.getElementById("order-symbol");
|
||||||
@@ -1419,8 +1432,10 @@ refreshOrderDefaults();
|
|||||||
if(typeof initOrderEntryModelSelect === "function") initOrderEntryModelSelect();
|
if(typeof initOrderEntryModelSelect === "function") initOrderEntryModelSelect();
|
||||||
refreshPriceSnapshotConditional();
|
refreshPriceSnapshotConditional();
|
||||||
function refreshPriceSnapshotConditional(){
|
function refreshPriceSnapshotConditional(){
|
||||||
|
if(priceSnapshotInflight) return;
|
||||||
|
priceSnapshotInflight = true;
|
||||||
const page = document.body.getAttribute("data-page") || "";
|
const page = document.body.getAttribute("data-page") || "";
|
||||||
fetch("/api/price_snapshot").then(r=>r.json()).then(data=>{
|
fetchJsonWithTimeout("/api/price_snapshot", 25000).then(data=>{
|
||||||
const updatedEl = document.getElementById("price-last-updated");
|
const updatedEl = document.getElementById("price-last-updated");
|
||||||
if(data.updated_at && updatedEl) updatedEl.innerText = data.updated_at;
|
if(data.updated_at && updatedEl) updatedEl.innerText = data.updated_at;
|
||||||
if(data.force_close && window.TimeCloseUI && TimeCloseUI.paintForceCloseHeader){
|
if(data.force_close && window.TimeCloseUI && TimeCloseUI.paintForceCloseHeader){
|
||||||
@@ -1474,7 +1489,7 @@ function refreshPriceSnapshotConditional(){
|
|||||||
} else if (typeof data.options_unrealized_pnl !== "undefined") {
|
} else if (typeof data.options_unrealized_pnl !== "undefined") {
|
||||||
paintRealtimePnlFromSnapshot(data);
|
paintRealtimePnlFromSnapshot(data);
|
||||||
}
|
}
|
||||||
}).catch(()=>{});
|
}).catch(()=>{}).finally(()=>{ priceSnapshotInflight = false; });
|
||||||
}
|
}
|
||||||
function formatLiveHoldDurationFromMs(openedMs, nowMs){
|
function formatLiveHoldDurationFromMs(openedMs, nowMs){
|
||||||
if(openedMs == null || openedMs === "" || !Number.isFinite(Number(openedMs))) return "—";
|
if(openedMs == null || openedMs === "" || !Number.isFinite(Number(openedMs))) return "—";
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ const ORDER_ENTRY_MODEL_CODE_TO_CATEGORY = {{ entry_model_code_to_category | toj
|
|||||||
window.__INSTANCE_DISPLAY__ = {{ display | tojson }};
|
window.__INSTANCE_DISPLAY__ = {{ display | tojson }};
|
||||||
</script>
|
</script>
|
||||||
<script src="/static/instance_settings_prefs.js?v=21"></script>
|
<script src="/static/instance_settings_prefs.js?v=21"></script>
|
||||||
<script src="/static/instance_live.js?v=6"></script>
|
<script src="/static/instance_live.js?v=7"></script>
|
||||||
<script src="/static/instance_embed.js?v=31"></script>
|
<script src="/static/instance_embed.js?v=31"></script>
|
||||||
<script src="/static/instance_mobile_nav.js?v=2"></script>
|
<script src="/static/instance_mobile_nav.js?v=2"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -1654,6 +1654,17 @@ function accountSnapshotFundingMissing(data){
|
|||||||
return !hasFunding && !hasTotal && !hasTrading;
|
return !hasFunding && !hasTotal && !hasTrading;
|
||||||
}
|
}
|
||||||
let accountSnapshotRetryCount = 0;
|
let accountSnapshotRetryCount = 0;
|
||||||
|
let accountSnapshotInflight = false;
|
||||||
|
let priceSnapshotInflight = false;
|
||||||
|
|
||||||
|
function fetchJsonWithTimeout(url, timeoutMs) {
|
||||||
|
const ms = timeoutMs != null ? timeoutMs : 25000;
|
||||||
|
const ac = new AbortController();
|
||||||
|
const timer = setTimeout(function () { ac.abort(); }, ms);
|
||||||
|
return fetch(url, { signal: ac.signal, credentials: "same-origin" })
|
||||||
|
.then(function (r) { return r.json(); })
|
||||||
|
.finally(function () { clearTimeout(timer); });
|
||||||
|
}
|
||||||
function applyAccountSnapshot(data){
|
function applyAccountSnapshot(data){
|
||||||
if(!data || typeof data !== "object") return;
|
if(!data || typeof data !== "object") return;
|
||||||
if(typeof data.show_perp_funds !== "undefined"){
|
if(typeof data.show_perp_funds !== "undefined"){
|
||||||
@@ -1753,8 +1764,10 @@ function applyAccountSnapshot(data){
|
|||||||
}
|
}
|
||||||
function refreshAccountSnapshot(opts){
|
function refreshAccountSnapshot(opts){
|
||||||
const options = opts || {};
|
const options = opts || {};
|
||||||
|
if(accountSnapshotInflight && !options.force) return;
|
||||||
|
accountSnapshotInflight = true;
|
||||||
const qs = options.force ? "?force=1" : "";
|
const qs = options.force ? "?force=1" : "";
|
||||||
fetch("/api/account_snapshot" + qs).then(r=>r.json()).then(data=>{
|
fetchJsonWithTimeout("/api/account_snapshot" + qs, 25000).then(data=>{
|
||||||
applyAccountSnapshot(data);
|
applyAccountSnapshot(data);
|
||||||
if(accountSnapshotFundingMissing(data) && !options.force && accountSnapshotRetryCount < 3){
|
if(accountSnapshotFundingMissing(data) && !options.force && accountSnapshotRetryCount < 3){
|
||||||
accountSnapshotRetryCount += 1;
|
accountSnapshotRetryCount += 1;
|
||||||
@@ -1767,7 +1780,7 @@ function refreshAccountSnapshot(opts){
|
|||||||
accountSnapshotRetryCount += 1;
|
accountSnapshotRetryCount += 1;
|
||||||
setTimeout(() => refreshAccountSnapshot({ silent: true }), 1200 * accountSnapshotRetryCount);
|
setTimeout(() => refreshAccountSnapshot({ silent: true }), 1200 * accountSnapshotRetryCount);
|
||||||
}
|
}
|
||||||
});
|
}).finally(()=>{ accountSnapshotInflight = false; });
|
||||||
}
|
}
|
||||||
|
|
||||||
{% if ui_open_guard_enabled %}
|
{% if ui_open_guard_enabled %}
|
||||||
@@ -1925,8 +1938,10 @@ refreshOrderDefaults();
|
|||||||
refreshPriceSnapshotConditional();
|
refreshPriceSnapshotConditional();
|
||||||
setInterval(refreshAccountSnapshot, {{ balance_refresh_seconds * 1000 }});
|
setInterval(refreshAccountSnapshot, {{ balance_refresh_seconds * 1000 }});
|
||||||
function refreshPriceSnapshotConditional(){
|
function refreshPriceSnapshotConditional(){
|
||||||
|
if(priceSnapshotInflight) return;
|
||||||
|
priceSnapshotInflight = true;
|
||||||
const page = document.body.getAttribute("data-page") || "";
|
const page = document.body.getAttribute("data-page") || "";
|
||||||
fetch("/api/price_snapshot").then(r=>r.json()).then(data=>{
|
fetchJsonWithTimeout("/api/price_snapshot", 25000).then(data=>{
|
||||||
const updatedEl = document.getElementById("price-last-updated");
|
const updatedEl = document.getElementById("price-last-updated");
|
||||||
if(data.updated_at && updatedEl) updatedEl.innerText = data.updated_at;
|
if(data.updated_at && updatedEl) updatedEl.innerText = data.updated_at;
|
||||||
if(data.force_close && window.TimeCloseUI && TimeCloseUI.paintForceCloseHeader){
|
if(data.force_close && window.TimeCloseUI && TimeCloseUI.paintForceCloseHeader){
|
||||||
@@ -2000,7 +2015,7 @@ function refreshPriceSnapshotConditional(){
|
|||||||
} else if (typeof data.options_unrealized_pnl !== "undefined") {
|
} else if (typeof data.options_unrealized_pnl !== "undefined") {
|
||||||
paintRealtimePnlFromSnapshot(data);
|
paintRealtimePnlFromSnapshot(data);
|
||||||
}
|
}
|
||||||
}).catch(()=>{});
|
}).catch(()=>{}).finally(()=>{ priceSnapshotInflight = false; });
|
||||||
}
|
}
|
||||||
function formatLiveHoldDurationFromMs(openedMs, nowMs){
|
function formatLiveHoldDurationFromMs(openedMs, nowMs){
|
||||||
if(openedMs == null || openedMs === "" || !Number.isFinite(Number(openedMs))) return "—";
|
if(openedMs == null || openedMs === "" || !Number.isFinite(Number(openedMs))) return "—";
|
||||||
@@ -2033,6 +2048,7 @@ setInterval(refreshPriceSnapshotConditional, {{ price_refresh_seconds * 1000 }})
|
|||||||
<script src="/static/records_review_page.js?v=4"></script>
|
<script src="/static/records_review_page.js?v=4"></script>
|
||||||
<script src="/static/options_expiry_countdown.js?v=1"></script>
|
<script src="/static/options_expiry_countdown.js?v=1"></script>
|
||||||
<script src="/static/instance_dashboard.js?v=5"></script>
|
<script src="/static/instance_dashboard.js?v=5"></script>
|
||||||
|
<script src="/static/instance_live.js?v=7"></script>
|
||||||
<script src="/static/account_ledger.js?v=1"></script>
|
<script src="/static/account_ledger.js?v=1"></script>
|
||||||
<script>
|
<script>
|
||||||
window.__INSTANCE_DISPLAY__ = {{ display | tojson }};
|
window.__INSTANCE_DISPLAY__ = {{ display | tojson }};
|
||||||
|
|||||||
Reference in New Issue
Block a user