src — mini-react scheduling (stages 8–11)
tiny-test.mjs test runner (plumbing)
fake-dom.mjs instrumented DOM, shared with fw-02 (plumbing)
scheduler-harness.mjs deterministic clock + task queue (plumbing)
spec.mjs executable specification — run this
runtime.js ← YOU WRITE THIS
node spec.mjs
Prerequisite: bi-11. These tests describe behaviour whose justification lives there.
Additional required exports beyond fw-02
setScheduler({ now, schedule }) // injected clock + continuation primitive
setTimeSlice(ms) // budget before the work loop yields
startTransition(fn) // updates inside fn are low priority
Why time is injected
Interruptible rendering cannot be tested against real time — you get flaky tests that assert nothing. So the runtime takes its clock and its "schedule a continuation" primitive as inputs and the test drives both, stepping one continuation at a time.
This is not a testing trick. It is why Chromium's scheduler is testable at all, and why React's scheduler has an injectable host config. Making time an input rather than an ambient effect is a design property, and you should carry it into your own systems.
What the tests actually enforce
They deliberately do not prescribe a Fiber structure. They assert observable properties:
- Stage 9 — a 60 ms render with a 5 ms slice must span multiple continuations. A recursive
renderer cannot satisfy this at all; that failure is the derivation in
CONCEPTS.md§2 step 2, made concrete. - Stage 10 — the DOM must not change while render work is pending, and exactly one continuation may perform DOM operations. Render builds; only commit mutates.
- Stage 11 — an urgent update must beat in-progress transition work, and a superseded render must leave no trace. That second one is why the render phase has to be side-effect free — not because "React is functional," but because discardable work cannot have done anything yet.
If stage 10 fails you will have seen a half-updated UI in a test rather than in production, which is the whole reason to build it this way.
What stage 11 caught when this spec was written
The reference implementation used to validate this spec failed the last test, and the reason is worth knowing before you hit it yourself.
Its scheduler treated a new update arriving mid-render as "same priority, keep going." So a render
already half-finished with n = 1 continued after the state became 2, and the commit contained
components that had seen the old state and components that had seen the new one.
That is tearing — and it is the same defect, at framework level, that useSyncExternalStore
exists to prevent at application level (fw-11).
The fix is one line of policy: any new update makes in-flight render work stale, so discard it and restart. Note what makes that legal — you can only throw work away if it has not done anything yet. Stage 11's two tests are therefore the same requirement stated twice: render must be pure so that it can be discarded, and it must be discarded so that output is consistent.
You are likely to write the same bug. The test will catch it.