StateViewSlice
For a short summary of StateViewSlice, see Reventless Components Overview.
This component follows the Reventless Component Structure Pattern, using separate files for interface definitions (StateViewSlice.res), builder logic (StateViewSlice_Builder.res), and callback/handler logic (StateViewSlice_Callback.res).
Overview
The StateViewSlice is a DCB (Dynamic Consistency Boundary) component that projects events from a shared DcbEventLog into a QueryDb-backed read model. It implements the projection pattern where events are transformed into state updates.
Purpose and Responsibilities
- Responsibility: Listen to events from DcbEventLog and project them into a QueryDb for efficient reading
- In: Events from DcbEventLog (subscribed via EventCollector)
- Out: State updates to QueryDb
- Key Feature: Complements StateChangeSlice by providing read-optimized views of the event-sourced state
Relationship with DCB
StateViewSlice works alongside StateChangeSlice in the DCB architecture:
Component Spec
A StateViewSlice is split into two files:
<Name>.res— the spec (@@reventless.spec): the localconsumedEventandstate@schematypes.<Name>_Projection.res— the projection (@@reventless.projection): a singlelet project = event => [...]function (ONE argument).
The spec file. @@reventless.spec injects name, module Id, moduleUrl,
let config = config(), and let subIdConfig = None:
@@reventless.spec
@schema
type consumedEvent =
| Created({id: string, name: string})
| Updated({id: string, name: string})
| Deleted({id: string})
@schema
type state = {name: string}
The projection file. @@reventless.projection injects open Reventless.Projection
(so Set, Update, UpdateWithDefault, Delete are in scope unqualified) and
brings the spec module into scope. project takes ONE argument — the event:
@@reventless.projection
let project = event =>
switch event {
| Created({id, name}) => [Set(id, {name: name})]
| Updated({id, name}) => [Update(id, state => {...state, name})]
| Deleted({id}) => [Delete(id)]
}
There is no DcbEventLogSpec reference — the slice declares its own local
consumedEvent union.
Spec Fields Explained
In the spec file (@@reventless.spec injects name, module Id, moduleUrl):
| Field | Type | Description |
|---|---|---|
consumedEvent | @schema type | The local subset of event variants this slice projects |
state | @schema type | State type for the read model (schema auto-generated) |
In the _Projection.res file:
| Field | Type | Description |
|---|---|---|
project | consumedEvent => array<Projection.action<state>> | Function to transform an event into state actions |
Runtime Behavior
Event Processing Flow
Projection Actions
In the _Projection.res file, @@reventless.projection injects
open Reventless.Projection, so the action constructors (Set, Update,
UpdateWithDefault, Delete) are in scope unqualified:
@@reventless.projection
let project = event =>
switch event {
| ItemCreated({itemId, name}) => [Set(itemId, {name, createdAt: Date.now()})]
| ItemRenamed({itemId, newName}) => [Update(itemId, state => {...state, name: newName})]
| ItemDeleted({itemId}) => [Delete(itemId)]
| StockAdjusted({itemId, delta}) =>
// UpdateWithDefault handles case where item doesn't exist yet
[UpdateWithDefault(itemId, {count: 0}, state => {...state, count: state.count + delta})]
}
Key Design Annotations
StateViewSlice supports the same PPX annotations on @schema type state as ReadModel. Annotations on state fields automatically generate let makeId, let subIdConfig, and let config. These annotations go on the spec file's @schema type state:
@@reventless.spec
@schema
type state = {
@id orderId: string, // generates: let makeId
@subId lineItemId: string, // generates: let subIdConfig — enables sort key queries
@index categoryId: string, // generates: let config with a secondary index
@resolves({table: "Products", field: "product"}) productId: string,
quantity: int,
}
@schema
type consumedEvent =
| LineItemAdded({orderId: string, lineItemId: string, productId: string, categoryId: string, quantity: int})
| LineItemRemoved({orderId: string, lineItemId: string})
The project function lives in the sibling _Projection.res file, where
@@reventless.projection injects open Reventless.Projection (so Set,
Update, UpdateWithDefault, and Delete are in scope unqualified):
@@reventless.projection
let project = event =>
switch event {
| LineItemAdded({orderId, lineItemId, productId, categoryId, quantity}) =>
[Set(lineItemId, {orderId, lineItemId, productId, categoryId, quantity})]
| LineItemRemoved({lineItemId}) => [Delete(lineItemId)]
}
For the full annotation reference, see PPX annotations.
Comparison with ReadModel
| Aspect | ReadModel | StateViewSlice |
|---|---|---|
| Event Source | Multiple EventTopics | Single DcbEventLog |
| Mappings | Complex mapping system | Single projection function |
| Spec | Reventless.ReadModel.Spec | Custom Spec with project function |
| Key annotations | @id, @subId, @index, @resolves on state fields | Same — identical PPX annotation support |
| Use Case | General-purpose read models | DCB-specific view projections |
Comparison with StateChangeSlice
| Aspect | StateChangeSlice | StateViewSlice |
|---|---|---|
| Purpose | Handle commands and decide on events | Project events into read model |
| Input | Commands from CommandTopic | Events from DcbEventLog EventTopic |
| Output | Appends events to DcbEventLog | Updates QueryDb state |
| Pattern | Decision/command pattern | Projection pattern |
Pulumi Outputs
type outputs = {
resources: array<Reventless.Adapter.resource>,
queryDb: QueryDb.outputs,
}
The StateViewSlice creates its own QueryDb:
- DynamoDB table for state storage
- AppSync resolvers for querying
- Related IAM roles and policies
Related Components
- DcbEventLog - Shared event log for DCB slices
- StateChangeSlice - Processes commands and appends events
- QueryDb - Read model storage
- ReadModel - General-purpose read model component
- Plugin - Hosts DCB slices and creates shared infrastructure
- EventCollector - Consumes events for projection
- Usage Guide - How to use StateViewSlice in your application