Projections
Immutable operations are what NodalMerge stores. Projections are what your product reads. Between the two sits one deterministic function: same history in, same materialized view out, every time, on every peer. If operations are the primitive that makes convergence possible, projections are the primitive that makes convergence usable — they’re the other half of the same idea, not a feature bolted onto it.Why this is a primitive, not a feature
Most systems that separate “log” from “read model” treat the read model as one specific integration — a search index, a cache, a materialized SQL view. NodalMerge treats the shape of that separation itself as the core abstraction, and reuses it everywhere:- Map, text, and list state —
resolve/resolve_canonical/resolve_listare projections over the room DAG, incrementally maintained so reads stay O(live keys) or O(items) instead of O(history). See benchmarks/text-engine-performance for what that costs in practice. - Speculative vs. canonical lanes — two projections of the same history, one including local optimistic writes and one reflecting only accepted outcomes. See architecture/speculative-vs-authoritative.
- Registered query projections —
query.registerdefines a descriptor (a prefix, a shape) once;projection.buildmaterializes it at a given checkpoint;projection.readpages through the result;projection.invalidatedrops it when the query definition changes. See api-reference/websocket-commands for the full command contract. - Replay to a point in time — a projection isn’t pinned to “now.” The same function run against a bounded prefix of history produces the state as of that checkpoint, which is what makes deterministic replay, audit, and debugging possible without a separate snapshot mechanism. See architecture/replay-and-branching.
What makes a projection safe to treat this way
A projection is a pure function of the history it was built from:- Deterministic: the same operations, in the same causal order, always produce the same materialized view — this is inherited directly from the same merge determinism that makes peer convergence work at all (see architecture/crdt-model).
- Rebuildable: because it’s derived, not authoritative, a projection can be thrown away and recomputed from history at any time. There is no migration step where a projection’s internal representation and the source of truth can drift permanently out of sync — worst case, you rebuild it.
- Checkpoint-addressable: projections are built against an explicit
checkpoint (
"latest", a sequence number, a content hash), not implicitly against “whatever the DAG looks like right now.” That’s what makes two peers’ projections comparable and what makes replay-to-a-point-in-time well-defined. - Invalidatable, not just updatable: when a query’s own definition
changes (not just its input data), the projection built from the old
definition is retired explicitly via
projection.invalidaterather than silently drifting.
What this buys you as a developer
Without a shared projection model, “read model” tends to mean N different bespoke mechanisms — one for your UI’s live state, one for search, one for an audit export, one for offline caching — each with its own invalidation bugs. With projections as the primitive, all of those are the same operation (replay history into a view) with different descriptors and checkpoints, which means:- Debugging a divergent read is always the same question: which history, replayed through which projection, at which checkpoint?
- Adding a new read shape is registering a query descriptor, not building a new subsystem.
- Caching is safe by construction — a stale or corrupted projection is never a source-of-truth problem, only a rebuild.