# script-module-state

> Talos next · Since next

a behaviour's `update`, `updateAll`, `pausedUpdate` or an event hook (`onEvent`, `onCollision`, …) or a function they reach writes a module-level `let` or `var` (`shots++`, `last = id`), or changes a module-level array, `Map`, `Set` or object (`seen.push(id)`, `cache.set(k, v)`, `marks[id] = 1`), directly, through a local alias (`const ref = st; ref.n++`), by a helper handed it (`add(seen, 1)`), or through a module-level class instance's method (`gun.fire()`), or writes a variable a closure keeps that a module-level call returned (`const tick = makeCounter()`), in a host built for networked play (`HostConfig::networked()`); a rollback re-simulates ticks and does not restore module variables, so peers drift; a variable written only in `init` and `dispose`, or in a helper only those reach (a course read once, a per-entity registry filled in `init` and emptied in `dispose`), is not reported, because a re-simulation runs neither again, unless it is also written in a per-tick or event hook or in a function `init` or `dispose` makes (a listener, a timer), or the write reads the variable's old value (`id++`, `n += 1`, `x = x + 1`): an `init` run again counts twice; a lazy cache (a write guarded by its own unset test, `if (!t) t = [1, 2]` or `t ??= build()`, of a value built from constants alone) is not reported either, but a flag (`ready = true`) is; the caveat is that an entity spawned or despawned on a predicted tick runs `init` or `dispose` there and a restore does not undo it, so such a registry must tolerate a stale entry (check the entity is `alive` before using one); said once per module version and variable, at the first attach, with the line (the `.ts` line when the module has a source map) and the hook; the module still runs

| | |
|---|---|
| Severity | `warning` |

## Meaning

a behaviour's `update`, `updateAll`, `pausedUpdate` or an event hook (`onEvent`, `onCollision`, …) or a function they reach writes a module-level `let` or `var` (`shots++`, `last = id`), or changes a module-level array, `Map`, `Set` or object (`seen.push(id)`, `cache.set(k, v)`, `marks[id] = 1`), directly, through a local alias (`const ref = st; ref.n++`), by a helper handed it (`add(seen, 1)`), or through a module-level class instance's method (`gun.fire()`), or writes a variable a closure keeps that a module-level call returned (`const tick = makeCounter()`), in a host built for networked play (`HostConfig::networked()`); a rollback re-simulates ticks and does not restore module variables, so peers drift; a variable written only in `init` and `dispose`, or in a helper only those reach (a course read once, a per-entity registry filled in `init` and emptied in `dispose`), is not reported, because a re-simulation runs neither again, unless it is also written in a per-tick or event hook or in a function `init` or `dispose` makes (a listener, a timer), or the write reads the variable's old value (`id++`, `n += 1`, `x = x + 1`): an `init` run again counts twice; a lazy cache (a write guarded by its own unset test, `if (!t) t = [1, 2]` or `t ??= build()`, of a value built from constants alone) is not reported either, but a flag (`ready = true`) is; the caveat is that an entity spawned or despawned on a predicted tick runs `init` or `dispose` there and a restore does not undo it, so such a registry must tolerate a stale entry (check the entity is `alive` before using one); said once per module version and variable, at the first attach, with the line (the `.ts` line when the module has a source map) and the hook; the module still runs

## Fix

keep the state in `ctx.state` (`ctx.state.shots = (ctx.state.shots ?? 0) + 1`), which a snapshot restore puts back; a module-level `const` that is only read, a variable only `init` and `dispose` set (and not counted up there), a lazily built constant cache, and anything written while the module body evaluates, is not state
