const DEFAULT_COOLDOWN_SECONDS = 60; const sceneExpiresAt = new Map(); const requireFunction = (value, name) => { if (typeof value !== "function") { throw new TypeError(`${name} must be a function`); } return value; }; export const createAuthSmsCooldown = ({ sceneCode, onChange, now = Date.now, setIntervalFn = setInterval, clearIntervalFn = clearInterval, }) => { if (typeof sceneCode !== "string" || !sceneCode.trim()) { throw new TypeError("sceneCode must be a non-empty string"); } const normalizedSceneCode = sceneCode.trim(); const publish = requireFunction(onChange, "onChange"); const readNow = requireFunction(now, "now"); const scheduleInterval = requireFunction(setIntervalFn, "setIntervalFn"); const cancelInterval = requireFunction(clearIntervalFn, "clearIntervalFn"); let timer = null; const stopTimer = () => { if (timer === null) return; cancelInterval(timer); timer = null; }; const sync = () => { const expiresAt = sceneExpiresAt.get(normalizedSceneCode) || 0; const remaining = Math.max( 0, Math.ceil((expiresAt - Number(readNow())) / 1000), ); if (remaining === 0) { sceneExpiresAt.delete(normalizedSceneCode); stopTimer(); } else if (timer === null) { timer = scheduleInterval(sync, 1000); } publish(remaining); return remaining; }; const start = (seconds = DEFAULT_COOLDOWN_SECONDS) => { if (!Number.isInteger(seconds) || seconds < 1 || seconds > 300) { throw new TypeError("cooldown seconds must be an integer from 1 to 300"); } sceneExpiresAt.set( normalizedSceneCode, Number(readNow()) + seconds * 1000, ); return sync(); }; return Object.freeze({ start, sync, dispose: stopTimer, }); };