lazily 0.5.1 copy "lazily: ^0.5.1" to clipboard
lazily: ^0.5.1 copied to clipboard

Lazy reactive primitives for Dart — Slots, Cells, and Signals with automatic dependency tracking and cache invalidation, plus the lazily-spec wire protocol for mirroring graph state across processes a [...]

lazily (Dart) #

Lazy reactive primitives for Dart — Slots, Cells, and Signals with automatic dependency tracking and cache invalidation. Pure Dart: Flutter, web, and native.

A port of the lazily reactive family (lazily-rs, lazily-py, lazily-js, lazily-zig).

The reactive family #

  • Slot — a lazily-computed cached value that automatically tracks its dependencies and recomputes only when read after an upstream change.
  • Cell — a mutable source value that invalidates dependent Slots/Signals when it changes.
  • Signal — an eager derived value that recomputes the instant a dependency changes, with no intermediate unset value.

Values are lazy by default. When you need eager push-style semantics, reach for Signal.

Usage #

import 'package:lazily/lazily.dart';

final ctx = Context();
final a = Cell<int>(ctx, 2);
final b = Cell<int>(ctx, 3);

// Lazy: computes on first read, caches, recomputes only when a or b changes.
final sum = Slot<int>(ctx, (_) => a.value + b.value);
print(sum()); // 5

a.value = 10;
print(sum()); // 13

// Eager: recomputes immediately when a dependency changes.
final parity = Signal<String>(ctx, (_) => a.value.isEven ? 'even' : 'odd');
print(parity.value); // even
a.value = 11;
print(parity.value); // odd (already updated before the read)

A [Cell] also supports persistent observers (the hook for Flutter ValueNotifier bridges and setState wrappers):

final count = Cell<int>(ctx, 0);
final dispose = count.subscribe((v) => print('now $v'));
count.value = 1; // prints "now 1"
dispose();

Context #

All reactives that react to each other must share a Context. The context holds an identity-keyed cache and the computation stack used for automatic dependency tracking. One context per reactive graph is the contract.

State machine #

StateMachine is a finite state machine backed by a Cell, so any slot or signal that reads state is invalidated on transition:

final m = StateMachine<String, String>(
  ctx, 'Red',
  (s, e) => e == 'advance'
      ? const {'Red': 'Green', 'Green': 'Yellow', 'Yellow': 'Red'}[s]
      : null,
);
m.send('advance'); // true -> 'Green'

State chart #

StateChart is a full Harel/SCXML hierarchical state machine — the native counterpart of lazily-formal's LazilyFormal.StateChart and lazily-rs's / lazily-kt's state charts. It is compute, not protocol: it is never serialized as a distinct wire kind. The active configuration lives in a Cell, so any slot or signal reading configuration, activeLeaves, or matches is invalidated on a real transition; a no-op (configuration unchanged) is suppressed by the cell's structural-equality guard.

send is deterministic by construction — a total function of (chart, configuration, history, guards, event), mirroring the Lean StateChart.send. A chart is built from the declarative JSON form (lazily-spec/docs/state-charts.md) via ChartDef.fromJson.

Implemented subset (per the spec's implementation-status note): compound (hierarchical) states with default initial descent, orthogonal (parallel) regions, shallow and deep history (record-on-exit / restore-on-enter), entry/exit/transition actions (exit innermost-first → transition → entry outermost-first), named guards resolved at send time (fail-closed), and external + internal transitions. run actions and {"expr": …} context guards are rejected explicitly. final states are accepted as leaves without raising completion (done) events, matching lazily-py and lazily-kt.

import 'package:lazily/lazily.dart';

final def = ChartDef.fromJson({
  'initial': 'on',
  'states': {
    'root': {'initial': 'on'},
    'on': {
      'parent': 'root', 'initial': 'playing',
      'on': {'toggle': 'off'},           // handled by 'on', bubbles from a child
    },
    'playing': {'parent': 'on', 'on': {'pause': 'paused'}},
    'paused':  {'parent': 'on', 'on': {'play': 'playing'}},
    'off':     {'parent': 'root', 'on': {'toggle': 'on'}},  // re-enters -> 'playing'
  },
});
final chart = StateChart(ctx, def);

chart.activeLeaves();            // ['playing']
chart.send('toggle');            // true; off -> on -> playing (initial)
chart.matches('on');             // true
chart.lastActions();             // exit → transition → entry actions

Conformance #

lazily-dart replays the shared lazily-spec conformance fixtures:

  • State-chart fixtures mirrored into test/conformance/statechart/ are replayed by test/statechart_conformance_test.dart, asserting accepted, active, matches, and actions identically to every other binding (lazily-rs / lazily-kt / lazily-py / lazily-zig / lazily-js). When the sibling lazily-spec checkout is present on disk, the canonical fixtures are preferred, so this harness also guards against cross-family drift.

Formal model (lazily-formal) #

dart test also builds the lazily-formal Lean 4 model — the executable reference behind the state-chart fixtures and the deterministic send lazily-dart inherits. tool/formal_check.dart runs lake build over the sibling lazily-formal checkout (located via the LAZILY_FORMAL_PATH env var, then the src/lazily-dartsrc/lazily-formal submodule layout); it SKIPs gracefully when the submodule or the lake toolchain is absent — so pub.dev consumers and shallow clones are not broken. CI uses a full checkout + elan, so the proofs are verified for real there.

Each lazily-formal module that has a Dart counterpart has a matching property test that names the universal theorem it mirrors — the guarantees no finite fixture suite can establish:

lazily-formal module Dart test file Mirrored theorems
StateMachine / StateChart test/statechart_properties_test.dart enabled_empty_rejects, send_preserves_chart, determinism-by-construction, single_region_refines_flat_machine, single_region_enabled_at_most_one, parallel_region_confluence, recordHistory_idempotent, send_actions_empty_when_rejected
Reactive test/reactive_properties_test.dart setCell_equal_preserves_graph, setCell_different_invalidates_dependents, recomputeSlot_equal_preserves_dependents, recomputeSlot_different_invalidates_dependents, signal_materialized_after_recompute

lazily-spec IPC #

The package:lazily/ipc.dart library implements the language-agnostic lazily-spec wire protocol (Snapshot, Delta, NodeState, ...) so a Dart graph's state can be mirrored to remote observers across processes and languages. It round-trips the canonical fixtures from lazily-spec/conformance/.

Feature coverage #

The full lazily capability set across every binding. Legend: ✅ shipped · ~ partial · absent or not applicable. The canonical matrix with per-cell notes and platform carve-outs lives in lazily-spec § Cross-Language Coverage.

Feature Rust Python Kotlin JS Dart Zig
Reactive graph — Cell / Slot / Signal / Effect / memo / batch ~ ~ ~
Thread-safe context (lock-backed)
Async reactive context
Flat state machine
Harel state charts
Keyed cell collections (CellMap / CellTree) + reconcile ~
Memoized semantic tree (SemTree)
Stable-id alignment (manufactured identity)
Free-text character CRDT (TextCrdt)
TextCrdt delta sync (version_vector / delta_since / apply_delta)
Move-aware sequence CRDT (SeqCrdt)
Registers (LWW / MV) + PnCounter + CellCrdt
IPC wire — Snapshot + Delta + CrdtSync
Shared-memory blob path (ShmBlobArena) ~ ~
Distributed CRDT plane (CrdtPlaneRuntime / anti-entropy) ~
Distributed plane — WebRTC transport + signaling
State projection / mirror
Causal receipts (CausalReceipts outcome projection)
C-ABI FFI boundary
Permission boundary (PeerPermissions / RemoteOp)
Capability negotiation (SessionHandshake)
Instrumentation / benchmarks

Status #

Conforms to the full lazily-spec Binding Conformance Matrix — every MUST layer is implemented:

Layer Where
Reactive core (Cell / Slot / Signal) package:lazily/lazily.dart
Keyed cell collections (CellMap / CellTree / reconciliation) package:lazily/lazily.dart
Flat state machine package:lazily/lazily.dart
Harel state charts package:lazily/lazily.dart
Async reactive context package:lazily/async_context.dart
IPC (Snapshot + Delta + CrdtSync) package:lazily/ipc.dart
Distributed CRDT plane (CrdtSync / WireStamp / Hlc / StampFrontier) package:lazily/ipc.dart
C-ABI FFI boundary (LazilyFfi*, CrdtSync = 3) package:lazily/ffi.dart
Permission boundary (RemoteOp / PeerPermissions) package:lazily/ipc.dart
Capability negotiation package:lazily/capability.dart
Formal model verification (lazily-formal Lean proofs in dart test) tool/formal_check.dart + test/formal_check_test.dart

Distributed sync wiring to live merge: crdt root cells, WebRTC transports, and the deeper CRDT collection layers (SemTree, SeqCrdt, TextCrdt, StableId) are not ported yet — see lazily-rs for the full feature set.

Keyed cell collections #

CellMap<K, V> is a composition of cells, not a new cell kind. Each entry is an ordinary Cell; a dedicated membership cell tracks the key set, and a dedicated order cell tracks the ordered key list, so the three reactivity planes are independent:

  • writing one entry's value invalidates only that entry's value readers;
  • adding/removing a key invalidates membership readers (len / containsKey) and order readers (keys), but not unrelated entry value readers;
  • a pure reorder (atomic move) invalidates order readers only.
final ctx = Context();
final scores = CellMap<String, int>(ctx)
  ..set('alice', 10)
  ..set('bob', 20);

final leaderboard = Slot<List<String>>(ctx, (_) => scores.keys());
leaderboard(); // ['alice', 'bob']

scores.moveTo('bob', 0);
leaderboard(); // ['bob', 'alice']  — recomputed (order changed)

reconcileDiff is the move-minimized keyed reconciliation (#lzkeyrecon): diffs two keyed sequences by stable key and emits the minimal {insert, remove, move, update} op set, holding the longest-increasing subsequence fixed so keys already in relative order do not move.

CellTree<K, V> is the ordered keyed tree — each node is (stable id, value cell, ordered keyed child collection), inheriting per-level reactivity and the atomic-move guarantee.

Distributed CRDT plane #

The CRDT plane rides the same lazily-ipc transport as Snapshot/Delta: CrdtSync is a third IpcMessage variant. State-based and idempotent — out-of-order, duplicated, or batched delivery all converge.

import 'package:lazily/ipc.dart';

final a = CrdtPlane(1);
final stamp = a.tick(12345);                              // local event
final op = CrdtOp.newOp(nodeId, stamp, [1, 2, 3]);        // state to merge
final frame = CrdtSync(
  frontier: a.frontier.toWire(),                          // per-peer stamp frontier
  ops: [op],
);
// → IpcMessage.ofCrdtSync(frame).encodeJson()  is the wire form.

StampFrontier.merge is commutative, associative, idempotent; the causal-stability watermark (stabilityWatermark) is the min over membership — only once every replica has observed a tombstone may it be collected (isCollectable).

FFI boundary #

package:lazily/ffi.dart exposes LazilyFfiBytes, LazilyFfiStatus, and LazilyFfiMessageKind (with CrdtSync = 3). A frame is just serialized IpcMessage bytes; the channel decodes each accepted frame as IpcMessage and re-encodes canonical JSON.

import 'package:lazily/ffi.dart';
import 'package:lazily/ipc.dart';

final frame = LazilyFfiBytes(IpcMessage.ofCrdtSync(sync).encodeJson());
final c = lazilyFfiKindJson(frame);
expect(c.kind, LazilyFfiMessageKind.crdtSync);

lazily-dart declares the ffi = host capability (Dart has dart:ffi).

Capability negotiation #

package:lazily/capability.dart ships the compatibility handshake. Peers fail closed on protocol_id, protocol_major_version, codec, ordered_reliable, or a required feature the other does not offer.

Async reactive context #

package:lazily/async_context.dart is a separate reactive surface for async/future-returning computations, with the full Empty → Computing → Resolved | Error state machine, revision tracking (stale completions discarded), in-flight deduplication, the re-resolve contract, an equality memoAsync guard, serialized async effects (cleanup-before-body), batching, and disposal.

Development #

dart pub get
dart analyze --fatal-infos
dart test

dart test builds lazily-formal (Lean proofs) when the sibling submodule + lake toolchain are present, and SKIPs otherwise. To verify the proofs in a standalone clone:

git clone https://github.com/lazily-hub/lazily-formal.git ../lazily-formal
# (or) set LAZILY_FORMAL_PATH=/path/to/lazily-formal
dart run tool/formal_check.dart

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, the flat StateMachine kernel, and the full Harel StateChart); the executable reference behind the state-chart fixtures and the deterministic send lazily-dart inherits.
  • lazily-rs / lazily-py / lazily-zig — sibling reactive cores.
0
likes
0
points
1.58k
downloads

Publisher

unverified uploader

Weekly Downloads

Lazy reactive primitives for Dart — Slots, Cells, and Signals with automatic dependency tracking and cache invalidation, plus the lazily-spec wire protocol for mirroring graph state across processes and languages. Pure Dart: Flutter, web, and native.

Repository (GitHub)
View/report issues

Topics

#reactive #signals #reactive-programming #state #flutter

License

unknown (license)

More

Packages that depend on lazily