Files
pikasTech-HWLAB/web/hwlab-cloud-web/src/composables/useWorkbenchNowTicker.ts
T
2026-06-24 19:23:29 +08:00

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";
}