Skip to main content
The TimePort is the seam every pacing mechanism in the mobile app is built on: one monotonic clock, one wall clock, and a per-run scheduler for timers and frames.

Quick Start

1

Pick the real adapter at boot

createWebTime() builds a TimePort over the browser’s clock and frame loop. Pass it as time: to createApp.
2

Read a monotonic instant

Call time.nowMs() for delay budgets and elapsed measurements. Call time.epochMs() only when a record needs a real timestamp.
3

Poll or paint through a scheduler

Create one scheduler per run, then use every, setTimer + clearTimer, and requestFrame. Dispose it at teardown.

How It Works

The port carries two clocks and a scheduler factory, and each member has one job.

Why Two Clocks

One clock measures durations, the other stamps records, and collapsing them reintroduces the bug the port was carved to prevent. When the phone’s clock is corrected mid-turn — a common event on a wake from sleep — a wall-clock elapsed measurement can jump backwards. The publish gate’s MAX_HELD_CHARS and the coalescer’s maxDelayMs are both measured against nowMs, so pacing stays sane through a correction. Records that need to be sortable across devices use epochMs.

Executable Specification

The port’s first conformance suite runs the same cases against the fake clock and the web adapter, so the two cannot drift.
The fake and the real adapter run the same cases. every’s period was found discarded in the fake in two separate rounds; a test that only ever drives the fake cannot see it a third time.
Each of these cases is also driven by the contract fixture — time_every_fires_once and time_clear_does_nothing — so a case that lost its assertion is caught by the assertion no longer failing when the adapter is deliberately broken. See Adapter Conformance.
If you write a new time adapter (React Native, a different desktop shell), the contract needs a requestAnimationFrame shim in Node — the web adapter targets a browser. Without the shim requestFrame throws ReferenceError and the adapter cannot be contract-tested at all.

Common Patterns

The coalescer’s flush tick is a repeating every, stopped at dispose.
The publish gate arms a one-shot timer and clears it when a frame arrives.

Best Practices

Collapsing them makes elapsed measurements jump when the OS corrects the clock.
A scheduler that outlives its run carries closed gates into the next answer.
A leaked interval keeps the event loop alive for the app’s lifetime.
A gate that stays armed after a cancel forces a paint that no longer belongs.
A TimePort.every fake that never fires the tick makes every pacing test pass for the wrong reason.

Mobile Architecture

The coalescer and publish gate the port paces.

Shell & Adapters

The sibling port every conformance suite pins.