ddd_toolkit_core/replay.rs
1use crate::AggregateRoot;
2use crate::event_store::{StreamHistory, StreamRevision};
3use crate::snapshot::{AggregateSnapshot, SnapshotUse};
4
5/// **English:** Aggregate hooks for reconstructing state from stored events or a snapshot.
6/// Implementations should apply historical events without recording new pending events.
7/// The replay helpers do not clear queues, check aggregate identity, publish events, or catch
8/// panics from these hooks. Error and panic conditions are defined by each implementation.
9///
10/// **日本語:** 保存イベントまたはスナップショットから状態を復元するための集約フックです。
11/// 実装では履歴イベントの適用で新たな保留イベントを記録しないようにします。
12/// 再生ヘルパーはキューの消去、集約識別子の確認、イベント発行、フックのパニック捕捉をしません。
13/// エラーとパニックの条件は各実装が定めます。
14pub trait AggregateReplay: AggregateRoot {
15 /// **English:** Failure to create the pre-event state.
16 ///
17 /// **日本語:** イベント適用前の状態の作成失敗です。
18 type InitializationError;
19 /// **English:** Failure to apply a historical event.
20 ///
21 /// **日本語:** 履歴イベントの適用失敗です。
22 type ApplicationError;
23 /// **English:** State representation consumed when restoring a snapshot.
24 ///
25 /// **日本語:** スナップショット復元時に消費する状態表現です。
26 type Snapshot;
27 /// **English:** Failure to classify a snapshot, distinct from choosing full replay.
28 ///
29 /// **日本語:** スナップショット分類の失敗です。全件再生を選ぶこととは区別されます。
30 type SnapshotValidationError;
31 /// **English:** Failure to construct aggregate state from an accepted snapshot.
32 ///
33 /// **日本語:** 採用したスナップショットからの集約状態構築失敗です。
34 type SnapshotStateError;
35
36 /// **English:** Creates state for `id` before any historical events are applied.
37 /// Returns an implementation-defined error if initialization fails.
38 ///
39 /// **日本語:** 履歴イベント適用前の `id` の状態を作成します。
40 /// 初期化に失敗した場合は実装定義のエラーを返します。
41 fn initial_state(id: &Self::Id) -> Result<Self, Self::InitializationError>
42 where
43 Self: Sized;
44
45 /// **English:** Updates state using one borrowed historical event, without emitting it again.
46 /// Returns an implementation-defined error on failure; mutations before that error are not undone.
47 ///
48 /// **日本語:** 借用した履歴イベント 1 件で状態を更新し、そのイベントを再発行しません。
49 /// 失敗時は実装定義のエラーを返します。それ以前の状態変更は元に戻されません。
50 fn apply_event(&mut self, event: &Self::Event) -> Result<(), Self::ApplicationError>;
51
52 /// **English:** Classifies `snapshot` against the complete history's `stream_revision`.
53 /// Return [`SnapshotUse::Applicable`] to restore it, or another classification to replay all events.
54 /// An error aborts replay rather than falling back. Compatibility and staleness policy belong here.
55 ///
56 /// **日本語:** 完全な履歴の `stream_revision` に照らして `snapshot` を分類します。
57 /// 復元するなら [`SnapshotUse::Applicable`]、全件再生するなら他の分類を返します。
58 /// エラー時はフォールバックせず再生を中断します。互換性と古さの方針をここで定めます。
59 fn classify_snapshot(
60 snapshot: &AggregateSnapshot<Self::Snapshot>,
61 stream_revision: StreamRevision,
62 ) -> Result<SnapshotUse, Self::SnapshotValidationError>;
63
64 /// **English:** Consumes an accepted snapshot to construct the aggregate for `id`.
65 /// Returns an implementation-defined state error; replay does not retry from the initial state.
66 ///
67 /// **日本語:** 採用済みスナップショットを消費し、`id` の集約を構築します。
68 /// 失敗時は実装定義の状態エラーを返し、初期状態からの再試行は行いません。
69 fn from_snapshot(
70 id: &Self::Id,
71 snapshot: AggregateSnapshot<Self::Snapshot>,
72 ) -> Result<Self, Self::SnapshotStateError>
73 where
74 Self: Sized;
75}
76
77/// **English:** Reconstructed aggregate paired with the history's final revision.
78/// This revision can be used as [`crate::ExpectedRevision::Revision`] for a subsequent append.
79///
80/// **日本語:** 復元した集約と履歴の最終リビジョンです。
81/// このリビジョンは次の追加の [`crate::ExpectedRevision::Revision`] として利用できます。
82#[derive(Debug, Eq, PartialEq)]
83pub struct RehydratedAggregate<A> {
84 /// **English:** Aggregate after all selected historical events were applied.
85 ///
86 /// **日本語:** 選択された履歴イベントをすべて適用した後の集約です。
87 pub aggregate: A,
88 /// **English:** Final revision of the complete input history, including when no tail events remain.
89 ///
90 /// **日本語:** 後続イベントが残っていない場合も含め、入力された完全な履歴の最終リビジョンです。
91 pub revision: StreamRevision,
92}
93
94/// **English:** First failure encountered during full-history reconstruction.
95///
96/// **日本語:** 全履歴からの復元で最初に発生した失敗です。
97#[derive(Debug, Eq, PartialEq)]
98pub enum ReplayError<I, P> {
99 /// **English:** Creating the initial aggregate state failed.
100 ///
101 /// **日本語:** 集約の初期状態の作成に失敗しました。
102 Initialization(I),
103 /// **English:** Applying a historical event failed.
104 ///
105 /// **日本語:** 履歴イベントの適用に失敗しました。
106 Application(P),
107}
108
109/// **English:** First failure during snapshot-assisted reconstruction; errors do not trigger fallback.
110///
111/// **日本語:** スナップショットを用いた復元で最初に発生した失敗です。エラー時はフォールバックしません。
112#[derive(Debug, Eq, PartialEq)]
113pub enum SnapshotReplayError<I, P, V, S> {
114 /// **English:** Creating initial state for full replay failed.
115 ///
116 /// **日本語:** 全件再生のための初期状態の作成に失敗しました。
117 Initialization(I),
118 /// **English:** The snapshot classification hook returned an error.
119 ///
120 /// **日本語:** スナップショット分類フックがエラーを返しました。
121 SnapshotValidation(V),
122 /// **English:** Restoring the accepted snapshot failed.
123 ///
124 /// **日本語:** 採用したスナップショットの復元に失敗しました。
125 SnapshotState(S),
126 /// **English:** An applicable snapshot's tail could not be selected by [`StreamHistory::events_after`].
127 /// This includes a snapshot ahead of history or an unrepresentable next index.
128 ///
129 /// **日本語:** 採用可能なスナップショットの後続部分を [`StreamHistory::events_after`] で選べませんでした。
130 /// 履歴より先のスナップショットや、表現できない次の添字が該当します。
131 SnapshotRevision {
132 /// **English:** Revision carried by the accepted snapshot.
133 ///
134 /// **日本語:** 採用したスナップショットが保持するリビジョンです。
135 snapshot_revision: StreamRevision,
136 /// **English:** Final revision of the supplied history.
137 ///
138 /// **日本語:** 指定された履歴の最終リビジョンです。
139 history_revision: StreamRevision,
140 },
141 /// **English:** Applying an event from the selected history failed.
142 ///
143 /// **日本語:** 選択された履歴のイベント適用に失敗しました。
144 Application(P),
145}
146
147/// **English:** Initializes `A` for `id` and applies every event in `history` in stored order.
148/// Returns the reconstructed aggregate and history revision. The first hook error is wrapped in
149/// [`ReplayError`]; the local aggregate is dropped on error. No storage or publishing is performed,
150/// and hook panics propagate. The caller must supply history belonging to `id`.
151///
152/// **日本語:** `id` の `A` を初期化し、`history` の全イベントを保存順に適用します。
153/// 復元した集約と履歴リビジョンを返します。最初のフックエラーを [`ReplayError`] で包み、
154/// エラー時はローカルな集約を破棄します。保存や発行は行わず、フックのパニックは伝播します。
155/// 呼び出し側は `id` に属する履歴を指定する必要があります。
156///
157/// **English:** This counter restores two historical increments without queuing new events.
158///
159/// **日本語:** このカウンターは新規イベントを保留せず、履歴の加算 2 件を復元します。
160///
161/// ```rust
162/// use std::convert::Infallible;
163/// use ddd_toolkit_core::{
164/// AggregateReplay, AggregateRoot, AggregateSnapshot, DomainEvent, Entity,
165/// SnapshotUse, StreamHistory, StreamRevision, replay_aggregate,
166/// };
167/// struct Increment(u64);
168/// impl DomainEvent for Increment {}
169/// struct Counter { id: u64, total: u64, pending: Vec<Increment> }
170/// impl Entity for Counter {
171/// type Id = u64;
172/// fn id(&self) -> &u64 { &self.id }
173/// }
174/// impl AggregateRoot for Counter {
175/// type Event = Increment;
176/// fn record_event(&mut self, event: Increment) { self.pending.push(event); }
177/// fn pull_events(&mut self) -> Vec<Increment> { std::mem::take(&mut self.pending) }
178/// }
179/// impl AggregateReplay for Counter {
180/// type InitializationError = Infallible;
181/// type ApplicationError = &'static str;
182/// type Snapshot = u64;
183/// type SnapshotValidationError = Infallible;
184/// type SnapshotStateError = Infallible;
185/// fn initial_state(id: &u64) -> Result<Self, Infallible> {
186/// Ok(Self { id: *id, total: 0, pending: vec![] })
187/// }
188/// fn apply_event(&mut self, event: &Increment) -> Result<(), &'static str> {
189/// self.total = self.total.checked_add(event.0).ok_or("counter overflow")?;
190/// Ok(())
191/// }
192/// fn classify_snapshot(_: &AggregateSnapshot<u64>, _: StreamRevision)
193/// -> Result<SnapshotUse, Infallible> { Ok(SnapshotUse::Applicable) }
194/// fn from_snapshot(id: &u64, snapshot: AggregateSnapshot<u64>) -> Result<Self, Infallible> {
195/// Ok(Self { id: *id, total: snapshot.into_parts().0, pending: vec![] })
196/// }
197/// }
198/// let history = StreamHistory::try_new(vec![Increment(2), Increment(3)], StreamRevision::new(1))
199/// .unwrap();
200/// let mut restored = replay_aggregate::<Counter>(&7, &history).unwrap();
201/// assert_eq!(restored.aggregate.total, 5);
202/// assert_eq!(restored.revision, StreamRevision::new(1));
203/// assert!(restored.aggregate.pull_events().is_empty());
204/// ```
205pub fn replay_aggregate<A>(
206 id: &A::Id,
207 history: &StreamHistory<A::Event>,
208) -> Result<RehydratedAggregate<A>, ReplayError<A::InitializationError, A::ApplicationError>>
209where
210 A: AggregateReplay,
211{
212 let mut aggregate = A::initial_state(id).map_err(ReplayError::Initialization)?;
213 for event in history.events() {
214 aggregate
215 .apply_event(event)
216 .map_err(ReplayError::Application)?;
217 }
218
219 Ok(RehydratedAggregate {
220 aggregate,
221 revision: history.revision(),
222 })
223}
224
225/// **English:** Reconstructs `A` using an optional snapshot and a complete `history` for `id`.
226/// With no snapshot, or a [`SnapshotUse::Stale`] or [`SnapshotUse::Inapplicable`] classification,
227/// initializes state and replays all events. For [`SnapshotUse::Applicable`], selects events strictly
228/// after the snapshot revision, restores state, then applies that tail in order. A snapshot at the
229/// history's final revision requires no event applications.
230///
231/// Classification errors abort; an invalid tail yields [`SnapshotReplayError::SnapshotRevision`]
232/// before state restoration. Initialization, restoration, and application errors are wrapped in
233/// their corresponding variants with no fallback or retry. On failure the local state is dropped.
234/// Returns the complete history's revision on success. No storage, publishing, identity checking,
235/// or panic catching is performed; the caller must supply matching history and snapshot data.
236///
237/// **日本語:** `id` の完全な `history` と任意のスナップショットから `A` を復元します。
238/// スナップショットがない場合や、分類が [`SnapshotUse::Stale`] または
239/// [`SnapshotUse::Inapplicable`] の場合は初期状態から全件再生します。
240/// [`SnapshotUse::Applicable`] ならスナップショットのリビジョンより後のイベントを選び、
241/// 状態を復元して後続イベントを順に適用します。履歴の最終リビジョンと一致すれば適用は不要です。
242///
243/// 分類エラーは処理を中断し、後続部分を選べなければ状態復元前に
244/// [`SnapshotReplayError::SnapshotRevision`] を返します。初期化・復元・適用のエラーは
245/// 対応するバリアントで包み、フォールバックや再試行はしません。失敗時はローカル状態を破棄します。
246/// 成功時は完全な履歴のリビジョンを返します。保存、発行、識別子検証、パニック捕捉は行いません。
247/// 呼び出し側は対応する履歴とスナップショットを指定する必要があります。
248#[allow(clippy::type_complexity)]
249pub fn replay_with_optional_snapshot<A>(
250 id: &A::Id,
251 history: &StreamHistory<A::Event>,
252 snapshot: Option<AggregateSnapshot<A::Snapshot>>,
253) -> Result<
254 RehydratedAggregate<A>,
255 SnapshotReplayError<
256 A::InitializationError,
257 A::ApplicationError,
258 A::SnapshotValidationError,
259 A::SnapshotStateError,
260 >,
261>
262where
263 A: AggregateReplay,
264{
265 let (mut aggregate, events) = match snapshot {
266 None => (
267 A::initial_state(id).map_err(SnapshotReplayError::Initialization)?,
268 history.events(),
269 ),
270 Some(snapshot) => {
271 let snapshot_use = A::classify_snapshot(&snapshot, history.revision())
272 .map_err(SnapshotReplayError::SnapshotValidation)?;
273 match snapshot_use {
274 SnapshotUse::Applicable => {
275 let revision = snapshot.revision();
276 let events = match history.events_after(revision) {
277 Some(events) => events,
278 None => {
279 return Err(SnapshotReplayError::SnapshotRevision {
280 snapshot_revision: revision,
281 history_revision: history.revision(),
282 });
283 }
284 };
285 (
286 A::from_snapshot(id, snapshot)
287 .map_err(SnapshotReplayError::SnapshotState)?,
288 events,
289 )
290 }
291 SnapshotUse::Stale | SnapshotUse::Inapplicable => (
292 A::initial_state(id).map_err(SnapshotReplayError::Initialization)?,
293 history.events(),
294 ),
295 }
296 }
297 };
298
299 for event in events {
300 aggregate
301 .apply_event(event)
302 .map_err(SnapshotReplayError::Application)?;
303 }
304
305 Ok(RehydratedAggregate {
306 aggregate,
307 revision: history.revision(),
308 })
309}