Date: 2026-05-29
Time: 07:59
reconstruct_stateThis is a standalone utility for temporal state reconstruction — the core operation in event sourcing. Given a stream of events and a set of handler functions, it replays events in order to rebuild what the aggregate's state looked like at a specific point in time.
It exists as a complement to the Projection class. Where Projection maintains a continuously-updated read model across *all* streams, reconstruct_state is a one-shot, single-stream fold. You'd use it when you need the state of one specific aggregate (e.g., one order, one account) without maintaining a long-lived projection.
Preconditions:
store must be a valid EventStore with the stream already populated.handlers keys must match the event_type strings used when events were appended.state dict in place (not return a new one).Postconditions:
up_to.Invariants:
read_stream, which is insertion order within the stream.upto is provided, no event with eventid > up_to is applied.| Parameter | Type | Description |
|-----------|------|-------------|
| store | EventStore | The event store to read from. |
| stream_id | str | Identifies which aggregate's event stream to replay. |
| handlers | dict[str, Callable] | Maps event type names to functions with signature (state: dict, event: Event) -> None. Each handler mutates state in place. |
| upto | Optional[int] | If provided, stops replay once an event's eventid exceeds this value. This is a *global* event ID, not a stream-local version number. |
Edge cases:
streamid doesn't exist, readstream returns [] and the result is {}.handlers is empty, no events are applied regardless of what's in the stream — returns {}.up_to is 0, no events are applied (all event IDs are ≥ 1).event_type has no matching handler are silently skipped.Returns a dict — the accumulated state after replay. The shape is entirely determined by the handlers; the function itself imposes no structure. The caller must know what shape to expect based on the handlers it passed in.
Returns an empty dict when the stream is empty, the stream doesn't exist, or no handlers matched any events.
1. Fetch all events for the stream via store.readstream(streamid). Note: this calls readstream with the default fromversion=0, so it reads the entire stream history.
2. Initialize an empty state dict.
3. Iterate through events in order:
upto is set and the current event's eventid exceeds it, break — stop processing entirely. This works because events are ordered by event_id.handler(state, event). The handler mutates state in place.4. Return the accumulated state.
None on the store. The function is read-only with respect to the EventStore.
However, the handlers themselves may have side effects — the function doesn't enforce purity. The state dict is created locally and passed to handlers, so state mutation is contained to the return value.
There is no error handling. If a handler raises, the exception propagates uncaught and the caller gets a partially-applied state (lost, since state is a local variable). There's no transactional rollback.
If read_stream fails (shouldn't under normal operation since it's an in-memory list lookup), that also propagates uncaught.
Typical usage — reconstruct a single aggregate's current state:
handlers = {
"AccountOpened": lambda s, e: s.update({"balance": 0, "owner": e.data["owner"]}),
"Deposited": lambda s, e: s.update({"balance": s["balance"] + e.data["amount"]}),
"Withdrawn": lambda s, e: s.update({"balance": s["balance"] - e.data["amount"]}),
}
current = reconstruct_state(store, "account-123", handlers)
Time-travel query — see what the state was at a past point:
past_state = reconstruct_state(store, "account-123", handlers, up_to=42)
Caller obligations:
state in place. A handler that returns a new dict without modifying state will silently lose its changes.EventStore and its read_stream method — the only external dependency.Event dataclass — handlers receive these, so the caller implicitly depends on its shape (eventid, eventtype, data, etc.).ProjectionThis function does the same fold operation as Projection.catch_up, but with key differences:
| | reconstruct_state | Projection |
|---|---|---|
| Scope | Single stream | All streams (global read) |
| Lifecycle | One-shot | Long-lived, incremental |
| Time travel | Built-in via up_to | Not supported |
| Snapshots | No | Yes |
| Live updates | No | Yes (LiveProjection) |
1. Handlers must mutate, not return. The signature is Callable with no return type constraint. A handler returning a new dict instead of mutating state would silently produce wrong results.
2. upto is a global event ID, not a stream-local version. The parameter name doesn't make this clear, and readstream uses from_version which is *also* a global event ID despite the name suggesting a stream-local version. This naming inconsistency could lead to bugs.
3. Events in the stream are ordered by eventid. The break on upto assumes monotonically increasing IDs. This holds because _streams stores indices in append order, but it's not documented as a guarantee.
4. Handler dict keys exactly match event.event_type strings. No normalization, no case folding — mismatched casing silently skips the event.