Open notxcain opened 5 years ago
Thanks. Yes, can see the excellent modularity. Trying to knit a few things I know of and thinking through a few aspects.
I'd like to
behavior algebra
and the free monad
machinery runnable in both server and client
For example am looking at frees.io
how they do this part since they handle scala.js
so there is a chance to reuse.In aecor
we have @autoFunctorK
and frees.io
has @free
and @tagless
http://frees.io/docs/core/algebras/
These seem to be very similar - again, looking there because of scala.js
support and to better understand where the awareness of "journal" comes in.
behavior algebra
decoupled from the "journal" - I found this uncommittedEvents
design pattern very useful for managing state between client and server as lists of domain event
s.Please see https://tech.zilverline.com/2011/02/10/towards-an-immutable-domain-model-monads-part-5 for details.
This pattern is like a "journal" but decoupled from the actual persistence mechanism.
A code example for an idea.
Note ReturnVal(Left(true), events)
- this allows state mutation effects from server to be re-played on client thus updating client state and UI.
The pattern works in reverse: domain event
s from client side effects / state mutations can be re-played on server thus allowing enforcing some business rules that cannot be evaluated in the client.
override def registerCollateral(customerId: CustomerId, collateral: Collateral)
: Future[ReturnVal[Boolean]] = {
val p = Promise[ReturnVal[Boolean]]()
try {
val customer = CustomerRepository.findCustomerById(customerId)
if (!customer.isDefined) {
p.failure(BusinessException(s"Could not find customer $customerId"))
}
else {
CustomerServiceEventStore.command_RegisterCollateral(customer.get, collateral)
val events = customer.get.uncommittedEvents
customer.get.markCommitted()
p.success(ReturnVal(Left(true), events))
}
} catch {
case NonFatal(e) =>
Logger.error(CLS + "registerCollateral: " + e)
p.failure(e)
}
p.future
}
Showing all of this to make the case that this is not so much about scala.js
as about an event sourcing
flavored programming model that works with "client" as well.
I wonder if you would agree to review this uncommittedEvents
s simple pattern and consider that there might not be a need for full journal in client.
Is there a way to achieve this same effect of events detached from journal with aecor
already somehow?
Yes, there is almost everything you need.
sealed abstract class CustomerEvent
final case class CollateralRegistered(collateral: Collateral) extends CustomerEvent
sealed abstract class CustomerRejection
object CustomerNotFound extends CustomerRejection
def update(state: Option[CustomerState], e: CustomerEvent): Folded[Option[CustomerState]] = ???
@autoFunctorK(false)
trait Customer[F[_]] {
def registerCollateral(collateral: Collateral): F[Unit]
}
final class CustomerActions[F[_]](
implicit
F: MonadActionReject[F, Option[CustomerState], CustomerEvent, CustomerRejection]
) extends Customer[F] {
import F._
def registerCollateral(collateral: Collateral): F[Unit] =
read.flatMap {
case Some(customer) =>
append(CollateralRegistered(collateral))
case None =>
reject(CustomerNotFound)
}
}
type In[A] = ActionT[EitherT[IO, CustomerRejection, ?], Option[CustomerState], CustomerEvent, ?]
type Out[A] = IO[Either[CustomerRejection, (Chain[CustomerEvent], A))
val customerActions = new CustomerActions[In]
val customers: CustomerId => Customer[Out] = {
customerId =>
customerActions.mapK(new (In ~> Out) {
def apply[A](action: In[A]): Out[A] =
CustomerRepository.findCustomerById(customerId)
.flatMap { customerState =>
action.run(customerState, _.update(_)).value.flatMap {
case Right(Next(a @ (events, _))) => CustomerRepository.appendEvents(customerId, events).as(Right(a))
case Left(rejection) => IO(Left(rejection))
case Right(Impossible) => IO.raiseError(new IllegalStateException)
}
}
}
I don't know what markCommitted
does, but I think it mutates state somehow which I prefer to avoid, so you need some kind of journal anyway to persist events, easy to implement using cats.effect.concurrent.Ref
. Also I assumed that findCustomerById
and appendEvents
are in IO[_]
.
@SemanticBeeng I can only see slight problems with java.time.*
classes used in some places, in particular aecor.util.Clock
.
Most would be covered by https://github.com/scala-js/scala-js-java-time, but ZonedDateTime
is not there yet, for example.
So I would first move Clock to schedule module, and then core would surely cross-compile.
Excellent example Denis.
Reviewed this thoroughly and looks very good.
Not certain yet how CustomerState
fits with uncommittedEvents
but almost there.
case Right(Next(a @ (events, _))) => CustomerRepository.appendEvents(customerId, events).as(Right(a))
made it clear that persistence is in the hands of the user and not forced by framework.
In this previous project I reused this little pattern from the article above
trait AggregateRoot[Event] {
protected def applyEvent: Event => Unit
def uncommittedEvents: Iterable[Event] = _uncommittedEvents.clone()
def markCommitted() = _uncommittedEvents.clear()
def loadFromHistory(history: Iterable[Event]) = history.foreach(applyEvent)
def record(event: Event) {
applyEvent(event)
_uncommittedEvents += event
}
private val _uncommittedEvents = mutable.Queue[Event]()
}
Wip on fitting this in with your example but can see things to try.
So by the example above you mean that such kind of code should cross-build?
@SemanticBeeng I think it’s better to move our discussion from the closed PR #50.
I’m very into adding Scala.js support. You don’t need Akka for single-machine environment (assuming we run entities in browser). The runtime is very easy to implement. All components are already here.
We need to decide how the event journal should be implemented, in-mem or using browser local storage. It’s up to you to decide, I’ll help you to implement it, but I don’t have experience with browser specific API