v0.5.110: stop carrying stale live badges; probe live status in local serve.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
voson
2026-07-30 01:11:13 +08:00
co-authored by Cursor
parent d6212169af
commit fe1b13b7c1
10 changed files with 125 additions and 56 deletions
+8 -6
View File
@@ -4749,9 +4749,9 @@ function syncStreamerCardLive(card, row) {
/**
* Visit-triggered live refresh: the Pages Function coalesces concurrent
* visitors through a 5-minute edge cache. Local `serve_relations.py` returns
* an empty stub (no probing), so that path keeps the data.json is_live
* fallback. Fires at most once per page load.
* visitors through a 5-minute edge cache; the local development server uses
* the same platform probes with a short in-memory cache. Fires at most once
* per page load.
*/
let liveStatusFetched = false;
@@ -4776,9 +4776,11 @@ function refreshStreamerLiveStatus() {
for (const row of rows) {
const cell = row && row.id ? probed[row.id] : null;
if (!cell || typeof cell.is_live !== "boolean") continue;
if (cell.stale) console.info(`live-status: ${row.id} using stale carry-over`);
if (row.is_live !== cell.is_live) {
row.is_live = cell.is_live;
// A failed/stale probe is unknown, never evidence that a room is live.
const isLive = cell.stale ? false : cell.is_live;
if (cell.stale) console.info(`live-status: ${row.id} probe stale; hiding badge`);
if (row.is_live !== isLive) {
row.is_live = isLive;
changed = true;
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
/* Local defaults; production export overwrites via export_relations_site.py. */
var SITE_VERSION = "0.5.109";
var SITE_VERSION = "0.5.110";
var ABILITY_VIDEO_BASE = "";
var STATIC_ASSET_BASE = "";
+34 -36
View File
@@ -15,12 +15,10 @@
* the SSR pace chunks: roomStore.roomInfo.room.status (2 live / 4 offline);
* the embedded web_rid must match the requested one.
*
* Soft-fail everywhere: douyin blocks from datacenter IPs are expected. When a
* single probe fails, the streamer carries over the last known is_live from
* data.json / the previous (stale) cache entry with `stale: true`; when every
* probe fails the stale cache entry is served wholesale (X-Live-Cache:
* stale-override), or an empty payload when nothing was ever cached
* (X-Live-Cache: error). The handler never throws a 500 for probe failures.
* Soft-fail everywhere: douyin blocks from datacenter IPs are expected. A
* failed probe is reported as offline with `stale: true`; a stale positive is
* never carried over because the live badge must only reflect a successful
* current probe. The handler never throws a 500 for probe failures.
*
* Named exports double as the local test surface; the Pages runtime only
* routes onRequest* handlers.
@@ -292,6 +290,23 @@ function emptyPayload() {
return { probed_at: new Date().toISOString(), ttl: FRESH_TTL_S, streamers: {} };
}
/** Failed probes must never preserve a stale live badge. */
function staleOfflinePayload(source) {
const streamers = {};
const previous =
source && source.streamers && typeof source.streamers === "object"
? source.streamers
: {};
for (const id of Object.keys(previous)) {
streamers[id] = { is_live: false, stale: true };
}
return {
probed_at: (source && source.probed_at) || new Date().toISOString(),
ttl: FRESH_TTL_S,
streamers,
};
}
async function putCache(body) {
const cached = new Response(JSON.stringify(body), {
headers: {
@@ -324,17 +339,12 @@ async function probeFresh(requestUrl, cachedData) {
}
if (!targets.length) {
if (cachedData) return { body: cachedData, cacheState: "stale-override" };
if (cachedData) {
return { body: staleOfflinePayload(cachedData), cacheState: "stale-override" };
}
return { body: emptyPayload(), cacheState: "error" };
}
const seeded = seedLiveFromPayload(dataPayload);
const cachedStreamers =
cachedData && cachedData.streamers && typeof cachedData.streamers === "object"
? cachedData.streamers
: {};
// Prefer previous edge cache over the daily snapshot when both exist.
const staleStreamers = { ...seeded, ...cachedStreamers };
const probed = await probeAll(targets);
const streamers = {};
@@ -346,31 +356,19 @@ async function probeFresh(requestUrl, cachedData) {
freshCount += 1;
continue;
}
// Per-streamer soft-fail: carry over the last known state, marked stale.
const prev = staleStreamers[t.id];
if (prev && typeof prev.is_live === "boolean") {
streamers[t.id] = { is_live: prev.is_live, stale: true };
}
// Unknown is not live: never carry a stale positive badge forward.
streamers[t.id] = { is_live: false, stale: true };
}
if (freshCount === 0) {
// Total probe failure (e.g. douyin blocking this colo): serve the stale
// snapshot if one exists, otherwise an explicitly empty payload.
if (cachedData) return { body: cachedData, cacheState: "stale-override" };
if (Object.keys(seeded).length) {
const body = {
probed_at: new Date().toISOString(),
ttl: FRESH_TTL_S,
streamers: Object.fromEntries(
Object.entries(seeded).map(([id, cell]) => [
id,
{ is_live: cell.is_live, stale: true },
])
),
};
return { body, cacheState: "stale-override" };
}
return { body: emptyPayload(), cacheState: "error" };
// Total probe failure (e.g. douyin blocking this colo): keep the target
// shape but suppress every unconfirmed live badge.
const body = {
probed_at: new Date().toISOString(),
ttl: FRESH_TTL_S,
streamers,
};
return { body, cacheState: "stale-override" };
}
const body = { probed_at: new Date().toISOString(), ttl: FRESH_TTL_S, streamers };
+5 -5
View File
@@ -5,8 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>DOTA2 上分帝</title>
<link rel="icon" href="/ui-icon/dota2_logo.png" type="image/png" />
<link rel="stylesheet" href="/style.css?v=0.5.109" />
<script src="/mobile-gate.js?v=0.5.109"></script>
<link rel="stylesheet" href="/style.css?v=0.5.110" />
<script src="/mobile-gate.js?v=0.5.110"></script>
</head>
<body>
<h1 class="sr-only">DOTA2 上分帝</h1>
@@ -178,8 +178,8 @@
<section class="detail" id="detail" aria-live="polite"></section>
<script src="/config.js?v=0.5.109"></script>
<script src="/router.js?v=0.5.109"></script>
<script src="/app.js?v=0.5.109"></script>
<script src="/config.js?v=0.5.110"></script>
<script src="/router.js?v=0.5.110"></script>
<script src="/app.js?v=0.5.110"></script>
</body>
</html>