@lazily-hub/js
v0.3.0
Published
JavaScript lazily IPC protocol helpers plus FFI state-projection consumer bindings. NOT a reactive-core port.
Readme
@lazily-hub/js
JavaScript helpers for the lazily reactive-signals family: the
lazily-spec IPC wire types, a full-Harel state-chart interpreter, and
an FFI state-projection consumer for the agent-doc binary.
Not a reactive-core port. lazily-js is a state-projection consumer with no reactive graph of its own — the same role
lazily-ktplays on the JVM. The reactive cores live inlazily-rs,lazily-py, andlazily-zig; when a chart's state must be authoritative or shared, it runs in lazily-rs and lazily-js observes it via the snapshot/delta projection. A state chart or any other compute runs natively here as pure logic — never over FFI (routing it to a RustContextwould be circular).
Pure ES modules, zero runtime dependencies for the IPC and state-chart modules
(koffi is loaded lazily, only when the FFI projection transport is used).
Packages
lazily-js ships three entry points:
| Import | What it is |
|--------|-----------|
| @lazily-hub/js | lazily-spec IPC wire types — Snapshot, Delta, DeltaOp, IpcMessage, NodeState, IpcValue, PeerPermissions |
| @lazily-hub/js/statechart | Full Harel/SCXML state-chart interpreter (compute, not protocol) |
| @lazily-hub/js/state-projection | koffi FFI consumer of the agent-doc DocumentStateProjection |
IPC wire types
Every wire value is a frozen, immutable object that round-trips the canonical
externally-tagged JSON shape via toWire() / fromWire(). IpcMessage adds
encodeJson() / decodeJson() for direct transport.
import {
Snapshot,
NodeSnapshot,
Delta,
DeltaOp,
IpcMessage,
} from "@lazily-hub/js";
const snapshot = new Snapshot({
epoch: 1,
nodes: [NodeSnapshot.payload(0, "cell", new Uint8Array([1, 2, 3]))],
});
const msg = IpcMessage.snapshot(snapshot);
const bytes = msg.encodeJson(); // Uint8Array of externally-tagged JSON
const back = IpcMessage.decodeJson(bytes); // round-trips losslesslyDeltas are sequential by epoch; a non-contiguous base_epoch is the resync
signal:
const delta = Delta.next(1, [DeltaOp.cellSet(0, new Uint8Array([9]))]);
delta.isNextAfter(1); // true
delta.applyStatus(1); // { kind: "apply" }
delta.applyStatus(0); // { kind: "resync_required", lastEpoch: 0, baseEpoch: 1, epoch: 2 }PeerPermissions is the per-peer capability ACL (read / write /
trigger_effect over node ids); Snapshot and Delta expose
filterReadable(permissions, peer) so a producer never leaks a node a peer may
not read:
import { PeerPermissions, RemoteOp } from "@lazily-hub/js";
const perms = new PeerPermissions();
perms.allow(7, RemoteOp.read(0));
perms.canRead(7, 0); // true
perms.canRead(7, 1); // falseDeltaOp covers all seven variants — CellSet, SlotValue, Invalidate,
NodeAdd, NodeRemove, EdgeAdd, EdgeRemove. NodeState is
Payload / SharedBlob / Opaque; IpcValue is Inline / SharedBlob, with
ShmBlobRef carrying the shared-memory blob descriptor (offset, len,
generation, epoch, checksum).
NodeSnapshot and the NodeAdd op carry an optional wire-stable NodeKey
(key), the "/"-joined keyed address from the spec § NodeKey. It is
omitted from JSON when absent and decodes to null when missing, so pre-key
fixtures round-trip unchanged. Construction enforces the spec bounds: path
≤ 1024 bytes, ≤ 32 segments, no empty segments (leading/trailing/double "/").
IpcMessage is the externally-tagged frame: Snapshot / Delta / CrdtSync.
The third variant carries the multi-writer CRDT anti-entropy plane
(the spec § Distributed): a CrdtSync frame advertises a per-peer stamp
frontier and ships a batch of CrdtOps. lazily-js is a consumer, so it
decodes/encodes and filters these frames rather than producing CRDT edits — but
it no longer throws when the third variant arrives on the shared transport.
import { CrdtSync, CrdtOp, WireStamp, IpcMessage, PeerPermissions, OpKind } from "@lazily-hub/js";
const sync = new CrdtSync({
frontier: [{ peer: 1, stamp: new WireStamp({ wallTime: 200, logical: 0, peer: 1 }) }],
ops: [
CrdtOp.keyed(2, "scores/alice",
new WireStamp({ wallTime: 180, logical: 3, peer: 2 }),
Uint8Array.of(30)),
],
});
const msg = IpcMessage.crdtSync(sync);
msg.encodeJson(); // byte-compatible with lazily-rs serde_json
const perms = new PeerPermissions();
perms.allowMany(1, OpKind.Read, [2]);
sync.filterReadable(perms, 1); // omits non-readable ops, keeps the full frontierCrdtOp mirrors lazily-rs's derived serde: a keyless op serializes key: null
(not omitted — unlike NodeSnapshot/NodeAdd, which omit the field). The
frontier is the (peer, WireStamp) tuple array, emitted as [peer, stamp]
pairs.
State chart
statechart.js is the native JavaScript counterpart of
lazily-formal's LazilyFormal.StateChart and lazily-rs's
src/statechart.rs. Because lazily-js has no reactive graph, the active
configuration is a plain Set (not a Cell) — the transition is pure logic
with zero system dependencies, exactly as the spec requires for lazily-js /
lazily-kt.
Implemented subset (per the spec's implementation-status note): compound states,
orthogonal (parallel) regions, shallow + deep history (record-on-exit /
restore-on-enter), entry/exit/transition actions (exit innermost-first →
transition → entry outermost-first), named guards (fail-closed), and external +
internal transitions. run actions, {"expr": …} context guards, and
final/completion (done) are rejected explicitly.
import { ChartDef, StateChart } from "@lazily-hub/js/statechart";
const def = ChartDef.fromChart(chartJson);
const chart = new StateChart(def);
chart.activeLeaves(); // initial leaves, sorted
chart.send("TICK", {}); // true if any transition was taken
chart.matches("playing"); // hierarchical "state-in" predicate
chart.configuration(); // full active configuration (leaves + ancestors)
chart.lastActions(); // exit → transition → entry actions from the last sendsend is deterministic by construction — a total function of
(chart, configuration, history, event, guards), mirroring the Lean
StateChart.send. Named guards resolve via the guards map passed to send
(absent / unknown name → fail-closed false).
State-projection consumer (FFI)
state-projection.js wraps the agent-doc binary's C-ABI state-projection
surface via koffi. koffi is resolved lazily — importing the module
does not load the native library; only loadAgentDocFFI() does.
import {
loadAgentDocFFI,
StateProjectionClient,
documentHash,
buildStateEvent,
projectionSummary,
} from "@lazily-hub/js/state-projection";
const ffi = loadAgentDocFFI("/path/to/libagent_doc.so");
const doc = documentHash("plan.md");
const client = new StateProjectionClient(doc, ffi);
client.on("projection", (json) => {
console.log(projectionSummary(json));
});
client.refresh(); // pull the latest DocumentStateProjection
client.recordStateEvent(JSON.stringify(buildStateEvent(doc, "editor.save", {}, "1")));StateProjectionClient is an EventEmitter that emits "projection" on each
refresh(); projectionSummary / compactProjectionSummary reduce the raw
projection JSON into editor-visible status fields. This is an optional
transport — the IPC and state-chart modules work without any native binary.
Conformance
lazily-js replays the shared lazily-spec conformance fixtures:
- IPC fixtures in
test/conformance/round-trip throughIpcMessage.fromWire/toWire(test/ipc.test.js). This includes theagent-doc/snapshot + delta pair, whosetype_tagvocabulary is checked againstschemas/agent-doc-state.jsonand whose phase assertions decode the inline serde_json payload bytes. - The state-chart interpreter is validated against the same Harel fixtures every
binding uses (
test/statechart.test.js).
The arena_blob.json fixture is intentionally not replayed here: it is
kind: "Arena", explicitly not a wire type, and scopes the in-process
ShmBlobArena host contract to the reactive cores (lazily-rs / -py / -zig).
lazily-js is a state-projection consumer with no arena host of its own.
Development
make check # == npm run build && npm testnpm run build—node --checksyntax validation of the three modules.npm test—node --test test/*.test.js.
See also
lazily-spec— language-agnostic wire protocol + the conformance fixtures (IPC and state-chart) every binding replays.lazily-formal— Lean 4 formal model (shared primitives, flat FSM kernel, full HarelStateChart); the executable reference behind the state-chart fixtures and the deterministicsendlazily-js inherits.lazily-kt— the JVM analogue of this consumer role.lazily-rs/lazily-py/lazily-zig— the reactive cores.
