Schedule¶
Purpose¶
Schedule is a multiphase runner.
It groups system functions under named phases (e.g. "input", "update", "render"), then executes those phases in a chosen order. At phase boundaries, it can automatically:
- flush deferred structural commands (
world.flush(), only if commands are pending) - deliver events to the next phase (
world.swapEvents())
This lets you build a deterministic pipeline (input → simulation → rendering → audio, etc.) without running into "structural change during iteration" problems.
Construction¶
A Schedule is independent from World, you pass the world at run time.
Adding systems to phases¶
add(world: WorldApi, phase: string, fn: SystemFn): { after, before }¶
Registers fn under phase for the given world.
- You can register multiple systems under the same phase (they run in insertion order).
- Returns an object with
after()andbefore()methods for chaining phase constraints.
Example:
1 2 3 4 5 |
Phase ordering constraints¶
Constraints are phase-level (not system-level): they affect the relative order of phases, not the order of systems within a phase.
after(otherPhase: string): this¶
Constrain the most recently added phase to run after otherPhase.
You can chain multiple constraints:
after()must be called afteradd(...). Calling it before anyadd(...)throws an error.
before(otherPhase: string): this¶
Constrain the most recently added phase to run before otherPhase.
before()must be called afteradd(...). Calling it before anyadd(...)throws an error.
Selecting a phase order¶
Schedule.run() chooses a phase order using the following precedence:
- If
run(..., phaseOrder)is provided, it is used as-is. - Else, if
setOrder([...])was called, the stored order is used. - Else, an order is computed from
.after()/.before()constraints (stable topological sort).
setOrder(phases: string[]): this¶
Set a default phase order used by run(world, dt) when no phaseOrder is passed.
Phase boundary behavior¶
setBoundaryMode(mode: "auto" | "manual"): this¶
Controls what happens after each phase:
"auto"(default):- if
world.cmd().hasPending()→world.flush() - always
world.swapEvents()
- if
"manual":- do nothing automatically; the caller is responsible for
world.flush()/world.swapEvents()
- do nothing automatically; the caller is responsible for
Running phases¶
run(world: WorldApi, dt: number, phaseOrder?: string[]): void¶
Runs the schedule for a single tick:
- Executes phases in the chosen order.
- Executes all systems registered under each phase.
- Applies phase boundary behavior according to
setBoundaryMode().
Example (explicit order):
Example (computed order from constraints):
1 2 3 4 5 6 7 |
Errors and lifecycle notes¶
- System errors: If a system throws,
Schedulerethrows a wrapped error with context:"[phase=<phase> system=<name>] <message>"
- Cyclic constraints: If constraints contain a cycle and no explicit order is provided,
run()throws. - No phase order: If no phase order can be determined (no explicit order, no stored order, and nothing scheduled),
run()throws:Schedule.run requires a phase order (pass it as an argument or call schedule.setOrder([...]))
Lifecycle conflict detection¶
Schedule.run() and World.update() are mutually exclusive on the same World instance:
- If you register systems via
world.addSystem()and then callschedule.run(), an error is thrown. - If you register systems via
schedule.add()and then callworld.update(), an error is thrown.
This prevents confusing behavior from mixing two different system execution models in the same world.
Choose ONE approach:
| Approach | Register systems with | Run with |
|---|---|---|
| Simple (single-phase) | world.addSystem(fn) |
world.update(dt) |
| Multi-phase | schedule.add(world, phase, fn) |
schedule.run(world, dt, phases) |
Relationship to World.update(dt)¶
| Feature | World.update(dt) |
Schedule.run(world, dt, phases) |
|---|---|---|
| System registration | world.addSystem(fn) |
schedule.add(world, phase, fn) |
| Phase support | Single implicit phase | Multiple named phases |
| Phase ordering | N/A | Via after()/before() or explicit order |
| Command flush | Once at end | After each phase (if pending) |
| Event swap | Once at end | After each phase |
| Best for | Simple game loops, prototyping | Complex pipelines, deterministic ordering |