Skip to content

Reload Lifecycle Hooks

The full swap pipeline runs in this order: dispose → run the new chunk → merge → accept__on_reload. Each step is optional and errors in any hook are logged but never abort the swap.

Post-swap notification: __on_reload

The simplest way to react to a reload is to define pub fn __on_reload(name: str) in the module. The function is called last, after the full merge, and is useful for structured logs, telemetry, or invalidating secondary caches.

In the example below, plugin.zolo counts how many times it has been reloaded and prints that counter on every swap:

Edit the formula in process; the hook counts and displays the number of reloads.

plugin.zolo
// Feature: __on_reload(name) hook

// Syntax: `pub fn __on_reload(name: str)` — fired AFTER the swap.

// When to use: structured logging, invalidating secondary caches,

// re-emitting status messages, dev telemetry.

//

// Fired last, after the merge and after __hot_accept.

// Errors here are logged but do not abort the swap.


use std::package
use std::stats

let stats = #{reloads: 0}

pub fn process(input: int) -> int {
  // Edit the formula below to see the reload in action.

  return input * 2
}

pub fn __on_reload(name: str) {
  let live = package.loaded["plugin"]
  live.stats.reloads = live.stats.reloads + 1
  print(">>> [hook] module '{name}' reloaded ({live.stats.reloads}x)")
}

Requires the Zolo CLI/host — open in the playground or run locally.

main.zolo
// Feature: custom notification on swap
// Syntax: define `pub fn __on_reload(name)` in the module.
// When to use: integrate with your own logging/telemetry system.

use plugin::{process}

fn main() {
  print("ENTER calls process(7). Edit plugin.zolo and watch the hook fire.")
  while true {
    let line = io::read("*l")
    if line is nil || line == "q" {
      break
    }
    print("process(7) = {process(7)}")
  }
}

Requires the Zolo CLI/host — open in the playground or run locally.

Explicit migration: #[hot_dispose] and #[hot_accept]

When automatic type-based preservation is not enough — for example, when changing the state schema — the @hot_dispose and @hot_accept attributes give full control.

@hot_dispose is called before the new chunk runs and must return a snapshot of everything that needs to survive. @hot_accept is called after the merge and receives that snapshot, and can migrate fields or rebuild derived structures.

Change the version field from 2 to 3 and observe the migration being applied.

cache.zolo
// Feature: explicit migration with __hot_dispose / __hot_accept

// Syntax: `#[hot_dispose] fn ...` and `#[hot_accept] fn ...(prev: any)`.

// When to use: the state's schema changed, OR we want to rebuild

// derived caches, OR the preservation defaults are not sufficient.

//

// Pipeline:

//   1. BEFORE the swap: we call `dispose` -> returns a snapshot.

//   2. New chunk runs (fresh state).

//   3. AFTER the merge: `accept(snapshot)` re-installs / migrates.


use std::package

let entries: any = #{}
let version: int = 2

pub fn add(key: str, value: any) {
  let live = package.loaded["cache"]
  live.entries[key] = value
}

pub fn dump() {
  let live = package.loaded["cache"]
  var n = 0
  for (_, _) in live.entries {
    n = n + 1
  }
  print("cache: {n} entries, schema v{live.version}")
}

@hot_dispose
fn save_state() -> any {
  let live = package.loaded["cache"]
  print(">>> dispose: capturing {live.version}")
  return #{
    entries: live.entries,
    version: live.version,
  }
}

@hot_accept
fn load_state(prev: any) {
  let live = package.loaded["cache"]
  if prev == nil {
    print(">>> accept: no previous state, skipping")
    return
  }
  if prev.version == live.version {
    // same schema: full migration.

    live.entries = prev.entries
    print(">>> accept: schema v{prev.version} preserved in full")
  } else {
    // different schema: copy compatible fields.

    live.entries = prev.entries ?? #{}
    print(">>> accept: migrated v{prev.version} -> v{live.version}")
  }
}

Requires the Zolo CLI/host — open in the playground or run locally.

main.zolo
// Feature: controlled state migration
// Syntax: `#[hot_dispose]` + `#[hot_accept]` in the module.
// When to use: schema migration, explicit rebuild, fine control
// when the preservation defaults are not enough.

use cache::{add, dump}

fn main() {
  add("user:1", #{name: "Ana"})
  add("user:2", #{name: "Bia"})
  dump()
  print("ENTER reprints. Edit cache.zolo (e.g. change format or bump version) and save.")
  while true {
    let line = io::read("*l")
    if line is nil || line == "q" {
      break
    }
    dump()
  }
}

Requires the Zolo CLI/host — open in the playground or run locally.

Challenge

In the cache.zolo module, add a field created_at: str = "unknown" to the snapshot inside @hot_dispose and make @hot_accept log it in the print. Confirm that the value appears correctly even after multiple reloads.

enespt-br