API Reference
W API Reference
Section titled “W API Reference”// Node reducer — call inside Behaviors.collectW.reduce(state, pulse, nodeId, handlers) → newState
// Stability check — use in _isStableW.stable(nodes, pulse) → boolean
// Export world state to world.app (call from world program)W.export(Renkon, { node1, node2, ... }, isStable)
// Self-hosting clock mixin — spread into handlersW.localReflector(tickMsg, innerTickDelay) → handlersMixin
// Strip infrastructure fields (_queue, _nextAt, _depth, _lt)W.getState(node) → { ...userFields }Handler Context (ctx)
Section titled “Handler Context (ctx)”ctx.wallTime // current logical wallTime (= lt)ctx.logicalTime // current logicalTime (= lt)ctx.depth // current feedback depth
ctx.future(delay, msg, payload) // schedule at wallTime + delayctx.send(targetId, msg, payload) // cross-node via outboxctx.feedback(msg, payload, maxDepth) // depth-tracked future at wallTime + _fbStepMsctx.futureInf(msg, payload) // fireAt = wallTime — re-enqueues every drain passctx.localReflector(tickMsg, delay) // sub-tick self-hosting clock stepctx Primitives
Section titled “ctx Primitives”| Primitive | Semantics |
|---|---|
ctx.future(delay, msg, payload) | Schedule msg after delay logical ticks |
ctx.send(nodeId, msg, payload) | Cross-node message via evalGen-gated outbox |
ctx.feedback(msg, payload, maxDepth) | Depth-tracked future at wallTime + _fbStepMs (convergence loops) |
ctx.futureInf(msg, payload) | fireAt = wallTime — re-enqueues every drain pass |
ctx.localReflector(tickMsg, delay, payload) | Sub-tick self-hosting clock step, with optional user payload |
ctx.future — Payload and Idempotency Guards
Section titled “ctx.future — Payload and Idempotency Guards”payload is any plain serialisable value (scalar, array, or object). The handler receives it as the second argument p:
ctx.future(delay, "beat", { depth: 2, cycleId: s.cycleId });
beat: (s, p, ctx) => { if (p.cycleId !== s.cycleId) return s; // stale — ignore // p.depth, p.cycleId available here}Why pass cycleId? Sub-tick futures fire asynchronously within the drain loop. If a new macro pulse arrives and resets the cycle before an earlier future fires, the handler will see both the old and new futures. Without a guard, the stale future corrupts state.
This is essential in cascading future chains — such as the Fractal Heartbeat:
// ECG / Fractal Heartbeat — depth cascade via ctx.futurebeat: (s, p, ctx) => { if (p.cycleId !== s.cycleId) return s; // stale cycle guard const { depth, delay } = p; ctx.future(delay, "beat", { depth, delay, cycleId: s.cycleId }); // re-fire self const childDelay = delay / 2; ctx.future(childDelay, "beat", { depth: depth + 1, delay: childDelay, cycleId: s.cycleId }); // spawn child depth}ctx.localReflector — Self-Hosting Clock
Section titled “ctx.localReflector — Self-Hosting Clock”ctx.localReflector(tickMsg, delay, payload) accepts an optional third argument merged into the tick pulse. This lets the local clock carry application-specific state across ticks:
// Phase-accumulating local clock...W.localReflector("tick", 0.05),
tick: (s, p, ctx) => { const phase = ((s.phase ?? 0) + 0.01) % 1; // advance phase each tick ctx.localReflector("tick", 0.05, { phase }); // pass phase forward in payload return { ...s, phase };}The payload is available on the next tick’s pulse p as p.phase.
Important:
W.localReflectordefines a__macrohandler. Do not also define__macroin the same handler object — the spread will silently overwrite one of them.
Sub-Tick Scheduling
Section titled “Sub-Tick Scheduling”Futures with delay < SUBTICK_MS = 1 drain within the current pulse:
future(0) → fireAt = wallTime → drains now (same drain pass)future(0.5) → fireAt = wallTime+0.5 → drains now (0.5 < SUBTICK_MS=1)future(1) → fireAt = wallTime+1 → waits next tickfuture(60) → fireAt = wallTime+60 → waits 60 ticksAll sub-tick steps are deterministic — every peer runs the same drain loop with the same logical wallTime. This enables:
- Synchronous multi-step computation —
future(0)chains drain in one pass - Zeno series — geometrically decreasing delays converging toward 1 tick
- Self-hosting clock nodes — via
W.localReflector
Incremental __macro
Section titled “Incremental __macro”__macro is called at most once per logicalTime. The application chooses what to do:
Total __macro — reschedules everything every cycle. Correct when every cycle produces new work.
Incremental __macro — only schedules work when inputs changed:
__macro: (s, p, ctx) => { const cur = _computeInput(p.logicalTime); const prev = _computeInput(p.logicalTime - 1); if (cur === prev) return { ...s }; // idle — zero queue churn // schedule futures for the new input}started guard — fire __macro once to bootstrap, then let futures drive cycles:
__macro: (s, p, ctx) => { if (s.started) return s; ctx.future(0, "startCycle", { cycleId: 1 }); return { ...s, started: true };}W.rng — Deterministic PRNG
Section titled “W.rng — Deterministic PRNG”The evaluator includes a deterministic xorshift128+ PRNG (W.rng) to ensure all peers produce identical random sequences from the same seed.
WARNING: never use
Math.random()inside world nodes — it is non-deterministic and will cause peers to desync.
W.rng.next() // → float in [0, 1)W.rng.nextInt(n) // → integer in [0, n)W.rng.seed(lt) // re-seed from logicalTime (deterministic per-cycle reset)W.rng.state() // → { s0, s1, s2, s3 } (snapshot)W.rng.restore(state) // restore from snapshotThe session seed is set once at startup via makeMeta:
const SESSION_RNG_SEED = { s0: 0x12345678, s1: 0x9abcdef0, s2: 0xdeadbeef, s3: 0xcafebabe };const meta = makeMeta(peerId, SESSION_RNG_SEED, REFLECTOR_MS);RNG Example — Deterministic Sequence Per Cycle
Section titled “RNG Example — Deterministic Sequence Per Cycle”const rngNode = Behaviors.collect( { values: [], cycleId: 0, started: false }, reflector, (state, pulse) => W.reduce(state, pulse, "rngNode", { __macro: (s, p, ctx) => { if (s.started) return s; ctx.future(0, "generate", { cycleId: 1, step: 0, values: [] }); return { ...s, started: true }; }, generate: (s, p, ctx) => { if (p.cycleId !== s.cycleId && p.cycleId > 1) return s; // stale guard if (p.step === 0) W.rng.seed(p.cycleId); // re-seed per cycle const v = W.rng.next(); const values = [...(p.values || []), v]; if (values.length < RNG_STEPS) { ctx.future(0, "generate", { cycleId: p.cycleId, step: p.step + 1, values }); } else { ctx.future(RNG_CYCLE_TICKS, "generate", { cycleId: p.cycleId + 1, step: 0, values: [] }); } return { ...s, values, cycleId: p.cycleId }; }, }));Local Reflector as Simulation Speed Control
Section titled “Local Reflector as Simulation Speed Control”The local reflector’s tick size directly defines the unit of computation — it can be used to decouple the simulation resolution from the network synchronisation rate.
outer tick = network synchronisation boundary (every 50ms real time)inner tick = simulation integration step (every 1/N logical units)
ratio N = inner_ticks_per_outer_tick = 1 / innerTickDelaySpeed Multiplier Table
Section titled “Speed Multiplier Table”innerTickDelay | Steps per outer tick | Equivalent simulation rate |
|---|---|---|
0.5 | 2 | 2× per network tick |
0.1 | 10 | 10× per network tick |
0.01 | 100 | 100× per network tick |
0.001 | 1000 | 1000× (near float floor) |
Physics Simulation Example
Section titled “Physics Simulation Example”const physics = Behaviors.collect( { pos: 0, vel: 1, step: 0, _localActive: false }, reflector, (state, pulse) => W.reduce(state, pulse, "physics", { ...W.localReflector("simulate", PHYSICS_STEP_VAL),
simulate: (s, p, ctx) => { const dt = p._innerTickDelay; const newPos = s.pos + s.vel * dt; const newVel = s.vel * 0.99; // damping const newStep = s.step + 1;
if (newStep < STEPS_VAL) { ctx.localReflector("simulate", dt); // reschedule at same rate }
return { ...s, pos: newPos, vel: newVel, step: newStep }; },
applyForce: (s, p, ctx) => { return { ...s, vel: s.vel + p.force }; }, }));Feedback Loop — ctx.feedback
Section titled “Feedback Loop — ctx.feedback”ctx.feedback("respond", { value, cycleId }, 64);ctx.feedback() schedules a message at wallTime + _fbStepMs and increments the wave’s depth counter by 1. If depth >= maxDepth the call is a silent no-op.
Wave Depth Diagram with Feedback
Section titled “Wave Depth Diagram with Feedback”Logical time T │ pulse ├── depth 0 estimator.__macro → ctx.future(0, "sendObserve") ├── depth 0 estimator.sendObserve → ctx.send("corrector", "observe") ├── depth 0 corrector.observe → ctx.feedback("respond") ├── depth 1 corrector.respond → ctx.send("estimator", "refine") ├── depth 1 estimator.refine → delta > ε → ctx.feedback("continueRefine") ├── depth 2 estimator.continueRefine → ctx.send("corrector", "observe") ├── depth 2 corrector.observe → ctx.feedback("respond") ├── depth 3 ...loop continues... └── depth N delta < ε → no ctx.feedback → queues drain → stableComplete Example — Fixed-Point Bisection
Section titled “Complete Example — Fixed-Point Bisection”Two nodes — estimator and corrector — running a bisection toward the nearest integer:
// estimator: proposes value, refines on correction__macro: (s, p, ctx) => { if (p.logicalTime % FB_CYCLE_MS !== 1) return s; const initial = 50 + 49 * Math.sin(p.wallTime * 0.0023); ctx.future(0, "sendObserve", { value: initial, cycleId: p.logicalTime, wt: p.wallTime }); return { ...s, value: initial, iterations: 0, cycleId: p.logicalTime };},sendObserve: (s, p, ctx) => { if (p.cycleId !== s.cycleId) return s; ctx.send("corrector", "observe", { value: p.value, cycleId: p.cycleId }); return s;},refine: (s, p, ctx) => { if (p.cycleId !== s.cycleId) return s; const delta = Math.abs(p.correction - s.value); const refined = (s.value + p.correction) / 2; if (delta > 0.01) ctx.feedback("continueRefine", { value: refined, cycleId: s.cycleId }, MAX_FB_DEPTH); return { ...s, value: refined, iterations: s.iterations + 1 };},
// corrector: computes midpoint toward nearest integerobserve: (s, p, ctx) => { if (p.cycleId < s.cycleId) return s; const target = Math.round(p.value); const correction = (p.value + target) / 2; ctx.feedback("respond", { correction, cycleId: p.cycleId }, MAX_FB_DEPTH); return { ...s, correction, cycleId: p.cycleId };},respond: (s, p, ctx) => { if (p.cycleId !== s.cycleId) return s; ctx.send("estimator", "refine", { correction: p.correction, cycleId: p.cycleId }); return s;},Parameters: EPSILON=0.01, MAX_FB_DEPTH=64, cycle every 80 ticks