Caching States
If no event carries the whole state, so that the state can not start from the latest event of one type (see Reading Long Streams), the store can keep the states of the most recently used subjects in memory instead, so that the next command on one of them reads only the events written since.
To do so, hand over the WithStateCache option with the number of subjects to keep when creating the store:
store := architecturekit.NewStore(client, "https://library.eventsourcingdb.io", architecturekit.WithStateCache(10_000))
Once the cache is full, the least recently used subject makes room. Such a subject is read in full again the next time, or from the latest event of the type given to FromLatest, if there is one (see Reading Long Streams).
The cache only holds what was read, never what a command has written. Every command reads all events after the ones its state was built from, including those written by other processes, so the cache stays correct if several processes write to the same subjects.
The cache tells states apart by their type, not by the object. A state that is built anew for every command, for example by a function that returns it, is cached as well. This is why two different states that read the same subject need two different types: if one of them counts the loans of a book and the other one its returns, declare types such as LoanCount and ReturnCount rather than using int for both. If two states of the same type meet on the same subject, but differ in their initial value, in the event types they have Evolve rules for, ignore, or have upcasters for, or in FromLatest, Execute and Load return an error of the category ErrPermanent (see Handling Errors).
Note that the cache can not compare the Evolve functions themselves. Two states of the same type that are built alike, but compute something else, are not told apart.
Note that the very same state is not compared at all. The initial values of two different states of the same type are compared as reflect.DeepEqual compares them, except that every value equals itself: two functions in them are equal if both are nil or both are not, since functions can not be compared, and two floating-point numbers if they are equal or both NaN, as are the parts of two complex numbers. Only a map with a NaN key does not equal itself, since Go never finds such a key again.
A cached state is handed to several commands, possibly at the same time. That is safe for a state that consists of values only, such as the Book state (see Defining State). A state that holds slices, maps, pointers, channels, functions, or interfaces, such as a field of type error or any, is only cached if it has a Clone function, which returns a copy that shares no data with the original. An interface counts as well, since it can hold any of the others. For example, a shelf that collects the IDs of the books that BookShelved events put on it needs one:
type Shelf struct {
BookIDs []string
}
var shelfState = architecturekit.NewState(Shelf{}).
Evolve(func(shelf Shelf, event BookShelved) Shelf {
shelf.BookIDs = append(shelf.BookIDs, event.BookID)
return shelf
}).
Clone(func(shelf Shelf) Shelf {
return Shelf{BookIDs: slices.Clone(shelf.BookIDs)}
})
Without a Clone function, such a state is read as without a cache. The same function lets Step and StepStored leave a state unchanged (see Stepping Through States), and copies the initial value at the start of every read, which an initial value with a map, a pointer or a channel that is not nil, or a slice with room for elements, requires (see Defining State).
Note that as an initial value, Shelf{}, whose BookIDs are nil, does not require a Clone function, and neither does Shelf{BookIDs: []string{}}, since appending to a slice without room for elements allocates a new array. Shelf{BookIDs: make([]string, 0, 10)} does, since appending to it writes into the array that every copy shares.
Note that a time.Time counts as a value, since its location never changes.
Note that a number of 0 keeps no states, the same as leaving out the option, so that a configuration can turn the cache off. A negative number makes WithStateCache panic, and calling Clone twice, or with nil, panics as well.