1import { deadlineStore, minuteTick } from "@t3tools/client-runtime/clock";
2import { useState, useSyncExternalStore } from "react";
3
4/** Re-renders once when `deadlineMs` (epoch ms) is reached; `null` never passes. */
5export function useDeadlinePassed(deadlineMs: number | null): boolean {
6 const store = deadlineStore(deadlineMs);
7 return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
8}
9
10/**
11 * `Date.now()` captured at mount and re-read when `deadlineMs` changes or
12 * passes, so comparisons against the deadline flip exactly once.
13 */
14export function useNowAfter(deadlineMs: number | null): number {
15 const passed = useDeadlinePassed(deadlineMs);
16 const readNow = () => ({ deadlineMs, passed, nowMs: Date.now() });
17 const [snapshot, setSnapshot] = useState(readNow);
18 // The clock is read inside the updater: the compiler caches a bare
19 // `Date.now()` in render once per instance, so a later adjustment would reuse
20 // the first reading. React discards this render and re-runs with the update.
21 if (snapshot.deadlineMs !== deadlineMs || snapshot.passed !== passed) setSnapshot(readNow);
22 return snapshot.nowMs;
23}
24
25/**
26 * `minuteTick`'s UTC `YYYY-MM-DDTHH:mm` as epoch ms. The snapshot carries no
27 * offset, and `Date.parse` reads offset-less date-times as local time.
28 */
29export function minuteTickToEpochMs(minute: string): number {
30 return Date.parse(`${minute}:00Z`);
31}
32
33/** Epoch ms floored to the minute, advancing each wall-clock minute. */
34export function useNowMinuteMs(): number {
35 return minuteTickToEpochMs(
36 useSyncExternalStore(minuteTick.subscribe, minuteTick.getSnapshot, minuteTick.getSnapshot),
37 );
38}