Files
next-tjwater-frontend/src/features/workbench/peripheral-sample/use-scada-playback.ts
T

126 lines
4.5 KiB
TypeScript

import { useEffect, useRef, useState } from "react";
import type { ScadaReplay } from "../api/scada-replay-client";
export const SCADA_PLAYBACK_SPEED = 180;
interface PlaybackOptions {
active: boolean;
enabled: boolean;
busy: boolean;
start: number;
end: number;
now: number;
intervalSeconds: number | null | undefined;
restart: boolean;
advance: () => Promise<ScadaReplay | null>;
}
/** Browser-owned demo clock. Each boundary waits for one durable Server step. */
export function useScadaPlayback(options: PlaybackOptions) {
const latest = useRef(options);
latest.current = options;
const [playing, setPlaying] = useState(false);
const [started, setStarted] = useState(false);
const [newRound, setNewRound] = useState(false);
const [time, setTime] = useState(options.now);
const running = useRef(false);
const pending = useRef(false);
const position = useRef(options.now);
const processed = useRef(options.now);
const mounted = useRef(true);
const restartPrepared = useRef(false);
const pause = () => { running.current = false; setPlaying(false); };
const reset = () => {
pause();
setStarted(false);
setNewRound(false);
restartPrepared.current = false;
processed.current = position.current = latest.current.start;
setTime(latest.current.start);
};
useEffect(() => {
mounted.current = true;
const hidden = () => { if (document.visibilityState !== "visible") pause(); };
document.addEventListener("visibilitychange", hidden);
return () => {
mounted.current = false;
running.current = false;
document.removeEventListener("visibilitychange", hidden);
};
}, []);
useEffect(() => {
pause();
setStarted(false);
setNewRound(false);
position.current = latest.current.now;
processed.current = latest.current.now;
setTime(latest.current.now);
}, [options.start, options.end]);
useEffect(() => {
processed.current = options.now;
position.current = options.now;
setTime(options.now);
if (options.now >= latest.current.end) pause();
}, [options.now]);
useEffect(() => {
if (!options.active || !options.enabled || options.restart) pause();
}, [options.active, options.enabled, options.restart]);
useEffect(() => { restartPrepared.current = false; }, [options.restart]);
useEffect(() => {
if (!playing) return;
let previousTick = performance.now();
const timer = window.setInterval(() => {
const current = latest.current;
const tick = performance.now();
const elapsed = Math.min(tick - previousTick, 250);
previousTick = tick;
if (!running.current || !current.active || !current.enabled || document.visibilityState !== "visible"
|| pending.current || current.busy || !current.intervalSeconds) return;
const interval = current.intervalSeconds * 1000;
const target = Math.min(current.end, current.start
+ (Math.floor((processed.current - current.start) / interval) + 1) * interval);
position.current = Math.min(target, position.current + elapsed * SCADA_PLAYBACK_SPEED);
setTime(position.current);
if (position.current < target) return;
pending.current = true;
const requestedFrom = processed.current;
void current.advance().then((next) => {
if (!mounted.current) return;
if (!next || next.status === "failed" || next.step_pending) {
pause();
return;
}
const nextTime = Date.parse(next.virtual_now);
if (nextTime <= requestedFrom && !current.restart) {
pause();
return;
}
processed.current = position.current = nextTime;
setNewRound(false);
setTime(nextTime);
if (next.status === "completed" || nextTime >= current.end) pause();
}).catch(() => { if (mounted.current) pause(); }).finally(() => {
pending.current = false;
previousTick = performance.now();
});
}, 100);
return () => window.clearInterval(timer);
}, [playing]);
const play = () => {
if (!options.active || !options.enabled || options.busy || pending.current || !options.intervalSeconds) return;
if ((options.restart && !restartPrepared.current) || position.current >= options.end) {
processed.current = position.current = options.start;
restartPrepared.current = true;
setNewRound(true);
setTime(options.start);
}
running.current = true;
setStarted(true);
setPlaying(true);
};
return { playing, started, newRound, time, play, pause, reset };
}