Clock
Clock: the one source of time for Flux and the code built on it. In a game it is the engine's own: now is os.clock(), wall is the server's synchronized time, and delay, wait, defer, cancel and connect are task.* and the RunService frame signals. Each call costs one branch over calling those directly.
Tests switch it to a manual clock (manual(true); headless Luau is always manual), where time only moves when the test says so: step(seconds) advances it in frames and runs what came due, in order.
local Clock = Flux.Clock
Clock.manual(true)
Clock.delay(1, function() print("one second later") end)
Clock.step(1) -- prints, in 60 frames of 1/60 sA manual frame of dt seconds:
- advances
nowbydtand resumes every timer andwaitnow due, earliest deadline first (ties in the order they were scheduled); a timer scheduled for zero seconds while they run waits for the next frame, like the engine's; - calls the
PreRender/RenderSteppedsubscribers withdt; - steps Flux itself: every spring and tween by
dt, then aFlux.flush(); - calls the
Heartbeatsubscribers withdt; - after each of those phases,
drain()s the work deferred withdefer.
reset() drops every pending timer, wait, deferred call and manual subscriber (between tests); now keeps counting, so time never runs backwards. wall() on a manual clock starts at 2026-01-01T00:00:00Z.
Types
Phase type
type Phase = "PreRender" | "RenderStepped" | "Heartbeat"Functions
now
Clock.now(): numberSeconds on a monotonic clock, for measuring intervals: os.clock() in a game, virtual time on a manual clock.
wall
Clock.wall(): numberUnix time in seconds, for dates and countdowns shared between players: workspace:GetServerTimeNow() in a game, 2026-01-01T00:00:00Z + now() on a manual clock.
delay
Clock.delay<A...>(seconds: number?, fn: ((A...) -> ...any) | thread, ...: A...): threadRuns fn(...) (or resumes a thread) after seconds, like task.delay. Returns the thread, for cancel.
wait Yields
Clock.wait(seconds: number?): numberYields the running thread for seconds (default: one frame), like task.wait, and returns the time it waited. On a manual clock it waits for a step to pass the deadline, so it needs a coroutine to yield.
defer
Clock.defer<A...>(fn: ((A...) -> ...any) | thread, ...: A...): threadRuns fn(...) (or resumes a thread) once the current work finishes, like task.defer. On a manual clock it waits for drain() (which every step frame calls). Returns the thread, for cancel.
cancel
Clock.cancel(thread: thread)Cancels a thread from delay, defer or a wait, like task.cancel.
connect
Clock.connect(phase: Phase, fn: (dt: number) -> ()): () -> ()Calls fn(dt) every frame at phase ("PreRender", "RenderStepped" or "Heartbeat"). Returns a function that disconnects it. A manual clock calls it from step instead of the engine's frames.
manual
Clock.manual(on: boolean)Tests only: turns the manual clock on or off. Headless Luau is always manual (manual(false) does nothing there). Turning it on starts virtual time at os.clock(); turning it off drops what was pending, like reset().
isManual
Clock.isManual(): booleanTests only: whether the clock is manual.
drain
Clock.drain()Tests only: runs deferred work until none is left, including work it defers in turn. Errors after 10000 calls, when deferred work keeps deferring more.
step
Clock.step(seconds: number, frameLength: number?)Tests only: advances a manual clock by seconds, in frames of at most frameLength seconds (default 1/60). step(0) runs one frame of zero length: due timers, subscribers, a Flux step and the deferred work.
reset
Clock.reset()Tests only: drops every pending timer, wait, deferred call and manual subscriber. now keeps its value.