feat: drive timeline timing with a virtual clock instead of real timers#74
Merged
Conversation
Timing relied on wall-clock `setTimeout` (1ms per dash, `Tn` ms per
timer), making it slow, machine-dependent and prone to drift between
timelines meant to line up. Replace it with a deterministic virtual
`Clock` measured in frames: dashes advance the clock and `Tn` timers
resolve off it, so timing is consistent across runs regardless of the
event loop.
- Add the `Clock` default implementation and the `Clockable` interface
- Inject a clock via `Timeline.create(..., { clock })`, defaulting to a
fresh one; pass the same instance to advance timelines in lockstep
- Thread the clock through `parse`/item constructors via
`TimelineItemOptions`
- Document driving real timers in code under test with a fake-timers
`Clockable` adapter
BREAKING CHANGE: `TimelineParsable.parse` and `TimelineItem` constructors
now accept a `TimelineItemOptions` argument; custom parsers should forward
`options` to super so they share the timeline's clock. `TimelineTimer` is
backed by a `Clockable` rather than real wall-clock time.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This will make extending more deterministic
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Timing relied on wall-clock
setTimeout— 1ms per dash,Tnms per timer. That made the timeline slow, machine-dependent, and prone to drift between two timelines that are meant to line up frame-for-frame. The whole point of marble/timeline testing is deterministic timing, so the real-timer dependency was the core flaw.What
Replace wall-clock timing with a deterministic virtual
Clockmeasured in frames:Clock(default impl) +Clockableinterface — a frame counter withnow,wait(frames), andadvance(frames?). NosetTimeout, noDate.now().Timeline.create(str, { clock }). Omit it and each timeline gets its own; pass the same instance to advance two timelines in lockstep so their timers line up on every run.Timeline→parse(timeline, { clock })→ each item constructor viaTimelineItemOptions.TimelineTimernow derivesfinished/timeLeftfromclock.nowand gets its promise fromclock.wait(ms).Clockableadapter over@sinonjs/fake-timersso a timeline shares one clock with the realsetInterval/setTimeoutin code under test.Tests
tsc --noEmitclean, build clean,bun test→ 7 pass (originals + new coverage for frame advancement, deterministic timer completion, shared-clock identity, and theClockableinjection seam).Breaking changes
TimelineParsable.parseandTimelineItemconstructors now take aTimelineItemOptionsargument. Custom parsers should forwardoptionstosuper/the item constructor to share the timeline's clock.TimelineTimeris backed by aClockablerather than real wall-clock time.🤖 Generated with Claude Code