452524f80a
This reverts commit b61fb4e65c.
80 lines
2.5 KiB
TypeScript
80 lines
2.5 KiB
TypeScript
// SPEC: PJ2026-010401 Web工作台 draft-2026-06-20-p1-view-local-timing-ticker; PJ2026-0104010803 唯一投影 draft-2026-06-20-p1-view-local-timing-ticker.
|
|
// Responsibility: Shared browser-only render ticker for relative Workbench timing labels; it never writes server-state or projection facts.
|
|
|
|
import { onBeforeUnmount, readonly, ref, watch, type Ref } from "vue";
|
|
|
|
const TICK_INTERVAL_MS = 1000;
|
|
|
|
const nowMs = ref(Date.now());
|
|
const activeSubscribers = new Map<symbol, boolean>();
|
|
let timer: number | null = null;
|
|
let visibilityListenerAttached = false;
|
|
|
|
export function useWorkbenchNowTicker(enabled: Ref<boolean>) {
|
|
const subscriber = Symbol("workbench-now-ticker");
|
|
activeSubscribers.set(subscriber, false);
|
|
ensureVisibilityListener();
|
|
|
|
const stopWatch = watch(() => enabled.value, (value) => {
|
|
activeSubscribers.set(subscriber, value === true);
|
|
reconcileTimer();
|
|
}, { immediate: true });
|
|
|
|
onBeforeUnmount(() => {
|
|
stopWatch();
|
|
activeSubscribers.delete(subscriber);
|
|
reconcileTimer();
|
|
detachVisibilityListenerIfIdle();
|
|
});
|
|
|
|
return { nowMs: readonly(nowMs), refreshNow };
|
|
}
|
|
|
|
function refreshNow(): void {
|
|
nowMs.value = Date.now();
|
|
}
|
|
|
|
function reconcileTimer(): void {
|
|
// Passive observer pages may be backgrounded while still being the source of
|
|
// web-probe samples. Keep the render ticker alive for active subscribers so
|
|
// elapsed/recent labels advance monotonically instead of jumping on resume.
|
|
const shouldRun = isBrowser() && hasActiveSubscriber();
|
|
if (shouldRun && timer === null) {
|
|
refreshNow();
|
|
timer = window.setInterval(refreshNow, TICK_INTERVAL_MS);
|
|
return;
|
|
}
|
|
if (!shouldRun && timer !== null) {
|
|
window.clearInterval(timer);
|
|
timer = null;
|
|
}
|
|
}
|
|
|
|
function hasActiveSubscriber(): boolean {
|
|
for (const active of activeSubscribers.values()) {
|
|
if (active) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function ensureVisibilityListener(): void {
|
|
if (visibilityListenerAttached || typeof document === "undefined") return;
|
|
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
visibilityListenerAttached = true;
|
|
}
|
|
|
|
function detachVisibilityListenerIfIdle(): void {
|
|
if (activeSubscribers.size > 0 || !visibilityListenerAttached || typeof document === "undefined") return;
|
|
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
visibilityListenerAttached = false;
|
|
}
|
|
|
|
function handleVisibilityChange(): void {
|
|
refreshNow();
|
|
reconcileTimer();
|
|
}
|
|
|
|
function isBrowser(): boolean {
|
|
return typeof window !== "undefined";
|
|
}
|