Skip to content

dizzy.engine.store

dizzy.engine.store

The event store — the truth an engine appends to before anything else runs.

A content-addressed DAG (:mod:dizzy.engine.dagstore) behind an append/iterate interface. Each append becomes a dagstore event whose hashed payload is {"ingested_at": <utc iso>, "event": <domain payload>}ingested_at is stamped once, at first append, and travels with the fact (fold-on-replicate reuses it, so both time axes survive). Identity is the content hash; order is parent pointers; iteration is canonical topological order.

seq is not stored. The :class:Envelope still carries one as a derived iteration index — this node's chain is linear, so topological order equals append order — which keeps "events since N" consumers working.

Floats in payloads are stringified before hashing: the canonical form forbids them for cross-language stability, and pydantic's lax coercion restores them on reconstruct (float(repr(x)) == x exactly).

What makes this feature-agnostic. The store needs to map a stored event's type name back to a class to rehydrate it, and that map comes from the feat — :attr:FeatGraph.events <dizzy.engine.registry.FeatGraph.events>, not a scan of the generated events module. The resolution is lazy, so a process that only appends (a worker draining a queue) never imports the generated definitions package at all.

Cross-thread: one shared DagStore connection guarded by a lock. Cross-process: DagStore's BEGIN IMMEDIATE serializes writers on the node.

DEFAULT_STORE_PATH = Path('data') / 'events.db' module-attribute

Where the stream lands when neither an argument nor $DIZZY_STORE_PATH says otherwise. Relative to the process's cwd — a host that runs from anywhere should pass a path or set the variable.

Envelope dataclass

One appended fact, as the stream knows it.

Source code in dizzy/src/dizzy/engine/store.py
62
63
64
65
66
67
68
69
70
71
72
73
74
75
@dataclass
class Envelope:
    """One appended fact, as the stream knows it."""

    id: str
    """Content hash — the event's stream identity."""
    type: str
    """Event name in snake_case, i.e. the name the feat declares."""
    ingested_at: datetime
    """UTC, stamped at first append."""
    payload: dict
    parents: tuple = ()
    seq: int = -1
    """DERIVED iteration index, not stored."""

id instance-attribute

Content hash — the event's stream identity.

type instance-attribute

Event name in snake_case, i.e. the name the feat declares.

ingested_at instance-attribute

UTC, stamped at first append.

seq = -1 class-attribute instance-attribute

DERIVED iteration index, not stored.

EventStore

Content-addressed event store. Path from arg > $DIZZY_STORE_PATH > default.

Source code in dizzy/src/dizzy/engine/store.py
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
class EventStore:
    """Content-addressed event store. Path from arg > ``$DIZZY_STORE_PATH`` > default."""

    def __init__(
        self,
        path: str | Path | None = None,
        event_classes: Mapping[str, type] | None = None,
        graph: FeatGraph | None = None,
    ):
        """*event_classes* maps feat event name -> class, for
        :meth:`reconstruct_event`. Omit both it and *graph* and the store reads
        the ambient feat file when (and only when) something first reconstructs.
        """
        if path is None:
            path = os.environ.get("DIZZY_STORE_PATH") or DEFAULT_STORE_PATH
        self.path = Path(path)
        self.path.parent.mkdir(parents=True, exist_ok=True)
        self._lock = threading.Lock()
        self.dag = DagStore(str(self.path), check_same_thread=False)
        self._event_classes = dict(event_classes) if event_classes is not None else None
        self._graph = graph

    @property
    def event_classes(self) -> Mapping[str, type]:
        """The feat's event map, resolved on first use.

        Deferred because appending needs no classes: a worker that only writes
        the stream should not pay to import the generated definitions package.
        """
        if self._event_classes is None:
            self._event_classes = dict((self._graph or default_graph()).events)
        return self._event_classes

    def append(self, event: Any, ingested_at: datetime | None = None) -> Envelope:
        """Append one event and return its envelope.

        ``ingested_at`` is stamped NOW unless supplied — a caller supplies it
        only when replaying or replicating an already-stamped fact.
        """
        stamped = ingested_at or datetime.now(UTC)
        wrapped = {
            "ingested_at": stamped.isoformat(),
            "event": _stringify_floats(event.model_dump(mode="json")),
        }
        with self._lock:
            dag_event = self.dag.append(snake_case(type(event).__name__), wrapped)
            seq = len(self.dag) - 1
        return Envelope(
            id=dag_event.id,
            type=dag_event.type,
            ingested_at=stamped,
            payload=wrapped["event"],
            parents=dag_event.parents,
            seq=seq,
        )

    def iterate(self) -> Iterator[Envelope]:
        """Yield all envelopes in canonical (topological) order."""
        with self._lock:
            events = list(self.dag.iterate())
        for i, ev in enumerate(events):
            yield Envelope(
                id=ev.id,
                type=ev.type,
                ingested_at=datetime.fromisoformat(ev.payload["ingested_at"]),
                payload=ev.payload["event"],
                parents=ev.parents,
                seq=i,
            )

    def heads(self) -> tuple:
        with self._lock:
            return self.dag.heads()

    # ── Replication surface ─────────────────────────────────────────────────
    #
    # Replicated facts arrive already hashed and already stamped, so they do
    # not go through append(): they are ADDED, keeping the id the peer minted.

    def add_replicated(self, event: Any) -> Envelope:
        """Ingest a dagstore event fetched from a peer, returning its envelope.

        The hash is verified on arrival by the DAG, and every parent must
        already be present — replication delivers ancestry first.
        """
        with self._lock:
            self.dag.add(event)
        return Envelope(
            id=event.id,
            type=event.type,
            ingested_at=datetime.fromisoformat(event.payload["ingested_at"]),
            payload=event.payload["event"],
            parents=event.parents,
        )

    def raw_event(self, event_id: str) -> Any:
        """The stored dagstore event, wrapper payload and all — what a peer
        asks for over the wire. Raises KeyError if absent."""
        with self._lock:
            return self.dag.get(event_id)

    def __contains__(self, event_id: str) -> bool:
        with self._lock:
            return event_id in self.dag

    def __len__(self) -> int:
        with self._lock:
            return len(self.dag)

    def reconstruct_event(self, envelope: Envelope) -> Any:
        """Rebuild the event instance from an envelope."""
        return reconstruct_event(envelope, self.event_classes)

event_classes property

The feat's event map, resolved on first use.

Deferred because appending needs no classes: a worker that only writes the stream should not pay to import the generated definitions package.

__init__(path=None, event_classes=None, graph=None)

event_classes maps feat event name -> class, for :meth:reconstruct_event. Omit both it and graph and the store reads the ambient feat file when (and only when) something first reconstructs.

Source code in dizzy/src/dizzy/engine/store.py
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
def __init__(
    self,
    path: str | Path | None = None,
    event_classes: Mapping[str, type] | None = None,
    graph: FeatGraph | None = None,
):
    """*event_classes* maps feat event name -> class, for
    :meth:`reconstruct_event`. Omit both it and *graph* and the store reads
    the ambient feat file when (and only when) something first reconstructs.
    """
    if path is None:
        path = os.environ.get("DIZZY_STORE_PATH") or DEFAULT_STORE_PATH
    self.path = Path(path)
    self.path.parent.mkdir(parents=True, exist_ok=True)
    self._lock = threading.Lock()
    self.dag = DagStore(str(self.path), check_same_thread=False)
    self._event_classes = dict(event_classes) if event_classes is not None else None
    self._graph = graph

append(event, ingested_at=None)

Append one event and return its envelope.

ingested_at is stamped NOW unless supplied — a caller supplies it only when replaying or replicating an already-stamped fact.

Source code in dizzy/src/dizzy/engine/store.py
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
def append(self, event: Any, ingested_at: datetime | None = None) -> Envelope:
    """Append one event and return its envelope.

    ``ingested_at`` is stamped NOW unless supplied — a caller supplies it
    only when replaying or replicating an already-stamped fact.
    """
    stamped = ingested_at or datetime.now(UTC)
    wrapped = {
        "ingested_at": stamped.isoformat(),
        "event": _stringify_floats(event.model_dump(mode="json")),
    }
    with self._lock:
        dag_event = self.dag.append(snake_case(type(event).__name__), wrapped)
        seq = len(self.dag) - 1
    return Envelope(
        id=dag_event.id,
        type=dag_event.type,
        ingested_at=stamped,
        payload=wrapped["event"],
        parents=dag_event.parents,
        seq=seq,
    )

iterate()

Yield all envelopes in canonical (topological) order.

Source code in dizzy/src/dizzy/engine/store.py
149
150
151
152
153
154
155
156
157
158
159
160
161
def iterate(self) -> Iterator[Envelope]:
    """Yield all envelopes in canonical (topological) order."""
    with self._lock:
        events = list(self.dag.iterate())
    for i, ev in enumerate(events):
        yield Envelope(
            id=ev.id,
            type=ev.type,
            ingested_at=datetime.fromisoformat(ev.payload["ingested_at"]),
            payload=ev.payload["event"],
            parents=ev.parents,
            seq=i,
        )

add_replicated(event)

Ingest a dagstore event fetched from a peer, returning its envelope.

The hash is verified on arrival by the DAG, and every parent must already be present — replication delivers ancestry first.

Source code in dizzy/src/dizzy/engine/store.py
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
def add_replicated(self, event: Any) -> Envelope:
    """Ingest a dagstore event fetched from a peer, returning its envelope.

    The hash is verified on arrival by the DAG, and every parent must
    already be present — replication delivers ancestry first.
    """
    with self._lock:
        self.dag.add(event)
    return Envelope(
        id=event.id,
        type=event.type,
        ingested_at=datetime.fromisoformat(event.payload["ingested_at"]),
        payload=event.payload["event"],
        parents=event.parents,
    )

raw_event(event_id)

The stored dagstore event, wrapper payload and all — what a peer asks for over the wire. Raises KeyError if absent.

Source code in dizzy/src/dizzy/engine/store.py
188
189
190
191
192
def raw_event(self, event_id: str) -> Any:
    """The stored dagstore event, wrapper payload and all — what a peer
    asks for over the wire. Raises KeyError if absent."""
    with self._lock:
        return self.dag.get(event_id)

reconstruct_event(envelope)

Rebuild the event instance from an envelope.

Source code in dizzy/src/dizzy/engine/store.py
202
203
204
def reconstruct_event(self, envelope: Envelope) -> Any:
    """Rebuild the event instance from an envelope."""
    return reconstruct_event(envelope, self.event_classes)

reconstruct_event(envelope, event_classes)

Rebuild the event instance an envelope stands for.

event_classes is a feat-name -> class map; FeatGraph.events is one.

Source code in dizzy/src/dizzy/engine/store.py
78
79
80
81
82
83
84
85
86
87
88
89
90
def reconstruct_event(envelope: Envelope, event_classes: Mapping[str, type]) -> Any:
    """Rebuild the event instance an envelope stands for.

    *event_classes* is a feat-name -> class map; ``FeatGraph.events`` is one.
    """
    cls = event_classes.get(envelope.type)
    if cls is None:
        raise KeyError(
            f"unknown event type in stream: {envelope.type!r} — the feat does "
            f"not declare it, so this stream was written by a different "
            f"feature (or by a newer version of this one)"
        )
    return cls(**envelope.payload)