Skip to content

API Reference

// Node reducer — call inside Behaviors.collect
W.reduce(state, pulse, nodeId, handlers) → newState
// Stability check — use in _isStable
W.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 handlers
W.localReflector(tickMsg, innerTickDelay) → handlersMixin
// Strip infrastructure fields (_queue, _nextAt, _depth, _lt)
W.getState(node) → { ...userFields }

ctx.wallTime // current logical wallTime (= lt)
ctx.logicalTime // current logicalTime (= lt)
ctx.depth // current feedback depth
ctx.future(delay, msg, payload) // schedule at wallTime + delay
ctx.send(targetId, msg, payload) // cross-node via outbox
ctx.feedback(msg, payload, maxDepth) // depth-tracked future at wallTime + _fbStepMs
ctx.futureInf(msg, payload) // fireAt = wallTime — re-enqueues every drain pass
ctx.localReflector(tickMsg, delay) // sub-tick self-hosting clock step

PrimitiveSemantics
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.future
beat: (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(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.localReflector defines a __macro handler. Do not also define __macro in the same handler object — the spread will silently overwrite one of them.


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 tick
future(60) → fireAt = wallTime+60 → waits 60 ticks

All sub-tick steps are deterministic — every peer runs the same drain loop with the same logical wallTime. This enables:

  • Synchronous multi-step computationfuture(0) chains drain in one pass
  • Zeno series — geometrically decreasing delays converging toward 1 tick
  • Self-hosting clock nodes — via W.localReflector

__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 };
}

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 snapshot

The 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 / innerTickDelay
innerTickDelaySteps per outer tickEquivalent simulation rate
0.522× per network tick
0.11010× per network tick
0.01100100× per network tick
0.00110001000× (near float floor)
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 };
},
})
);

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.

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 → stable

Complete 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 integer
observe: (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