From 89757465dae185017ce167440966dd95d035a77e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Max=20Leuth=C3=A4user?=
<1417198+max-leuthaeuser@users.noreply.github.com>
Date: Thu, 29 Jan 2026 10:47:48 +0100
Subject: [PATCH] Replaced Akka with ZIO
Akka is not OSS anymore.
---
.gitignore | 2 +
.../internal/rpa/RolePlayingAutomaton.scala | 152 +++++++++++++-----
project/Dependencies.scala | 14 +-
.../RolePlayingAutomatonTest.scala | 1 -
4 files changed, 122 insertions(+), 47 deletions(-)
diff --git a/.gitignore b/.gitignore
index d2fb731d..c76e1b15 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,8 @@
.DS_Store
.bsp/
.idea/
+.vscode/
+.metals/
project/
target/
*.iml
diff --git a/core/src/main/scala/scroll/internal/rpa/RolePlayingAutomaton.scala b/core/src/main/scala/scroll/internal/rpa/RolePlayingAutomaton.scala
index 82dc2639..cf8df429 100644
--- a/core/src/main/scala/scroll/internal/rpa/RolePlayingAutomaton.scala
+++ b/core/src/main/scala/scroll/internal/rpa/RolePlayingAutomaton.scala
@@ -1,17 +1,8 @@
package scroll.internal.rpa
-import akka.actor.Actor
-import akka.actor.ActorRef
-import akka.actor.ActorSystem
-import akka.actor.FSM
-import akka.actor.LoggingFSM
-import akka.actor.Props
import scroll.internal.compartment.impl.Compartment
-import scroll.internal.rpa.RolePlayingAutomaton.RPAData
-import scroll.internal.rpa.RolePlayingAutomaton.RPAState
-import scroll.internal.rpa.RolePlayingAutomaton.Start
-import scroll.internal.rpa.RolePlayingAutomaton.Stop
-import scroll.internal.rpa.RolePlayingAutomaton.Uninitialized
+import scroll.internal.rpa.RolePlayingAutomaton._
+import zio.{ Fiber, Queue, Runtime, Unsafe, ZIO }
import scala.reflect.ClassTag
import scala.reflect.classTag
@@ -43,27 +34,58 @@ object RolePlayingAutomaton {
case object Terminate extends RPAData
- protected class RPABuilder[T <: AnyRef: ClassTag]() {
+ final case class Event(data: RPAData, stateData: RPAData)
- infix def For(comp: Compartment): ActorRef =
- ActorSystem().actorOf(Props(classTag[T].runtimeClass, comp), s"rpa_${comp.hashCode()}")
+ final case class Transition(state: RPAState, data: RPAData = Uninitialized)
+ trait RPARef {
+ def !(data: RPAData): Unit
+ }
+
+ protected class RPABuilder[T <: RolePlayingAutomaton: ClassTag]() {
+
+ infix def For(comp: Compartment): RPARef = {
+ val instance = instantiate[T](comp)
+ instance.ref
+ }
+
+ }
+
+ def Use[T <: RolePlayingAutomaton: ClassTag]: RPABuilder[T] = new RPABuilder[T]()
+
+ private def instantiate[T <: RolePlayingAutomaton: ClassTag](comp: Compartment): T = {
+ val clazz = classTag[T].runtimeClass
+ val constructors = clazz.getDeclaredConstructors.toList
+ val withComp = constructors.find { ctor =>
+ val params = ctor.getParameterTypes
+ params.length == 1 && params.head.isAssignableFrom(comp.getClass)
+ }
+
+ val ctor = withComp.orElse(constructors.find(_.getParameterCount == 0)).getOrElse {
+ throw new IllegalArgumentException(
+ s"Unable to instantiate ${clazz.getName} for compartment ${comp.getClass.getName}"
+ )
+ }
+
+ ctor.setAccessible(true)
+ if (ctor.getParameterCount == 1) {
+ ctor.newInstance(comp).asInstanceOf[T]
+ } else {
+ ctor.newInstance().asInstanceOf[T]
+ }
}
- def Use[T <: AnyRef: ClassTag]: RPABuilder[T] = new RPABuilder[T]()
}
/** Use this trait to implement your own [[scroll.internal.compartment.impl.Compartment]] specific role playing
- * automaton. Please read the documentation for [[akka.actor.FSM]] carefully, since the features from that are
- * applicable for role playing automatons.
+ * automaton. This implementation uses ZIO to run a lightweight, single-threaded event loop.
*
* Remember to call run() when you want to start this automaton in your
* [[scroll.internal.compartment.impl.Compartment]] instance.
*
* This automaton will always start in state [[scroll.internal.rpa.RolePlayingAutomaton.Start]], so hook in there.
*
- * Final state is always [[scroll.internal.rpa.RolePlayingAutomaton.Stop]], that will terminate the actor system for
- * this [[akka.actor.FSM]].
+ * Final state is always [[scroll.internal.rpa.RolePlayingAutomaton.Stop]], which terminates the internal loop.
*
* Use the factory method RolePlayingAutomaton.Use to gain an instance of your specific FSM, e.g.:
*
@@ -95,32 +117,84 @@ object RolePlayingAutomaton {
* Some predefined event types for messaging are available in the companion object. You may want to define your own
* states and event types. Simply use a companion object for this as well.
*/
-trait RolePlayingAutomaton extends Actor with LoggingFSM[RPAState, RPAData] {
+trait RolePlayingAutomaton {
- /** Starts this automaton. Needs to be called first! Will set the initial state to
- * [[scroll.internal.rpa.RolePlayingAutomaton.Start]].
- */
- def run(): Unit = {
- log.debug(s"Starting RPA '${self.path}'")
- startWith(Start, Uninitialized)
- initialize()
- }
+ private val runtime = Runtime.default
- /** Stops this automaton. Will set state to [[scroll.internal.rpa.RolePlayingAutomaton.Stop]] and terminates the actor
- * system for this [[akka.actor.FSM]].
- */
- def halt(): State = {
- val _ = context.system.terminate()
- stop()
+ private val queue: Queue[RPAData] = Unsafe.unsafe { implicit u =>
+ runtime.unsafe.run(Queue.unbounded[RPAData]).getOrThrow()
}
- when(Stop) {
- FSM.NullFunction
+ private var currentState: RPAState = Start
+ private var currentData: RPAData = Uninitialized
+ private var handlers: Map[RPAState, PartialFunction[Event, Transition]] = Map.empty
+ private var transitionHandler: PartialFunction[(RPAState, RPAState), Unit] = PartialFunction.empty
+ private var fiber: Option[Fiber.Runtime[Nothing, Unit]] = None
+
+ protected val self: RPARef = new RPARef {
+ override def !(data: RPAData): Unit = enqueue(data)
}
- onTransition { case _ -> Stop =>
- log.debug(s"Stopping RPA '${self.path}'")
- val _ = halt()
+ private[rpa] def ref: RPARef = self
+
+ /** Register event handler for a state. */
+ def when(state: RPAState)(handler: PartialFunction[Event, Transition]): Unit =
+ handlers = handlers.updated(state, handler)
+
+ /** Register transition handler. Multiple handlers can be chained. */
+ def onTransition(handler: PartialFunction[(RPAState, RPAState), Unit]): Unit =
+ transitionHandler = transitionHandler.orElse(handler)
+
+ def goto(state: RPAState, data: RPAData = Uninitialized): Transition = Transition(state, data)
+
+ /** Starts this automaton. Needs to be called first! Will set the initial state to
+ * [[scroll.internal.rpa.RolePlayingAutomaton.Start]].
+ */
+ def run(): Unit = start()
+
+ /** Stops this automaton by interrupting the processing fiber. */
+ def halt(): Unit = {
+ currentState = Stop
+ fiber.foreach { f =>
+ Unsafe.unsafe { implicit u =>
+ runtime.unsafe.run(f.interrupt).getOrThrow()
+ }
+ }
+ fiber = None
}
+ private def start(): Unit =
+ if (fiber.isEmpty) {
+ currentState = Start
+ currentData = Uninitialized
+ val loop = processLoop
+ val startedFiber = Unsafe.unsafe { implicit u =>
+ runtime.unsafe.run(loop.forkDaemon).getOrThrow()
+ }
+ fiber = Some(startedFiber)
+ }
+
+ private def enqueue(data: RPAData): Unit =
+ Unsafe.unsafe { implicit u =>
+ runtime.unsafe.run(queue.offer(data)).getOrThrow()
+ }
+
+ private def processLoop: ZIO[Any, Nothing, Unit] =
+ queue.take.flatMap { message =>
+ val state = currentState
+ val data = currentData
+ handlers.get(state) match {
+ case Some(handler) if handler.isDefinedAt(Event(message, data)) =>
+ val Transition(nextState, nextData) = handler(Event(message, data))
+ currentState = nextState
+ currentData = nextData
+ if (transitionHandler.isDefinedAt(state -> nextState)) {
+ transitionHandler(state -> nextState)
+ }
+ if (nextState == Stop) ZIO.unit else processLoop
+ case _ =>
+ processLoop
+ }
+ }
+
}
diff --git a/project/Dependencies.scala b/project/Dependencies.scala
index 660f8d04..e726d29b 100644
--- a/project/Dependencies.scala
+++ b/project/Dependencies.scala
@@ -7,7 +7,7 @@ trait Dependencies {
object v {
val scalaVersion = "3.8.1"
- val akkaVersion = "2.8.8"
+ val zioVersion = "2.1.24"
val scalatestVersion = "3.2.19"
val chocoVersion = "4.10.18"
val guavaVersion = "33.5.0-jre"
@@ -18,12 +18,12 @@ trait Dependencies {
}
val coreDependencies = Seq(
- "com.typesafe.akka" %% "akka-actor" % v.akkaVersion,
- "com.google.guava" % "guava" % v.guavaVersion,
- "org.choco-solver" % "choco-solver" % v.chocoVersion,
- "org.eclipse.emf" % "org.eclipse.emf.common" % v.emfcommonVersion,
- "org.eclipse.emf" % "org.eclipse.emf.ecore" % v.emfecoreVersion,
- "org.eclipse.uml2" % "org.eclipse.uml2.uml" % v.umlVersion
+ "dev.zio" %% "zio" % v.zioVersion,
+ "com.google.guava" % "guava" % v.guavaVersion,
+ "org.choco-solver" % "choco-solver" % v.chocoVersion,
+ "org.eclipse.emf" % "org.eclipse.emf.common" % v.emfcommonVersion,
+ "org.eclipse.emf" % "org.eclipse.emf.ecore" % v.emfecoreVersion,
+ "org.eclipse.uml2" % "org.eclipse.uml2.uml" % v.umlVersion
)
val coreDependenciesOverrides = Seq(
diff --git a/tests/src/test/scala/scroll/tests/parameterized/RolePlayingAutomatonTest.scala b/tests/src/test/scala/scroll/tests/parameterized/RolePlayingAutomatonTest.scala
index 910fc26a..9a5972a5 100644
--- a/tests/src/test/scala/scroll/tests/parameterized/RolePlayingAutomatonTest.scala
+++ b/tests/src/test/scala/scroll/tests/parameterized/RolePlayingAutomatonTest.scala
@@ -1,6 +1,5 @@
package scroll.tests.parameterized
-import akka.actor.actorRef2Scala
import org.scalatest.concurrent.Waiters._
import org.scalatest.time.SpanSugar._
import scroll.internal.rpa.RolePlayingAutomaton