react-use-precision-timer
Version:
A versatile precision timer hook for React. Doubles as a stopwatch.
312 lines (311 loc) • 13.1 kB
JavaScript
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.useTimer = void 0;
const React = __importStar(require("react"));
const react_sub_unsub_1 = require("react-sub-unsub");
/** Milliseconds representing forever in the future. */
const never = Number.MAX_SAFE_INTEGER;
/**
* See documentation: [useTimer](https://justinmahar.github.io/react-use-precision-timer/?path=/story/docs-usetimer--docs)
*
* A versatile precision timer hook for React. Doubles as a stopwatch.
*
* - Based on `setTimeout()` and timestamps, not `setInterval()` or ticks.
* - Features perfect mean interval accuracy, meaning it doesn't wander.
* - Resilient to expensive callback operations and low timer delays.
* - Can be used as a timer or a stopwatch.
* - Supports starting, stopping, pausing, and resuming.
* - Includes accessors for everything under the sun.
*
* @param options The TimerOptions for the timer.
* @param callback The callback to call when the timer fires. Use React.useCallback() for this. Must provide a `delay` for the timer to fire. If you'd like, you can determine if any calls were missed by checking the `overdueCallCount` argument. This value will indicate how many calls were missed due to a very short timer delay or time-consuming callback.
*/
const useTimer = (options = {}, callback) => {
const [firstRun, setFirstRun] = React.useState(true);
const [renderTime, setRenderTime] = React.useState(Date.now());
const startedRef = React.useRef(false);
const startTimeRef = React.useRef(never);
const lastFireTimeRef = React.useRef(never);
const nextFireTimeRef = React.useRef(never);
const pauseTimeRef = React.useRef(never);
const resumeTimeRef = React.useRef(never);
const periodElapsedPauseTimeRef = React.useRef(0);
const totalElapsedPauseTimeRef = React.useRef(0);
// Memoized options
const delay = React.useMemo(() => {
var _a, _b;
const s = (_a = options.speedMultiplier) !== null && _a !== void 0 ? _a : 1;
const d = (_b = options.delay) !== null && _b !== void 0 ? _b : 0;
return s === 0 ? 0 : s > 0 && d > 0 ? Math.max(1, Math.round(d * (1 / s))) : d;
}, [options.delay, options.speedMultiplier]);
const runOnce = React.useMemo(() => options.runOnce, [options.runOnce]);
const fireOnStart = React.useMemo(() => options.fireOnStart, [options.fireOnStart]);
const startImmediately = React.useMemo(() => options.startImmediately, [options.startImmediately]);
const isStarted = React.useCallback(() => {
return startedRef.current;
}, []);
const isStopped = React.useCallback(() => {
return !isStarted();
}, [isStarted]);
const isPaused = React.useCallback(() => {
return isStarted() && pauseTimeRef.current !== never;
}, [isStarted]);
const isRunning = React.useCallback(() => {
return isStarted() && !isPaused();
}, [isPaused, isStarted]);
const getEffectiveDelay = React.useCallback(() => {
return delay;
}, [delay]);
const getStartTime = React.useCallback(() => {
if (isStarted()) {
return startTimeRef.current;
}
return -1;
}, [isStarted]);
const getLastFireTime = React.useCallback(() => {
return lastFireTimeRef.current < never && !!delay ? lastFireTimeRef.current : -1;
}, [delay]);
const getNextFireTime = React.useCallback(() => {
if (isRunning() && !!delay) {
return nextFireTimeRef.current;
}
return -1;
}, [isRunning, delay]);
const getPauseTime = React.useCallback(() => {
if (isPaused()) {
return pauseTimeRef.current;
}
return -1;
}, [isPaused]);
const getResumeTime = React.useCallback(() => {
if (isStarted() && resumeTimeRef.current < never) {
return resumeTimeRef.current;
}
return -1;
}, [isStarted]);
const getElapsedStartedTime = React.useCallback(() => {
if (isStarted()) {
return Date.now() - startTimeRef.current;
}
return 0;
}, [isStarted]);
const getElapsedRunningTime = React.useCallback(() => {
if (isStarted()) {
if (isPaused()) {
return pauseTimeRef.current - startTimeRef.current - totalElapsedPauseTimeRef.current;
}
else {
return Date.now() - startTimeRef.current - totalElapsedPauseTimeRef.current;
}
}
return 0;
}, [isPaused, isStarted]);
const getPeriodElapsedPausedTime = React.useCallback(() => {
let additionalElapsedPauseTime = 0;
if (isPaused()) {
additionalElapsedPauseTime = Date.now() - pauseTimeRef.current;
}
return periodElapsedPauseTimeRef.current + additionalElapsedPauseTime;
}, [isPaused]);
const getTotalElapsedPausedTime = React.useCallback(() => {
let additionalElapsedPauseTime = 0;
if (isPaused()) {
additionalElapsedPauseTime = Date.now() - pauseTimeRef.current;
}
return totalElapsedPauseTimeRef.current + additionalElapsedPauseTime;
}, [isPaused]);
const getElapsedResumedTime = React.useCallback(() => {
if (isRunning()) {
return Date.now() - resumeTimeRef.current;
}
return 0;
}, [isRunning]);
const getRemainingTime = React.useCallback(() => {
const currentTime = Date.now();
if (isStarted() && !!delay) {
if (isRunning()) {
return Math.max(0, nextFireTimeRef.current - currentTime);
}
else if (isPaused()) {
const edgeTime = lastFireTimeRef.current !== never ? lastFireTimeRef.current : startTimeRef.current;
return Math.max(0, delay - (pauseTimeRef.current - edgeTime - periodElapsedPauseTimeRef.current));
}
}
return 0;
}, [isPaused, isRunning, isStarted, delay]);
const start = React.useCallback((startTimeMillis = Date.now()) => {
const newNextFireTime = delay
? Math.max(startTimeMillis, fireOnStart ? startTimeMillis : startTimeMillis + delay)
: never;
startTimeRef.current = startTimeMillis;
lastFireTimeRef.current = never;
nextFireTimeRef.current = newNextFireTime;
pauseTimeRef.current = never;
resumeTimeRef.current = startTimeMillis;
periodElapsedPauseTimeRef.current = 0;
totalElapsedPauseTimeRef.current = 0;
startedRef.current = true;
setRenderTime(Date.now());
}, [delay, fireOnStart]);
const stop = React.useCallback(() => {
startTimeRef.current = never;
lastFireTimeRef.current = never;
nextFireTimeRef.current = never;
pauseTimeRef.current = never;
resumeTimeRef.current = never;
periodElapsedPauseTimeRef.current = 0;
totalElapsedPauseTimeRef.current = 0;
startedRef.current = false;
setRenderTime(Date.now());
}, []);
const pause = React.useCallback(() => {
if (isRunning()) {
pauseTimeRef.current = Date.now();
resumeTimeRef.current = never;
setRenderTime(Date.now());
}
}, [isRunning]);
const resume = React.useCallback(() => {
if (isStarted() && isPaused()) {
const currentTime = Date.now();
nextFireTimeRef.current = currentTime + getRemainingTime();
periodElapsedPauseTimeRef.current = 0;
totalElapsedPauseTimeRef.current = totalElapsedPauseTimeRef.current + (currentTime - pauseTimeRef.current);
pauseTimeRef.current = never;
resumeTimeRef.current = currentTime;
setRenderTime(Date.now());
}
}, [isStarted, isPaused, getRemainingTime]);
React.useEffect(() => {
const subs = new react_sub_unsub_1.Subs();
const checkTimer = () => {
// If it's a timer and it isn't paused...
if (delay && !isPaused()) {
const now = Date.now();
// If the timer is up...
if (now >= nextFireTimeRef.current) {
// Check if we're overdue on any events being fired (super low delay or expensive callback).
// To do this, we divide the time elapsed beyond the next expected fire time by the delay,
// and floor the result. In other words, find how overdue we are, then divide by the delay.
const overdueCalls = lastFireTimeRef.current !== never ? Math.max(0, Math.floor((now - nextFireTimeRef.current) / delay)) : 0;
lastFireTimeRef.current = now;
periodElapsedPauseTimeRef.current = 0;
// Calculate and set the next time the timer should fire, accounting for overdue calls (if any)
const overdueElapsedTime = overdueCalls * delay;
const newFireTime = Math.max(now, nextFireTimeRef.current + delay + overdueElapsedTime);
nextFireTimeRef.current = newFireTime;
// Call the callback
if (typeof callback === 'function') {
try {
callback(overdueCalls);
}
catch (e) {
console.error(e);
}
}
// If it repeats
if (!runOnce) {
// Set a timeout to check and fire the timer when time's up
subs.setTimeout(() => {
// Check if the timer can fire
checkTimer();
}, Math.max(newFireTime - Date.now(), 1));
}
else {
// If it doesn't repeat, stop the timer.
stop();
}
}
// Time is not up yet. Set a timeout to check and fire when time's up
else if (nextFireTimeRef.current < never) {
subs.setTimeout(() => {
// Check if the timer can fire
checkTimer();
// Home in on the exact time to fire.
}, Math.max(nextFireTimeRef.current - Date.now(), 1));
}
}
};
// Check if the timer can fire
checkTimer();
return subs.createCleanup();
}, [callback, delay, isPaused, renderTime, runOnce, stop]);
// Start immediately if this is our first run.
React.useEffect(() => {
if (firstRun) {
setFirstRun(false);
if (startImmediately) {
start();
}
}
}, [firstRun, startImmediately, start]);
return React.useMemo(() => {
return {
start,
stop,
pause,
resume,
isStarted,
isStopped,
isRunning,
isPaused,
getEffectiveDelay,
getStartTime,
getLastFireTime,
getNextFireTime,
getPauseTime,
getResumeTime,
getRemainingTime,
getElapsedStartedTime,
getElapsedRunningTime,
getTotalElapsedPausedTime,
getPeriodElapsedPausedTime,
getElapsedResumedTime,
};
}, [
getEffectiveDelay,
getElapsedResumedTime,
getElapsedRunningTime,
getElapsedStartedTime,
getLastFireTime,
getNextFireTime,
getPauseTime,
getPeriodElapsedPausedTime,
getRemainingTime,
getResumeTime,
getStartTime,
getTotalElapsedPausedTime,
isPaused,
isRunning,
isStarted,
isStopped,
pause,
resume,
start,
stop,
]);
};
exports.useTimer = useTimer;
;