A library for lazy evaluation with context caching
Project description
lazily
Lazy reactive primitives for Python — the Cell kernel (Source / Computed
cells, plus Effect) with automatic dependency tracking and cache invalidation,
plus the language-agnostic lazily-spec wire protocol for mirroring graph state
across processes and languages.
Overview
lazily is the Python port of the Cell kernel (#lzcellkernel): two value
kinds — Source and Computed — plus the value-less Effect sink. Cell is
the value node the Source handle is bound to.
Source— a value written from outside (set/merge); the writable kind. Construct withsource/cell(handleSource, native classCell, slotSourceSlot/CellSlot). AMergeCellis aSourcewhose write folds under a non-KeepLatestpolicy (Cell ≡ Source(KeepLatest)).Computed— a value computed from upstream, via a compute function. Construct withcomputed(ctx, f). Guarded by default and lazy by default.Effect— a value-less sink outside the hierarchy; nothing can depend on it.
A Computed is lazy by default: dependents are marked dirty on invalidation
but only recompute when accessed. When you need eager push-style semantics —
recompute immediately, observe v1 → v2 with no unset window — make it
eager: computed(ctx, f).eager(). Going eager attaches a scheduled puller
Effect over the backing memo (eagerness is graph state — an _eager bit plus a
side table — not a distinct type), so N writes inside one batch re-materialize
the computed once, at the flush. Every cell is guarded — an equal
recompute is suppressed by the PartialEq guard (matching TC39
Signal.Computed), so unchanged values never cascade downstream work. There is
no unguarded derived mode: computed is the guarded derived constructor,
and the former separate memo construction is retired.
Migration note (v2 Cell kernel,
#lzcellkernel). The v1 value vocabulary is removed:Signal/signal/signal_def,formula/formula_def/FormulaCell,SourceCell/SourceCellSlot, and the.drive()/.undrive()/is_driven/is_activemethods are gone. Use the v2 spelling instead — an eagerComputediscomputed(ctx, f).eager(); the lifecycle is.eager()/.lazy()/.is_eager(). The construction sugarcell/cell_defis deprecated in favour ofsource/source_def, and the derived-valueslotdecorator is deprecated in favour of the guardedcomputed(slot_defremains as the storage-sense factory). Python has no compile-time read/write split (see the design's §4): the split is a convention — aSourcehasset/merge, aComputeddoes not — not a runtime gate.
There is no dedicated Context class — a plain dict is the context, so the
Rust reference's ctx.computed(f) is spelled computed(ctx, f) here. Slot is
retained as the storage position that holds a node: a Slot uses itself as
the dictionary key that caches its value, so any dict works as the reactive
"world" (lazily-spec §5.0 "Slot-as-storage"). It is the Python analog of
lazily-rs's surviving storage-sense Slot; construct it directly with
Slot(callable=…) when a raw storage node is genuinely needed.
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 | Go | C++ |
|---|---|---|---|---|---|---|---|---|
Reactive graph — two cell kinds (nodes SourceCell / ComputedCell; handles Source<T, M> / Computed<T>) + Effect sink + eager Computed (computed().eager()) / all cells guarded / batch |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Keyed-map materialization (SlotMap) — mint-on-access derived slots: transparency + deferral (#lzmatmode) |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Thread-safe keyed map (ThreadSafeSlotMap) — Send + Sync + materialization confluence (#lzmatmode) |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Async keyed map (AsyncSlotMap) — eventual transparency (#lzmatmode) |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Keyed-map sync — membership propagation + materialize-on-ingest + derived-aggregate transparency (#lzfamilysync) |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Thread-safe context (lock-backed) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Async reactive context | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Flat state machine | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Harel state charts | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Keyed reactive maps (ReactiveMap: CellMap / SlotMap) + CellTree + reconcile |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Memoized semantic tree (SemTree) |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Stable-id alignment (manufactured identity) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Reactive queue (QueueCell SPSC/MPSC + QueueStorage adapter) |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Broadcast topic (TopicCell) — independent cursors + durable replay + safe GC (#lztopiccell) |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Competing-consumer work queue (WorkQueueCell) — exclusive leases + ack/nack + redelivery + DLQ (#lzworkqueue) |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Merge algebra + Source<T, M> — associative MergePolicy (KeepLatest/Sum/Max/SetUnion/RawFifo), Cell ≡ Source<KeepLatest>, read-any-cell/write-Source split (#relaycell) |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
RelayCell — conflating relay + BackpressurePolicy + SpillStore + Transport + Inbox/Outbox + Rate/Window/Expiry/Priority/keyed policies (#relaycell) |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Free-text character CRDT (TextCrdt) |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
TextCrdt delta sync (version_vector / delta_since / apply_delta) |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
CrdtTree lossless document contract (#lzcrdttree) |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Move-aware sequence CRDT (SeqCrdt) |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Lossless tree CRDT core (LosslessTreeCrdt, M1) |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Lossless tree — dotted-frontier anti-entropy | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Lossless tree — concurrent merge convergence | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Registers (LWW / MV) + PnCounter + CellCrdt |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
IPC wire — Snapshot + Delta + CrdtSync |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Shared-memory blob path (ShmBlobArena) |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Cross-process zero-copy transport (BlobBackend / shm / arrow) |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Distributed CRDT plane (CrdtPlaneRuntime / anti-entropy) |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Reliable sync — resync coordinator + at-least-once durable outbox + OR-set/LWW liveness (#lzsync) |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Storage-independent durable outbox (OutboxStore + shared outbox protocol; SQLite/Room/IndexedDB/file adapters) |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Reliable-sync transport seam + full-duplex SyncDriver loop (IpcSink/IpcSource, #sync-driver) |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Distributed plane — WebRTC transport + signaling | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| State projection / mirror | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Causal receipts (CausalReceipts outcome projection) |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Message-passing + RPC command plane (command-plane-v1) |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| C-ABI FFI boundary | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Permission boundary (PeerPermissions / RemoteOp) |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Capability negotiation (SessionHandshake) |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Instrumentation / benchmarks | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Temporal sources — TimerCell / IntervalCell / CronCell / DeadlineCell over a logical clock (#lztime) |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Rate-shaping operators — DebounceCell / ThrottleCell / SampleCell / ProbabilisticSampleCell (#lzrateshape) |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Membership + failure detection — MembershipCell (SWIM + Phi-accrual) / PeerSet / PeerChangeEvent (#lzmemb) |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Distributed coordination — LeaseCell / LeaderCell / LockCell / SemaphoreCell / BarrierCell+QuorumCell (#lzcoord) |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Presence + ephemeral plane — PresenceCell / AwarenessCell / EphemeralCell + Ephemeral/Durable markers (#lzpresence) |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Stream windowing — TumblingWindow / SlidingWindow / SessionWindow over the merge algebra (#lzwindow) |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Fault tolerance — CircuitBreakerCell / RetryPolicyCell / BulkheadCell / TimeoutCell (#lzresilience) |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Embedded-service plane — HealthCell / ReadinessCell / DiscoveryCell / ServiceRegistry (#lzservice) |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Installation
pip install lazily
Example usage
from lazily import CellSlot, cell, slot
# Cells hold a value that can be updated.
name = CellSlot[dict, dict, str]()
# Slots are functions that depend on cells and other slots.
@slot
def greeting(ctx: dict) -> str:
print("Calculating greeting...")
return f"Hello, {name(ctx).value}!"
# A CellSlot can also have a default value.
@cell
def response(ctx: dict) -> str:
return "How are you?"
@slot
def greeting_and_response(ctx: dict) -> str:
print("Calculating greeting_and_response...")
return f"{greeting(ctx)} {response(ctx).value}"
ctx = {}
name(ctx).value = "World"
# First access: runs the function
print(greeting(ctx))
# Calculating greeting...
# 'Hello, World!'
# Second access: uses cache (no print)
print(greeting(ctx))
# 'Hello, World!'
# Dependencies also access cached values
print(greeting_and_response(ctx))
# Calculating greeting_and_response...
# 'Hello, World! How are you?'
# Dependencies also cached
print(greeting_and_response(ctx))
# 'Hello, World! How are you?'
# Update cell: invalidates cache
name(ctx).value = "Lazily"
# Access again: re-runs the function
print(greeting_and_response(ctx))
# Calculating greeting_and_response...
# Calculating greeting...
# 'Hello, Lazily! How are you?'
# Another access: uses cache
print(greeting_and_response(ctx))
# 'Hello, Lazily! How are you?'
Core Concepts
Context
A plain dict is the context. It owns all cached Slot values; Slots store their
cache under themselves as keys. The current implementation is single-threaded —
create one dict per reactive graph.
Slot
A Slot wraps a compute function (ctx) -> T; the result is cached after first
access. Dependencies are discovered automatically via a global slot_stack —
any Slot or Cell read during computation becomes a dependency and re-subscribes
on every recompute, so conditional branches update the dependency graph with no
manual cleanup. When a dependency invalidates, the Slot only marks its cache
dirty; it does not recompute until called again.
| Type | Purpose |
|---|---|
BaseSlot[C_in, C_ctx, T] |
Base slot without subscriber support |
Slot[C_in, C_ctx, T] |
Storage-sense slot with dependency tracking and invalidation |
slot |
Deprecated — use computed (guarded) or Slot(callable=…) (storage) |
slot_def(resolve_ctx) |
Storage-sense decorator factory for a custom context resolver |
Source cell
A Source cell (native class Cell) holds a mutable value. Reading cell.value
inside a Computed or Effect auto-subscribes that reader; assigning
cell.value = x (or cell.set(x)) compares old and new via != and, only if
changed, cascades invalidation to dependents. Construct with source (the v1
cell name is deprecated).
| Type | Purpose |
|---|---|
Cell[T] / Source[T] |
Mutable source value with subscription support |
CellSlot[C_in, C_ctx, T] |
Slot that returns a Source cell |
source |
Decorator: CellSlot with an identity resolver (canonical) |
source_def(resolve_ctx) |
Decorator factory for a custom context resolver |
cell / cell_def |
Deprecated v1 aliases of source / source_def |
Eager Computed
An eager Computed — computed(ctx, f).eager() — is the counterpart to a
lazy Computed. Where a lazy cell marks itself dirty on invalidation and
recomputes on the next read, an eager one recomputes the instant a dependency is
invalidated, before the mutating call returns. The value is always materialized,
so observers never see an intermediate unset value.
from lazily import CellSlot, computed
n = CellSlot[dict, dict, int]()
ctx: dict = {}
n(ctx).value = 1
doubled = computed(ctx, lambda c: n(c).value * 2).eager() # eager: materialized now
print(doubled.value) # 2
n(ctx).value = 5 # doubled recomputes immediately
print(doubled.value) # 10 — already current, no lazy read needed
An eager Computed is composed from existing primitives, not a parallel
engine: a memoized backing Slot supplies glitch-free, guarded recomputation,
and a small puller Effect re-materializes it after every invalidation to supply
the eagerness. It inherits the guard (an equal recompute suppresses the
downstream cascade). .lazy() (or .dispose()) removes the eager puller — the
value stays readable but reverts to lazy (recompute-on-read) behavior.
| Method | Purpose |
|---|---|
computed(ctx, f) |
Lazy, guarded derived value bound to a context |
computed(ctx, f).eager() |
The eager form (idempotent; returns the same handle) |
.lazy() / .is_eager() |
Revert to lazy / query eagerness |
computed_def(resolve_ctx) |
Decorator factory for a context-cached lazy Computed |
StateMachine
StateMachine[S, E] is a finite state machine backed by a reactive Cell, so
its state participates in dependency tracking like any other reactive value.
Construct it with StateMachine(ctx, initial, transition) where transition is
a pure (state, event) -> next_state | None (returning None rejects the
event). send(event) returns whether the transition was accepted; a
self-transition to an equal state is accepted but suppressed by the Cell's
PartialEq guard.
Reactive collections, async, and thread-safe contexts
lazily-py also implements the lazily-spec compute-layer MUSTs, each ported
from its Lean formal model in lazily-formal:
ReactiveMap/CellMap/SlotMap/CellTree— keyed reactive collections (#reactivemap) with independent value/membership/order signals and atomic move. One genericReactiveMapover a handle kind;CellMap(input cells, addsset+ eagerentry) andSlotMap(derived slots, lazyget_or_insert_with+ eagermaterialize_all) are its specializations.QueueCell— a reactive FIFO queue (SPSC primitive with an MPSC-via-batchusage rule) with a pluggableQueueStoragebackend. Reader-kind invalidation (head/len/is_empty/is_full/closed), bounded reactive backpressure viais_full, and the closure lifecycle (drain / Closed-distinct-from-Empty / idempotent).reconcile_ops— move-minimized keyed reconciliation (LIS kernel).AsyncSlot/AsyncEffect— the async slot lifecycle with stale-completion discard, and cleanup-before-body effect scheduling.ThreadSafeContext— a lock-serializedbatchboundary that coalesces writes into one invalidation pass.lazily.ffi— the C-ABI FFI boundary (LazilyFfiStatus,LazilyFfiMessageKindincl.CrdtSync = 3,LazilyFfiBytes).
The test suite gates on lazily-formal's lake build (every theorem checks)
and mirrors the named Lean theorems as property tests. See SPEC.md for the
full compliance surface.
Reactive queue — lazily.queue
QueueCell is a FIFO collection composed of reactive cells — not a new cell
kind — that adds queue semantics (push to tail, pop from head) to the reactive
graph. It is an SPSC primitive; MPSC is a usage rule on the same primitive —
multiple producers push inside one batch, which serializes the pushes into a
deterministic order. The reactive shell wraps a pluggable QueueStorage backend
(default VecDequeStorage); the shell owns the reader-kind version cells and
invalidates by reader kind — a push to a non-empty queue does NOT invalidate the
head reader, a pop does.
from lazily import QueueCell, QueuePopError, batch
ctx = {}
q: QueueCell[str] = QueueCell(ctx)
q.try_push("a")
q.try_push("b")
assert q.head() == "a"
assert q.len() == 2
assert q.try_pop() == "a"
# Bounded queue → reactive backpressure via is_full.
bq = QueueCell[int].with_capacity(ctx, 2)
bq.try_push(1)
bq.try_push(2)
assert bq.is_full()
assert bq.try_push(3).label == "Full" # reject at capacity
assert bq.try_pop() == 1
assert not bq.is_full() # pop freed a slot → is_full reader invalidated
# MPSC: multiple producers push inside one batch → one invalidation pass.
batch(lambda: (q.try_push("p1"), q.try_push("p2")))
# Closure: pop on closed+empty returns Closed (distinct from Empty).
q.close()
assert q.is_closed()
assert q.try_push("x").label == "Closed"
Competing-consumer work queue
WorkQueueCell provides exclusive FIFO claims with visibility deadlines,
worker-scoped acknowledgements, tail retries, and bounded dead-letter handling.
Item ids survive retries while each claim gets a fresh delivery id.
from lazily import WorkQueueCell
work = WorkQueueCell[str](ctx, visibility_timeout=10, max_deliveries=3)
work.push("job")
delivery = work.claim("worker-a", 100)
assert delivery is not None
assert work.ack("worker-a", delivery.delivery_id)
The reader-kind independence law (a push to a non-empty queue does not change
head, so the head reader is not invalidated) comes for free from the Cell
!= (PartialEq) guard: after each op the shell re-derives each reader-kind cell
from storage and writes it back, and a cell whose value did not change is not
invalidated.
IPC — the lazily-spec wire protocol
lazily.ipc implements the language-agnostic lazily-spec
wire protocol, so a Python graph's state can be mirrored to remote observers
across processes and languages. The JSON encoding is byte-compatible with the
Rust (lazily-rs), Zig (lazily-zig), and TypeScript (@lazily/signaling)
bindings, and is validated against the canonical lazily-spec/conformance
fixtures (vendored under tests/conformance/).
Two message kinds flow over any transport (WebSocket text, WebRTC data, FFI buffer):
Snapshot— the full graph state at an epoch (nodes,edges,roots).Delta— an ordered batch of the 7DeltaOpvariants (CellSet,SlotValue,Invalidate,NodeAdd,NodeRemove,EdgeAdd,EdgeRemove) applied with epoch sequencing and fail-closed resync.
from lazily import (
Snapshot, NodeSnapshot, EdgeSnapshot, ShmBlobRef,
Delta, DeltaOp, IpcMessage,
)
# Build and serialize a snapshot — encode_json() returns transport-agnostic bytes.
snap = Snapshot(
epoch=7,
nodes=[
NodeSnapshot.payload(1, "i32", bytes([1, 2, 3])),
NodeSnapshot.opaque(2, "opaque-type"),
NodeSnapshot.shared_blob(3, "text/plain", ShmBlobRef(0, 16, 1, 7, 999)),
],
edges=[EdgeSnapshot(2, 1), EdgeSnapshot(3, 1)],
roots=[1, 2],
)
wire = IpcMessage.of_snapshot(snap).encode_json()
assert IpcMessage.decode_json(wire).snapshot == snap
# An incremental delta carrying mutations.
delta = Delta.next(40, [
DeltaOp.cell_set(1, bytes([10])),
DeltaOp.invalidate(3),
])
IpcMessage.of_delta(delta).encode_json()
A PeerPermissions boundary gates what is shared: it is default-deny, so only
nodes a peer is explicitly allowed to read are serialized into a snapshot or
delta — non-allowlisted nodes are omitted entirely.
Shared-memory blobs — ShmBlobArena
ShmBlobArena lets a Python process host blob payloads (not just carry
ShmBlobRef descriptors). It is a bytearray-backed, append-only arena with a
40-byte header (LZSH magic + FNV-1a-64 checksum) and wraparound, ported from
the lazily-rs ShmBlobArena<B> and byte-compatible with the Rust and Zig
arenas. The module exports ShmBlobArena, ShmBlobArenaError (with its variant
subclasses), and SHM_BLOB_HEADER_LEN.
Lossless tree CRDT — lazily.lossless_tree_crdt
LosslessTreeCrdt (#lzlosstree) is a single rooted concrete-syntax tree whose
leaves own every rendered byte — render(tree) == source_text for valid,
invalid, and unknown source alike. Where TextCrdt is a flat lossless floor,
this is the structured tree that can itself be the wire authority. Element nodes
own structure only; all text lives in leaf nodes tagged Token / Trivia /
Raw / Error, so unknown/invalid spans round-trip exactly as Raw/Error
leaves rather than being discarded.
from lazily import LeafKind, LosslessTreeCrdt, SeedElement, SeedLeaf
from lazily.lossless_tree_crdt import ROOT
tree = LosslessTreeCrdt(peer=1)
heading = tree.create_node(ROOT, None, SeedElement("heading"))
tree.create_node(heading, None, SeedLeaf(LeafKind.TOKEN, "# "))
title = tree.create_node(heading, None, SeedLeaf(LeafKind.RAW, "Título"))
assert tree.render() == "# Título"
# Op-based delta sync: fork, diverge, converge through a dotted frontier.
other = tree.fork(peer=2)
other.edit_leaf(title, 0, 0, "X")
tree.apply_update(other.diff(tree.frontier()))
assert tree.render() == other.render()
Leaf text embeds TextCrdt wholesale; child order is a fractional index
(key_between); the clock is a Lamport TreeOpId. Anti-entropy is op-based over
a dotted, non-contiguous version frontier (TreeVersionFrontier) — a dot
set (contiguous prefix + sparse holes), never a per-peer max, so a missing
interior op stays representable and re-requestable. Leaf-local wire offsets are
UTF-8 bytes (byte_to_char). The wire codec (tree_update_to_wire /
tree_update_from_wire) validates against lazily-spec's
lossless-tree-delta.json, and all nine conformance/lossless-tree/ fixtures
replay.
Command / RPC message plane — lazily.command
command-plane-v1 is an additive sibling to Snapshot / Delta /
CrdtSync: four evented frames (CommandSubmit / CommandCancel /
CommandEvents / CommandProjection) that carry command traffic, not cell
state. lazily owns the envelope; the namespace owns the IpcValue payload, which
lazily never decodes.
The single hard rule: terminal authority is the causal receipt. A command is
terminal only when a terminal CausalReceipt for its command_id folds in
(applied, or rejected — including the cancelled / superseded /
timed_out reasons). observed / accepted / started events are progress
only; a transport ACK is never terminal.
from lazily import (
CommandPolicy, CommandRpcClient, CommandSubmit, DedupePolicy,
applied_receipt,
)
from lazily.ipc import IpcValue
class Transport:
def __init__(self):
self.sent = []
def send(self, message):
self.sent.append(message)
client = CommandRpcClient(Transport())
cmd_id = client.submit(CommandSubmit(
command_id="cmd-1", causation_id="cmd-1", source="plugin",
target="controller", namespace="agent-doc", name="editor_route",
authority_generation=1, idempotency_key="doc:run", deadline_ms=0,
policy=CommandPolicy(DedupePolicy.SAME_IDEMPOTENCY_KEY, False, True),
payload_type="agent-doc.editor_route.v1", payload_hash="sha256:…",
payload=IpcValue.of(b"{…}"),
required_features=["command-plane-v1"],
))
# `call` resolves ONLY on a terminal receipt — never an ACK or `accepted`.
client.ingest_receipt(applied_receipt("rcpt-1", cmd_id, "controller", 1))
assert client.poll_call(cmd_id).kind.value == "resolved"
CommandProjection is the pure reducer (generation guards, idempotency,
cancel-before-terminal-only, terminal-conflict-fails-closed, reconnect
equivalence); CommandRpcClient is the derived RPC facade. The wire codec
validates against lazily-spec's message-passing.json, and all eight
conformance/message-passing/ fixtures replay.
Benchmarks
Wall-clock benchmarks live in BENCHMARKS.md, covering both
the in-library micro-suite (reactive core, keyed reconciliation, CellMap,
TextCrdt, CRDT plane) and a large spreadsheet-shaped scale suite that
mirrors the lazily-rs / lazily-go scale groups (N input cells + N formula
slots, formula[i] = input[i] + input[i-1]). The scale suite is measured up to
a full 10,000,000-cell Google Sheets workbook (N = 5,000,000); a one-cell
edit plus a 1,000-cell viewport read stays in the ~75 µs range regardless of
sheet size, because the lazy pull model recomputes only the ~2 formulas that
read the edited input.
make bench # micro-suite
make bench-scale # scale suite (default N = 1,000,000)
# or directly, with a custom size:
uv run python -m lazily.benchmarks
LAZILY_SCALE_N=5000000 uv run python -m lazily.scale_bench # 10M-cell workbook
See BENCHMARKS.md for the full results, hardware, and honest
notes on CPython's per-node overhead.
The lazily family
lazily-py is one binding in a cross-language reactive family that shares the
lazily-spec wire protocol:
| Binding | Language | Package |
|---|---|---|
lazily-rs |
Rust | lazily (crates.io) |
lazily-py |
Python | lazily (PyPI) |
lazily-zig |
Zig | GitHub |
@lazily/signaling |
TypeScript / Cloudflare Worker | npm |
lazily-spec |
— | wire protocol + conformance fixtures |
lazily-formal |
— | Lean 4 formal model (FSM kernel + Harel state chart) |
See lazily-spec for the canonical
Snapshot/Delta schemas, the IPC Lean proofs of the epoch/memo/batch invariants,
and the conformance fixtures every IPC-capable binding validates against. The
language-agnostic formal model — the flat FSM kernel and the full Harel state
chart — lives in lazily-formal.
Development
This project uses uv. Run the local
CI-equivalent suite — type-check (ty), lint (ruff), the runnable README
example, and the test suite — with:
uv run poe precommit
SPEC.md is the authoritative specification for the Python primitives and the
lazily-spec compliance notes.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file lazily-0.35.0.tar.gz.
File metadata
- Download URL: lazily-0.35.0.tar.gz
- Upload date:
- Size: 328.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0574147232e1b84daab35a2f01a9543b3d2a29c2628a29e4b98181d5cbc38d32
|
|
| MD5 |
e7b8646eeec93e764e4bb59b7f0c3fbe
|
|
| BLAKE2b-256 |
8e546f6c62fa18ee4a7f019796f913cf1732217c4ec62d4ff5052226821276fb
|
File details
Details for the file lazily-0.35.0-cp314-cp314-win_amd64.whl.
File metadata
- Download URL: lazily-0.35.0-cp314-cp314-win_amd64.whl
- Upload date:
- Size: 333.3 kB
- Tags: CPython 3.14, Windows x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
022483d8f5f34d02cefc9c652e8de147f2ecac20cc3329083b9d7ec953dba046
|
|
| MD5 |
be6be1a346454d1be20c412b50672339
|
|
| BLAKE2b-256 |
b302366e4f0eba972fb2f1d40b34224b124ef4f77d4dabf0ee83792bc4c7011b
|
File details
Details for the file lazily-0.35.0-cp314-cp314-musllinux_1_2_x86_64.whl.
File metadata
- Download URL: lazily-0.35.0-cp314-cp314-musllinux_1_2_x86_64.whl
- Upload date:
- Size: 519.1 kB
- Tags: CPython 3.14, musllinux: musl 1.2+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bb02a6936e45a150c4220e3044167c2210a81f34a7dd107ce4ff448e5bf26e53
|
|
| MD5 |
13772427b3fadac5ad787c7229724308
|
|
| BLAKE2b-256 |
4569e471255d7618d942b5e5c1c7a319d34acd4faa21418fb52295ade68e099b
|
File details
Details for the file lazily-0.35.0-cp314-cp314-musllinux_1_2_aarch64.whl.
File metadata
- Download URL: lazily-0.35.0-cp314-cp314-musllinux_1_2_aarch64.whl
- Upload date:
- Size: 522.1 kB
- Tags: CPython 3.14, musllinux: musl 1.2+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
14a63edce5d24b261ea16cf88e68f504de33034096cdaa68f3a1c8afab13b7a4
|
|
| MD5 |
bbd97ab50bfb9d773a66fb69a5478816
|
|
| BLAKE2b-256 |
8741b999916c93818da3d5d33c4a66f414e56deb32bdde2ff2fae323821ad04a
|
File details
Details for the file lazily-0.35.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.
File metadata
- Download URL: lazily-0.35.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
- Upload date:
- Size: 520.9 kB
- Tags: CPython 3.14, manylinux: glibc 2.17+ x86-64, manylinux: glibc 2.28+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3bd258c37f02aeec51fed58730851c1ac5622c73c2a7701badcd5208eea1b4e6
|
|
| MD5 |
c1b40c3260071b1bc8fb57daa289b72d
|
|
| BLAKE2b-256 |
892c6f557dc3a2a9d6a75420e76dc32f1ef3d03bf92683340eea9e8841d28e4c
|
File details
Details for the file lazily-0.35.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl.
File metadata
- Download URL: lazily-0.35.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl
- Upload date:
- Size: 515.8 kB
- Tags: CPython 3.14, manylinux: glibc 2.17+ ARM64, manylinux: glibc 2.28+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2042979398665fbfd4e42d0a761460a83533591096c859a1d640a32f4abf7700
|
|
| MD5 |
25d401ce9e0c9f611cdea16ea8c46d33
|
|
| BLAKE2b-256 |
2dc08123c2a23c3def5fdb158b7a7df1573857ba41a38e8b6c4c0ee17f595188
|
File details
Details for the file lazily-0.35.0-cp314-cp314-macosx_11_0_arm64.whl.
File metadata
- Download URL: lazily-0.35.0-cp314-cp314-macosx_11_0_arm64.whl
- Upload date:
- Size: 370.5 kB
- Tags: CPython 3.14, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
98a935e8335412431a89eff102f9c2d6d8b30217aac15272ac57972db039ce04
|
|
| MD5 |
0dafe9182b6c9aefe87001ac0875f123
|
|
| BLAKE2b-256 |
d6d5d2c8eb0dd53b7fbe421bfa1b0a54d13e656b5e09d33262043906724a377f
|
File details
Details for the file lazily-0.35.0-cp314-cp314-macosx_10_15_x86_64.whl.
File metadata
- Download URL: lazily-0.35.0-cp314-cp314-macosx_10_15_x86_64.whl
- Upload date:
- Size: 377.7 kB
- Tags: CPython 3.14, macOS 10.15+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
14adaae8934396cbf541ae8679b45f077ada473b89dc14d4a394fa2f550531d3
|
|
| MD5 |
7db39448c87a4ce94eb3c51fcbc43ca7
|
|
| BLAKE2b-256 |
4b75becf4013259703960e0ed61284e60fb2c221c4b455f560f41bdc02dce294
|
File details
Details for the file lazily-0.35.0-cp313-cp313-win_amd64.whl.
File metadata
- Download URL: lazily-0.35.0-cp313-cp313-win_amd64.whl
- Upload date:
- Size: 332.3 kB
- Tags: CPython 3.13, Windows x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e0987cc561d1fe9b82a7e326752eae5913832a76f2a8105e1a443b256110229e
|
|
| MD5 |
b47eff76276d8127e8db82eb9aaef72d
|
|
| BLAKE2b-256 |
e6d1f5c3e2422170a48577eaee2d00c13bb9e4132c08e2f9a3c5d629fc1de00f
|
File details
Details for the file lazily-0.35.0-cp313-cp313-musllinux_1_2_x86_64.whl.
File metadata
- Download URL: lazily-0.35.0-cp313-cp313-musllinux_1_2_x86_64.whl
- Upload date:
- Size: 518.3 kB
- Tags: CPython 3.13, musllinux: musl 1.2+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
72fa1cf4591b50fbbe596108601f830977f6564acc6fef2585d9aea55bbee95e
|
|
| MD5 |
f3cf8c484e453ae08728a8ab5f6264f7
|
|
| BLAKE2b-256 |
34d599ca43df74285e35cd411483f16b152315e814709c9758b169c0693f5526
|
File details
Details for the file lazily-0.35.0-cp313-cp313-musllinux_1_2_aarch64.whl.
File metadata
- Download URL: lazily-0.35.0-cp313-cp313-musllinux_1_2_aarch64.whl
- Upload date:
- Size: 520.9 kB
- Tags: CPython 3.13, musllinux: musl 1.2+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
126cc8a6676021c6200e1ead9fdff4cac500d4b721ad59c12d08bca504ebdf37
|
|
| MD5 |
0a25870c56d48989c4c920fb4e3516b3
|
|
| BLAKE2b-256 |
c8db98586be023cacbe34f771b98dd46107796fa6682966d328b17cbfbb2e79e
|
File details
Details for the file lazily-0.35.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.
File metadata
- Download URL: lazily-0.35.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
- Upload date:
- Size: 520.1 kB
- Tags: CPython 3.13, manylinux: glibc 2.17+ x86-64, manylinux: glibc 2.28+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ab5045455efb1200bcf8aa09eea161050360b5989e16e162421a38a7baef9c86
|
|
| MD5 |
266d6661fb28aff079d3e86da98e28c7
|
|
| BLAKE2b-256 |
ec9bb5605ae0a3abe5c50fcc07b28cf50da925a58dfeef84f6bdc806dfb395d4
|
File details
Details for the file lazily-0.35.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl.
File metadata
- Download URL: lazily-0.35.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl
- Upload date:
- Size: 513.6 kB
- Tags: CPython 3.13, manylinux: glibc 2.17+ ARM64, manylinux: glibc 2.28+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
004bb45cb591a23a3776c4f5d580c8d877e51cc417533a924467c81b68dd716d
|
|
| MD5 |
3a7b4ce3e02d438b331c3bd6ca94b30d
|
|
| BLAKE2b-256 |
b6a44f4034a7320d430e37b0ac94235e790d8dbe17f5566fa1cff66929ef5e69
|
File details
Details for the file lazily-0.35.0-cp313-cp313-macosx_11_0_arm64.whl.
File metadata
- Download URL: lazily-0.35.0-cp313-cp313-macosx_11_0_arm64.whl
- Upload date:
- Size: 370.8 kB
- Tags: CPython 3.13, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1cd2d01290e7a3bc25f304d287c99806f9b04bde192e42c4be5d6a7aeca7402e
|
|
| MD5 |
569955c0aca3d1d201274f6c46914609
|
|
| BLAKE2b-256 |
026ce1b46f78b9d28f546f322be48ea4f7081b9659dd037ab1cad7a4c988bf00
|
File details
Details for the file lazily-0.35.0-cp313-cp313-macosx_10_13_x86_64.whl.
File metadata
- Download URL: lazily-0.35.0-cp313-cp313-macosx_10_13_x86_64.whl
- Upload date:
- Size: 378.7 kB
- Tags: CPython 3.13, macOS 10.13+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
55f0f40e3a7657494d2ba6bb450ec6972429bcd88e7db76e70f66ab7590fe333
|
|
| MD5 |
02ce7303c2c50061739b6e54b8f63d17
|
|
| BLAKE2b-256 |
d0cddee305c579febd45bd017d709349a0618b0a6ff03978ad99c385181590c9
|
File details
Details for the file lazily-0.35.0-cp312-cp312-win_amd64.whl.
File metadata
- Download URL: lazily-0.35.0-cp312-cp312-win_amd64.whl
- Upload date:
- Size: 331.7 kB
- Tags: CPython 3.12, Windows x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dd1d5dceb3d6dfef3cad386a5cc8a6a3e12f572d2131e791c0e0a4c2e0a9369f
|
|
| MD5 |
ddaf3c2a5870a1a23d28856e5c6e4d70
|
|
| BLAKE2b-256 |
e2bbf2db20a047c34ea811de9ad4cf8fd6893937a217fe5fa2f4b54f74a1312f
|
File details
Details for the file lazily-0.35.0-cp312-cp312-musllinux_1_2_x86_64.whl.
File metadata
- Download URL: lazily-0.35.0-cp312-cp312-musllinux_1_2_x86_64.whl
- Upload date:
- Size: 521.3 kB
- Tags: CPython 3.12, musllinux: musl 1.2+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cbce5d65859cb0939d4c6de47004e752c9422a45260e26eb13b62587d0e284be
|
|
| MD5 |
35ae6f19eb79846fa3711c2806445624
|
|
| BLAKE2b-256 |
c9ae8fceb4fec907ee9027f656a0e0038455e61e36118faa9ea3e42d5a00df14
|
File details
Details for the file lazily-0.35.0-cp312-cp312-musllinux_1_2_aarch64.whl.
File metadata
- Download URL: lazily-0.35.0-cp312-cp312-musllinux_1_2_aarch64.whl
- Upload date:
- Size: 524.2 kB
- Tags: CPython 3.12, musllinux: musl 1.2+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ba3ce77d8327c79ca78cc37ebc0be07303263223f45fcf006066ef558f3860bd
|
|
| MD5 |
fe8f47f8c3f481f199e796516d3209ee
|
|
| BLAKE2b-256 |
e0f31d0eaf5db789dfa0a69319051588063c8c8841492c27269310fdf1846314
|
File details
Details for the file lazily-0.35.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.
File metadata
- Download URL: lazily-0.35.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
- Upload date:
- Size: 523.3 kB
- Tags: CPython 3.12, manylinux: glibc 2.17+ x86-64, manylinux: glibc 2.28+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
210e164df8e5e8bd974c0912baca693f3191ca601a127a330a92c060a9cc6a7c
|
|
| MD5 |
351d25c24cd05b4cfa1d45341cbaea26
|
|
| BLAKE2b-256 |
3b49da8ec62546c2b8d06b45da51547f92209002ac9c34608c8485c87a0527c1
|
File details
Details for the file lazily-0.35.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl.
File metadata
- Download URL: lazily-0.35.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl
- Upload date:
- Size: 518.1 kB
- Tags: CPython 3.12, manylinux: glibc 2.17+ ARM64, manylinux: glibc 2.28+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3f7b15cc074096abde1d1ef02f7ff953f5d7cd28d0add9ee57661fd6251052ec
|
|
| MD5 |
4dcbc6c26c67f93ef73e7f353640c073
|
|
| BLAKE2b-256 |
832115c29749928a9f0bbff6ae0a30e41405e68f433bcce04159b4ef49b09398
|
File details
Details for the file lazily-0.35.0-cp312-cp312-macosx_11_0_arm64.whl.
File metadata
- Download URL: lazily-0.35.0-cp312-cp312-macosx_11_0_arm64.whl
- Upload date:
- Size: 371.4 kB
- Tags: CPython 3.12, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
82b1f643cc6073cc6f8d5cf0f2636466ea67931ec04c5248c15b035ce84b7977
|
|
| MD5 |
4794c02f523e5e4ef18a9d05c2b44251
|
|
| BLAKE2b-256 |
818e6a143fd5587285208a67a25d063c1c6830c84f432a113d3651e255b92bec
|
File details
Details for the file lazily-0.35.0-cp312-cp312-macosx_10_13_x86_64.whl.
File metadata
- Download URL: lazily-0.35.0-cp312-cp312-macosx_10_13_x86_64.whl
- Upload date:
- Size: 380.0 kB
- Tags: CPython 3.12, macOS 10.13+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e38377fbd07d5390ec4729fc5fb707361d802f966ec4a3561feda995c5047131
|
|
| MD5 |
a8713b06810fa52355d9a2ea175eddf0
|
|
| BLAKE2b-256 |
6bd0d985fcfa4fd6dd78ecdb3815ea9271aaa21e7dc2d69f0341435e020cad0f
|