Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scala.js support #54

Open
notxcain opened this issue Nov 8, 2018 · 4 comments
Open

Scala.js support #54

notxcain opened this issue Nov 8, 2018 · 4 comments

Comments

@notxcain
Copy link
Owner

notxcain commented Nov 8, 2018

@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

@SemanticBeeng
Copy link

SemanticBeeng commented Nov 8, 2018

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

  1. Have the 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.

  1. Have the 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 events.

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 events 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 uncommittedEventss 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?

@notxcain
Copy link
Owner Author

notxcain commented Nov 8, 2018

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[_].

@vpavkin
Copy link
Collaborator

vpavkin commented Nov 8, 2018

@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.

@SemanticBeeng
Copy link

SemanticBeeng commented Nov 8, 2018

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants