Skip to content

feat: drive timeline timing with a virtual clock instead of real timers#74

Merged
johngeorgewright merged 3 commits into
masterfrom
clock
Jun 5, 2026
Merged

feat: drive timeline timing with a virtual clock instead of real timers#74
johngeorgewright merged 3 commits into
masterfrom
clock

Conversation

@johngeorgewright
Copy link
Copy Markdown
Collaborator

Why

Timing relied on wall-clock setTimeout — 1ms per dash, Tn ms 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 Clock measured in frames:

  • Clock (default impl) + Clockable interface — a frame counter with now, wait(frames), and advance(frames?). No setTimeout, no Date.now().
  • Injected, default-freshTimeline.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.
  • Threaded through parsing — the clock flows Timelineparse(timeline, { clock }) → each item constructor via TimelineItemOptions.
  • TimelineTimer now derives finished/timeLeft from clock.now and gets its promise from clock.wait(ms).
  • Docs — new "Consistent timing" section, plus a "Driving real timers" example showing a Clockable adapter over @sinonjs/fake-timers so a timeline shares one clock with the real setInterval/setTimeout in code under test.

Tests

tsc --noEmit clean, build clean, bun test7 pass (originals + new coverage for frame advancement, deterministic timer completion, shared-clock identity, and the Clockable injection seam).

Breaking changes

TimelineParsable.parse and TimelineItem constructors now take a TimelineItemOptions argument. Custom parsers should forward options to super/the item constructor to share the timeline's clock. TimelineTimer is backed by a Clockable rather than real wall-clock time.

🤖 Generated with Claude Code

johngeorgewright and others added 3 commits June 5, 2026 10:51
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
@johngeorgewright johngeorgewright merged commit 07b3baf into master Jun 5, 2026
1 check passed
@johngeorgewright johngeorgewright deleted the clock branch June 5, 2026 10:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant