lazily 0.29.0
lazily: ^0.29.0 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 [...]
Changelog #
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog,
and this project adheres to Semantic Versioning
(with the pre-1.0 convention that 0.minor may break between minor bumps).
0.29.0 #
Added #
- Portable logical-clock
Timer, caller-drivenTimeout<T>, andRevisionBarrierproduction APIs with synchronous and Future adapters. - Canonical stdlib fixture replay and independently advertised interop peer capabilities for all three portable features.
- A JavaScript-target browser check for exact uint64 JSON projection.
Fixed #
- Revision barriers reject and latch regressing logical clocks before invoking cancellation, and preserve terminal outcomes established during reentrant or asynchronous cancellation callbacks.
0.28.0 #
Added #
- The complete queue family now binds all three execution flavors.
ThreadSafeQueueCell/ThreadSafeTopicCell/ThreadSafeWorkQueueCellrun through Dart's shipped single-isolate run-to-completion guard, while theAsync*counterparts mint memoized reader kinds onAsyncContext. Queue storage, cursor movement, and lease decisions remain synchronous on every flavor, as required by the Core contract. AsyncContext.computed,get,isSet,clear, andclearSlots: local synchronous derivations resolve inline on the async graph, and multi-root invalidation clears one atomic frontier.- One enforced nine-row flavor ledger and canonical replay of all eleven
queue-family fixtures against
QueueCell,TopicCell, andWorkQueueCellon single-threaded, thread-safe, and async contexts. Every flavor asserts a positive step count and the nestedexpected.invalidatesmatrices. - A production interop peer adapter for capability-negotiated cross-binding network-suite tests.
Changed #
TopicCell.advancenow returns the element passed by the cursor (ornullfor a no-op), matching the canonical topic contract. A newly-created subscription now invalidates its pre-existing reader node just like a reconnect.- Signaling codecs reject malformed frames, and graph-backed
ComputedMapentries preserve their equality guard.
0.27.1 #
Changed #
- Coverage table sync only.
lazily-rsshipped the thread-safe and async flavors of the queue family (QueueCell,TopicCell,WorkQueueCell), so six rows of the shared conformance table flipped for the Rust column andmake coverage-syncregenerated the copy embedded in this README. No Dart behaviour changed — those flavors remain unbound here.
0.27.0 #
Added #
- The
ReactiveMapCore surface now binds every flavor. Ordering and atomic move bound only the single-threaded map; the thread-safe and async maps exposed the present set and nothing else. All flavors now carrykeys,len,is_empty,contains_key,position,move_to/move_before/move_after, andremove, each with membership and order signals minted on its own graph. A move touches no entry handle and awaits nothing, so it is neither thread- nor async-coloured. - A shared, graph-agnostic
KeyedOrdercore holding the present set, the key order, and the move algebra. It deliberately holds no reactivity: membership and order invalidation is a graph write, so each flavor owns its own cells. - The canonical ordering fixtures now replay against all three flavors, with
invalidation measured by recompute count rather than a cache flag, plus
directional move coverage the canonical corpus does not provide (its only
move_beforestep moves a key that already follows its anchor, so theanchor - 1branch was never exercised).
Fixed #
- The thread-safe and async maps stored plain values and their context field was
annotated
// ignore: unused_field— dead. Both are graph-backed now, so a per-entry read registers an edge and a write invalidates that entry's readers and no other.
0.26.0 #
Changed #
- Renamed the keyed maps to
SourceMap/ComputedMap, finishing the v2 kernel migration: the node kinds becameSourceandComputed, and the map names now say which kind of entry they hold instead of the pre-v2Cell/Slotvocabulary.CellMap->SourceMap,SlotMap->ComputedMap, and theThreadSafe*/Async*variants alongside them.
Deprecated #
- The old names are kept as deprecated aliases of the new ones, so existing callers still
compile. Conformance runners accept both the old and new
modelspellings in a fixture; the corpus emits only the new. Fixture FILE names are unchanged.
0.25.0 - 2026-07-22 #
Changed #
- Cell kernel v2 —
Source/Computed/Effect, guardedcomputed,memoremoved (#lzcellkernel). Supersedes the v1SourceCell/FormulaCellscheme below (which was committed but never published), resolving the handle-vs-node overload.Cellis now the value-node concept, not a type — there is noCell<T, K>genus. The two concrete cell handles areSource<T>(get/set/merge; wasSourceCell/Cell) andComputed<T>(get,.eager(),.lazy(),isEager; wasFormulaCell).Cellis retained as a compatibility spelling ofSource. Constructors:formula(ctx, f)→computed(ctx, f);.drive()/.undrive()→.eager()/.lazy();isDriven→isEager(internal_driven/_drivenBy→_eager/_eagerBy). computedis guarded, always; theMemotype is removed. An equal recompute (==) suppresses the downstream cascade (TC39Signal.Computed); there is no unguarded mode and no separatememokind — its equality guard folded intoComputed, backed by a private guarded slot. A freshly constructed lazyComputedcarries the inline push-guard; aComputedreverted from eager via.lazy()is an unguarded lazy pull (clause 4:dispose_signal_reverts_to_lazyrequires an invalidating write to NOT re-materialize).Slotis retained as the lower-level unguarded callable computed — the primitive for non-==values. The formerMemocall sites (SemTree, the instrumentation benchmark, the core tests) now useComputed.- The eager construction is now
computed(ctx, f).eager(). The puller stays an ordinary scheduledEffectover a plain backingSlot(the backing's inline guard is switched off while eager), so N writes inside onebatchcoalesce into ONE re-materialization at the flush (clause 3) — the guarded backing is never allowed to recompute inline per-source.Signal(ctx, f)is retained as a back-compat alias for an eagerComputed. - (v1, superseded above) The Cell kernel:
SourceCell/FormulaCellover theCellgenus (#lzcellkernel). One genus,Cell<T, K>, over two value kinds — a node's value comes either from outside it (SourceCell, the writableset/mergekind; the concrete class keeps the nameCell) or from upstream of it (FormulaCell, computed via a formula) — withEffectthe value-less sink. New surface:source(ctx, v),formula(ctx, f)(lazy and guarded by default), andformula(ctx, f).drive()(eager).Slotis retained as the storage/computation position and the unguarded lazy computed value. - The eager construction is now
formula(ctx, f).drive(), retiring the standaloneSignalnode. Drivenness is graph state — a_drivenbit plus a_drivenByside table holding the pullerEffect, cleared onundrive/ disposal (reactive-graph.md§9.3.3)..drive()is idempotent and returns the same handle. The puller is now an ordinary scheduledEffectover a plain backingSlotrather than a bespoke_SignalSlot+_pendingSignalslane: because the only way to make a formula eager is to attach that scheduled effect, the#lzsignaleagerper-write puller is structurally unrepresentable. Clause-3 coalescing (once per batch) is inherited from the effect scheduler; thesignal_*conformance fixtures stay green. Dart has no compile-time read/write split (design §4) — the kind is convention, never a runtime gate.Signal(ctx, f)/Signal<T>are retained as thin back-compat aliases for a drivenFormulaCell.
Fixed #
Signalre-materialized once per invalidated source inside abatchinstead of once at batch exit (#lzsignaleagerclause 3)._SignalSlotre-pulled its owningSignalinline fromonInvalidate, and_flushBatchcascades once per written cell — so a two-cell batch ran the signal's compute twice and a three-write batch over two cells ran it twice again. The value was correct either way, which is why it went unnoticed: this is a cost multiplier proportional to the number of distinct sources written per batch, not a wrong answer. The eager pull is now scheduled ontoContext._pendingSignalsand drained at the same flush point as effects, dedup'd by identity, so N writes produce exactly one re-materialization. Pulls drain ahead of effects, so an effect body still observes the materialized value. Clauses 1, 2, and 4 are unchanged. (Superseded within Unreleased by the Cell-kernel migration above: the puller is now an ordinary scheduledEffect, so the_SignalSlotand_pendingSignalslane are gone and clause-3 coalescing comes from the effect scheduler. The fixtures below still pin the behaviour.)
Testing #
- The reactive-graph conformance runner replays the corpus's three signal
fixtures —
signal_materializes_without_a_read,signal_materializes_once_per_batch, anddispose_signal_reverts_to_lazy— against bothContextandAsyncContext. Adds thesignal,dispose_signal, andbatchops and thecomputes_ofobservable, the cumulative per-node compute count that is the only thing distinguishing an eager signal from the lazy memo it is built on.signal_materializes_once_per_batchis what caught the batch defect above; the other two were already green. The runner now dual-accepts the Cell-kerneldrive/undriveops alongsidesignal/dispose_signal; under v2defineSignalbuildscomputed(...).eager()anddisposeSignalcalls.lazy()(#lzcellkernel); no fixtures were added or renamed.
0.24.0 #
Added #
- Disposal, teardown scopes, and degree introspection (
#lzspecedgeindex). Ported fromlazily-rs, the only binding that had shipped these APIs, and landed on both graphs dart ships.Context.disposeNode/disposeSlot/disposeCell/disposeEffect, plusSlot.dispose()andCell.dispose(). Idempotent. Detaches both edge directions and marks the surviving dependent cone dirty; reading a disposed node throws the newDisposedNodeError. Without disposal a node is permanent — dart handles are ordinary references and the graph holds a strong reverse edge to every dependent, so a source's dependent list grows without bound under subscribe/unsubscribe churn.Context.scope()->TeardownScopewithend(),disarm(),adopt(),length, and the node factories, plusContext.withScope(body). Dart has no destructor, solazily-rs's scope-ends-on-drop does not transfer:withScopeis the lexical form andend()covers the scope whose lifetime is a connection or subscription spanning an async gap. Teardown is reverse creation order.Context.dependentCount/dependencyCount/isNodeDisposedover a new sealedGraphNode— counts, never edge lists, so graph shape is assertable without a path to the internals and no storage strategy is pinned by the contract.- The same surface on
AsyncContext:disposeNode,dependentCount,dependencyCount,isNodeDisposed,isEffectActive,scope()->AsyncTeardownScope,withScope, and a sealedAsyncGraphNode.
Fixed #
AsyncContexteffect disposal leaked its upstream edges.disposeAsyncremoved the effect from the context's effect set but left it in_dependents[dep]for every dependency it had read, so a subscribe/unsubscribe cycle grew each source's dependent set without bound even though the live subscriber count was constant — the leakchurn_returns_to_baselinepins.
Testing #
reactive_graph_conformance_test.dartnow replays all 9 reactive-graph fixtures against bothContextandAsyncContext(was 1 of 9 against each). Adds every remaining op (effect,dispose,begin_scope,end_scope,disarm,fanout,dispose_fanout,churn,dispose_stale_handle), every remaining assertion kind, thescenariosfixture shape, and theobservationally_equalrelation. A divergence ledger asserts in both directions: 220 ops and 290 assertions, zero divergences.- Adds direct tests for the two disposal semantics the shared corpus does not discriminate — that effects reached by the disposal walk are not scheduled, and that scope teardown runs in reverse creation order. Both were established by mutation: each mutant left the whole 9-fixture corpus green.
Removed — the Cell observer API (BREAKING) #
-
Cell.subscribeand its disposer are gone. Observation in a reactive graph is a declared dependency edge, not a registered callback. A per-cell listener registry bypasses the graph, ignoresContext.batch, breaks glitch-freedom, and costs memory on every cell whether or not anything is listening — that per-reactive cost is the decisive objection. Four of the eight bindings (rs,cpp,js,kt) never had this API; lazily will not have it in any binding.Use
Effectfor side effects (FlutterValueNotifierbridges,setStatewrappers, logging, I/O): it is the eager-push primitive, it batches, and it disposes. UseTopic(lib/src/queue.dart) when a consumer genuinely needs a stream of every transition rather than the settled value —TopicandQueuesubscription are unaffected. The dependency graph itself, including slot/computed dependent tracking, is unchanged.This reverts the observer storage work (
#lzdartobservercow): the slot list, the lazily rebuilt snapshot, the compaction pass, andbenchmark/observer_cow_audit.dartare deleted rather than fixed. -
test/reactive_graph_conformance_test.dartremoved with the API. The runner only implemented the observer op vocabulary (subscribe,unsubscribe,set_cell, …); every non-observer fixture inlazily-spec/conformance/reactive-graphwas already reported as skipped-for-unimplemented-ops. With the observer fixtures gone it would load fixtures, execute none, and report green — a vacuously passing suite is the exact anti-pattern the#lzspecconfwork eliminated. A runner for the disposal/teardown fixtures is separate work with a separate vocabulary.test/conformance_fixture_drift_test.dartand sibling-first fixture resolution are unaffected.
Changed — StateMachine.onTransition is an effect (BREAKING behaviour) #
-
onTransitionis now implemented withEffect, mirroringlazily-rsStateMachine::on_transition. It still fires only on a real state change, is still not called on registration, and still returns a disposer.Because an effect observes through a dependency edge, it participates in batching:
A -> B -> Cinside a singleContext.batchnow reports one transition(A, C)rather than two. This is intended — a batch asserts that its intermediate states were never observable. A consumer that needs every individual transition should publish them to aTopic.
Audited — no defect found (#lzspecedgeindex, pending-effect scan) #
- Audited the pending/scheduled-effect scan quadratic; absent in Dart.
lazily-rsandlazily-cpp(run_effect) andlazily-kt(disposeEffect) each scanned the pending effect collection for an id that could not be there, costing O(W^2) per publish or per teardown. Dart is immune by construction on both paths:Context._scheduledEffectsis aSet.identity(), so flush and dispose both deregister via an O(1) hash lookup, andEffect.disposealready declines to remove from_pendingEffectsby value (it would corrupt the FIFO head pointer). No fix was required and no behaviour changed. - The
lazily-kt"empty collection still scans" trap does not apply. In Kotlin an emptiedArrayDequestill scanned its never-shrinking backing array becauseindexOf's wraparound branch triggers whenhead >= tail. Dart's growableList.clear()setslength = 0andindexOfbounds onlength, so scanning a drained pending list is genuinely free — measured, not assumed (see the teardown-quiescent column below).
Added — benchmarks #
-
benchmark/effect_pending_audit.dartcloses a blind spot:benchmark/edge_index_load.dartdrives pull-based reads through computed slots and never constructs anEffect, so no rung of it touches the pending collection. The new harness holds total work fixed (65,536 effect bodies per rung) and varies only fan-out width, runs one process per rung so a tracing GC cannot smear rungs together, and carries a fan-out-2 control arm at equal node count so results are read as wide/control ratios rather than absolute growth. -
Context.naivePendingScan, a compile-time flag (-Dlazily.naive_pending_scan=true), restores the naive scan on both paths so the audit can prove a detection margin. The scan result is discarded, so behaviour is identical, and the constant tree-shakes out of normal builds. A flat column is only evidence when the same harness reports a steep one for the naive build. Measured over widths 64 -> 65,536, three interleaved passes:arm fixed forced-naive margin publish (wide/control) 0.4-0.6x 12.6-31.4x ~30x publish (absolute) 1.4-1.6x 39.2-50.2x ~30x teardown, pending saturated 2.2-3.4x 168-294x ~90x teardown, pending drained 1.6-2.1x 2.0-2.5x n/a (nothing to scan) A batch does not saturate the pending list —
_cellChangedonly records the cell while_batchDepth > 0and defers the cascade to_flushBatch— so the saturated arm disposes its cohort from inside the first effect's body, which runs with the rest of the cohort still queued behind it.
Fixed — performance (#lzspecedgeindex, dependency-edge index) #
- Wide fan-out no longer registers edges in O(n^2).
_addDependent/_addDependencydeduped by an unconditional linear scan of the edge list, so building a width-N fan-out cost ~N^2/2 comparisons and every propagation paid it again. Once an edge list reachesedgeIndexPromoteThreshold(128) it now promotes to a hash index and registration returns to amortized O(1), demoting only atedgeIndexDemoteThreshold(32). The dedup strategy is not observable — the edge set is identical either way, as the spec requires. - Edge removal is O(1) while indexed. The index stores each dependent's
position, so
_removeDependentswap-removes instead of scanning; tearing down a wide fan-out is O(n) overall rather than O(n^2). The unindexed path keepsList.remove's order-preserving behaviour. - Hysteresis on demotion. Demote is a quarter of promote, not a shared boundary: edges are removed and re-registered on every recompute, so a list sitting exactly at a single boundary would rebuild its index every recompute.
- A cleared list never keeps a stale index. Both bulk-clear sites
(
_detachUpstream,_invalidateInto, includingMemo's override) drop the index with the list, so the next computation's edges cannot alias the previous run's. - Threshold measured for Dart, not copied from another binding. The
pure-strategy scan/hash crossover is ~60 under AOT and ~96 under the JIT; the
hybrid's own regression sits on whichever threshold is chosen, and a sweep of
the real implementation puts the knee at 128. See the doc comment on
edgeIndexPromoteThresholdfor the sweep table. - Measured (ns/registration vs. the unfixed tree, medians): degree 8..97 and 192+ within noise, degree 128 up 1.31x, degree 512 down to 0.48x, degree 1024 down to 0.23x. On the width ladder the wide-vs-narrow control ratio goes from 208x at width 262144 (unfixed) to 1.0-1.4x flat out to width 10,000,000.
Added #
benchmark/edge_index_load.dart— manual, on-demand width-ladder load test (one process per rung, fan-out-2 control arm at equal node count, climb/project/refuse memory policy) plus a--narrowlow-degree regression guard. Not run in CI.test/edge_index_test.dart— behavioural coverage across the promote and demote thresholds: fan-out and fan-in correctness, duplicate-read dedup on the indexed path, and a shrinking wide node not retaining stale entries.
0.23.0 #
Added — performance (#lzdartstreamingjson, Phase 4 streaming JSON) #
- Streaming
writeJson(StringSink)on the batched IPC types. Added avoid writeJson(StringSink sink)method toSnapshot,Delta,CrdtSync,CrdtOp, and theIpcMessagesealed family (IpcMessageSnapshot,IpcMessageDelta,IpcMessageCrdtSync,IpcMessageResyncRequest,IpcMessageOutboxAck). Each writes its canonical JSON tokens directly into the sink, eliminating the intermediateMap<String, Object>(and, for the batch variants, thenodes/edges/ops/frontierListallocations) thattoWire()materializes beforejsonEncodewalks the map. For large IPC batches this is the largest single allocation source. Output is byte-identical tojsonEncode(toWire());toWire()is retained unchanged for compatibility. IpcMessage.encodeJsonStreaming()— streaming equivalent ofencodeJson(). Builds the JSON viawriteJsoninto aStringBufferinstead ofjsonEncode(toWire()), so neither the outerMap<String, Object>nor the per-batch innerList/Mapallocations are materialized. Returns the sameUint8Listshape asencodeJson().- Pragmatic scope: inner small types (
WireStamp,NodeKey,ShmBlobRef,NodeState/IpcValuevariants,NodeSnapshot,EdgeSnapshot,DeltaOpvariants,StampFrontierEntry,ResyncRequest,OutboxAck) still go throughjsonEncode(inner.toWire())at their use sites — they are not the dominant allocation source.CrdtSync.writeJsonrecurses intoCrdtOp.writeJsonso each op in a CRDT batch skips its ownMap<String, Object?>allocation. - Tests: added
streaming JSON (#lzdartstreamingjson)group intest/ipc_test.dartpinningencodeJsonStreaming()toencodeJson()byte for byte across all fiveIpcMessagevariants, round-tripping the streamed bytes back throughdecodeJson, and verifying empty-batch edge cases.
0.22.0 #
Changed — performance (Phase 2 quick wins: #lzdartinlines, #
#lzdartuint8list, #lzdarthashint, #lzdartutf8fix, #lzdartreconcileidx,
#lzdartobservercow)
@pragma('vm:prefer-inline')on 11 hot leaf helpers (#lzdartinlines). Added to the comparators that gate every CRDT ordering pass (Position.compareTo,OpId.compareTo,TreeOpId.compareTo,SortKey.compareTo), the UTF-8/FNV byte loops (_utf8Len,_fnv1a), the wire_listEquals, and the reactive-core edge/cache helpers (_addDependent,_addDependency,_track,_isCachedInGeneration). No observable behavior change; 5–15% on JIT micro, larger in AOT.Position.frac/SortKey.fracareUint8List(#lzdartuint8list). The fractional-index byte key is now a compactUint8Listinstead of a growableList<int>.keyBetweenwrites into aBytesBuilderand returns aUint8List;Position.compareTo/SortKey.compareToindex raw bytes with no per-element boxing. Wire (toWire/fromWire) still carries a JSON byte array. The ordering micro-path is faster and the layout is more AOT-friendly; end-to-endorder()is allocation-dominated so the JIT gain is modest there.contentHashusesintmath, notBigInt(#lzdarthashint). Replaced theBigInt-per-byte loop (~10–50× slower) with the same fixed-widthintFNV-1a-64 core already used byshm_blob_arena._fnv1a. The two now share that core explicitly;contentHashpreserves the full 64-bit wire-stable range (serialized as 16-char unsigned hex via a two-half formatter), while_fnv1akeeps its 63-bit fold for the unsignedShmBlobReffields — reconciling the former latent output-range divergence. Also fixes an empty-string bug (the oldBigInt.parse(offset.toRadixString (16))negated the offset basis, so''hashed to-340d…instead of the canonicalcbf29ce484222325)._utf8Bytescorrectness +Uint8List(#lzdartutf8fix). Switched froms.codeUnits(UTF-16 halves) tos.runes(Unicode scalars) and added the 4-byte branch, so supplementary-plane characters (emoji, etc.) now emit valid UTF-8 instead of invalid 3-byte-per-surrogate sequences. Builds viaBytesBuilder. BMP/ASCII hashes are unchanged.reconcileDiffcommon-key index (#lzdartreconcileidx). The innercommonKeys.indexOf(k)(O(N) per common key ⇒ O(N²) move-minimization) is replaced by a pre-builtcommonIdxByKeymap (O(1) lookup). Overall reconciliation drops from O(N²) to O(N log N). Measured 2.6× faster at 500 entries, 4.5× at 1k, 7.3× at 2k (per-elem cost is flat after, was growing linearly before).Cell._notifyObserverscopy-on-write (#lzdartobservercow). The observer list is now an immutable snapshot replaced on subscribe/unsubscribe, so_notifyObserversiterates it directly with an indexed loop and drops the per-write.toList()allocation. Reentrant subscribe/dispose during notification swap in a fresh list, leaving the in-flight snapshot stable.
Performance (benchmark deltas vs 0.21.0) #
- Micro (
LAZILY_MICRO_ITERS=100000):contentHash realistic text18.40 → 7.38 µs (2.5×);reconcileDiff 100-entry list33.14 → 25.81 µs (22% at N=100; the asymptotic win grows with size — see scale table below). Reactive core (Cell read/write,Slot recompute,Memo) unchanged within noise (the inlines + observer COW are structural; the JIT bench is allocation-bound). reconcileDiffscaling (focused bench, before → after): N=500 350.5 → 135.2 µs (2.6×); N=1000 1309.5 → 292.5 µs (4.5×); N=2000 4288.1 → 591.7 µs (7.3×). Per-element cost is flat after (~290 ns) vs growing linearly before (331 → 701 → 1309 → 2144 ns/elem) — the classic O(N²)→O(N) knee.
0.21.0 #
Changed — performance (CRDT plane, Phase 1 of tasks/agent-doc/plans/lazily-perf-memory-audit.md) #
TextCrdt._orderedIdscache (#lztextordcache). The DFS pre-order is now memoized and invalidated on every mutation that changes the element set (insert/insertStr/merge/applyDelta/gcWith). Repeatedtext()/len()between mutations drops from O(N log N) per call to O(N) total instead of N × O(N log N). Tombstone flips (delete) only invalidate the live cache (the full DFS includes tombstones).TextCrdt.insertStrorigin chaining (#lztextinsertchain).insertStrrewrites from N per-charinsertcalls to one_orderedIds()pass + N chain appends. Drops O(N² log N) → O(N log N); concurrent inserts still sort by peer tiebreak.TextCrdtelement map keyed by(counter, peer)record (#lzopidkeytuple). Dart 3 records have value==/hashCode, so the previousMap<String, _TextElem>(one string allocation + parse per lookup/traversal) becomesMap<(int, int), _TextElem>._TextElem.idstores theOpIddirectly so iteration no longer reconstructs it from a string key.TextCrdt.len/tombstoneCountfold count (#lzcrdtlenfold). Replaced_orderedIds().length(O(N log N) + sort allocation) with an O(N) fold over_elems.values.LosslessTreeCrdtparent→children index (#lzlivelchildidx). A new_childrenByParentmap replaces the O(N) full-scan in_liveChildren.render()drops from O(N²) to O(N). Maintained at everyCreateNode/SplitLeafsite; tombstones stay in the bucket (logical delete);forkrebuilds from the copied node map.
0.20.0 #
Changed #
- Reactive-core performance + memory pass, porting the proven
lazily-rshot-path patterns. Observable semantics are unchanged (all 399 tests +lazily-formalLean proofs stay green); the wins are in allocation and per-op work:- On-node value cache (drops the
Context._cacheMap.identity). Slot /Memo/_SignalSlotvalues now live as direct fields on the node (_cachedValue+ a_cacheGen), so a cached read is a single field load with no map lookup or hashing.Context.clear()becomes an O(1) generation bump. The single biggest win forviewport_recalc. - Iterative DFS invalidation over a
Context-owned reusable stack, replacing the recursive cascade that allocated a.toList()snapshot per node (mirrorsmark_frontier_locked).Memo's eager-recompute-and-guard now expands into the same shared stack. Reentrant cascades (eagerSignalrecompute) fall back to a local stack. - Small-list edges instead of a per-node
Seton both sides (mirrorsSmallVec<[SlotId; 2]>). The edge list is allocated lazily on first edge and identity-deduped; nodes that never connect allocate nothing. Removes two emptySetallocations per node (4M at the 2M-cell scale benchmark). _flushEffectsuses a head pointer instead of O(n)removeAt(0)(mirrorsVecDeque::pop_front);Effect.disposeno longer shifts the queue.Cell.subscribestores the typed observer directly (drops the per-subscribe wrapper closure) and_notifyObserversearly-outs when there are no observers.- Lazy batch sets —
_batchedCells/_batchedSlotsare allocated on firstbatch()entry instead of eagerly at everyContextconstruction. _detachUpstreamearly-returns when there are no upstream edges.
- On-node value cache (drops the
Performance (benchmark deltas vs 0.19.0) #
- Micro:
Cell read/write0.0512 → 0.0424 µs;Slot recompute0.2036 → 0.1170 µs;Memo equality guard0.1540 → 0.0777 µs;batch coalesce2.163 → 1.276 µs. - Scale (2M cells):
build490 → 252 ms;cold_full_recalc573 → 308 ms;viewport_recalc32.8 µs → 7.7 µs;full_recalc_invalidate_all782 → 239 ms. - Scale (10M cells / full Google Sheets workbook):
cold_full_recalc4.02 → 1.08 s;full_recalc_invalidate_all5.00 → 1.21 s;viewport_recalc29.5 → 7.58 µs.
0.19.0 #
Added #
- Realtime + distributed primitive families at full parity with lazily-rs:
temporal sources (
#lztime), rate-shaping operators (#lzrateshape), membership + failure detection (#lzmemb— SWIM + Phi-accrual), coordination (#lzcoord— lease/leader/lock/semaphore/barrier), presence + ephemeral plane (#lzpresence), stream windowing (#lzwindow), fault tolerance (#lzresilience), and the embedded-service plane (#lzservice).
0.18.0 #
Added #
WorkQueueCellcompeting-consumer delivery (#lzworkqueue). Exclusive FIFO claims use stable item ids and fresh delivery ids, worker-scoped ack/nack settlement, strict visibility expiry, tail redelivery, bounded dead-letter handling, and independent reactive count readers.
0.17.2 #
Fixed #
- TopicCell lifecycle conformance. Durable subscriber cursors remain frozen while disconnected, reads are unavailable offline, advances at the tail are no-ops, invalid ephemeral snapshots are rejected, and GC preserves absolute cursor offsets.
0.17.1 #
Fixed #
- Serialized monotonic outbox cursors. Every
StoredOutboxoperation refreshes the persisted acknowledgement cursor, so a stale handle cannot regress replay or retention semantics after another handle advances the same durable store.
0.17.0 #
Added #
CrdtTree(#lzcrdttree).TextCrdtimplements the generic lossless document contract with identity-preserving snapshot/delta and merge.- Storage-independent durable outbox (
#lzdurableoutbox).OutboxStoreprovides five ordered-byte operations,StoredOutboxowns cursor and replay semantics, andFileOutboxStoresupplies a flushed append-only restart adapter. Cursor records fold bymax, preventing stale-handle regression.
0.16.0 #
Added #
TopicCellbroadcast topics (#lztopiccell). Independent absolute subscriber cursors, durable offline replay, ephemeral disconnect lifecycle, per-subscriber reactive invalidation, snapshot restore, and safe prefix GC at the slowest durable cursor.
0.15.0 #
Added #
- RelayCell — Phases 2–6 (
#relaycell). The algebra-typed conflating relay, ported from lazily-rs:RelayCell<T>(hot head under aMergePolicy, reactiveBackpressurePolicy,Overflowblock/dropNewest/dropOldest/conflate/spill, demand-drivendepth/isFull/isEmptyslots; rejectsconflatefor a non-conflating policy);SpillStore<T>paged durable tail (reconstructspill_lossless,replayUnackedidempotent replay, ack-before-reclaim);RelayTransport<T>seam (InProcTransport/FramedTransport);Outbox<T>/Inbox<T>role facades (producer backpressure viaisFull; remote credit meter); and the Phase-6 policiesRatePolicy/WindowPolicy/ExpiryPolicy/PriorityStorage<T>/KeyedRelay<K,T>. Logical-clock time for determinism.
0.14.0 #
Added #
- Merge algebra +
MergeCell(Phase 1,#relaycell).MergePolicy<T>(an associative fold with commutative/idempotent/conflates flags) with factories keepLatest/sum/max/setUnion/rawFifo;MergeCell<T>generalizesCell(Cell ≡ MergeCell(KeepLatest)), a source whose write is a merge. Law-tests + cross-languagemergecell_algebra.jsonfixture replay.
0.13.0 #
Changed #
- Demand-driven queue reader-kinds + optional
peek/capacity(Phase 0,#relaycell).QueueCellreader-kinds (head/len/isEmpty/isFull) are now demand-driven memoizedSlots (were eagerly-setCells): a successful push/pop derives no reader value and invalidates only the readers whose value provably changed.peek/capacitybecome optionalQueueStoragecapabilities (default method bodies returningnull) — the minimal contract istryPush/tryPop/len/isClosed/close, so a raw-channel-style backend conforms directly (nohead/isFullreader). Observable semantics are unchanged; all conformance fixtures stay green.
0.12.0 #
Added #
- Reliable Sync (
#lzsync+#sync-driver). The delivery-reliability layer over theSnapshot/Delta/CrdtSyncplanes (lazily-spec § Reliable Sync), at parity with thelazily-rs/lazily-kt/lazily-jsreferences:ResyncCoordinator— receiver-sideApply/RequestSnapshot/Ignoredecision function, multi-epoch-span aware, single-request-per-gap suppression.DurableOutboxinterface +InMemoryOutbox— append-before-send,ackThroughretention,replayFromcursor (at-least-once → exactly-once).OrSet(add-wins) +WireLwwRegister<V>liveness cells on the CrdtSync plane.SyncDriver+IpcSink/IpcSource/Clock/SnapshotProviderseams — the full-duplex drain → retain-on-fail → receive/route → advertise-ack loop.ResyncRequest/OutboxAckIpcMessagecontrol frames (FFI kinds 4/5). Replays the 5conformance/reliable-sync/fixtures + SyncDriver loop-shape tests (17 new; 329 total). Dart is now ✅ on both reliable-sync coverage rows.
0.11.0 #
Keyed collections unified on ReactiveMap<K, V, H> (#reactivemap). Mirrors
lazily-spec v0.27.0 and the lazily-rs reference: one generic keyed primitive
ReactiveMap<K, V, H> (reactive membership + order, getOrInsertWith
mint-on-access, remove, move*) over a handle-kind abstraction, with two
specializations:
CellMap<K, V>=ReactiveMap<K, V, Cell<V>>— input-cell entries; adds cell-onlyset+ eager value-minting (entry/entryWith).SlotMap<K, V>=ReactiveMap<K, V, Slot<V>>— derived-slot entries;getOrInsertWithmints a slot on first access (lazy materialization),materializeAllpre-mints the keyset (eager); noset.
The same pattern applies to the concurrency flavors: ThreadSafeReactiveMap /
ThreadSafeCellMap / ThreadSafeSlotMap and AsyncReactiveMap /
AsyncCellMap / AsyncSlotMap.
BREAKING. Removes ReactiveFamily, CellFamily, the MaterializationMode
enum + kDefaultMaterializationMode, cellFamily, and the
ThreadSafeReactiveFamily / AsyncReactiveFamily types (and their
eager*/lazy* factories). Behavior is unchanged — there is no eager/lazy mode
flag: eager = pre-mint loop (materializeAll), lazy = mint-on-access
(getOrInsertWith). The 3 materialization conformance suites pass against the
shared lazily-spec fixtures (now "model": "SlotMap").
0.10.0 #
Full feature parity. The remaining concurrency rows land in the Dart
column of the lazily-spec cross-language matrix (—/~ → ✅) — Dart now
ships every row.
Thread-safe context (lock-backed) — lib/src/thread_safe.dart.
ThreadSafeContext wraps a Context behind a reentrant run-to-completion
guard. Dart isolates have no shared mutable heap, so — exactly as JavaScript
ships this layer on its single-realm event loop — synchronous code runs to
completion and already serializes access; the guard is a reentrant depth
counter, not an OS lock. Ships the pure batch-flush kernel (applyBatch /
flushBatch / unionDependents) as a faithful port of the
LazilyFormal.ThreadSafe model, property-tested independently of the live
graph (flushBatch_singleton_eq_setCell and the coalesced-frontier laws).
Thread-safe reactive family — lib/src/thread_safe_reactive_family.dart.
ThreadSafeReactiveFamily<K, V>: the guarded, value-caching flavor of
ReactiveFamily with the same eager/lazy contract, observational transparency,
present-set monotonicity, and materialization confluence
(materialize_present_comm / materialize_observe_comm). Factories
eagerSlotFamily / lazySlotFamily / eagerCellFamily / lazyCellFamily.
Async reactive family — lib/src/async_reactive_family.dart.
AsyncReactiveFamily<K, V> adds a resolution axis (pending → resolved via
drive) orthogonal to the present-set. Non-blocking observe returns
(null, false) while pending and (value, true) once resolved — the
eventual-transparency law (AsyncMaterialization.lean).
Reactive family sync (#lzfamilysync) — lib/src/distributed.dart.
CrdtPlaneRuntime gains registerFamilyLww / familySetLww / familyKeys /
familyValueLww / familyCountTrue / membershipEpoch. A keyed op for an
unregistered family entry materializes on ingest (membership propagates,
values are adopted, LWW updates converge, re-ingest is idempotent, and a
derived aggregate converges) — replays
conformance/familysync/materialize_on_ingest.json
(FamilySync.lean: applyOp_absent_adopts, present_merge, applyOp_idem,
aggregate_converges).
Shared-memory blob path (~ → ✅) — lib/src/shm_blob_arena.dart.
ShmBlobArena.transfer packages a validated blob as a ShmBlobTransfer
(descriptor + TransferableTypedData) for a zero-copy cross-isolate move —
Dart's isolate-model counterpart of the mmap/SharedArrayBuffer shared-memory
path. The receiver adopts the moved buffer and re-validates the header.
test/shm_isolate_test.dart demonstrates real multi-isolate parallelism:
zero-copy blob hand-off and family CRDT convergence across isolates,
order-independent.
0.9.0 #
Two cross-language coverage rows land in the Dart column (— → ✅):
Reactive family (ReactiveFamily) + materialization mode (#lzmatmode).
A unified keyed reactive family (lib/src/reactive_family.dart, exported from
package:lazily/lazily.dart) that maps keys to per-entry reactive nodes and
abstracts over the entry's handle kind:
EntryKind.cellentries are input cells — always materialized, any mode.EntryKind.slotentries are derived slots — governed by materialization mode.MaterializationMode.eager(the required default) allocates every declared node up front;MaterializationMode.lazydefers each derived node to its first read ("materialize on pull"), keyed rather than handle-addressed.
Materialization mode is orthogonal to entry kind and never observable on the
value axis (observe returns identical values under either mode). cellFamily
is the input-cell specialization. Mirrors
lazily-rs/src/reactive_family.rs and the lazily-spec/cell-model.md
ReactiveFamily vehicle; pinned by the shared
conformance/materialization/*.json fixtures and the lazily-formal
Materialization proofs (observe_canonical,
cell_entries_materialized_in_every_mode, slot_entries_deferred_under_lazy,
materialize_present_monotone, lazy_present_subset_eager).
Cross-process zero-copy transport (BlobBackend / shm / arrow, #lzzcpy).
Large IPC payloads spill to a pluggable blob backend and cross the wire as a
small descriptor rather than a copy (lib/src/transport.dart, exported from
package:lazily/ipc.dart):
BlobBackendadapter seam:write(bytes) → ShmBlobRef,readView(desc)zero-copy resolve,advanceEpoch().InProcessBackendandArrowBackendin-process adapters (the isolate model has no cross-process shared memory — theshared_memory: partialcarve-out; a real POSIXshmregion would needdart:ffi).BlobRouterreceiver-side multi-backend resolver routing by the descriptor'sbackenddiscriminator, plusspillMessage/spillValue/resolveValuepolicy.
ShmBlobRef gains an optional backend discriminator (BlobBackendKind —
shm (default) / arrow / in_process); it is omitted from the wire when
shm, so every legacy descriptor round-trips byte-identically. Mirrors
lazily-rs/src/transport.rs and lazily-spec/docs/zero-copy-transport.md;
pinned by conformance/delta_zero_copy_arrow.json and the lazily-formal
ZeroCopyTransport proofs (resolve_write, resolve_wrong_backend,
resolve_stale_generation, resolve_corrupt_checksum, transport_roundtrip).
0.8.0 #
Reactive queue — QueueCell SPSC/MPSC primitive + QueueStorage adapter
seam. Dart now ships the reactive queue row from the lazily-spec cross-language
matrix (Dart column → ✅). Mirrors lazily-spec/cell-model.md § "Reactive
queues" and lazily-formal/LazilyFormal/QueueCell.lean.
Added — Reactive queue (package:lazily/src/queue.dart) #
QueueCell<T>— a reactive FIFO queue: SPSC primitive with an MPSC usage rule (multiple producers push inside aContext.batch; there is no separate MPSC type). The reactive shell owns five reader-kind version cells (head/len/is_empty/is_full/closed) and invalidates by reader kind — a push to a non-empty queue does NOT invalidate theheadreader, a pop does. This reader-kind independence falls out of the!=guard onCell.value's setter: after each op the shell re-derives each reader-kind cell from the storage and writes it back in one atomicbatch, and a cell whose value did not change is not invalidated.QueueStorage<T>— the pluggable storage adapter interface. The shell / storage split keeps the reactive shell storage-agnostic; aRaftQueueStorage(embedded consensus, per the distributed-queue PRD) or an external-broker adapter (KafkaStorage, etc.) plugs into the same reactive shell.VecDequeStorage<T>— the referenceListQueue-backed FIFO, optionally bounded. Unbounded is the default; bounded exposes reactive backpressure viaisFull(a pop that transitions full → not-full invalidatesis_fullreaders — the backpressure recovery signal). Overflow policy is reject (tryPushat capacity returnsFull; elements are never silently dropped).- Closure lifecycle — close is idempotent and terminal; pop on closed +
non-empty drains (returns the next element); pop on closed + empty returns
Closed(distinct fromEmpty); push on closed returnsClosed. QueuePushError/QueuePopError/QueuePopResult— sealed union types for the observable rejection labels.- Conformance — replays all 5
lazily-spec/conformance/collections/ queuecell_*.jsonfixtures (spsc_push_pop,popped_head_observation,mpsc_multi_writer,bounded_backpressure,closure_lifecycle) using the live reactive graph as invalidation probes, plus unit tests forVecDequeStorageFIFO/bounded/zero-capacity, closure drain, bounded backpressure, reader-kind independence, pluggable custom storage, and snapshot round-trip. 250 tests pass.
0.7.0 #
Lossless tree CRDT + command/RPC message plane. Dart now ships the two
remaining feature rows from the lazily-spec cross-language matrix (Dart
column → ✅ on lossless-tree ×3 + message-passing). The only Dart — is
Thread-safe context (isolate carve-out) and the ~ is Shared-memory
blob path (I/O-channel fallback) — both documented platform carve-outs.
Added — Lossless tree CRDT (package:lazily/src/lossless_tree_crdt.dart) #
LosslessTreeCrdt— lossless concrete-syntax tree CRDT (M1). Leaves own every rendered byte; internal Element nodes own structure only, so invalid/unknown spans round-trip exactly as Raw/Error leaves. Ops: CreateNode / Tombstone / Reorder / LeafEdit / SplitLeaf / MergeLeaves, plus op-based delta sync over a dotted non-contiguous version frontier (TreeVersionFrontier). Leaves embedTextCrdt; child order reusesSeqCrdt's fractional index (keyBetween); the clock is a LamportTreeOpId. Leaf-local text offsets on the wire are UTF-8 bytes (viautf8_offsets). Wire parity with lazily-rs/kt/js. Replays all 9 lazily-spec/conformance/lossless-tree/ fixtures.
Added — Command/RPC message plane (command-plane-v1, package:lazily/src/command.dart) #
CommandSubmit/CommandCancel/CommandEvents/CommandProjection— the evented command message family, an additive sibling to Snapshot/Delta/CrdtSync. Terminal authority is the causal receipt, not the event or transport. RPC is a facade (CommandRpcClient.call/submit/cancel) over theCommandProjectionreducer; a unarycallresolves only on a terminal projection. Wire parity with lazily-rs/kt/js. Replays all 8 lazily-spec/conformance/message-passing/ fixtures.
0.6.0 #
Full feature-parity release: every feature row in the lazily-spec
cross-language coverage matrix that can run on the Dart platform is now
shipped (✅). Dart moves from ~/— to ✅ on 13 rows — the
reactive core, all CRDT collection types, the distributed plane, signaling,
state projection, causal receipts, and instrumentation.
The only — remaining is Thread-safe context — a legitimate Dart
carve-out per the spec (Dart isolates are a process/actor-isolation model
with no shared address space, so thread_safe: none is declared). The
~ remaining is Shared-memory blob path — the arena + header validation
ship, but cross-process shared memory is carried Inline per the platform
carve-out.
Added — Reactive core completion (package:lazily/lazily.dart) #
Effect— side-effect observer with cleanup-before-rerun semantics. Tracks dependencies dynamically; reruns after the current cascade (or at batch exit).Memo<T>— a [Slot] subclass with an equality guard. A dirty memo that recomputes equal suppresses the downstream cascade (noSlotValue, noInvalidate), implementing the memo-equality invariant.Context.batch— depth-counted, coalesced batch. Cell writes inside a batch defer their cascades until the outermost exit; effects flush once.
Added — CRDT collection types #
TextCrdt(package:lazily/src/text_crdt.dart) — Fugue/RGA-style character CRDT. Sticky tombstones, deterministic order (pre-order DFS, siblings descending by OpId), state-based merge (C/A/I), GC. Delta sync:versionVector(),deltaSince(theirVv),applyDelta(ops).SeqCrdt<Id, V>(package:lazily/src/seq_crdt.dart) — Move-aware sequence CRDT. Three independent LWW registers per element (value, fractional-index position, deleted); a move is a single LWW reassignment. HLC-stamped; concurrent moves converge to the later stamp.Hlc/HlcStamp/LwwRegister<V>/Position— the HLC clock and LWW register primitives backing SeqCrdt.MvRegister<V>/PnCounter/CellCrdt<T>(package:lazily/src/registers.dart) — multi-value register, PN counter, and reactive-cell-backed CRDT bridge.SemTree<V, D>(package:lazily/src/sem_tree.dart) — memoized semantic tree. One memo slot per node; editing a node recomputes only its ancestor chain; a non-changing fold result suppresses downstream.- Stable-id alignment (
package:lazily/src/stable_id.dart) —Block/BlockKey/align/assignStableKeys. Three layers: anchors, FNV-1a content hashes, word-LCS similarity (≥ 0.5 → Edited).
Added — Distributed plane + receipts + signaling #
CrdtPlaneRuntime(package:lazily/src/distributed.dart) — state-based anti-entropy runtime with op-log dedup, per-node LWW cells,converged()output, idempotent re-ingest.- Causal receipts (
package:lazily/src/causal_receipts.dart) —CausalReceipt/CausalReceiptswire types,ReceiptProjection(monotonic ledger: stale-gen ignored, duplicate no-op, terminal conflict). - Signaling (
package:lazily/src/signaling.dart) —SignalingRoomwith anti-spooffromstamping,ClientMessage/ServerMessagesealed unions, permission modes (open / allowlist). - State projection (
package:lazily/src/state_projection.dart) —StateProjectionMirror(coalesced flush delta),documentHash,buildStateEvent.
Added — Infrastructure #
ShmBlobArena(package:lazily/src/shm_blob_arena.dart) — in-process blob arena with generation/epoch header validation.- Instrumentation (
package:lazily/src/instrumentation.dart) —benchmark()harness +runBenchmarkSuite()for the reactive core, collections, and CRDT types.
Conformance #
All 30+ lazily-spec conformance fixtures now replay:
collections/textcrdt_convergence.json(6 scenarios)collections/textcrdt_delta_sync.json(4 scenarios)collections/seqcrdt_convergence.json(6 scenarios)collections/semtree_incremental.json(3 scenarios)collections/stableid_alignment.json(6 scenarios)distributed/anti_entropy_converge.json(3 scenarios)receipts/causal_receipts.jsonsignaling/anti_spoof_session.json(7-step transcript)
212 tests pass, incl. the lazily-formal Lean proof build.
0.5.1 #
Docs-only sync against the latest lazily-spec
coverage.json. No code or API changes; re-verified against the current
lazily-formal Lean proofs.
Changed #
- README coverage table resynced from
lazily-spec/coverage.jsonvianode scripts/sync-coverage.mjs— adds the Causal receipts (CausalReceiptsoutcome projection) row now tracked by the spec. lazily-dart remains—on that layer (not yet ported); the table is the single source of truth for cross-language coverage.
0.5.0 #
This release makes lazily-formal
part of the test suite — dart test now builds the Lean 4 model and verifies
the proofs the Dart implementation mirrors, closing the formal-compliance
side of the lazily-spec
Binding Conformance Matrix alongside the wire layers shipped in 0.1–0.4.
Added — Formal model in the test suite #
tool/formal_check.dart— a proof-verification hook (mirroringlazily-js/scripts/formal-check.mjs) that runslake buildover the siblinglazily-formalcheckout, located via theLAZILY_FORMAL_PATHenv var and then thesrc/lazily-dart↔src/lazily-formalsubmodule layout. SKIPs with a clear notice (exit 0) when the submodule or thelaketoolchain is absent, so pub.dev consumers and shallow clones are not broken. CI verifies the proofs for real under a full checkout + elan.test/formal_check_test.dart— wires the Lean build intodart testso a broken proof fails the suite.test/statechart_properties_test.dart— property tests mirroring theLazilyFormal.StateChart/StateMachinetheorems (determinism by construction,enabled_empty_rejects,send_preserves_chart,single_region_refines_flat_machine,single_region_enabled_at_most_one,parallel_region_confluence,recordHistory_idempotent,send_actions_empty_when_rejected).test/reactive_properties_test.dart— property tests mirroring theLazilyFormal.Reactivetheorems (setCell_equal_preserves_graph,setCell_different_invalidates_dependents,recomputeSlot_equal_preserves_dependents,recomputeSlot_different_invalidates_dependents,signal_materialized_after_recompute).
Changed #
- CI now checks out
lazily-formalas a sibling and installs elan sodart testruns the formal proof verification instead of SKIP-ing. A dedicatedleanjob (mirroring lazily-kt) builds the canonicallazily-formal+lazily-specLean models independently.
0.4.0 #
This release closes the lazily-spec Binding Conformance Matrix — every MUST layer is now implemented. lazily-dart conforms to the keyed cell collections, distributed CRDT, C-ABI FFI boundary, capability negotiation, and async reactive context layers alongside the reactive core, state charts, IPC, and permission boundary shipped in 0.1–0.3.
Added — Keyed cell collections (package:lazily/lazily.dart) #
CellMap<K, V>— a reactive composition of per-entry cells plus a dedicated membership cell and a dedicated order cell, 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 and order readers; a pure atomic move (moveTo/moveBefore/moveAfter) bumps only the order signal once and keeps the moved entry's sameCellhandle, dependents, and lineage (it is not a remove + re-mint).#lzcellfamily,#lzcellmove.CellTree<K, V>— the ordered keyed tree. A node is(stable id, value cell, ordered keyed child collection), so per-level membership/order reactivity and the atomic-move guarantee are inherited node-by-node.CellFamily<K, V>— a parameterized factory of reactive cells keyed byK(à la Recoil/JotaiatomFamily).reconcileDiff+DiffOp— the move-minimized keyed reconciliation (#lzkeyrecon). Diffs two keyed sequences by stable key (not position), emitting the minimal{insert, remove, move, update}op set; the longest-increasing-subsequence over prior indices is held fixed so keys already in relative order do not move.CellMap.reconcileapplies it per-cell. O(n log n) patience-sort LIS.- Collections conformance — mirrors the shared
lazily-spec/conformance/collections/fixtures (cellmap_independence,cellmap_atomic_move,keyed_reconciliation_lis) and replays them intest/collections_conformance_test.dart, asserting value/membership/order reactivity-independence, stable-handle invariance, and the LIS op set identically to every sibling binding (lazily-rs / lazily-kt / lazily-js).
Added — Distributed CRDT plane (package:lazily/ipc.dart) #
WireStamp/CrdtOp/CrdtSyncwire types (protocol.md § Distributed).CrdtSyncrides the same lazily-ipc transport asSnapshot/Deltaas a thirdIpcMessagevariant.CrdtOp.keyis always present on the wire (nullwhen unset, mirroring lazily-rs derived serde); the frontier is a list of[peer, WireStamp]2-tuple arrays (perschemas/distributed.json). Canonical bytes verified byte-identical to the lazily-rs serde reference.Hlc/HlcStamp— the hybrid logical clock (Karger-Shrinkman-Levine) that produces the runtime stamps. Caller-supplied wall time keeps the clock deterministic.tick(local) andobserve(remote) preserve the monotonic invariant.StampFrontier— the per-peer stamp frontier with its commutative/associative/idempotentmerge(formallystampJoin_{comm,assoc,idem}) and the causal-stability watermark (theminover membership —nulluntil every member has been observed).CrdtPlane— wires theHlc+StampFrontier+ live membership. Local edits (tick) and remote observations (observeRemote) fold into the frontier;stabilityWatermarkis what the tombstone-GC contract consumes, andisCollectableisdelete stamp ≤ watermark.CrdtSync.filterReadable— permission-filtered frame that omits ops for non-readable nodes entirely (omission, not redaction) while retaining the frontier advertisement in full.
Added — C-ABI FFI boundary (package:lazily/ffi.dart) #
LazilyFfiBytes,LazilyFfiStatus(Ok/Empty/NullPointer/ InvalidMessage/EncodeFailed/Panic), andLazilyFfiMessageKind(Unknown=0/Snapshot=1/Delta=2/CrdtSync=3). TheCrdtSync = 3discriminant is normative (the FFI message kind MUST include it). Mirrorslazily-rs/src/ffi.rs.- Channel contract:
lazilyFfiValidateJson,lazilyFfiKindJson(classify by decoding),lazilyFfiCloneJson(decode + re-encode canonical JSON), and theLazilyFfiChannelrelay. The frame is just serializedIpcMessagebytes — no custom header; the kind is derived by full decode. - lazily-dart declares the
ffi = hostcapability (Dart hasdart:ffi; it never takes thenonecarve-out reserved for browser/Worker JS).
Added — Capability negotiation (package:lazily/capability.dart) #
CapabilityHandshake+CapabilityCheck— the compatibility handshake exchanged before any graph state flows. Peers fail closed onprotocol_id,protocol_major_version,codec,ordered_reliable, or a required feature the peer does not offer. Standalone frame, not anIpcMessagevariant.BindingCapabilities+FfiCapability— the binding-level conformance declaration: every MUST layer advertised as implemented,ffi = host.
Added — Async reactive context (package:lazily/async_context.dart) #
AsyncContext— a separate reactive surface forasync/future-returning computations (compute, not protocol). Distinct handles (AsyncCellHandle/AsyncSlotHandle/AsyncEffectHandle) — not an overload of the synchronousContext.- The full
Empty → Computing → Resolved | Errorstate machine with revision tracking (a stale completion is discarded, never published), in-flight deduplication (concurrentgetAsynccallers await the same future), the re-resolve contract (benign-race windows don't panic),memoAsync(equality memo guard), async effects (serialized reruns, cleanup-before-body ordering), batch (synchronous boundary; async reruns fire after the outermost batch exits), and disposal (cancels in-flight computations). Honors all five properties of the cancellation contract (docs/async.md § Cancellation contract).
Changed #
IpcMessageis now a three-variant sum:Snapshot | Delta | CrdtSync. TheisCrdtSync/crdtSyncaccessors andIpcMessage.ofCrdtSyncconstructor are added; existingSnapshot/Deltacode is unchanged.
0.3.0 #
Changed #
StateChartis now a full Harel/SCXML chart, the native counterpart oflazily-formal'sLazilyFormal.StateChartand lazily-rs's / lazily-kt's charts. It is rebuilt around a JSONChartDef(lazily-spec/docs/state-charts.md) and a configuration-setCell(active leaves plus all active ancestors).sendis deterministic by construction — a total function of(chart, configuration, history, guards, event), mirroring the LeanStateChart.send. Breaking: replaces the previous code-first genericStateChart<S,E>/ChartState/ChartTransitionAPI.
Added #
- Orthogonal (parallel) regions with document-order descent and
conflict-free concurrent advancement — witnesses the formal
parallel_region_confluence(under pairwise-disjoint exit sets every enabled transition is taken; the result depends only on the enabled set, not its order). - Shallow + deep history (record-on-exit / restore-on-enter, with
defaultfallback on first entry) — witnessesrecordHistory_idempotentand the history restore lemmas. - External + internal transitions (LCA chosen per the formal
lcaOf:sourcefor internal-to-source,lca(activeLeaf, target)otherwise). - Sourced action trace —
lastActions()returns the ordered names fired by the initial entry or the most recentsend(exit innermost-first → transition → entry outermost-first), witnessingsend_actions_empty_when_rejectedandstepActions_sourcingobservably. - Named guards, resolved at
sendtime (fail-closed on an absent/unknown name) — witnessesenabled_empty_rejects. runactions and{"expr": …}context guards rejected explicitly per the spec's implementation-status note.finalstates are accepted as leaves without raising completion (done) events, matching lazily-py and lazily-kt.- State-chart conformance — mirrors the shared
lazily-spec/conformance/statechart/fixtures (compound, parallel, shallow + deep history, guarded, entry/exit/transition actions) and replays them intest/statechart_conformance_test.dart, assertingaccepted,active,matches, andactionsidentically to every sibling binding.
0.2.0 #
Added #
package:lazily/ipc.dart— the lazily-spec wire protocol:Snapshot,Delta,DeltaOp(all 7 variants),NodeState,IpcValue,IpcMessage,ShmBlobRef, and the optional wire-stableNodeKeykeyed address. Pure Dart (dart:convert+dart:typed_dataonly): Flutter, web, and native. Round-trips the canonical conformance fixtures fromlazily-spec.- lazily-lean transition helpers — runtime mirrors of the Lean 4 formal
model invariants (
lazily-spec/formal/lean/LazilyFormal/IPC.lean):Delta.next/isNextAfter/applyStatus(epoch sequencing + fail-closed gap resync),cellSetOps(PartialEq cell guard),memoOps(memo equality suppression),signalOps(eager Signal materialization, never a bareInvalidate), andBatchFlush(coalesced no-duplicate frontier, single epoch advance). - Permission boundary —
OpKind,RemoteOp,PeerPermissions(default-deny per-peer allowlist; read / write / trigger_effect gated independently), plus permission-filteredSnapshot/Delta(omission, not redaction).
0.1.0 #
Added #
- Reactive core:
Slot(lazy memoized derived),Cell(mutable source),Signal(eager derived), and the sharedContextwith automatic dependency tracking and cache invalidation. StateMachine<S, E>— a finite state machine backed by a reactiveCell.StateChart— a Harel-style hierarchical state machine (composite states, event bubbling, LCA entry/exit resolution, guards, transition/entry/exit actions) backed by a reactiveCell.