Function: reconstructstate in event-sourcing-store/eventstore.py

Date: 2026-05-29

Time: 07:59

reconstruct_state

Purpose

This 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.

Contract

Preconditions:

Postconditions:

Invariants:

Parameters

| 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:

Return Value

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.

Algorithm

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:

4. Return the accumulated state.

Side Effects

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.

Error Handling

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.

Usage Patterns

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:

Dependencies

Relationship to Projection

This 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) |

Assumptions Not Enforced by the Type System

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.