Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
62 lines
1.4 KiB
TypeScript
62 lines
1.4 KiB
TypeScript
"use client"
|
|
|
|
import { useSyncExternalStore } from "react"
|
|
|
|
import { getDelayUntilNextMinute } from "@/features/home/lib/date-time"
|
|
|
|
type Listener = () => void
|
|
|
|
const listeners = new Set<Listener>()
|
|
let currentTime: Date | null = null
|
|
let timeoutId: number | undefined
|
|
|
|
function emitCurrentTime(): Date {
|
|
const nextTime = new Date()
|
|
currentTime = nextTime
|
|
listeners.forEach((listener) => listener())
|
|
return nextTime
|
|
}
|
|
|
|
function scheduleNextTick() {
|
|
const observedTime = emitCurrentTime()
|
|
|
|
// 每次 tick 都重新对齐;标签页休眠或主线程阻塞后不会把延迟永久带入后续分钟。
|
|
timeoutId = window.setTimeout(
|
|
scheduleNextTick,
|
|
getDelayUntilNextMinute(observedTime),
|
|
)
|
|
}
|
|
|
|
function startClock() {
|
|
scheduleNextTick()
|
|
}
|
|
|
|
function stopClock() {
|
|
if (timeoutId !== undefined) window.clearTimeout(timeoutId)
|
|
timeoutId = undefined
|
|
currentTime = null
|
|
}
|
|
|
|
function subscribe(listener: Listener) {
|
|
listeners.add(listener)
|
|
if (listeners.size === 1) startClock()
|
|
|
|
return () => {
|
|
listeners.delete(listener)
|
|
if (listeners.size === 0) stopClock()
|
|
}
|
|
}
|
|
|
|
function getSnapshot() {
|
|
return currentTime
|
|
}
|
|
|
|
function getServerSnapshot() {
|
|
return null
|
|
}
|
|
|
|
export function useClock(): Date | null {
|
|
// 服务端与 hydration 首帧固定为空;订阅建立后再读取浏览器时间,避免首屏不一致。
|
|
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
|
|
}
|