On this page
- 4.1 Three values, three different purposes
- 4.2 Built-in properties are Store operations
- 4.3 Synchronous rules read current and write next
- 4.4 Registration has four separate responsibilities
- 4.5 Example: a synchronous patch variable with explicit reset
- 4.6 Example: parameter-driven Main state without reallocating Stores
- 4.7 Turtle custom state and birth initialization
- 4.8 Listeners, updaters, and scheduling cautions
- 4.9 State review checklist
4. Managing model state
Manual contents | Previous: Execution and lifecycle | Next: Programming Main | Supporting types reference
Use this chapter when adding a model variable, changing a built-in property, or diagnosing a reset or update-order problem. The lifecycle chapter explains when rules execute.
Lifecycle selection: examples in this chapter assume top-level
Project.lifecycle: "aligned"; see
the aligned contract.
Legacy differences are called out explicitly, not used as the teaching default.
Store operations themselves are the same in both modes. If you are new to the
API, begin with the fire tutorial: built-in color/nextColor
are enough for its first synchronous rule.
The shipped Life and Antz migrations illustrate two ownership patterns. Life
retains each patch's custom Store and color listener across world resets;
setup restores active-cell scheduling without registering another listener.
Antz retains patch Stocks but replaces turtles. Its model-specific world-reset
wrapper retires each old turtle's food Stock and grid link before replacement;
death performs the same cleanup through unlink. Local Main reset does not
retire those child resources. The runtime does not automatically infer
ownership of arbitrary Stock.create(..., main) registrations; custom models
must arrange appropriate cleanup when the owning object is discarded.
State ownership in aligned models
Allocate each Store once in the constructor, initialize/register it once in
init() (or wire it in the constructor if an early setter needs it), restore
it in the object's local reset(), and
rebuild population relationships in runtime-invoked setup(). For example:
class Main extends NLSimMain {
constructor(Patch) {
super();
this.Patch = Patch;
this.total = new Store(0);
}
init() {
super.init();
this.synchronize(this.total);
}
initCells() { this.initializePatches(this.Patch); }
reset() {
super.reset();
this.total.reinitialize();
}
setup() {
// Both configured populations now exist.
this.total.set(this.patches.toList().length);
}
}
Use this class only with the aligned Project option. A world reset is
main.resetWorld() (or the browser Reset command); aligned main.reset()
is local and does not recreate populations. Patch reset likewise restores
only local state. Turtle local reset restores its configured Store list but
does not call setup. World reset still recreates turtles; do not retain
references to the prior population.
Aligned birth helpers initialize inherited/default built-in state before
init(), run the customization callback, then defer setup until insertion.
Do not repeat init/setup inside the customization callback. None of these
changes automatically reset custom Stores, dispose listeners registered on
long-lived objects, or migrate legacy constructor/reset code.
4.1 Three values, three different purposes
A Store separates:
- Current: the committed value other synchronous rules normally read.
- Next: the pending value to commit at a shift boundary.
- Initial: the value or factory used to restore state on reinitialization.
The API implementation is in store.js.
| Operation | Current | Next | Reset baseline |
|---|---|---|---|
new Store(value) |
Set to value | Set to value | Set to value |
store.get() |
Read | Unchanged | Unchanged |
store.next = value |
Unchanged | Set to value | Unchanged |
store.set(value) |
Set to value immediately | Set to value | Unchanged |
store.initialize(value) |
Set to value immediately | Set to value | Replace |
store.shift() |
Set from next | Retained | Unchanged |
store.reinitialize() |
Restore from initial | Restore from initial | Retained |
shift() does not restore next to a fresh default after commitment: current and
next then normally contain the same value.
This standalone example shows the difference:
import {Store} from "@nwk/api/store.js";
const state = new Store(2);
state.next = 3;
console.assert(state.get() === 2);
state.shift();
console.assert(state.get() === 3);
state.reinitialize();
console.assert(state.get() === 2);
state.set(4);
state.reinitialize();
console.assert(state.get() === 2);
state.initialize(5);
state.next = 6;
state.reinitialize();
console.assert(state.get() === 5 && state.next === 5);
Manual shift is appropriate in this isolated demonstration. In a running model, register Stores and let Main commit them.
Factories and mutable objects
new Store(factory) and initialize(factory) call the zero-argument factory
and retain it for future reinitialization. A factory can produce fresh state.
Objects and arrays are not automatically cloned. Mutating a stored object in place can modify current, next, and initial state through shared references. Prefer scalar values or replace values deliberately; use a factory when a fresh object is required at reset. Do not use in-place mutation as a substitute for a next-state assignment.
4.2 Built-in properties are Store operations
For patch color and turtle appearance/motion, property names encode different operations:
| Example | Meaning |
|---|---|
patch.color = 1 |
Initialize current/next color and replace its reset baseline |
patch.currentColor = 1 |
Set current/next color immediately without replacing the baseline |
patch.nextColor = 1 |
Schedule color for commitment |
turtle.theta = angle |
Initialize heading and replace its reset baseline |
turtle.currentTheta = angle |
Change current/next heading immediately |
turtle.nextTheta = angle |
Schedule a heading change |
Turtles also have nextLocation, nextSize, nextShape, and nextSpeed.
These named properties are preferable to manipulating underscore-prefixed
implementation fields in ordinary model code.
Use public updater properties for built-in state as well:
this.colorUpdater = () => this.food.get();
this.thetaUpdater = () => this.mySniffer() + this.wiggle();
These are initialization fragments for a model defining food, mySniffer(),
and wiggle(). colorUpdater is available on patches and turtles; turtles also
provide thetaUpdater, sizeUpdater, and shapeUpdater. Assign the function,
not its evaluated result. For example, use this.colorUpdater = fn, not
this._color.setUpdater(fn) or this._color.updater = fn.
The public properties are the model-author interface; underscore-prefixed
built-in Stores are implementation details, not required user knowledge.
In aligned models, call super.patchStep() or super.turtleStep() to evaluate
configured built-in updaters and schedule their results. Patch's base step
evaluates colorUpdater. Turtle's base step first schedules trajectory movement,
then evaluates thetaUpdater, colorUpdater, sizeUpdater, and shapeUpdater
in that order. An unset updater does nothing. Results remain pending until shift.
Heading changes therefore affect the subsequent trajectory move, preserving
Antz's original ordering.
Do not also evaluate those updaters explicitly: that would run them twice. Evaluation happens per base-step call, not necessarily once per tick; patch visitation can invoke a rule more than once. A subclass that omits its base-step call also omits automatic updater evaluation.
Legacy models retain explicit evaluation and are not changed by this behavior.
Custom model-owned Stores, such as food, remain distinct from built-in state.
Do not generalize every property setter to the same behavior. For example,
initLocation(x, y) initializes both location Stores, while moveNext(x, y)
schedules position. See the implementation of the particular operation.
4.3 Synchronous rules read current and write next
Suppose two interacting values start at left = 1, right = 3, and should swap
simultaneously. Read both current values before commitment:
import {Store} from "@nwk/api/store.js";
const left = new Store(1);
const right = new Store(3);
left.next = right.get();
right.next = left.get();
console.assert(left.get() === 1 && right.get() === 3);
left.shift();
right.shift();
console.assert(left.get() === 3 && right.get() === 1);
Using immediate assignments instead would let the second rule observe the first rule's new value. The same issue arises when neighboring patches or interacting turtles share state.
Next-state writes do not by themselves resolve competing writes. If several rules write the same Store's next value, the last assignment wins. If a model needs accumulation or conflict resolution, define it explicitly rather than assuming the scheduler will combine proposals.
4.4 Registration has four separate responsibilities
A Store does not become synchronized merely because it is a field on Main, Patch, or Turtle.
| Concern | Existing mechanism | What it does not imply |
|---|---|---|
| Synchronization | main.synchronize(...stores) |
No automatic custom-state reset, patch activation, or output |
| Patch reporting | main.synchAndReport(patch, ...stores) |
Does not serialize arbitrary custom fields; reported patch data is coordinates/color |
| Patch activation | main.synchAndVisitNext(patch, ...stores) or explicit visitation |
Does not establish reset ownership |
| Activation and reporting | main.synchReportAndVisitNext(patch, ...stores) |
Does not create a general custom-state serializer |
| Reset | Owner explicitly restores its custom state | Synchronization alone is insufficient |
The registration methods install a Store's nextPinger callback. A next-state
write marks the Store as touched; Main commits touched Stores during shift.
Registration does not itself initialize values or queue an already pending write.
Register before scheduling changes.
These registrations replace the same callback slot; they are not additive.
Calling plain synchronize() after synchAndReport() on the same Store would
remove its report side effect. Avoid re-registering built-in Stores.
Implementation caveat: the combined helpers also assign an
InitializePinger member, but Store.initialize() uses initializePinger
(lowercase initial letter). Do not rely on the uppercase member as a working
initialization listener. Normal initialization writes next and invokes the
installed nextPinger; the mismatched member is a separate legacy defect.
Store.create(value, main) constructs a Store, adds it to main.stores, and
registers synchronization. It does not make NLSimMain.reset() automatically
walk that collection. Reset policy must still be explicit.
4.5 Example: a synchronous patch variable with explicit reset
This Patch subclass cools from ten units to zero and colors itself while warm.
It registers one persistent visit per patch, so it does not require active-cell
scheduling logic. Use it with a Main that calls
initializePatches(CoolingPatch), Project visitMode: "C", a two-entry cell
palette, and no cell-start color overrides for this example.
import {NLPatch} from "@nwk/api/nlpatch.js";
import {Store} from "@nwk/api/store.js";
class CoolingPatch extends NLPatch {
constructor(col, row, main) {
super(col, row, main);
this._heat = new Store(10);
}
init() {
this.main.synchronize(this._heat);
this.color = 1;
}
setup() {
this.main.visitAlways(this);
}
get heat() {
return this._heat.get();
}
patchStep() {
super.patchStep();
const nextHeat = Math.max(0, this.heat - 1);
this._heat.next = nextHeat;
this.nextColor = nextHeat > 0 ? 1 : 0;
}
reset() {
super.reset();
this._heat.reinitialize();
}
}
export default CoolingPatch;
Expected behavior: heat is still 10 after the first step rule but becomes 9
after shift. After ten ticks, heat and color are zero. Reset restores heat 10
and color 1 on the same patch object.
The base patch reset only handles color; the custom _heat reset is necessary.
Setup repeats the idempotent Set registration after world reset.
Color already has built-in reporting registration. _heat is synchronized but
is not independently transmitted to the browser. Main can aggregate it and
publish it to a configured display if required.
4.6 Example: parameter-driven Main state without reallocating Stores
This Main subclass starts a reserve from an input and consumes one unit per
tick. Supply Project input code "RESERVE", display code "AMOUNT", and
visitMode: "No" for this nonspatial-rule example. The spatial runtime still
needs its usual cell configuration and a supplied Patch class.
import {NLSimMain} from "@nwk/api/nlsimmain.js";
import {Store} from "@nwk/api/store.js";
class ReserveMain extends NLSimMain {
constructor(Patch) {
super();
this.Patch = Patch;
this._reserve = new Store(0);
}
init() {
super.init();
this.synchronize(this._reserve);
this.restoreReserve();
}
initCells() {
this.initializePatches(this.Patch);
}
reset() {
super.reset();
this.restoreReserve();
}
restoreReserve() {
this._reserve.initialize(this.getParam("RESERVE"));
}
coordinate() {
this._reserve.next = Math.max(0, this._reserve.get() - 1);
}
update() {
this.show("AMOUNT", this._reserve.get());
}
}
export default ReserveMain;
Allocate once in the constructor, register in one-time init(), and share the
parameter-reading operation between init and local reset. This does not
replay init, replace the Store, or append listeners.
Changing "RESERVE" alone does not rewrite the reserve. On world reset, Main's
local reset() reads the current parameter, restores current/next state, and
establishes the new baseline. It does not allocate another Store. The first update with a
starting reserve of 3 publishes 2 at time 1.
4.7 Turtle custom state and birth initialization
Allocate custom turtle state once in the constructor after super(id, main).
Register it with main.synchronize(). If using the individual turtle
reset() operation, either add the Store to the turtle's existing stores
array or restore it explicitly; that list does not automatically discover
arbitrary fields. Do not assume Main's Store set and Turtle's Store array have
interchangeable registration methods.
World reset recreates turtles, so constructor initialization runs on new objects. Births also construct new objects, but built-in values are then assigned from defaults or the parent. Use the birth customization callback for child-specific values rather than depending on initial-population hooks.
The Fng compatibility scenario demonstrates custom water state initialized for the child and split with its parent. Reset correctness also requires discarding references to the old population.
4.8 Listeners, updaters, and scheduling cautions
store.addListener(fn)appends a callback with arguments(store, oldValue, newValue). It is called byinitialize(),set(), andshift(), not by a barenextwrite.- A listener is not guaranteed to mean "the value actually changed". A shift can notify with equal values.
- Current
initialize()converts a falsy old value tonullin the listener arguments. Do not use that argument to distinguish old zero from no value. - On a custom Store,
setUpdater(fn)installs a zero-argument updater;autoNext()explicitly evaluates it and assigns the result to next. Custom Stores are not automatically evaluated by the base step. Built-in public updater properties use the aligned base-step behavior described above. - Listeners should normally observe values, not schedule more Store writes during commitment. Main iterates a live touched-Store set and clears it after commitment; reentrant updates can depend on iteration order. Such rules need explicit design and tests.
- Main's aligned
init()runs once; setup repeats. Do not append long-lived listeners unconditionally in setup, or register replacement turtles on Main without disposing the old registrations. Legacy Main init repeats too: Follower's accumulatingMODlisteners are a known legacy app-level example, not a recommended pattern.
4.9 State review checklist
The Diffuse/Dla migration provides two additional examples:
- Diffuse aliases heat to public patch
color,currentColor, andnextColor. The heat setter deliberately usescurrentColor(Storeset), notcolor(Storeinitialize), so ordinary heat writes do not replace the reset baseline. Its one-time init retainssynchReportAndVisitNext(this, this._color)because the registration helper requires the underlying Store; no public scalar accessor substitutes for registering scheduling/reporting callbacks. - Dla retains one custom status Store/listener per patch. Status changes schedule
public
nextColorand mark the patch for reporting. Reset also setscurrentColorbefore reseeding, so status/current/pending color agree immediately, even if an earlier color initialization changed the built-in reset baseline. Main's ordinaryradiusandctrfields require explicit local reset; count is published in setup after the world is restored. Turtles use publicnextTheta. - EGOL3's overridden public
colorsetter initializes a weighted level Store. That Store and its synchronization/initialization callbacks must exist in the constructor, before the runtime applies configured colors. Reset first restores the built-in color, then derives level from that color using current NBI/MAT parameters; it must not replay a stale numeric weight. The level initializer reseeds live cells and their rings after world scheduling clears. Main setup recounts all states after patch reset and retains its original RAM and parameter listener.
For every custom variable, answer:
- Who owns it, and does that object survive reset?
- Is it a synchronous Store or an intentionally immediate JavaScript field?
- Who registers it, and who initializes and resets it?
- Does each step read current and write next, or intentionally accumulate next?
- Is its owner actually visited by the chosen mode and patch scheduling?
- Must it affect color, display output, or patch activation separately?
- Can a birth, parameter change, or repeated setup bypass its initialization?
- Does reset create stale references or duplicate listeners?
The compatibility tests verify current/next/initial behavior, while shipped-app checks exercise real reset and interaction scenarios. These chapters describe current contracts, not a promise that every legacy helper or every registration path has been repaired.