Summary#
on every (<TimeSpan>) { … } is a component cadence hook: the runtime runs the block over and over, on the
interval you name, for as long as the component is mounted. It belongs to the on <event> family alongside
on mount / on unmount's on mount / on unmount and the reactive on change.
It is an auto-invoked action, so it can do everything an action can — assign fields, create rows with
new Entity{ … }, and call server functions. That last one is most of the point: "refresh this from the server
every thirty seconds" is the canonical cadence.
component Dashboard() {
int seconds = 0;
on every (TimeSpan.FromSeconds(1)) { seconds = seconds + 1; }
render { Text($"up for {seconds}s"); }
}Signature#
on every (<TimeSpan>) { <statements> } // the common form
on every (<TimeSpan>) (int elapsed) { <statements> } // …plus how many intervals passed since the last runThe interval is a TimeSpan, never a bare number — TimeSpan.FromSeconds(5),
TimeSpan.FromMilliseconds(250), TimeSpan.FromMinutes(1). A plain 500 is refused, because nothing in the source
would say whether it meant milliseconds or seconds and the two are a thousand-fold apart.
The hook is anonymous, and — uniquely — a component may declare more than one.
Description#
The runtime arms a timer for the interval you named. When it fires it runs the block, and only once the block has returned does it re-read the interval and arm the next one — so a body slower than its own cadence runs back-to-back instead of stacking copies of itself, and a body that awaits a server call holds the clock while it waits.
Because it is driven by the wall clock rather than the display, a hidden tab does not stop it: the browser may
throttle the timer, but time still passes and the missed intervals are reported to the next run (see
missed ticks are coalesced). That is the property that makes it right for a poll and wrong for an animation, and the
reverse of on frame — see on every or on frame?.
Several clocks in one component#
Each on every block is its own independent clock. This is the ordinary case, not an exotic one:
component Monitor() {
int frames = 0;
int fps = 0;
live var rows = Reading.ToList();
on frame (double dt) { frames = frames + 1; }
on every (TimeSpan.FromMilliseconds(500)) { fps = frames * 2; frames = 0; } // a display counter
on every (TimeSpan.FromSeconds(30)) { rows.Refresh(); } // a server poll
render { Text($"{fps} fps"); }
}A clock's identity is where it is declared, not the interval it happens to hold — so two blocks that name the same interval are still two clocks, and changing an interval retimes the existing clock rather than creating a new one.
Here is a whole page that compiles — one counter on a one-second clock, a second clock at a different cadence, and a
on mount beside them, so the three hooks are seen coexisting rather than described:
[Route("/uptime")]
[Render(CSR)]
component UptimePage() {
int seconds = 0;
int minutes = 0;
string label = "";
on mount { label = "counting"; }
on every (TimeSpan.FromSeconds(1)) { seconds = seconds + 1; }
on every (TimeSpan.FromMinutes(1)) { minutes = minutes + 1; }
render {
Stack(gap: 2) {
Text($"{label}: {seconds}s");
Text($"{minutes} minute(s)");
}
}
}Can the interval depend on state?#
The interval is an ordinary expression, re-read on each tick, so a cadence can depend on state:
component Game() {
int level = 1;
// Every ten levels takes a slice off the interval — the piece falls faster as you play.
on every (TimeSpan.FromSeconds(0.75 - (level - 1) * 0.06)) { StepDown(); }
}A change takes effect at the next tick: the interval already in flight runs to completion, then the clock re-reads and re-arms. Nothing cancels a pending tick early.
What happens to a tick that was missed?#
A page that could not keep up — a stalled tab, a body slower than its own interval — does not accumulate a queue of runs. The missed intervals are coalesced into the next run, which is told how many there were:
on every (TimeSpan.FromSeconds(1)) (int elapsed) {
clock = clock + elapsed; // count the time that really passed, not the ticks that really fired
}elapsed is 1 on an ordinary tick and higher only when the page fell behind. This is why there is no "what happens
on overrun?" setting: ignoring elapsed skips the backlog, and looping over it catches up — the choice is a line
of your code rather than a mode. It is the same shape Schedule uses for a missed server job, which records one
occurrence carrying the number it absorbed.
The next tick is armed only after the body returns — including a body that awaited a server call — so a slow body runs back-to-back rather than stacking copies of itself.
What happens when a beat fails?#
A body that throws — a poll whose request was refused, a row that was not there — has failed one beat. The clock keeps running:
- The first failure of a streak is reported, as an error, with the component, the member and the message. It is
in the browser console, in
__osy.logsand the developer panel, and inosy logs. - The failures after it are counted, not reported. A page held open over a broken endpoint writes one line, not one line per interval.
- The clock backs off. The wait before the next beat is the cadence doubled once for each failure in a row — two, four, eight times the cadence — and never more than 60 seconds. A cadence that is already longer than that is not stretched: a five-minute poll that fails is asked again in five minutes.
- The first beat that succeeds puts the clock back on its cadence and says how many beats had failed.
Only the clock that failed backs off; the ones beside it keep their own cadence. So the obvious code is the resilient
one — a poll needs no try of its own to survive a bad minute:
on every (TimeSpan.FromSeconds(6)) { plan = PlanOf(project); } // refused once: tried again in 12 s, then every 6 sCatch the failure yourself when the page should say something about it — "this may be behind" — because a caught exception is a beat that succeeded, and the clock stays on its cadence:
on every (TimeSpan.FromSeconds(6)) {
try { plan = PlanOf(project); missed = 0; }
catch (Exception e) { missed = missed + 1; }
}elapsed counts whole intervals since the last run, so the beat after a backed-off wait is told how much time really
passed. The developer panel (Ctrl+Shift+D) lists each clock under its component — on its cadence, or failing, how
many beats in a row and when it tries next — and __osy.components carries the same as clocks.
on frame is different, and deliberately: a frame body that throws stops its loop, because the next frame is
sixteen milliseconds away and would throw the same thing.
on every or on frame?#
Both repeat; they are for different things and neither substitutes for the other.
on frame (double dt) | on every (TimeSpan) | |
|---|---|---|
| driven by | the display (~60 times a second) | the wall clock, on your interval |
| hidden tab | pauses — the display is not drawing | keeps running (the browser may throttle it) |
| can reach the server | no | yes |
| how many per component | one | as many as you like |
| for | animation, physics, anything per-frame | polls, counters, "again in N seconds" |
Choose on frame when the answer to "how often?" is "every time the screen updates". Choose on every for
everything else — a five-minute poll on the frame clock would wake sixty times a second to do nothing 17,999 times out
of 18,000, and a poll on the frame clock silently stops the moment the reader switches tabs.
Examples#
The canonical cadence — refresh from the server on an interval, and a second clock at a different rate on the same
component, which is the part that has no equivalent in on frame:
entity Reading { int Value; }
[AllowAnonymous]
int Latest() { return Reading.OrderByDescending(r => r.Value).Select(r => r.Value).FirstOrDefault(); }
[Route("/dashboard")]
[AllowAnonymous]
component Dashboard() {
int latest = 0;
int seconds = 0;
// Reaches the SERVER — the whole point of the wall-clock hook.
on every (TimeSpan.FromSeconds(30)) { latest = Latest(); }
// A second clock, its own cadence. `elapsed` is 1 on an ordinary tick and higher only if the page fell behind,
// so counting it rather than counting ticks keeps the number honest across a stalled tab.
on every (TimeSpan.FromSeconds(1)) (int elapsed) { seconds = seconds + elapsed; }
render {
Stack(gap: 2) {
Text($"latest {latest}");
Text($"up {seconds}s");
}
}
}A clock outlives a beat that fails. The gauge below refuses the first time it is asked; the test fires the clock twice with Ui.Tick and the second beat is the proof that the first did not stop it:
[Principal] entity User {
[Required, MaxLength(200)] string Email;
security { allow read when IsAuthenticated; }
}
// The gauge does not answer the first time it is asked.
[AllowAnonymous]
int ReadGauge(int asked) {
if (asked == 1) { throw new Exception("the gauge did not answer"); }
return asked * 10;
}
[Route("/tide")]
[AllowAnonymous]
[Render(CSR)]
component Tide() {
int asked = 0;
int level = 0;
on every (TimeSpan.FromHours(1)) {
asked = asked + 1;
level = ReadGauge(asked);
}
render { Text($"Asked {asked} times, the tide is at {level}."); }
}
[Test]
void a_beat_that_fails_does_not_end_the_clock() {
Ui.Visit("/tide");
Assert.Visible("Asked 0 times, the tide is at 0.");
Ui.Tick(); // the gauge refuses: one failed beat
Assert.Visible("Asked 1 times, the tide is at 0.");
Ui.Tick(); // the clock is still armed, and this beat succeeds
Assert.Visible("Asked 2 times, the tide is at 20.");
}Limits — the clock runs on the client#
- The hook runs on the client. A cadence that must survive the page being closed is a server concern — declare a
Scheduleinstead, which is durable and operator-editable. - A body that throws does not stop its clock — see a beat that fails. An interval that does not evaluate to a duration does: there is no cadence to run on, and the log says so.
- The clock is disposed with the component, so navigating away stops it.
See also#
- on mount / on unmount —
on mount,on unmountand the per-frameon frame - on change — the reactive sibling: run a block when a value changes, rather than when time passes
- The reactivity & lifecycle model — how a field a cadence writes reaches the screen
- Ui.Tick — testing a cadence:
Ui.Tick()fires it from a UI test, so the test never waits on its timer