Skip to main content

Timers, subscriptions and presence

A hub hears about more than calls. Each of these is a method on Hub with a default that does nothing, so a hub implements only what it uses.

HookWhen
on_timer(ctx, name)A timer the hub set is due.
on_topic(ctx, msg)Another instance published on a topic this hub subscribes to.
on_session(ctx, player, Session::Joined / Left)A player's connection subscribed to this hub's first topic, or left its last one.
on_presence(ctx, &presence)Root hub only: players entered or left the app's world, or the count changed.

These arrive as calls with reserved method names starting with $. Players and other instances cannot call a $ method, so a hub can trust where these came from.

Timers​

fn spawn(ctx: &Ctx, _seed: &[u8]) -> Result<Self> {
ctx.timer_every("tick", 100)?; // every 100 ms
Ok(Self::default())
}

fn on_timer(&mut self, ctx: &Ctx, name: &str) -> Result<()> {
match name {
"tick" => self.step(ctx),
"wave" => self.spawn_wave(ctx),
_ => Ok(()),
}
}
  • ctx.timer_after(name, ms) runs on_timer once; ctx.timer_every(name, ms) repeats, first one period from now. Setting a name again replaces that timer, so the name is its dedupe key. ctx.cancel_timer(name) removes it and ctx.timers() lists what is pending.
  • Timers are kept in the hub's snapshot, so they survive restarts and moves.
  • They fire only while the hub runs. One that came due while it was stopped fires once when it starts again; a repeating timer does not replay the periods it missed, and continues one period from then.
  • A repeating timer that keeps up stays on its schedule. The shortest period is 10 ms, and a hub may have 1,024 timers. Every timer due at the same moment runs in one call, which shares one call's fuel and deadline.
  • An error returned from on_timer is logged; the timer stays as scheduled.

Cron schedules​

ckx_sdk::cron (ckx-sdk 0.7.0) reads the legacy automations' cron syntax. Parse the expression, arm its next run with ctx.timer_cron, and arm it again each time it fires:

const RESTOCK: &str = "0 */6 * * *"; // every six hours, on the hour (UTC)

fn spawn(ctx: &Ctx, _seed: &[u8]) -> Result<Self> {
ctx.timer_cron("restock", &Cron::parse(RESTOCK)?)?;
Ok(Self::default())
}

fn on_timer(&mut self, ctx: &Ctx, name: &str) -> Result<()> {
if name == "restock" {
self.restock();
ctx.timer_cron("restock", &Cron::parse(RESTOCK)?)?;
}
Ok(())
}
  • Five fields (minute hour day-of-month month day-of-week), or six with seconds first. *, ?, lists, ranges and steps, month and day names, L (the month's last day), 5L (its last Friday), 1#2 (its second Monday), and @yearly, @monthly, @weekly, @daily, @hourly.
  • When both day fields are restricted, a day matches either one: 0 0 13 * 5 runs on the 13th and on every Friday.
  • Times are UTC. Cron::with_utc_offset(minutes) shifts a schedule by a fixed offset; there are no named time zones or daylight saving, and legacy had none either.
  • Cron::next_after(ms) is the next run after a time, for a hub that arms timer_after itself.
  • A run that came due while the hub was stopped fires once when it's back. The runs it missed are skipped, not made up, as the legacy dispatcher skipped them.

What keeps a hub running​

A hub runs while it is used, then persists and stops once it has been unused for its type's evict_after_ms (five minutes by default). Calls use it, and so does anyone subscribed to its topics, a player or another hub.

A pending timer keeps an otherwise unused hub running only while players are in the app: connected to an execution host, or in the app's world. When nobody has been in the app for a type's window, the app's instances of that type stop, however busy they keep each other. They start again when a player calls or subscribes, and the root hub starts when a player enters the world.

Subscriptions between hubs​

ctx.subscribe("arena", &ctx.key, "hp")?;

fn on_topic(&mut self, _ctx: &Ctx, msg: TopicMsg<'_>) -> Result<()> {
if (msg.node_type, msg.topic) == ("arena", "hp") {
self.boss_hp = msg.decode::<Hp>()?.hp;
}
Ok(())
}

A hub may subscribe to a topic of any type its manifest entry lists in calls, wherever the other instance runs. Subscriptions are kept in the snapshot and renewed whenever the hub starts again; ctx.unsubscribe ends one. A hub may hold 256. Spokes do not subscribe.

Sessions​

on_session tells a hub when a player's connection subscribes to its first topic on the hub, and when that connection unsubscribes from its last one or closes. It counts connections, not players: a player connected twice joins twice.

Presence​

The root hub hears who is in the app's world:

fn on_presence(&mut self, ctx: &Ctx, p: &Presence) -> Result<()> {
self.players = p.count;
for j in &p.joined { self.present.insert(j.player); }
for l in &p.left { self.present.remove(&l.player); }
ctx.publish("players", &encode(&self.players)?);
Ok(())
}
  • joined and left list user ids (and the player's actor, when known). They arrive within seconds of a player entering or leaving the world.
  • count is the number of players the realtime servers report for the app. It trails the roster when players leave: a player who stops sending is counted until their session ends, a few minutes later.
  • An event can be lost when the root hub moves. count is current in every event, and a restarted root hub is told it again within a minute.
  • For a player count clients can watch, publish it from on_presence as above; players subscribe to the root hub's topic.

Saving now​

A hub is snapshotted on its interval. For a change that should not wait (a purchase, a checkpoint), ctx.persist_now() snapshots soon after the current call returns, at most once a second.

Coming from game models and automations​

Legacyck-exec
Compute ticks (tickHz)ctx.timer_every
Automation schedules (interval)ctx.timer_every
Automation schedules (cron)ctx.timer_cron with a Cron from ckx_sdk::cron, the same syntax
gameModelScheduleInvoke, a function's timers effectctx.timer_after (the name is the dedupe key)
player_joined, player_left, player_count_changedon_presence on the root hub
gameModelActivePlayerCount and its subscriptionPublish the count from on_presence; clients subscribe
function_invoked, property_changed, container_createdThe hub knows when it changes; others ctx.subscribe to its topics
gameModelSeedThe type's seed_b64 in the manifest

Next: connect from a game.