Add chart freshness diagnostics

This commit is contained in:
2569718930@qq.com
2026-06-10 15:34:34 +08:00
parent ba012736c9
commit 5a7d302d11
14 changed files with 833 additions and 42 deletions
+49 -4
View File
@@ -15,6 +15,13 @@ export type CityPatch = {
changes: Record<string, unknown>;
revision: number;
ts?: number;
delivery?: {
client_received_at_ms: number;
sse_emitted_at_ms?: number | null;
server_to_client_latency_sec?: number | null;
collector_to_client_latency_sec?: number | null;
source_to_collector_latency_sec?: number | null;
};
};
type ObservationPatchV1 = {
@@ -30,6 +37,7 @@ type ObservationPatchV1 = {
source_cadence_sec?: number | null;
revision?: number;
ts?: number;
sse_emitted_at_ms?: number | null;
payload?: Record<string, unknown>;
};
@@ -105,6 +113,34 @@ function resolveSseReplayLimit(cityCount: number) {
return Math.max(SSE_REPLAY_BASE_LIMIT, Math.min(SSE_REPLAY_MAX_LIMIT, requested));
}
function finiteNumber(value: unknown): number | null {
const number = Number(value);
return Number.isFinite(number) ? number : null;
}
function latencySeconds(fromMs: number | null, toMs: number) {
if (fromMs === null) return null;
return Math.max(0, Math.round((toMs - fromMs) / 1000));
}
function buildPatchDelivery(
patch: Partial<CityPatch> & ObservationPatchV1,
clientReceivedAtMs: number,
payload?: Record<string, unknown>,
) {
const sseEmittedAtMs = finiteNumber(patch.sse_emitted_at_ms);
const collectorReceivedAtMs = finiteNumber(patch.ts);
const sourceToCollectorLatencySec =
finiteNumber(payload?.latency_sec) ?? finiteNumber((patch.changes as Record<string, unknown> | undefined)?.latency_sec);
return {
client_received_at_ms: clientReceivedAtMs,
sse_emitted_at_ms: sseEmittedAtMs,
server_to_client_latency_sec: latencySeconds(sseEmittedAtMs, clientReceivedAtMs),
collector_to_client_latency_sec: latencySeconds(collectorReceivedAtMs, clientReceivedAtMs),
source_to_collector_latency_sec: sourceToCollectorLatencySec,
};
}
function currentConnectionKey() {
return `${useFallbackUrl ? "fallback" : "direct"}:${subscribedCityList().join("|")}:${lastRevision}`;
}
@@ -219,7 +255,10 @@ function registerCitySubscription(city: string) {
};
}
function normalizeLegacyPatch(patch: Partial<CityPatch>): CityPatch | null {
function normalizeLegacyPatch(
patch: Partial<CityPatch> & ObservationPatchV1,
clientReceivedAtMs: number,
): CityPatch | null {
const city = normalizeCityKey(patch.city);
const changes = patch.changes;
const revision = Number(patch.revision);
@@ -232,10 +271,14 @@ function normalizeLegacyPatch(patch: Partial<CityPatch>): CityPatch | null {
changes: changes as Record<string, unknown>,
revision,
ts: typeof patch.ts === "number" ? patch.ts : Date.now(),
delivery: buildPatchDelivery(patch, clientReceivedAtMs),
};
}
function normalizeV1Patch(patch: ObservationPatchV1): CityPatch | null {
function normalizeV1Patch(
patch: ObservationPatchV1,
clientReceivedAtMs: number,
): CityPatch | null {
const city = normalizeCityKey(patch.city);
const revision = Number(patch.revision);
const payload = patch.payload;
@@ -262,17 +305,19 @@ function normalizeV1Patch(patch: ObservationPatchV1): CityPatch | null {
changes,
revision,
ts: typeof patch.ts === "number" ? patch.ts : Date.now(),
delivery: buildPatchDelivery(patch, clientReceivedAtMs, payload),
};
}
function normalizeIncomingPatch(payload: unknown): CityPatch | null {
if (!payload || typeof payload !== "object") return null;
const patch = payload as Partial<CityPatch> & ObservationPatchV1;
const clientReceivedAtMs = Date.now();
if (patch.type === "city_patch" || !patch.type) {
return normalizeLegacyPatch(patch);
return normalizeLegacyPatch(patch, clientReceivedAtMs);
}
if (patch.type === V1_EVENT_TYPE) {
return normalizeV1Patch(patch);
return normalizeV1Patch(patch, clientReceivedAtMs);
}
return null;
}