description: "The session-projection registry for developers serving whole current values of log-derived per-session state to client carriers, and for maintainers of the drive contract."
English | 中文
Use dsh-session-projection when clients need current per-session state—such as todos, goals, or conversation statistics—without replaying the raw event log. Domains define synchronous projections from committed session events, and clients receive complete, schema-validated JSON values through snapshots and change notifications. Snapshots identify the last event reflected by every returned value, so carriers can pair state with the matching history cut. Projection state can be checkpointed for faster cold reads, while host-only projections remain private to the host.
Mount dsh-session-projection wherever client carriers need current values of log-derived session state. Domain plugins register units; carriers read snapshots and subscribe to the change feed; neither knows the other.
Choose it when a domain keeps state that clients should see without re-deriving it — a todo list, a goal snapshot, conversation statistics. The registry drives units eagerly over committed events, so any registered unit's value is current by construction. Skip it for host-only bookkeeping that no client reads: a unit without a wire block stays host-only. A host reader either declares sessionProjections in its plugin inject or fails explicitly when the registry or required key is absent. Contributors may preserve optional registration through ctx.inject(['sessionProjections'], ...).
A domain contributes one ProjectionDefinition per state key: a key, a state schema, an initial state, a synchronous fold apply(state, event), an optional wire block that projects state to a client view, and a stateVersion that bumps whenever the state fields or fold semantics change:
const definition = {
key: 'todo',
stateSchema: todoStateSchema,
stateVersion: 1,
init: (_header, _inheritedEventCount) => ({ items: [] }),
apply: (state, event) => event.type === 'todo/upsert'
? { items: event.data.items }
: state,
wire: {
viewSchema: todoViewSchema,
view: state => ({ items: state.items }),
},
}
init(header, inheritedEventCount) receives both lightweight metadata and the exact fork-inherited cut; it must not infer that cut from firstLiveSeq or session/end-seed. apply must be synchronous and must return the same state reference for events that do not concern the unit — an unchanged reference means zero downstream work. The registry compares consecutive raw wire.view results with Object.is; an object or array view must reuse its reference to suppress publication across internal-only state changes, while a structurally equal new object is still a change. A state-carrying log event must carry the complete post-change state, never a bare delta.
register(definition) installs the unit; registrants with the same key and stateVersion share its cells, while an incompatible version or invalid stateVersion throws. Registration is an effect on the calling fiber, so the last unload removes the key and its cached cells. Carriers read a consistent synchronous cut over every client-visible unit with snapshot(session) — { asOfSeq, values }, where asOfSeq is the seq of the last event every value reflects — and subscribe to per-change notifications with onChanged(listener). stateOf(session, key) reads one unit's live read-only host state without computing unrelated views.
const dispose = ctx.sessionProjections.register(definition)
const { asOfSeq, values } = ctx.sessionProjections.snapshot(session)
A domain that requires projected state declares sessionProjections as a Cordis service dependency; optional contributors may register under ctx.inject(['sessionProjections'], …). Carriers use ctx.get('sessionProjections') and omit their block or frames when the registry is absent.
Every unit's state is checkpointed — client-visible and host-only alike — through checkpoint(session), and the sibling session-projection-cache persists those checkpoints so cold reads skip full log loads. Checkpoint watermarks use SessionSeqCursor (-1 for an empty log), while replay starts use SessionLogOffset; restoreFloor and restore implement the read recipe without conflating an existing event with a log gap.
Read these pages when the package-level contract is not enough. They move from the unit contract to the read-model subsystem and the persisted cache.
None, as the projection registry serves client-facing read models of already-logged session state and registers nothing model-facing.
None; projections never assemble or send provider requests.
These limits define where the projection registry needs care at scale. They are current package constraints, not a task backlog.
dsh-session-projection-cache seed that fold from persisted rows instead.wire.viewSchema.parse rejects a Promise-returning view, but an apply that blocks or reads torn non-session state is a review concern.