@@ -19,29 +19,83 @@ import { PostgresTransaction } from '@/lib/postgres';
1919import { log } from '@/common/util/Logger' ;
2020import { POSIX } from '@/lib/time' ;
2121import { Future } from '@/lib/Future' ;
22+ import { TreeMap } from '@/lib/TreeMap' ;
23+ import { Nullable } from '@/lib/Maybe' ;
2224
2325type WithEventStore = < E , T > (
2426 onError : ( e : Error ) => E ,
2527 f : ( s : EventStore ) => Future < E , T > ,
2628) => Future < E , T > ;
2729
30+ type LoadedAggregate < T extends Aggregate < T > > = {
31+ aggregate : T ;
32+ lastEvent : EventInfo ;
33+ } ;
34+
2835class PostgresEventStore implements EventStore {
36+ // This cache allows us to efficiently call `find` and `try_find` multiple
37+ // times within a transaction. This makes reactions and commands simpler as
38+ // there is no need to manually apply to the aggregate the transformations
39+ // performed by newly emitted events in those functions. Instead we can just
40+ // call `find` again and load the latest version of the aggregate for free.
41+ private cache : TreeMap < Id < Aggregate < unknown > > , LoadedAggregate < any > > ;
42+
43+ // An instance of this class never lives loger than the transaction
44+ // it is associated with.
2945 constructor (
3046 private transaction : PostgresTransaction ,
3147 private readonly schemas : Schemas ,
3248 private readonly eventStoreTable : string ,
33- ) { }
49+ ) {
50+ this . cache = TreeMap . new_ ( ) ;
51+ }
3452
3553 async find < T extends Aggregate < T > > (
3654 cls : Constructor < T > ,
3755 aggregateId : Id < T > ,
38- ) : Promise < { aggregate : T ; lastEvent : EventInfo } > {
56+ ) : Promise < T > {
57+ return ( await this . _find ( cls , aggregateId ) ) . aggregate ;
58+ }
59+
60+ async try_find < T extends Aggregate < T > > (
61+ cls : Constructor < T > ,
62+ aggregateId : Id < T > ,
63+ ) : Promise < T | null > {
64+ const found = await this . _try_find ( cls , aggregateId ) ;
65+ return found ? found . aggregate : null ;
66+ }
67+
68+ private async _find < T extends Aggregate < T > > (
69+ cls : Constructor < T > ,
70+ aggregateId : Id < T > ,
71+ ) : Promise < LoadedAggregate < T > > {
72+ const found = await this . _try_find ( cls , aggregateId ) ;
73+
74+ if ( found == null ) {
75+ throw new Error ( `Unknown aggregate ID ${ aggregateId . value } ` ) ;
76+ }
77+
78+ return found ;
79+ }
80+
81+ private async _try_find < T extends Aggregate < T > > (
82+ cls : Constructor < T > ,
83+ aggregateId : Id < T > ,
84+ ) : Promise < Nullable < LoadedAggregate < T > > > {
85+ const found = this . cache_load ( aggregateId ) ;
86+ if ( found !== null ) {
87+ return found ;
88+ }
89+
3990 const events = await this . findAll ( aggregateId ) ;
40- const { lastEvent, aggregate } = this . schemas
41- . hydrate ( cls , events )
42- . unwrap ( ( e ) => e ) ;
4391
44- return { aggregate, lastEvent } ;
92+ if ( events . length === 0 ) {
93+ return null ;
94+ }
95+
96+ const loaded = this . schemas . hydrate ( cls , events ) . unwrap ( ( e ) => e ) ;
97+ this . cache_save ( loaded ) ;
98+ return loaded ;
4599 }
46100
47101 async emit < T extends Aggregate < T > > ( args : {
@@ -50,13 +104,14 @@ class PostgresEventStore implements EventStore {
50104 event_id ?: Id < Event < T > > ;
51105 correlation_id ?: Id < Event < T > > ;
52106 causation_id ?: Id < Event < T > > ;
53- } ) : Promise < void > {
107+ } ) : Promise < { event : Event < T > ; info : EventInfo } > {
54108 const event = args . event ;
55109 const event_id = args . event_id || Id . random ( ) ;
56110 let info : EventInfo ;
111+ let aggregate : T ;
57112 switch ( true ) {
58113 case event instanceof CreationEvent : {
59- const aggregate : T = event . createAggregate ( ) ;
114+ aggregate = event . createAggregate ( ) ;
60115 info = {
61116 event_id,
62117 aggregate_id : aggregate . aggregateId ,
@@ -68,16 +123,17 @@ class PostgresEventStore implements EventStore {
68123 break ;
69124 }
70125 case event instanceof TransformationEvent : {
71- const { aggregate , lastEvent } = await this . find (
126+ const found = await this . _find (
72127 args . aggregate ,
73128 event . values . aggregateId ,
74129 ) ;
130+ aggregate = found . aggregate ;
75131 info = {
76132 event_id,
77133 aggregate_id : aggregate . aggregateId ,
78134 aggregate_version : aggregate . aggregateVersion + 1 ,
79- correlation_id : lastEvent . correlation_id ,
80- causation_id : lastEvent . causation_id ,
135+ correlation_id : found . lastEvent . correlation_id ,
136+ causation_id : found . lastEvent . causation_id ,
81137 recorded_on : POSIX . now ( ) ,
82138 } ;
83139 break ;
@@ -87,6 +143,8 @@ class PostgresEventStore implements EventStore {
87143 }
88144
89145 await this . insert < Event < T > > ( { info, event } ) ;
146+ this . cache_save ( { aggregate, lastEvent : info } ) ;
147+ return { event, info } ;
90148 }
91149
92150 async doesEventAlreadyExist ( eventId : Id < Event < any > > ) : Promise < boolean > {
@@ -157,6 +215,16 @@ class PostgresEventStore implements EventStore {
157215 throw new Error ( `Failed to save event: ${ edata . info . event_id } : ${ error } ` ) ;
158216 }
159217 }
218+
219+ private cache_save < T extends Aggregate < T > > ( loaded : LoadedAggregate < T > ) : void {
220+ this . cache . set ( loaded . aggregate . aggregateId , loaded ) ;
221+ }
222+
223+ private cache_load < T extends Aggregate < T > > (
224+ id : Id < T > ,
225+ ) : Nullable < LoadedAggregate < T > > {
226+ return this . cache . get ( id ) . asNullable ( ) ;
227+ }
160228}
161229
162230// Prepare the database to be used as an event store.
0 commit comments