feat: encapsulate snapshot

This commit is contained in:
2025-08-11 14:04:38 +02:00
parent 91c3755533
commit 6da78b043d
4 changed files with 118 additions and 103 deletions

View File

@@ -153,7 +153,7 @@ export abstract class AggregateRoot<TEventFactory extends EventFactory> {
} }
await this.save(); await this.save();
const reducer = this.#store.aggregate.reducer(this.#self); const reducer = this.#store.aggregate.reducer(this.#self);
await this.#store.createSnapshot({ await this.#store.snapshot.create({
name: this.#self.name, name: this.#self.name,
stream, stream,
reducer, reducer,

View File

@@ -121,6 +121,14 @@ export class EventStore<TEventFactory extends EventFactory, TEventStoreAdapter e
* *
* @param aggregates - Aggregates to push events from. * @param aggregates - Aggregates to push events from.
* @param settings - Event settings which can modify insertion behavior. * @param settings - Event settings which can modify insertion behavior.
*
* @example
* ```ts
* const foo = eventStore.aggregate.from(Foo).setEvent(...);
* const bar = eventStore.aggregate.from(Bar).setEvent(...);
*
* await eventStore.aggregate.push([foo, bar]);
* ```
*/ */
push: async ( push: async (
aggregates: InstanceType<AggregateRootClass<TEventFactory>>[], aggregates: InstanceType<AggregateRootClass<TEventFactory>>[],
@@ -141,6 +149,11 @@ export class EventStore<TEventFactory extends EventFactory, TEventStoreAdapter e
* *
* @param name - Aggregate to instantiate. * @param name - Aggregate to instantiate.
* @param stream - Stream to retrieve snapshot from. * @param stream - Stream to retrieve snapshot from.
*
* @example
* ```ts
* const state = await eventStore.aggregate.getByStream(Aggregate, "stream");
* ```
*/ */
getByStream: async <TAggregate extends AggregateRootClass<TEventFactory>>( getByStream: async <TAggregate extends AggregateRootClass<TEventFactory>>(
aggregate: TAggregate, aggregate: TAggregate,
@@ -159,6 +172,11 @@ export class EventStore<TEventFactory extends EventFactory, TEventStoreAdapter e
* *
* @param name - Aggregate to instantiate. * @param name - Aggregate to instantiate.
* @param relation - Relation to retrieve snapshot from. * @param relation - Relation to retrieve snapshot from.
*
* @example
* ```ts
* const state = await eventStore.aggregate.getByRelation(Aggregate, "relation");
* ```
*/ */
getByRelation: async <TAggregate extends AggregateRootClass<TEventFactory>>( getByRelation: async <TAggregate extends AggregateRootClass<TEventFactory>>(
aggregate: TAggregate, aggregate: TAggregate,
@@ -177,6 +195,11 @@ export class EventStore<TEventFactory extends EventFactory, TEventStoreAdapter e
* *
* @param aggregate - Aggregate to instantiate. * @param aggregate - Aggregate to instantiate.
* @param snapshot - Optional snapshot to instantiate aggregate with. * @param snapshot - Optional snapshot to instantiate aggregate with.
*
* @example
* ```ts
* const foo = await eventStore.aggregate.from(Foo);
* ```
*/ */
from: <TAggregate extends AggregateRootClass<TEventFactory>>( from: <TAggregate extends AggregateRootClass<TEventFactory>>(
aggregate: TAggregate, aggregate: TAggregate,
@@ -189,6 +212,12 @@ export class EventStore<TEventFactory extends EventFactory, TEventStoreAdapter e
* Create a new reducer instance for the given aggregate. * Create a new reducer instance for the given aggregate.
* *
* @param aggregate - Aggregate to create a reducer for. * @param aggregate - Aggregate to create a reducer for.
*
* @example
* ```ts
* const reducer = eventStore.aggregate.reducer(Aggregate);
* const state = await eventStore.reduce({ name: "foo:reducer", stream: "stream-id", reducer });
* ```
*/ */
reducer: <TAggregate extends AggregateRootClass<TEventFactory>>( reducer: <TAggregate extends AggregateRootClass<TEventFactory>>(
aggregate: TAggregate, aggregate: TAggregate,
@@ -382,18 +411,11 @@ export class EventStore<TEventFactory extends EventFactory, TEventStoreAdapter e
* @param pending - List of non comitted events to append to the server events. * @param pending - List of non comitted events to append to the server events.
* *
* @example * @example
*
* ```ts * ```ts
* const reducer = eventStore.aggregate.reducer(Aggregate);
* const state = await eventStore.reduce({ stream, reducer }); * const state = await eventStore.reduce({ stream, reducer });
* ```
*
* @example
*
* ```ts
* const state = await eventStore.reduce({ relation: `foo:${foo}:bars`, reducer }); * const state = await eventStore.reduce({ relation: `foo:${foo}:bars`, reducer });
* ``` * ```
*
* Reducers are created through the `.makeReducer` and `.makeAggregateReducer` method.
*/ */
async reduce<TReducer extends Reducer>( async reduce<TReducer extends Reducer>(
{ name, stream, relation, reducer, ...query }: ReduceQuery<TReducer>, { name, stream, relation, reducer, ...query }: ReduceQuery<TReducer>,
@@ -404,7 +426,7 @@ export class EventStore<TEventFactory extends EventFactory, TEventStoreAdapter e
let state: InferReducerState<TReducer> | undefined; let state: InferReducerState<TReducer> | undefined;
let cursor: string | undefined; let cursor: string | undefined;
const snapshot = await this.getSnapshot(name, id); const snapshot = await this.snapshot.get(name, id);
if (snapshot !== undefined) { if (snapshot !== undefined) {
cursor = snapshot.cursor; cursor = snapshot.cursor;
state = snapshot.state; state = snapshot.state;
@@ -436,99 +458,92 @@ export class EventStore<TEventFactory extends EventFactory, TEventStoreAdapter e
|-------------------------------------------------------------------------------- |--------------------------------------------------------------------------------
*/ */
/** readonly snapshot = {
* Create a new snapshot for the given stream/relation and reducer. /**
* * Create a new snapshot for the given stream/relation and reducer.
* @param query - Reducer query to create snapshot from. *
* * @param query - Reducer query to create snapshot from.
* @example *
* ```ts * @example
* await eventStore.createSnapshot({ stream, reducer }); * ```ts
* ``` * await eventStore.createSnapshot({ stream, reducer });
* * await eventStore.createSnapshot({ relation: `foo:${foo}:bars`, reducer });
* @example * ```
* ```ts */
* await eventStore.createSnapshot({ relation: `foo:${foo}:bars`, reducer }); create: async <TReducer extends Reducer>({
* ``` name,
*/ stream,
async createSnapshot<TReducer extends Reducer>({ relation,
name, reducer,
stream, ...query
relation, }: ReduceQuery<TReducer>): Promise<void> => {
reducer, const id = stream ?? relation;
...query const events =
}: ReduceQuery<TReducer>): Promise<void> { stream !== undefined
const id = stream ?? relation; ? await this.getEventsByStreams([id], query)
const events = : await this.getEventsByRelations([id], query);
stream !== undefined ? await this.getEventsByStreams([id], query) : await this.getEventsByRelations([id], query); if (events.length === 0) {
if (events.length === 0) { return undefined;
return undefined; }
} await this.snapshots.insert(name, id, events.at(-1)!.created, reducer.reduce(events));
await this.snapshots.insert(name, id, events.at(-1)!.created, reducer.reduce(events)); },
}
/** /**
* Get an entity state snapshot from the database. These are useful for when we * Get an entity state snapshot from the database. These are useful for when we
* want to reduce the amount of events that has to be processed when fetching * want to reduce the amount of events that has to be processed when fetching
* state history for a reducer. * state history for a reducer.
* *
* @param streamOrRelation - Stream, or Relation to get snapshot for. * @param streamOrRelation - Stream, or Relation to get snapshot for.
* @param reducer - Reducer to get snapshot for. * @param reducer - Reducer to get snapshot for.
* *
* @example * @example
* ```ts * ```ts
* const snapshot = await eventStore.getSnapshot("foo:reducer", stream); * const snapshot = await eventStore.getSnapshot("foo:reducer", stream);
* console.log(snapshot); * console.log(snapshot);
* // { * // {
* // cursor: "jxubdY-0", * // cursor: "jxubdY-0",
* // state: { * // state: {
* // foo: "bar" * // foo: "bar"
* // } * // }
* // } * // }
* ``` *
* * const snapshot = await eventStore.getSnapshot("foo:reducer", `foo:${foo}:bars`);
* @example * console.log(snapshot);
* ```ts * // {
* const snapshot = await eventStore.getSnapshot("foo:reducer", `foo:${foo}:bars`); * // cursor: "jxubdY-0",
* console.log(snapshot); * // state: {
* // { * // count: 1
* // cursor: "jxubdY-0", * // }
* // state: { * // }
* // count: 1 * ```
* // } */
* // } get: async <TReducer extends Reducer, TState = InferReducerState<TReducer>>(
* ``` name: string,
*/ streamOrRelation: string,
async getSnapshot<TReducer extends Reducer, TState = InferReducerState<TReducer>>( ): Promise<{ cursor: string; state: TState } | undefined> => {
name: string, const snapshot = await this.snapshots.getByStream(name, streamOrRelation);
streamOrRelation: string, if (snapshot === undefined) {
): Promise<{ cursor: string; state: TState } | undefined> { return undefined;
const snapshot = await this.snapshots.getByStream(name, streamOrRelation); }
if (snapshot === undefined) { return { cursor: snapshot.cursor, state: snapshot.state as TState };
return undefined; },
}
return { cursor: snapshot.cursor, state: snapshot.state as TState };
}
/** /**
* Delete a snapshot. * Delete a snapshot.
* *
* @param streamOrRelation - Stream, or Relation to delete snapshot for. * @param streamOrRelation - Stream, or Relation to delete snapshot for.
* @param reducer - Reducer to remove snapshot for. * @param reducer - Reducer to remove snapshot for.
* *
* @example * @example
* ```ts * ```ts
* await eventStore.deleteSnapshot("foo:reducer", stream); * await eventStore.deleteSnapshot("foo:reducer", stream);
* ``` * await eventStore.deleteSnapshot("foo:reducer", `foo:${foo}:bars`);
* * ```
* @example */
* ```ts delete: async (name: string, streamOrRelation: string): Promise<void> => {
* await eventStore.deleteSnapshot("foo:reducer", `foo:${foo}:bars`); await this.snapshots.remove(name, streamOrRelation);
* ``` },
*/ };
async deleteSnapshot(name: string, streamOrRelation: string): Promise<void> {
await this.snapshots.remove(name, streamOrRelation);
}
} }
/* /*

View File

@@ -49,7 +49,7 @@ export default describe<Events>(".createSnapshot", (getEventStore) => {
}), }),
); );
await store.createSnapshot({ name: "user", stream, reducer: userReducer }); await store.snapshot.create({ name: "user", stream, reducer: userReducer });
const snapshot = await store.snapshots.getByStream("user", stream); const snapshot = await store.snapshots.getByStream("user", stream);

View File

@@ -81,7 +81,7 @@ export default describe<Events>(".reduce", (getEventStore) => {
}), }),
); );
await store.createSnapshot({ name: "user", stream, reducer: userReducer }); await store.snapshot.create({ name: "user", stream, reducer: userReducer });
const state = await store.reduce({ name: "user", stream, reducer: userReducer }); const state = await store.reduce({ name: "user", stream, reducer: userReducer });