From 5f8b8bf5516c74ee91c624539f8cd465893d6ca3 Mon Sep 17 00:00:00 2001 From: amiklushou Date: Tue, 3 Feb 2026 13:05:35 +0200 Subject: [PATCH 1/5] Update to support scalajs-react 3.0.0 --- .github/workflows/release.yml | 4 +- .github/workflows/scala.yml | 4 +- build.sbt | 10 +- .../{scala => scala-2.13}/diode/Circuit.scala | 0 .../src/main/scala-3/diode/Circuit.scala | 455 ++++++++++++++++++ .../main/scala-3/diode/macros/CaseClass.scala | 2 +- .../src/test/scala/diode/ModelRWTests.scala | 4 +- project/plugins.sbt | 2 +- 8 files changed, 468 insertions(+), 13 deletions(-) rename diode-core/shared/src/main/{scala => scala-2.13}/diode/Circuit.scala (100%) create mode 100644 diode-core/shared/src/main/scala-3/diode/Circuit.scala diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 142ab089..fe8ba3de 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,13 +11,13 @@ jobs: - uses: olafurpg/setup-scala@v13 - uses: olafurpg/setup-gpg@v3 - name: Cache dependencies - uses: actions/cache@v3.0.1 + uses: actions/cache@v4 with: path: ~/.cache/coursier/v1 key: ${{ runner.os }}-coursier-${{ hashFiles('build.sbt') }}-${{ hashFiles('project/*.scala') }} restore-keys: ${{ runner.os }}-coursier- - name: Cache .sbt - uses: actions/cache@v3.0.1 + uses: actions/cache@v4 with: path: ~/.sbt key: ${{ runner.os }}-sbt-${{ hashFiles('build.sbt') }}-${{ hashFiles('project/*.scala') }} diff --git a/.github/workflows/scala.yml b/.github/workflows/scala.yml index aeb9a21a..8298bb44 100644 --- a/.github/workflows/scala.yml +++ b/.github/workflows/scala.yml @@ -17,13 +17,13 @@ jobs: - name: Checking your code format run: sbt +scalafmtCheck - name: Cache dependencies - uses: actions/cache@v3.0.1 + uses: actions/cache@v4 with: path: ~/.cache/coursier/v1 key: ${{ runner.os }}-coursier-${{ hashFiles('build.sbt') }}-${{ hashFiles('project/*.scala') }} restore-keys: ${{ runner.os }}-coursier- - name: Cache .sbt - uses: actions/cache@v3.0.1 + uses: actions/cache@v4 with: path: ~/.sbt key: ${{ runner.os }}-sbt-${{ hashFiles('build.sbt') }}-${{ hashFiles('project/*.scala') }} diff --git a/build.sbt b/build.sbt index 4b142527..9f3ec5bb 100644 --- a/build.sbt +++ b/build.sbt @@ -8,8 +8,8 @@ ThisBuild / scalafmtOnCompile := true Global / onChangedBuildSource := ReloadOnSourceChanges -ThisBuild / scalaVersion := "2.13.15" -ThisBuild / crossScalaVersions := Seq("2.13.15", "3.1.3") +ThisBuild / scalaVersion := "2.13.17" +ThisBuild / crossScalaVersions := Seq("2.13.17", "3.7.4") val commonSettings = Seq( scalacOptions := Seq( @@ -32,7 +32,7 @@ val commonSettings = Seq( }.value, scalacOptions ++= scalaVerDependentSeq { case (2, 13) => Seq("-Werror") - case (3, _) => Seq("-Xfatal-warnings") + // case (3, _) => "-Xfatal-warnings" // there are a lot of scala3 related warnings now }.value, Compile / scalacOptions -= scalaVerDependent { case (2, _) => "-Ywarn-value-discard" @@ -87,7 +87,7 @@ val commonJsSettings = Seq( scalacOptions += sourceMapSetting.value, scalacOptions ++= scalaVerDependent { case (2, _) => "-P:scalajs:nowarnGlobalExecutionContext" - case (3, _) => "-scalajs:nowarnGlobalExecutionContext" + case (3, _) => "-scalajs" }.value ) @@ -147,7 +147,7 @@ lazy val diodeReact: Project = project .settings( name := "diode-react", libraryDependencies ++= Seq( - "com.github.japgolly.scalajs-react" %%% "core" % "2.1.1" + "com.github.japgolly.scalajs-react" %%% "core" % "3.0.0" ) ) .dependsOn(diode.js) diff --git a/diode-core/shared/src/main/scala/diode/Circuit.scala b/diode-core/shared/src/main/scala-2.13/diode/Circuit.scala similarity index 100% rename from diode-core/shared/src/main/scala/diode/Circuit.scala rename to diode-core/shared/src/main/scala-2.13/diode/Circuit.scala diff --git a/diode-core/shared/src/main/scala-3/diode/Circuit.scala b/diode-core/shared/src/main/scala-3/diode/Circuit.scala new file mode 100644 index 00000000..1a9ea2de --- /dev/null +++ b/diode-core/shared/src/main/scala-3/diode/Circuit.scala @@ -0,0 +1,455 @@ +package diode + +import scala.annotation.implicitNotFound +import scala.collection.immutable.Queue + +/** + * The `ActionType` type class is used to verify that only valid actions are dispatched. An implicit instance of + * `ActionType[A]` must be in scope when calling dispatch methods or creating effects that return actions. + * + * `ActionType` is contravariant, which means it's enough to have an instance of `ActionType` for a common supertype to be + * able to dispatch actions of its subtypes. For example providing an instance of `ActionType[Action]` allows dispatching + * any class that is a subtype of `Action`. + * + * @tparam A + * Action type + */ +@implicitNotFound( + msg = + "Cannot find an ActionType type class for action of type ${A}. Make sure to provide an implicit ActionType for dispatched actions." +) +trait ActionType[-A] + +trait Dispatcher { + def dispatch[A: ActionType](action: A): Unit + + def apply[A: ActionType](action: A) = dispatch(action) +} + +/** + * Base trait for actions. Use this as a basis for your action class hierarchy to get an automatic type class instance of + * `ActionType[Action]`. Note that this trait is just a helper, you don't need to use it for your actions as you can always + * define your own `ActionType` instances for your action types. + */ +trait Action + +object Action { + implicit object aType extends ActionType[Action] +} + +/** + * A batch of actions. These actions are dispatched in a batch, without calling listeners in-between the dispatches. + * + * @param actions + * Sequence of actions to dispatch + */ +class ActionBatch private (val actions: Seq[Any]) extends Action { + def :+[A: ActionType](action: A): ActionBatch = + new ActionBatch(actions :+ action) + + def +:[A: ActionType](action: A) = + new ActionBatch(action +: actions) + + def ++(batch: ActionBatch) = + new ActionBatch(actions ++ batch.actions) +} + +object ActionBatch { + def apply[A: ActionType](actions: A*): ActionBatch = new ActionBatch(actions) +} + +/** + * Use `NoAction` when you need to dispatch an action that does nothing + */ +case object NoAction extends Action + +trait ActionProcessor[M <: AnyRef] { + def process(dispatch: Dispatcher, action: Any, next: Any => ActionResult[M], currentModel: M): ActionResult[M] +} + +sealed trait ActionResult[+M] { + def newModelOpt: Option[M] = None + def effectOpt: Option[Effect] = None +} + +sealed trait ModelUpdated[+M] extends ActionResult[M] { + def newModel: M + override def newModelOpt: Option[M] = Some(newModel) +} + +sealed trait HasEffect[+M] extends ActionResult[M] { + def effect: Effect + override def effectOpt: Option[Effect] = Some(effect) +} + +sealed trait UpdateSilent + +object ActionResult { + + case object NoChange extends ActionResult[Nothing] + + final case class ModelUpdate[M](newModel: M) extends ModelUpdated[M] + + final case class ModelUpdateSilent[M](newModel: M) extends ModelUpdated[M] with UpdateSilent + + final case class EffectOnly(effect: Effect) extends ActionResult[Nothing] with HasEffect[Nothing] + + final case class ModelUpdateEffect[M](newModel: M, effect: Effect) extends ModelUpdated[M] with HasEffect[M] + + final case class ModelUpdateSilentEffect[M](newModel: M, effect: Effect) + extends ModelUpdated[M] + with HasEffect[M] + with UpdateSilent + + def apply[M](model: Option[M], effect: Option[Effect]): ActionResult[M] = (model, effect) match { + case (Some(m), Some(e)) => ModelUpdateEffect(m, e) + case (Some(m), None) => ModelUpdate(m) + case (None, Some(e)) => EffectOnly(e) + case _ => NoChange + } +} + +trait Circuit[M <: AnyRef] extends Dispatcher with ZoomTo[M, M] { + + type HandlerFunction = (M, Any) => Option[ActionResult[M]] + + private case class Subscription[T](listener: ModelRO[T] => Unit, cursor: ModelR[M, T], lastValue: T) { + def changed: Option[Subscription[T]] = { + if (cursor === lastValue) + None + else + Some(copy(lastValue = cursor.eval(model))) + } + + def call(): Unit = listener(cursor) + } + + private[diode] var model: M = initialModel + + /** + * Provides the initial value for the model + */ + protected def initialModel: M + + /** + * Handles all dispatched actions + * + * @return + */ + protected def actionHandler: HandlerFunction + + private val modelRW = new RootModelRW[M](model) + private var isDispatching = false + private var dispatchQueue = Queue.empty[Any] + private var listenerId = 0 + private var listeners = Map.empty[Int, Subscription[_]] + private var processors = List.empty[ActionProcessor[M]] + private var processChain = buildProcessChain + + private def buildProcessChain = { + // chain processing functions + processors.reverse.foldLeft(process _)((next, processor) => + (action: Any) => processor.process(this, action, next, model) + ) + } + + /** + * Zoom into the model using the `get` function + * + * @param get + * Function that returns the part of the model you are interested in + * @return + * A `ModelR[T]` giving you read-only access to part of the model + */ + def zoom[T](get: M => T)(implicit feq: FastEq[_ >: T]): ModelR[M, T] = + modelRW.zoom[T](get) + + def zoomMap[F[_], A, B](fa: M => F[A])(f: A => B)(implicit monad: Monad[F], feq: FastEq[_ >: B]): ModelR[M, F[B]] = + modelRW.zoomMap(fa)(f) + + def zoomFlatMap[F[_], A, B](fa: M => F[A])(f: A => F[B])(implicit monad: Monad[F], feq: FastEq[_ >: B]): ModelR[M, F[B]] = + modelRW.zoomFlatMap(fa)(f) + + /** + * Zoom into the model using `get` and `set` functions + * + * @param get + * Function that returns the part of the model you are interested in + * @param set + * Function that updates the part of the model you are interested in + * @return + * A `ModelRW[T]` giving you read/update access to part of the model + */ + def zoomRW[T](get: M => T)(set: (M, T) => M)(implicit feq: FastEq[_ >: T]): ModelRW[M, T] = modelRW.zoomRW(get)(set) + + def zoomMapRW[F[_], A, B](fa: M => F[A])(f: A => B)( + set: (M, F[B]) => M + )(implicit monad: Monad[F], feq: FastEq[_ >: B]): ModelRW[M, F[B]] = + modelRW.zoomMapRW(fa)(f)(set) + + def zoomFlatMapRW[F[_], A, B](fa: M => F[A])(f: A => F[B])( + set: (M, F[B]) => M + )(implicit monad: Monad[F], feq: FastEq[_ >: B]): ModelRW[M, F[B]] = + modelRW.zoomFlatMapRW(fa)(f)(set) + + /** + * Subscribes to listen to changes in the model. By providing a `cursor` you can limit what part of the model must change + * for your listener to be called. If omitted, all changes result in a call. + * + * @param cursor + * Model reader returning the part of the model you are interested in. + * @param listener + * Function to be called when model is updated. The listener function gets the model reader as a parameter. + * @return + * A function to unsubscribe your listener + */ + def subscribe[T](cursor: ModelR[M, T])(listener: ModelRO[T] => Unit): () => Unit = { + this.synchronized { + listenerId += 1 + val id = listenerId + listeners += id -> Subscription(listener, cursor, cursor.eval(model)) + () => this.synchronized(listeners -= id) + } + } + + /** + * Adds a new `ActionProcessor[M]` to the action processing chain. The processor is called for every dispatched action. + * + * @param processor + */ + def addProcessor(processor: ActionProcessor[M]): Unit = { + this.synchronized { + processors = processor :: processors + processChain = buildProcessChain + } + } + + /** + * Removes a previously added `ActionProcessor[M]` from the action processing chain. + * + * @param processor + */ + def removeProcessor(processor: ActionProcessor[M]): Unit = { + this.synchronized { + processors = processors.filterNot(_ == processor) + processChain = buildProcessChain + } + } + + /** + * Handle a fatal error. Override this function to do something with exceptions that occur while dispatching actions. + * + * @param action + * Action that caused the exception + * @param e + * Exception that was thrown + */ + def handleFatal(action: Any, e: Throwable): Unit = throw e + + /** + * Handle a non-fatal error, such as dispatching an action with no action handler. + * + * @param msg + * Error message + */ + def handleError(msg: String): Unit = throw new Exception(s"handleError called with: $msg") + + /** + * @param action + * the action that caused the effects + * @param error + * the Exception that was encountered while processing the effects + */ + def handleEffectProcessingError[A](action: A, error: Throwable): Unit = { + handleError(s"Error in processing effects for action $action: $error") + } + + /** + * Updates the model if it has changed (reference equality check) + */ + private def update(newModel: M) = { + if (newModel ne model) { + model = newModel + } + } + + /** + * The final action processor that does actual action handling. + * + * @param action + * Action to be handled + * @return + */ + private def process(action: Any): ActionResult[M] = + action match { + case b: ActionBatch => + // dispatch all actions in the sequence using internal dispatchBase to prevent + // additional calls to subscribed listeners + b.actions.foreach(a => dispatchBase(a)) + ActionResult.NoChange + case NoAction => + // ignore + ActionResult.NoChange + case _ => + actionHandler(model, action).getOrElse { + handleError(s"Action $action was not handled by any action handler") + ActionResult.NoChange + } + } + + /** + * Composes multiple handlers into a single handler. Processing stops as soon as a handler is able to handle the action. + * If none of them handle the action, `None` is returned + */ + def composeHandlers(handlers: HandlerFunction*): HandlerFunction = + (model, action) => { + handlers.foldLeft(Option.empty[ActionResult[M]]) { (a, b) => + a.orElse(b(model, action)) + } + } + + @deprecated("Use composeHandlers or foldHandlers instead", "0.5.1") + def combineHandlers(handlers: HandlerFunction*): HandlerFunction = composeHandlers(handlers: _*) + + /** + * Folds multiple handlers into a single function so that each handler is called in turn and an updated model is passed on + * to the next handler. Returned `ActionResult` contains the final model and combined effects. + */ + def foldHandlers(handlers: HandlerFunction*): HandlerFunction = + (initialModel, action) => { + handlers + .foldLeft((initialModel, Option.empty[ActionResult[M]])) { + case ((currentModel, currentResult), handler) => + handler(currentModel, action) match { + case None => + (currentModel, currentResult) + case Some(result) => + val (nextModel, nextResult) = currentResult match { + case Some(cr) => + val newEffect = (cr.effectOpt, result.effectOpt) match { + case (Some(e1), Some(e2)) => Some(e1 + e2) + case (Some(e1), None) => Some(e1) + case (None, Some(e2)) => Some(e2) + case (None, None) => None + } + val newModel = result.newModelOpt.orElse(cr.newModelOpt) + (newModel.getOrElse(currentModel), ActionResult(newModel, newEffect)) + case None => + (result.newModelOpt.getOrElse(currentModel), result) + } + (nextModel, Some(nextResult)) + } + } + ._2 + } + + /** + * Dispatch the action, call change listeners when completed + * + * @param action + * Action to dispatch + */ + def dispatch[A: ActionType](action: A): Unit = { + this.synchronized { + if (!isDispatching) { + try { + isDispatching = true + val oldModel = model + val silent = dispatchBase(action) + if (oldModel ne model) { + // walk through all listeners and update subscriptions when model has changed + val updated = listeners.foldLeft(listeners) { + case (l, (key, sub)) => + if (listeners.isDefinedAt(key)) { + // Listener still exists + sub.changed match { + case Some(newSub) => + // value at the cursor has changed, call listener and update subscription + if (!silent) sub.call() + l.updated(key, newSub) + case None => l // nothing interesting happened + } + } else { + l // Listener was removed since we started + } + } + + // Listeners may have changed during processing (subscribe or unsubscribe) + // so only update the listeners that are still there, and leave any new listeners that may be there now. + listeners = updated.foldLeft(listeners) { + case (l, (key, sub)) => + if (l.isDefinedAt(key)) + l.updated(key, sub) // Listener still exists for this key + else + l // Listener was removed for this key, skip it + } + } + } catch { + case e: Throwable => + handleFatal(action, e) + } finally { + isDispatching = false + } + // if there is an item in the queue, dispatch it + dispatchQueue.dequeueOption foreach { + case (nextAction, queue) => + dispatchQueue = queue + dispatch(nextAction)(using null) + } + } else { + // add to the queue + dispatchQueue = dispatchQueue.enqueue(action) + } + } + } + + /** + * Perform actual dispatching, without calling change listeners + */ + protected def dispatchBase[A](action: A): Boolean = { + import AnyAction._ + try { + processChain(action) match { + case ActionResult.NoChange => + // no-op + false + case ActionResult.ModelUpdate(newModel) => + update(newModel) + false + case ActionResult.ModelUpdateSilent(newModel) => + update(newModel) + true + case ActionResult.EffectOnly(effects) => + // run effects + effects + .run(a => dispatch(a)) + .recover { + case e: Throwable => handleEffectProcessingError(action, e) + }(effects.ec) + true + case ActionResult.ModelUpdateEffect(newModel, effects) => + update(newModel) + // run effects + effects + .run(a => dispatch(a)) + .recover { + case e: Throwable => handleEffectProcessingError(action, e) + }(effects.ec) + false + case ActionResult.ModelUpdateSilentEffect(newModel, effects) => + update(newModel) + // run effects + effects + .run(a => dispatch(a)) + .recover { + case e: Throwable => handleEffectProcessingError(action, e) + }(effects.ec) + true + } + } catch { + case e: Throwable => + handleFatal(action, e) + true + } + } +} diff --git a/diode-core/shared/src/main/scala-3/diode/macros/CaseClass.scala b/diode-core/shared/src/main/scala-3/diode/macros/CaseClass.scala index 5d12a8c8..6bc52667 100644 --- a/diode-core/shared/src/main/scala-3/diode/macros/CaseClass.scala +++ b/diode-core/shared/src/main/scala-3/diode/macros/CaseClass.scala @@ -8,7 +8,7 @@ private[macros] object CaseClass { if (obj.tpe.typeSymbol.caseFields.isEmpty) { Left(s"Type ${obj.tpe.typeSymbol.fullName} of ${obj.tpe.show} field must be a case class.") } else { - Right(new CaseClass[q.type]()(obj)) + Right(new CaseClass[q.type](obj)) } } diff --git a/diode-core/shared/src/test/scala/diode/ModelRWTests.scala b/diode-core/shared/src/test/scala/diode/ModelRWTests.scala index 4148487d..93aba1af 100644 --- a/diode-core/shared/src/test/scala/diode/ModelRWTests.scala +++ b/diode-core/shared/src/test/scala/diode/ModelRWTests.scala @@ -96,7 +96,7 @@ object ModelRWTests extends TestSuite { val rv1 = r1.value // normal zoomed values change reference on each call assert(rv1 ne r1.value) - val rm1 = cr.map((c: C) => c.s) + val rm1: ModelR[ModelOpt, Option[String]] = cr.map((c: C) => c.s) val rmi1 = mr.zoomMap(_.c)(_.i) // mapped values maintain reference assert(rm1.value eq rm1.value) @@ -117,7 +117,7 @@ object ModelRWTests extends TestSuite { var m = ModelOpt(1.0f, Some(C(4242, "some", Some(A(66, "deep"))))) val mr = new RootModelR(m) val cr = mr.zoomFlatMap(_.c)(_.o) - val crs = cr.map((a: A) => a.s) + val crs: ModelR[ModelOpt, Option[String]] = cr.map((a: A) => a.s) val v1 = crs.value println(v1) assert(v1 eq crs.value) diff --git a/project/plugins.sbt b/project/plugins.sbt index 1b6e2fc8..1b6d80ff 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,4 +1,4 @@ -val scalaJSVersion = sys.env.getOrElse("SCALAJS_VERSION", "1.17.0") +val scalaJSVersion = sys.env.getOrElse("SCALAJS_VERSION", "1.20.2") addSbtPlugin("org.scala-js" % "sbt-scalajs" % scalaJSVersion) From 61b73969109b8472a4f4cec43adfd5347f0a80ac Mon Sep 17 00:00:00 2001 From: amiklushou Date: Tue, 3 Feb 2026 13:27:49 +0200 Subject: [PATCH 2/5] Bump to scala 3.8.1 --- build.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index 9f3ec5bb..3fc758bb 100644 --- a/build.sbt +++ b/build.sbt @@ -9,7 +9,7 @@ ThisBuild / scalafmtOnCompile := true Global / onChangedBuildSource := ReloadOnSourceChanges ThisBuild / scalaVersion := "2.13.17" -ThisBuild / crossScalaVersions := Seq("2.13.17", "3.7.4") +ThisBuild / crossScalaVersions := Seq("2.13.17", "3.8.1") val commonSettings = Seq( scalacOptions := Seq( @@ -32,7 +32,7 @@ val commonSettings = Seq( }.value, scalacOptions ++= scalaVerDependentSeq { case (2, 13) => Seq("-Werror") - // case (3, _) => "-Xfatal-warnings" // there are a lot of scala3 related warnings now + case (3, _) => Seq("-Xfatal-warnings") }.value, Compile / scalacOptions -= scalaVerDependent { case (2, _) => "-Ywarn-value-discard" From 21a03995cd7904cc608122f4d5e041a203f70885 Mon Sep 17 00:00:00 2001 From: amiklushou Date: Tue, 3 Feb 2026 15:25:59 +0200 Subject: [PATCH 3/5] Fix Scala 3 warnings. --- .scalafmt.conf | 2 +- build.sbt | 20 +- .../main/scala/diode/util/RunAfterJS.scala | 2 +- .../main/scala/diode/util/RunAfterJVM.scala | 4 +- .../test/scala/diode/CircuitJVMTests.scala | 8 +- .../src/main/scala-3/diode/Circuit.scala | 455 ------------------ .../scala-3/diode/EffectSetExecutionOps.scala | 5 +- .../src/main/scala-3/diode/ZoomTo.scala | 2 +- .../main/scala-3/diode/macros/GenLens.scala | 2 +- .../src/main/scala/diode/ActionHandler.scala | 2 +- .../{scala-2.13 => scala}/diode/Circuit.scala | 31 +- .../shared/src/main/scala/diode/Effect.scala | 24 +- .../shared/src/main/scala/diode/ModelRW.scala | 80 +-- .../src/main/scala/diode/util/Retry.scala | 20 +- .../src/test/scala/diode/CircuitTests.scala | 30 +- .../src/test/scala/diode/EffectTests.scala | 10 +- .../src/test/scala/diode/ModelRWTests.scala | 12 +- .../test/scala/diode/util/RetryTests.scala | 10 +- .../main/scala/diode/data/AsyncAction.scala | 32 +- .../src/main/scala/diode/data/Pot.scala | 146 +++--- .../src/main/scala/diode/data/PotAction.scala | 122 ++--- .../src/main/scala/diode/data/PotMap.scala | 10 +- .../src/main/scala/diode/data/PotStream.scala | 8 +- .../src/main/scala/diode/data/PotVector.scala | 14 +- .../src/main/scala/diode/data/RefTo.scala | 4 +- .../scala/diode/data/PotActionTests.scala | 6 +- .../scala/diode/data/PotCollectionTests.scala | 8 +- .../src/test/scala/diode/data/PotTests.scala | 4 +- .../test/scala/diode/data/RefToTests.scala | 10 +- .../js/src/main/scala/diode/dev/Hooks.scala | 6 +- .../main/scala/diode/dev/PersistState.scala | 2 +- .../scala/diode/react/ReactConnector.scala | 19 +- .../src/main/scala/diode/react/package.scala | 2 +- doc/usage/Handlers.md | 103 ++-- .../example/DirectoryTreeHandlerTests.scala | 2 +- project/plugins.sbt | 2 + 36 files changed, 405 insertions(+), 814 deletions(-) delete mode 100644 diode-core/shared/src/main/scala-3/diode/Circuit.scala rename diode-core/shared/src/main/{scala-2.13 => scala}/diode/Circuit.scala (94%) diff --git a/.scalafmt.conf b/.scalafmt.conf index da491fa6..c0e65534 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -version = 3.5.8 +version = 3.10.6 style = default align { preset = "more" diff --git a/build.sbt b/build.sbt index 3fc758bb..3420d26c 100644 --- a/build.sbt +++ b/build.sbt @@ -9,10 +9,12 @@ ThisBuild / scalafmtOnCompile := true Global / onChangedBuildSource := ReloadOnSourceChanges ThisBuild / scalaVersion := "2.13.17" -ThisBuild / crossScalaVersions := Seq("2.13.17", "3.8.1") +ThisBuild / crossScalaVersions := Seq("2.13.17", "3.7.4") val commonSettings = Seq( - scalacOptions := Seq( + semanticdbEnabled := true, + semanticdbVersion := scalafixSemanticdb.revision, + scalacOptions := Seq( "-deprecation", "-encoding", "UTF-8", @@ -22,6 +24,8 @@ val commonSettings = Seq( scalacOptions ++= scalaVerDependentSeq { case (2, _) => Seq( + "-Xsource:3", + "-Wconf:cat=scala3-migration:w", "-Xlint", "-Ywarn-dead-code", "-Ywarn-numeric-widen", @@ -29,6 +33,12 @@ val commonSettings = Seq( "-language:experimental.macros", "-language:existentials" ) + case (3, _) => + // Suppress warnings for cross-compiled code that can't use Scala 3 syntax + Seq( + "-Wconf:msg=Implicit parameters should be provided:s", + "-Wconf:msg=has been deprecated.*uninitialized:s" + ) }.value, scalacOptions ++= scalaVerDependentSeq { case (2, 13) => Seq("-Werror") @@ -55,11 +65,11 @@ inThisBuild( homepage := Some(url("https://github.com/suzaku-io/diode")), licenses := List("Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0")), sonatypeProfileName := "io.suzaku", - developers := List( + developers := List( Developer("ochrons", "Otto Chrons", "", url("https://github.com/ochrons")) ), organization := "io.suzaku", - scmInfo := Some( + scmInfo := Some( ScmInfo( url("https://github.com/suzaku-io/diode"), "scm:git:git@github.com:suzaku-io/diode.git", @@ -147,7 +157,7 @@ lazy val diodeReact: Project = project .settings( name := "diode-react", libraryDependencies ++= Seq( - "com.github.japgolly.scalajs-react" %%% "core" % "3.0.0" + "com.github.japgolly.scalajs-react" %%% "core" % "2.1.1" ) ) .dependsOn(diode.js) diff --git a/diode-core/js/src/main/scala/diode/util/RunAfterJS.scala b/diode-core/js/src/main/scala/diode/util/RunAfterJS.scala index 45cdc99b..e6b31756 100644 --- a/diode-core/js/src/main/scala/diode/util/RunAfterJS.scala +++ b/diode-core/js/src/main/scala/diode/util/RunAfterJS.scala @@ -5,7 +5,7 @@ import scala.concurrent.duration.FiniteDuration import scala.scalajs.js.timers._ class RunAfterJS extends RunAfter { - override def runAfter[A](delay: FiniteDuration)(f: => A) = { + override def runAfter[A](delay: FiniteDuration)(f: => A): Future[A] = { val p = Promise[A]() setTimeout(delay)(p.success(f)) p.future diff --git a/diode-core/jvm/src/main/scala/diode/util/RunAfterJVM.scala b/diode-core/jvm/src/main/scala/diode/util/RunAfterJVM.scala index 58b0b682..9b8d6e8b 100644 --- a/diode-core/jvm/src/main/scala/diode/util/RunAfterJVM.scala +++ b/diode-core/jvm/src/main/scala/diode/util/RunAfterJVM.scala @@ -6,8 +6,8 @@ import scala.concurrent._ import scala.concurrent.duration.FiniteDuration class RunAfterJVM extends RunAfter { - override def runAfter[A](delay: FiniteDuration)(f: => A) = { - val p = Promise[A]() + override def runAfter[A](delay: FiniteDuration)(f: => A): Future[A] = { + val p = Promise[A]() val task = new Runnable { def run() = p.success(f) } diff --git a/diode-core/jvm/src/test/scala/diode/CircuitJVMTests.scala b/diode-core/jvm/src/test/scala/diode/CircuitJVMTests.scala index b1236183..ec29409d 100644 --- a/diode-core/jvm/src/test/scala/diode/CircuitJVMTests.scala +++ b/diode-core/jvm/src/test/scala/diode/CircuitJVMTests.scala @@ -23,7 +23,7 @@ object CircuitJVMTests extends TestSuite { class TestCircuit extends Circuit[Model] { import diode.ActionResult._ - override def initialModel = Model(Vector.empty) + override def initialModel = Model(Vector.empty) override protected def actionHandler: HandlerFunction = (model, action) => ({ @@ -41,7 +41,7 @@ object CircuitJVMTests extends TestSuite { }: PartialFunction[Any, ActionResult[Model]]).lift.apply(action) } - def tests = TestSuite { + def tests = Tests { def circuit = new TestCircuit "ParallelActions" - { @@ -97,14 +97,14 @@ object CircuitJVMTests extends TestSuite { "SequenceActions" - { val c = circuit val actions = for (i <- 0 until 1000) yield Append(i) - c.dispatch(ActionBatch(actions: _*)) + c.dispatch(ActionBatch(actions*)) assert(c.model.list.size == 1000) assert(c.model.list == Vector.range(0, 1000)) } "SequenceActionEffects" - { val c = circuit val actions = for (i <- 0 until 1000) yield RunEffects(Seq(() => Future(Append(i)))) - c.dispatch(ActionBatch(actions: _*)) + c.dispatch(ActionBatch(actions*)) // wait for futures to complete Thread.sleep(300) assert(c.model.list.size == 1000) diff --git a/diode-core/shared/src/main/scala-3/diode/Circuit.scala b/diode-core/shared/src/main/scala-3/diode/Circuit.scala deleted file mode 100644 index 1a9ea2de..00000000 --- a/diode-core/shared/src/main/scala-3/diode/Circuit.scala +++ /dev/null @@ -1,455 +0,0 @@ -package diode - -import scala.annotation.implicitNotFound -import scala.collection.immutable.Queue - -/** - * The `ActionType` type class is used to verify that only valid actions are dispatched. An implicit instance of - * `ActionType[A]` must be in scope when calling dispatch methods or creating effects that return actions. - * - * `ActionType` is contravariant, which means it's enough to have an instance of `ActionType` for a common supertype to be - * able to dispatch actions of its subtypes. For example providing an instance of `ActionType[Action]` allows dispatching - * any class that is a subtype of `Action`. - * - * @tparam A - * Action type - */ -@implicitNotFound( - msg = - "Cannot find an ActionType type class for action of type ${A}. Make sure to provide an implicit ActionType for dispatched actions." -) -trait ActionType[-A] - -trait Dispatcher { - def dispatch[A: ActionType](action: A): Unit - - def apply[A: ActionType](action: A) = dispatch(action) -} - -/** - * Base trait for actions. Use this as a basis for your action class hierarchy to get an automatic type class instance of - * `ActionType[Action]`. Note that this trait is just a helper, you don't need to use it for your actions as you can always - * define your own `ActionType` instances for your action types. - */ -trait Action - -object Action { - implicit object aType extends ActionType[Action] -} - -/** - * A batch of actions. These actions are dispatched in a batch, without calling listeners in-between the dispatches. - * - * @param actions - * Sequence of actions to dispatch - */ -class ActionBatch private (val actions: Seq[Any]) extends Action { - def :+[A: ActionType](action: A): ActionBatch = - new ActionBatch(actions :+ action) - - def +:[A: ActionType](action: A) = - new ActionBatch(action +: actions) - - def ++(batch: ActionBatch) = - new ActionBatch(actions ++ batch.actions) -} - -object ActionBatch { - def apply[A: ActionType](actions: A*): ActionBatch = new ActionBatch(actions) -} - -/** - * Use `NoAction` when you need to dispatch an action that does nothing - */ -case object NoAction extends Action - -trait ActionProcessor[M <: AnyRef] { - def process(dispatch: Dispatcher, action: Any, next: Any => ActionResult[M], currentModel: M): ActionResult[M] -} - -sealed trait ActionResult[+M] { - def newModelOpt: Option[M] = None - def effectOpt: Option[Effect] = None -} - -sealed trait ModelUpdated[+M] extends ActionResult[M] { - def newModel: M - override def newModelOpt: Option[M] = Some(newModel) -} - -sealed trait HasEffect[+M] extends ActionResult[M] { - def effect: Effect - override def effectOpt: Option[Effect] = Some(effect) -} - -sealed trait UpdateSilent - -object ActionResult { - - case object NoChange extends ActionResult[Nothing] - - final case class ModelUpdate[M](newModel: M) extends ModelUpdated[M] - - final case class ModelUpdateSilent[M](newModel: M) extends ModelUpdated[M] with UpdateSilent - - final case class EffectOnly(effect: Effect) extends ActionResult[Nothing] with HasEffect[Nothing] - - final case class ModelUpdateEffect[M](newModel: M, effect: Effect) extends ModelUpdated[M] with HasEffect[M] - - final case class ModelUpdateSilentEffect[M](newModel: M, effect: Effect) - extends ModelUpdated[M] - with HasEffect[M] - with UpdateSilent - - def apply[M](model: Option[M], effect: Option[Effect]): ActionResult[M] = (model, effect) match { - case (Some(m), Some(e)) => ModelUpdateEffect(m, e) - case (Some(m), None) => ModelUpdate(m) - case (None, Some(e)) => EffectOnly(e) - case _ => NoChange - } -} - -trait Circuit[M <: AnyRef] extends Dispatcher with ZoomTo[M, M] { - - type HandlerFunction = (M, Any) => Option[ActionResult[M]] - - private case class Subscription[T](listener: ModelRO[T] => Unit, cursor: ModelR[M, T], lastValue: T) { - def changed: Option[Subscription[T]] = { - if (cursor === lastValue) - None - else - Some(copy(lastValue = cursor.eval(model))) - } - - def call(): Unit = listener(cursor) - } - - private[diode] var model: M = initialModel - - /** - * Provides the initial value for the model - */ - protected def initialModel: M - - /** - * Handles all dispatched actions - * - * @return - */ - protected def actionHandler: HandlerFunction - - private val modelRW = new RootModelRW[M](model) - private var isDispatching = false - private var dispatchQueue = Queue.empty[Any] - private var listenerId = 0 - private var listeners = Map.empty[Int, Subscription[_]] - private var processors = List.empty[ActionProcessor[M]] - private var processChain = buildProcessChain - - private def buildProcessChain = { - // chain processing functions - processors.reverse.foldLeft(process _)((next, processor) => - (action: Any) => processor.process(this, action, next, model) - ) - } - - /** - * Zoom into the model using the `get` function - * - * @param get - * Function that returns the part of the model you are interested in - * @return - * A `ModelR[T]` giving you read-only access to part of the model - */ - def zoom[T](get: M => T)(implicit feq: FastEq[_ >: T]): ModelR[M, T] = - modelRW.zoom[T](get) - - def zoomMap[F[_], A, B](fa: M => F[A])(f: A => B)(implicit monad: Monad[F], feq: FastEq[_ >: B]): ModelR[M, F[B]] = - modelRW.zoomMap(fa)(f) - - def zoomFlatMap[F[_], A, B](fa: M => F[A])(f: A => F[B])(implicit monad: Monad[F], feq: FastEq[_ >: B]): ModelR[M, F[B]] = - modelRW.zoomFlatMap(fa)(f) - - /** - * Zoom into the model using `get` and `set` functions - * - * @param get - * Function that returns the part of the model you are interested in - * @param set - * Function that updates the part of the model you are interested in - * @return - * A `ModelRW[T]` giving you read/update access to part of the model - */ - def zoomRW[T](get: M => T)(set: (M, T) => M)(implicit feq: FastEq[_ >: T]): ModelRW[M, T] = modelRW.zoomRW(get)(set) - - def zoomMapRW[F[_], A, B](fa: M => F[A])(f: A => B)( - set: (M, F[B]) => M - )(implicit monad: Monad[F], feq: FastEq[_ >: B]): ModelRW[M, F[B]] = - modelRW.zoomMapRW(fa)(f)(set) - - def zoomFlatMapRW[F[_], A, B](fa: M => F[A])(f: A => F[B])( - set: (M, F[B]) => M - )(implicit monad: Monad[F], feq: FastEq[_ >: B]): ModelRW[M, F[B]] = - modelRW.zoomFlatMapRW(fa)(f)(set) - - /** - * Subscribes to listen to changes in the model. By providing a `cursor` you can limit what part of the model must change - * for your listener to be called. If omitted, all changes result in a call. - * - * @param cursor - * Model reader returning the part of the model you are interested in. - * @param listener - * Function to be called when model is updated. The listener function gets the model reader as a parameter. - * @return - * A function to unsubscribe your listener - */ - def subscribe[T](cursor: ModelR[M, T])(listener: ModelRO[T] => Unit): () => Unit = { - this.synchronized { - listenerId += 1 - val id = listenerId - listeners += id -> Subscription(listener, cursor, cursor.eval(model)) - () => this.synchronized(listeners -= id) - } - } - - /** - * Adds a new `ActionProcessor[M]` to the action processing chain. The processor is called for every dispatched action. - * - * @param processor - */ - def addProcessor(processor: ActionProcessor[M]): Unit = { - this.synchronized { - processors = processor :: processors - processChain = buildProcessChain - } - } - - /** - * Removes a previously added `ActionProcessor[M]` from the action processing chain. - * - * @param processor - */ - def removeProcessor(processor: ActionProcessor[M]): Unit = { - this.synchronized { - processors = processors.filterNot(_ == processor) - processChain = buildProcessChain - } - } - - /** - * Handle a fatal error. Override this function to do something with exceptions that occur while dispatching actions. - * - * @param action - * Action that caused the exception - * @param e - * Exception that was thrown - */ - def handleFatal(action: Any, e: Throwable): Unit = throw e - - /** - * Handle a non-fatal error, such as dispatching an action with no action handler. - * - * @param msg - * Error message - */ - def handleError(msg: String): Unit = throw new Exception(s"handleError called with: $msg") - - /** - * @param action - * the action that caused the effects - * @param error - * the Exception that was encountered while processing the effects - */ - def handleEffectProcessingError[A](action: A, error: Throwable): Unit = { - handleError(s"Error in processing effects for action $action: $error") - } - - /** - * Updates the model if it has changed (reference equality check) - */ - private def update(newModel: M) = { - if (newModel ne model) { - model = newModel - } - } - - /** - * The final action processor that does actual action handling. - * - * @param action - * Action to be handled - * @return - */ - private def process(action: Any): ActionResult[M] = - action match { - case b: ActionBatch => - // dispatch all actions in the sequence using internal dispatchBase to prevent - // additional calls to subscribed listeners - b.actions.foreach(a => dispatchBase(a)) - ActionResult.NoChange - case NoAction => - // ignore - ActionResult.NoChange - case _ => - actionHandler(model, action).getOrElse { - handleError(s"Action $action was not handled by any action handler") - ActionResult.NoChange - } - } - - /** - * Composes multiple handlers into a single handler. Processing stops as soon as a handler is able to handle the action. - * If none of them handle the action, `None` is returned - */ - def composeHandlers(handlers: HandlerFunction*): HandlerFunction = - (model, action) => { - handlers.foldLeft(Option.empty[ActionResult[M]]) { (a, b) => - a.orElse(b(model, action)) - } - } - - @deprecated("Use composeHandlers or foldHandlers instead", "0.5.1") - def combineHandlers(handlers: HandlerFunction*): HandlerFunction = composeHandlers(handlers: _*) - - /** - * Folds multiple handlers into a single function so that each handler is called in turn and an updated model is passed on - * to the next handler. Returned `ActionResult` contains the final model and combined effects. - */ - def foldHandlers(handlers: HandlerFunction*): HandlerFunction = - (initialModel, action) => { - handlers - .foldLeft((initialModel, Option.empty[ActionResult[M]])) { - case ((currentModel, currentResult), handler) => - handler(currentModel, action) match { - case None => - (currentModel, currentResult) - case Some(result) => - val (nextModel, nextResult) = currentResult match { - case Some(cr) => - val newEffect = (cr.effectOpt, result.effectOpt) match { - case (Some(e1), Some(e2)) => Some(e1 + e2) - case (Some(e1), None) => Some(e1) - case (None, Some(e2)) => Some(e2) - case (None, None) => None - } - val newModel = result.newModelOpt.orElse(cr.newModelOpt) - (newModel.getOrElse(currentModel), ActionResult(newModel, newEffect)) - case None => - (result.newModelOpt.getOrElse(currentModel), result) - } - (nextModel, Some(nextResult)) - } - } - ._2 - } - - /** - * Dispatch the action, call change listeners when completed - * - * @param action - * Action to dispatch - */ - def dispatch[A: ActionType](action: A): Unit = { - this.synchronized { - if (!isDispatching) { - try { - isDispatching = true - val oldModel = model - val silent = dispatchBase(action) - if (oldModel ne model) { - // walk through all listeners and update subscriptions when model has changed - val updated = listeners.foldLeft(listeners) { - case (l, (key, sub)) => - if (listeners.isDefinedAt(key)) { - // Listener still exists - sub.changed match { - case Some(newSub) => - // value at the cursor has changed, call listener and update subscription - if (!silent) sub.call() - l.updated(key, newSub) - case None => l // nothing interesting happened - } - } else { - l // Listener was removed since we started - } - } - - // Listeners may have changed during processing (subscribe or unsubscribe) - // so only update the listeners that are still there, and leave any new listeners that may be there now. - listeners = updated.foldLeft(listeners) { - case (l, (key, sub)) => - if (l.isDefinedAt(key)) - l.updated(key, sub) // Listener still exists for this key - else - l // Listener was removed for this key, skip it - } - } - } catch { - case e: Throwable => - handleFatal(action, e) - } finally { - isDispatching = false - } - // if there is an item in the queue, dispatch it - dispatchQueue.dequeueOption foreach { - case (nextAction, queue) => - dispatchQueue = queue - dispatch(nextAction)(using null) - } - } else { - // add to the queue - dispatchQueue = dispatchQueue.enqueue(action) - } - } - } - - /** - * Perform actual dispatching, without calling change listeners - */ - protected def dispatchBase[A](action: A): Boolean = { - import AnyAction._ - try { - processChain(action) match { - case ActionResult.NoChange => - // no-op - false - case ActionResult.ModelUpdate(newModel) => - update(newModel) - false - case ActionResult.ModelUpdateSilent(newModel) => - update(newModel) - true - case ActionResult.EffectOnly(effects) => - // run effects - effects - .run(a => dispatch(a)) - .recover { - case e: Throwable => handleEffectProcessingError(action, e) - }(effects.ec) - true - case ActionResult.ModelUpdateEffect(newModel, effects) => - update(newModel) - // run effects - effects - .run(a => dispatch(a)) - .recover { - case e: Throwable => handleEffectProcessingError(action, e) - }(effects.ec) - false - case ActionResult.ModelUpdateSilentEffect(newModel, effects) => - update(newModel) - // run effects - effects - .run(a => dispatch(a)) - .recover { - case e: Throwable => handleEffectProcessingError(action, e) - }(effects.ec) - true - } - } catch { - case e: Throwable => - handleFatal(action, e) - true - } - } -} diff --git a/diode-core/shared/src/main/scala-3/diode/EffectSetExecutionOps.scala b/diode-core/shared/src/main/scala-3/diode/EffectSetExecutionOps.scala index 82f9c9c2..35158f28 100644 --- a/diode-core/shared/src/main/scala-3/diode/EffectSetExecutionOps.scala +++ b/diode-core/shared/src/main/scala-3/diode/EffectSetExecutionOps.scala @@ -5,8 +5,5 @@ import scala.collection.BuildFrom trait EffectSetExecutionOps { self: EffectSet => private[diode] def executeWith[A](f: Effect => Future[A]): Future[Set[A]] = - Future.traverse(tail + head)(f(_))( - summon[BuildFrom[Set[Effect], A, Set[A]]], - (EffectSetExecutionOps.this: diode.EffectSet).ec - ) + Future.traverse(tail + head)(f(_))(using summon[BuildFrom[Set[Effect], A, Set[A]]], self.ec) } diff --git a/diode-core/shared/src/main/scala-3/diode/ZoomTo.scala b/diode-core/shared/src/main/scala-3/diode/ZoomTo.scala index 8f3c031f..0c59fede 100644 --- a/diode-core/shared/src/main/scala-3/diode/ZoomTo.scala +++ b/diode-core/shared/src/main/scala-3/diode/ZoomTo.scala @@ -13,7 +13,7 @@ trait ZoomTo[M, S] { * @param set * Function to update the model with a new value */ - def zoomRW[T](get: S => T)(set: (S, T) => S)(implicit feq: FastEq[_ >: T]): ModelRW[M, T] + def zoomRW[T](get: S => T)(set: (S, T) => S)(implicit feq: FastEq[? >: T]): ModelRW[M, T] /** * An easier way to zoom into a RW model by just specifying a single chained accessor for the field. This works for cases diff --git a/diode-core/shared/src/main/scala-3/diode/macros/GenLens.scala b/diode-core/shared/src/main/scala-3/diode/macros/GenLens.scala index 875afdc9..87117983 100644 --- a/diode-core/shared/src/main/scala-3/diode/macros/GenLens.scala +++ b/diode-core/shared/src/main/scala-3/diode/macros/GenLens.scala @@ -59,7 +59,7 @@ private[diode] object GenLens { field.asTerm match { case Inlined(_, _, Block(List(DefDef(_, _, _, Some(fieldChain))), _)) => val setExpr = generateSetExpression(fieldChain) - '{ $zoomer.zoomRW($field)($setExpr)($feq) } + '{ $zoomer.zoomRW($field)($setExpr)(using $feq) } case _ => reportIllegalFieldReference() } diff --git a/diode-core/shared/src/main/scala/diode/ActionHandler.scala b/diode-core/shared/src/main/scala/diode/ActionHandler.scala index 5344a302..2820f82b 100644 --- a/diode-core/shared/src/main/scala/diode/ActionHandler.scala +++ b/diode-core/shared/src/main/scala/diode/ActionHandler.scala @@ -108,6 +108,6 @@ abstract class ActionHandler[M, T](val modelRW: ModelRW[M, T]) { } object ActionHandler { - implicit def extractHandler[M <: AnyRef](actionHandler: ActionHandler[M, _]): (M, Any) => Option[ActionResult[M]] = + implicit def extractHandler[M <: AnyRef](actionHandler: ActionHandler[M, ?]): (M, Any) => Option[ActionResult[M]] = actionHandler.handleAction } diff --git a/diode-core/shared/src/main/scala-2.13/diode/Circuit.scala b/diode-core/shared/src/main/scala/diode/Circuit.scala similarity index 94% rename from diode-core/shared/src/main/scala-2.13/diode/Circuit.scala rename to diode-core/shared/src/main/scala/diode/Circuit.scala index 724cb018..4aab0d29 100644 --- a/diode-core/shared/src/main/scala-2.13/diode/Circuit.scala +++ b/diode-core/shared/src/main/scala/diode/Circuit.scala @@ -23,7 +23,7 @@ trait ActionType[-A] trait Dispatcher { def dispatch[A: ActionType](action: A): Unit - def apply[A: ActionType](action: A) = dispatch(action) + def apply[A: ActionType](action: A): Unit = dispatch(action) } /** @@ -142,13 +142,13 @@ trait Circuit[M <: AnyRef] extends Dispatcher with ZoomTo[M, M] { private var isDispatching = false private var dispatchQueue = Queue.empty[Any] private var listenerId = 0 - private var listeners = Map.empty[Int, Subscription[_]] + private var listeners = Map.empty[Int, Subscription[?]] private var processors = List.empty[ActionProcessor[M]] private var processChain = buildProcessChain private def buildProcessChain = { // chain processing functions - processors.reverse.foldLeft(process _)((next, processor) => + processors.reverse.foldLeft(process(_))((next, processor) => (action: Any) => processor.process(this, action, next, model) ) } @@ -161,13 +161,13 @@ trait Circuit[M <: AnyRef] extends Dispatcher with ZoomTo[M, M] { * @return * A `ModelR[T]` giving you read-only access to part of the model */ - def zoom[T](get: M => T)(implicit feq: FastEq[_ >: T]): ModelR[M, T] = + def zoom[T](get: M => T)(implicit feq: FastEq[? >: T]): ModelR[M, T] = modelRW.zoom[T](get) - def zoomMap[F[_], A, B](fa: M => F[A])(f: A => B)(implicit monad: Monad[F], feq: FastEq[_ >: B]): ModelR[M, F[B]] = + def zoomMap[F[_], A, B](fa: M => F[A])(f: A => B)(implicit monad: Monad[F], feq: FastEq[? >: B]): ModelR[M, F[B]] = modelRW.zoomMap(fa)(f) - def zoomFlatMap[F[_], A, B](fa: M => F[A])(f: A => F[B])(implicit monad: Monad[F], feq: FastEq[_ >: B]): ModelR[M, F[B]] = + def zoomFlatMap[F[_], A, B](fa: M => F[A])(f: A => F[B])(implicit monad: Monad[F], feq: FastEq[? >: B]): ModelR[M, F[B]] = modelRW.zoomFlatMap(fa)(f) /** @@ -180,16 +180,16 @@ trait Circuit[M <: AnyRef] extends Dispatcher with ZoomTo[M, M] { * @return * A `ModelRW[T]` giving you read/update access to part of the model */ - def zoomRW[T](get: M => T)(set: (M, T) => M)(implicit feq: FastEq[_ >: T]): ModelRW[M, T] = modelRW.zoomRW(get)(set) + def zoomRW[T](get: M => T)(set: (M, T) => M)(implicit feq: FastEq[? >: T]): ModelRW[M, T] = modelRW.zoomRW(get)(set) def zoomMapRW[F[_], A, B](fa: M => F[A])(f: A => B)( set: (M, F[B]) => M - )(implicit monad: Monad[F], feq: FastEq[_ >: B]): ModelRW[M, F[B]] = + )(implicit monad: Monad[F], feq: FastEq[? >: B]): ModelRW[M, F[B]] = modelRW.zoomMapRW(fa)(f)(set) def zoomFlatMapRW[F[_], A, B](fa: M => F[A])(f: A => F[B])( set: (M, F[B]) => M - )(implicit monad: Monad[F], feq: FastEq[_ >: B]): ModelRW[M, F[B]] = + )(implicit monad: Monad[F], feq: FastEq[? >: B]): ModelRW[M, F[B]] = modelRW.zoomFlatMapRW(fa)(f)(set) /** @@ -309,7 +309,7 @@ trait Circuit[M <: AnyRef] extends Dispatcher with ZoomTo[M, M] { } @deprecated("Use composeHandlers or foldHandlers instead", "0.5.1") - def combineHandlers(handlers: HandlerFunction*): HandlerFunction = composeHandlers(handlers: _*) + def combineHandlers(handlers: HandlerFunction*): HandlerFunction = composeHandlers(handlers*) /** * Folds multiple handlers into a single function so that each handler is called in turn and an updated model is passed on @@ -391,10 +391,11 @@ trait Circuit[M <: AnyRef] extends Dispatcher with ZoomTo[M, M] { isDispatching = false } // if there is an item in the queue, dispatch it + import AnyAction._ dispatchQueue.dequeueOption foreach { case (nextAction, queue) => dispatchQueue = queue - dispatch(nextAction)(null) + dispatch(nextAction) } } else { // add to the queue @@ -414,10 +415,10 @@ trait Circuit[M <: AnyRef] extends Dispatcher with ZoomTo[M, M] { // no-op false case ActionResult.ModelUpdate(newModel) => - update(newModel) + update(newModel.asInstanceOf[M]) false case ActionResult.ModelUpdateSilent(newModel) => - update(newModel) + update(newModel.asInstanceOf[M]) true case ActionResult.EffectOnly(effects) => // run effects @@ -428,7 +429,7 @@ trait Circuit[M <: AnyRef] extends Dispatcher with ZoomTo[M, M] { }(effects.ec) true case ActionResult.ModelUpdateEffect(newModel, effects) => - update(newModel) + update(newModel.asInstanceOf[M]) // run effects effects .run(a => dispatch(a)) @@ -437,7 +438,7 @@ trait Circuit[M <: AnyRef] extends Dispatcher with ZoomTo[M, M] { }(effects.ec) false case ActionResult.ModelUpdateSilentEffect(newModel, effects) => - update(newModel) + update(newModel.asInstanceOf[M]) // run effects effects .run(a => dispatch(a)) diff --git a/diode-core/shared/src/main/scala/diode/Effect.scala b/diode-core/shared/src/main/scala/diode/Effect.scala index 389707c5..45d1cb58 100644 --- a/diode-core/shared/src/main/scala/diode/Effect.scala +++ b/diode-core/shared/src/main/scala/diode/Effect.scala @@ -2,7 +2,7 @@ package diode import diode.util.RunAfter -import scala.concurrent._ +import scala.concurrent.* import scala.concurrent.duration.FiniteDuration import scala.language.implicitConversions @@ -99,7 +99,7 @@ abstract class EffectBase(val ec: ExecutionContext) extends Effect { self => * The effect function, returning a `Future[A]` */ class EffectSingle[A] private[diode] (f: () => Future[A], ec: ExecutionContext) extends EffectBase(ec) { - override def run(dispatch: Any => Unit) = f().map(dispatch)(ec) + override def run(dispatch: Any => Unit): Future[Unit] = f().map(dispatch)(ec) override def toFuture: Future[A] = f() } @@ -119,7 +119,7 @@ class EffectSeq(head: Effect, tail: Seq[Effect], ec: ExecutionContext) extends E prev.flatMap(_ => f(effect))(ec) } - override def run(dispatch: Any => Unit) = + override def run(dispatch: Any => Unit): Future[Unit] = executeWith(_.run(dispatch)) override def >>(that: Effect) = @@ -128,16 +128,16 @@ class EffectSeq(head: Effect, tail: Seq[Effect], ec: ExecutionContext) extends E override def <<(that: Effect) = new EffectSeq(that, head +: tail, ec) - override def size = + override def size: Int = head.size + tail.foldLeft(0)((acc, e) => acc + e.size) - override def toFuture = + override def toFuture: Future[Any] = executeWith(_.toFuture) - override def map[B: ActionType](g: Any => B) = + override def map[B: ActionType](g: Any => B): EffectSeq = new EffectSeq(head.map(g), tail.map(_.map(g)), ec) - override def flatMap[B: ActionType](g: Any => Future[B]) = + override def flatMap[B: ActionType](g: Any => Future[B]): EffectSeq = new EffectSeq(head.flatMap(g), tail.map(_.flatMap(g)), ec) } @@ -153,22 +153,22 @@ class EffectSet(val head: Effect, val tail: Set[Effect], ec: ExecutionContext) extends EffectBase(ec) with EffectSetExecutionOps { - override def run(dispatch: Any => Unit) = + override def run(dispatch: Any => Unit): Future[Unit] = executeWith(_.run(dispatch)).map(_ => ())(ec) override def +(that: Effect) = new EffectSet(head, tail + that, ec) - override def size = + override def size: Int = head.size + tail.foldLeft(0)((acc, e) => acc + e.size) - override def toFuture = + override def toFuture: Future[Set[Any]] = executeWith(_.toFuture) - override def map[B: ActionType](g: Any => B) = + override def map[B: ActionType](g: Any => B): EffectSet = new EffectSet(head.map(g), tail.map(_.map(g)), ec) - override def flatMap[B: ActionType](g: Any => Future[B]) = + override def flatMap[B: ActionType](g: Any => Future[B]): EffectSet = new EffectSet(head.flatMap(g), tail.map(_.flatMap(g)), ec) } diff --git a/diode-core/shared/src/main/scala/diode/ModelRW.scala b/diode-core/shared/src/main/scala/diode/ModelRW.scala index e59e0722..fac3e3c3 100644 --- a/diode-core/shared/src/main/scala/diode/ModelRW.scala +++ b/diode-core/shared/src/main/scala/diode/ModelRW.scala @@ -41,7 +41,7 @@ trait ModelRO[S] { * @param get * Function to go from current reader to a new value */ - def zoom[T](get: S => T)(implicit feq: FastEq[_ >: T]): NewR[T] + def zoom[T](get: S => T)(implicit feq: FastEq[? >: T]): NewR[T] /** * Maps over current reader into a new value provided by `f`. Reader type `S` must be of type `F[A]`, for example @@ -50,7 +50,7 @@ trait ModelRO[S] { * @param f * The function to apply */ - def map[F[_], A, B](f: A => B)(implicit ev: S =:= F[A], monad: Monad[F], feq: FastEq[_ >: B]): NewR[F[B]] = + def map[F[_], A, B](f: A => B)(implicit ev: S =:= F[A], monad: Monad[F], feq: FastEq[? >: B]): NewR[F[B]] = zoomMap((_: S) => ev(value))(f) /** @@ -60,7 +60,7 @@ trait ModelRO[S] { * @param f * The function to apply, must return a value of type `F[B]` */ - def flatMap[F[_], A, B](f: A => F[B])(implicit ev: S =:= F[A], monad: Monad[F], feq: FastEq[_ >: B]): NewR[F[B]] = + def flatMap[F[_], A, B](f: A => F[B])(implicit ev: S =:= F[A], monad: Monad[F], feq: FastEq[? >: B]): NewR[F[B]] = zoomFlatMap((_: S) => ev(value))(f) /** @@ -71,7 +71,7 @@ trait ModelRO[S] { * @param f * The function to apply */ - def zoomMap[F[_], A, B](fa: S => F[A])(f: A => B)(implicit monad: Monad[F], feq: FastEq[_ >: B]): NewR[F[B]] + def zoomMap[F[_], A, B](fa: S => F[A])(f: A => B)(implicit monad: Monad[F], feq: FastEq[? >: B]): NewR[F[B]] /** * Zooms into the model and flatMaps over the zoomed value, which must be of type `F[A]` @@ -81,7 +81,7 @@ trait ModelRO[S] { * @param f * The function to apply, must return a value of type `F[B]` */ - def zoomFlatMap[F[_], A, B](fa: S => F[A])(f: A => F[B])(implicit monad: Monad[F], feq: FastEq[_ >: B]): NewR[F[B]] + def zoomFlatMap[F[_], A, B](fa: S => F[A])(f: A => F[B])(implicit monad: Monad[F], feq: FastEq[? >: B]): NewR[F[B]] } /** @@ -112,7 +112,7 @@ trait ModelR[M, S] extends ModelRO[S] { * @param that * The other reader */ - def zip[SS](that: ModelR[M, SS])(implicit feqS: FastEq[_ >: S], feqSS: FastEq[_ >: SS]): ModelR[M, (S, SS)] + def zip[SS](that: ModelR[M, SS])(implicit feqS: FastEq[? >: S], feqSS: FastEq[? >: SS]): ModelR[M, (S, SS)] } /** @@ -144,7 +144,7 @@ trait ModelRW[M, S] extends ModelR[M, S] with ZoomTo[M, S] { * @param set * Function to update the model with a new value */ - def zoomRW[T](get: S => T)(set: (S, T) => S)(implicit feq: FastEq[_ >: T]): ModelRW[M, T] + def zoomRW[T](get: S => T)(set: (S, T) => S)(implicit feq: FastEq[? >: T]): ModelRW[M, T] /** * Zooms into the model and maps over the zoomed value, which must be of type `F[A]`. The `set` function is used to update @@ -159,7 +159,7 @@ trait ModelRW[M, S] extends ModelR[M, S] with ZoomTo[M, S] { */ def zoomMapRW[F[_], A, B](fa: S => F[A])(f: A => B)( set: (S, F[B]) => S - )(implicit monad: Monad[F], feq: FastEq[_ >: B]): ModelRW[M, F[B]] + )(implicit monad: Monad[F], feq: FastEq[? >: B]): ModelRW[M, F[B]] /** * Zooms into the model and flatMaps over the zoomed value, which must be of type `F[A]`. The `set` function is used to @@ -174,7 +174,7 @@ trait ModelRW[M, S] extends ModelR[M, S] with ZoomTo[M, S] { */ def zoomFlatMapRW[F[_], A, B](fa: S => F[A])(f: A => F[B])( set: (S, F[B]) => S - )(implicit monad: Monad[F], feq: FastEq[_ >: B]): ModelRW[M, F[B]] + )(implicit monad: Monad[F], feq: FastEq[? >: B]): ModelRW[M, F[B]] } @@ -189,22 +189,22 @@ trait ModelRW[M, S] extends ModelR[M, S] with ZoomTo[M, S] { trait BaseModelR[M, S] extends ModelR[M, S] { override def eval(model: M): S - override def value = eval(root.value) + override def value: S = eval(root.value) - override def zoom[T](get: S => T)(implicit feq: FastEq[_ >: T]) = + override def zoom[T](get: S => T)(implicit feq: FastEq[? >: T]): ZoomModelR[M, T] = new ZoomModelR[M, T](root, get compose this.eval) - override def zip[SS](that: ModelR[M, SS])(implicit feqS: FastEq[_ >: S], feqSS: FastEq[_ >: SS]) = + override def zip[SS](that: ModelR[M, SS])(implicit feqS: FastEq[? >: S], feqSS: FastEq[? >: SS]): ZipModelR[M, S, SS] = new ZipModelR[M, S, SS](root, eval, that.eval) override def zoomMap[F[_], A, B](fa: S => F[A])( f: A => B - )(implicit monad: Monad[F], feq: FastEq[_ >: B]): ModelR[M, F[B]] = + )(implicit monad: Monad[F], feq: FastEq[? >: B]): ModelR[M, F[B]] = new MapModelR(root, fa compose eval, f) override def zoomFlatMap[F[_], A, B](fa: S => F[A])( f: A => F[B] - )(implicit monad: Monad[F], feq: FastEq[_ >: B]): ModelR[M, F[B]] = + )(implicit monad: Monad[F], feq: FastEq[? >: B]): ModelR[M, F[B]] = new FlatMapModelR(root, fa compose eval, f) } @@ -212,11 +212,11 @@ trait BaseModelR[M, S] extends ModelR[M, S] { * Model reader for the root value */ class RootModelR[M <: AnyRef](get: => M) extends BaseModelR[M, M] { - def root = this + def root: RootModelR[M] = this - override def eval(model: M) = get + override def eval(model: M): M = get - override def value = get + override def value: M = get override def ===(that: M): Boolean = this eq that } @@ -224,15 +224,15 @@ class RootModelR[M <: AnyRef](get: => M) extends BaseModelR[M, M] { /** * Model reader for a zoomed value */ -class ZoomModelR[M, S](val root: ModelR[M, M], get: M => S)(implicit feq: FastEq[_ >: S]) extends BaseModelR[M, S] { - override def eval(model: M) = get(model) +class ZoomModelR[M, S](val root: ModelR[M, M], get: M => S)(implicit feq: FastEq[? >: S]) extends BaseModelR[M, S] { + override def eval(model: M): S = get(model) override def ===(that: S): Boolean = feq.eqv(value, that) } trait MappedModelR[F[_], M, B] { self: ModelR[M, F[B]] => protected def monad: Monad[F] - protected def feq: FastEq[_ >: B] + protected def feq: FastEq[? >: B] protected def mapValue: F[B] private var memoized = mapValue @@ -246,7 +246,7 @@ trait MappedModelR[F[_], M, B] { self: ModelR[M, F[B]] => memoized } - override def ===(that: F[B]) = monad.isEqual(value, that)(feq.eqv) + override def ===(that: F[B]): Boolean = monad.isEqual(value, that)(feq.eqv) } /** @@ -254,11 +254,11 @@ trait MappedModelR[F[_], M, B] { self: ModelR[M, F[B]] => */ class MapModelR[F[_], M, A, B](val root: ModelR[M, M], get: M => F[A], f: A => B)(implicit val monad: Monad[F], - val feq: FastEq[_ >: B] + val feq: FastEq[? >: B] ) extends BaseModelR[M, F[B]] with MappedModelR[F, M, B] { - override protected def mapValue = monad.map(get(root.value))(f) + override protected def mapValue: F[B] = monad.map(get(root.value))(f) } /** @@ -266,25 +266,25 @@ class MapModelR[F[_], M, A, B](val root: ModelR[M, M], get: M => F[A], f: A => B */ class FlatMapModelR[F[_], M, A, B](val root: ModelR[M, M], get: M => F[A], f: A => F[B])(implicit val monad: Monad[F], - val feq: FastEq[_ >: B] + val feq: FastEq[? >: B] ) extends BaseModelR[M, F[B]] with MappedModelR[F, M, B] { - override protected def mapValue = monad.flatMap(get(root.value))(f) + override protected def mapValue: F[B] = monad.flatMap(get(root.value))(f) } /** * Model reader for two zipped readers */ class ZipModelR[M, S, SS](val root: ModelR[M, M], get1: M => S, get2: M => SS)(implicit - feqS: FastEq[_ >: S], - feqSS: FastEq[_ >: SS] + feqS: FastEq[? >: S], + feqSS: FastEq[? >: SS] ) extends BaseModelR[M, (S, SS)] { // initial value for zipped private var zipped = (get1(root.value), get2(root.value)) // ZipModel uses optimized `get` functions to check if the contents of the tuple has changed or not - override def eval(model: M) = { + override def eval(model: M): (S, SS) = { // check if inner references have changed val v1 = get1(root.value) val v2 = get2(root.value) @@ -295,7 +295,7 @@ class ZipModelR[M, S, SS](val root: ModelR[M, M], get1: M => S, get2: M => SS)(i zipped } - override def ===(that: (S, SS)) = { + override def ===(that: (S, SS)): Boolean = { // using fast eq is required to support zip in subscribe feqS.eqv(get1(root.value), that._1) && feqSS.eqv(get2(root.value), that._2) } @@ -310,20 +310,20 @@ class ZipModelR[M, S, SS](val root: ModelR[M, M], get1: M => S, get2: M => SS)(i * Type of the reader/writer value */ trait BaseModelRW[M, S] extends ModelRW[M, S] with BaseModelR[M, S] { - override def zoomRW[U](get: S => U)(set: (S, U) => S)(implicit feq: FastEq[_ >: U]) = + override def zoomRW[U](get: S => U)(set: (S, U) => S)(implicit feq: FastEq[? >: U]): ZoomModelRW[M, U] = new ZoomModelRW[M, U](root, get compose eval, (s, u) => updatedWith(s, set(eval(s), u))) override def zoomMapRW[F[_], A, B](fa: S => F[A])(f: A => B)( set: (S, F[B]) => S - )(implicit monad: Monad[F], feq: FastEq[_ >: B]) = + )(implicit monad: Monad[F], feq: FastEq[? >: B]): MapModelRW[F, M, A, B] = new MapModelRW(root, fa compose eval, f)((s, u) => updatedWith(s, set(eval(s), u))) override def zoomFlatMapRW[F[_], A, B](fa: S => F[A])(f: A => F[B])( set: (S, F[B]) => S - )(implicit monad: Monad[F], feq: FastEq[_ >: B]) = + )(implicit monad: Monad[F], feq: FastEq[? >: B]): FlatMapModelRW[F, M, A, B] = new FlatMapModelRW(root, fa compose eval, f)((s, u) => updatedWith(s, set(eval(s), u))) - override def updated(newValue: S) = updatedWith(root.value, newValue) + override def updated(newValue: S): M = updatedWith(root.value, newValue) } /** @@ -333,17 +333,17 @@ class RootModelRW[M <: AnyRef](get: => M) extends RootModelR(get) with BaseModel override def updatedWith(model: M, value: M) = value // override for root because it's a simpler case - override def zoomRW[T](get: M => T)(set: (M, T) => M)(implicit feq: FastEq[_ >: T]) = + override def zoomRW[T](get: M => T)(set: (M, T) => M)(implicit feq: FastEq[? >: T]) = new ZoomModelRW[M, T](this, get, set) } /** * Model reader/writer for a zoomed value */ -class ZoomModelRW[M, S](root: ModelR[M, M], get: M => S, set: (M, S) => M)(implicit feq: FastEq[_ >: S]) +class ZoomModelRW[M, S](root: ModelR[M, M], get: M => S, set: (M, S) => M)(implicit feq: FastEq[? >: S]) extends ZoomModelR(root, get) with BaseModelRW[M, S] { - override def updatedWith(model: M, value: S) = set(model, value) + override def updatedWith(model: M, value: S): M = set(model, value) } /** @@ -351,10 +351,10 @@ class ZoomModelRW[M, S](root: ModelR[M, M], get: M => S, set: (M, S) => M)(impli */ class MapModelRW[F[_], M, A, B](root: ModelR[M, M], get: M => F[A], f: A => B)(set: (M, F[B]) => M)(implicit monad: Monad[F], - feq: FastEq[_ >: B] + feq: FastEq[? >: B] ) extends MapModelR(root, get, f) with BaseModelRW[M, F[B]] { - override def updatedWith(model: M, value: F[B]) = set(model, value) + override def updatedWith(model: M, value: F[B]): M = set(model, value) } /** @@ -362,8 +362,8 @@ class MapModelRW[F[_], M, A, B](root: ModelR[M, M], get: M => F[A], f: A => B)(s */ class FlatMapModelRW[F[_], M, A, B](root: ModelR[M, M], get: M => F[A], f: A => F[B])(set: (M, F[B]) => M)(implicit monad: Monad[F], - feq: FastEq[_ >: B] + feq: FastEq[? >: B] ) extends FlatMapModelR(root, get, f) with BaseModelRW[M, F[B]] { - override def updatedWith(model: M, value: F[B]) = set(model, value) + override def updatedWith(model: M, value: F[B]): M = set(model, value) } diff --git a/diode-core/shared/src/main/scala/diode/util/Retry.scala b/diode-core/shared/src/main/scala/diode/util/Retry.scala index bfd4f171..3cfffb20 100644 --- a/diode-core/shared/src/main/scala/diode/util/Retry.scala +++ b/diode-core/shared/src/main/scala/diode/util/Retry.scala @@ -41,7 +41,7 @@ object Retry { case object None extends RetryPolicy { override def canRetry(reason: Throwable) = false - override def retry[T <: AnyRef](reason: Throwable, effectProvider: RetryPolicy => Effect) = + override def retry[T <: AnyRef](reason: Throwable, effectProvider: RetryPolicy => Effect): Left[Throwable, Nothing] = Left(reason) } @@ -56,10 +56,12 @@ object Retry { * A filter to check if the cause of failure should prevent retrying. */ case class Immediate(retriesLeft: Int, filter: Throwable => Boolean = always) extends RetryPolicy { - override def canRetry(reason: Throwable) = + override def canRetry(reason: Throwable): Boolean = retriesLeft > 0 && filter(reason) - override def retry[T <: AnyRef](reason: Throwable, effectProvider: RetryPolicy => Effect) = { + override def retry[T <: AnyRef](reason: Throwable, + effectProvider: RetryPolicy => Effect + ): Either[Throwable, (RetryPolicy, Effect)] = { if (canRetry(reason)) { val nextPolicy = Immediate(retriesLeft - 1, filter) Right((nextPolicy, effectProvider(nextPolicy))) @@ -88,10 +90,12 @@ object Retry { filter: Throwable => Boolean = always )(implicit runner: RunAfter, ec: ExecutionContext) extends RetryPolicy { - override def canRetry(reason: Throwable) = + override def canRetry(reason: Throwable): Boolean = retriesLeft > 0 && filter(reason) - override def retry[T <: AnyRef](reason: Throwable, effectProvider: RetryPolicy => Effect) = { + override def retry[T <: AnyRef](reason: Throwable, + effectProvider: RetryPolicy => Effect + ): Either[Throwable, (RetryPolicy, Effect)] = { if (canRetry(reason)) { // calculate next delay time val nextDelay = (delay.toUnit(TimeUnit.MILLISECONDS) * exp).millis @@ -103,12 +107,12 @@ object Retry { } } - def apply(retries: Int) = + def apply(retries: Int): Immediate = Immediate(retries) - def apply(retries: Int, delay: FiniteDuration)(implicit runner: RunAfter, ec: ExecutionContext) = + def apply(retries: Int, delay: FiniteDuration)(implicit runner: RunAfter, ec: ExecutionContext): Backoff = Backoff(retries, delay) - def apply(retries: Int, delay: FiniteDuration, exp: Double)(implicit runner: RunAfter, ec: ExecutionContext) = + def apply(retries: Int, delay: FiniteDuration, exp: Double)(implicit runner: RunAfter, ec: ExecutionContext): Backoff = Backoff(retries, delay, exp) } diff --git a/diode-core/shared/src/test/scala/diode/CircuitTests.scala b/diode-core/shared/src/test/scala/diode/CircuitTests.scala index 6869559b..a0fc6857 100644 --- a/diode-core/shared/src/test/scala/diode/CircuitTests.scala +++ b/diode-core/shared/src/test/scala/diode/CircuitTests.scala @@ -28,7 +28,7 @@ object CircuitTests extends TestSuite { class AppCircuit(implicit val ec: ExecutionContext) extends Circuit[Model] { import diode.ActionResult._ - override def initialModel = Model("Testing", Data(42, true)) + override def initialModel = Model("Testing", Data(42, true)) override protected def actionHandler: HandlerFunction = (model, action) => ({ @@ -54,7 +54,7 @@ object CircuitTests extends TestSuite { override def handleError(msg: String): Unit = lastError = msg } - def tests = TestSuite { + def tests = Tests { implicit val ec = ExecutionContext.global def circuit = new AppCircuit @@ -120,9 +120,9 @@ object CircuitTests extends TestSuite { } "Listener" - { "Normal" - { - val c = circuit - var state: Model = null - var callbackCount = 0 + val c = circuit + var state: Model = null + var callbackCount = 0 def listener(cursor: ModelRO[String]): Unit = { state = c.model callbackCount += 1 @@ -140,10 +140,10 @@ object CircuitTests extends TestSuite { assert(state.s == "Listen4") } "Cursor" - { - val c = circuit - var state: Model = null - var state2: Model = null - var callbackCount = 0 + val c = circuit + var state: Model = null + var state2: Model = null + var callbackCount = 0 def listener1(cursor: ModelRO[Data]): Unit = { state = c.model callbackCount += 1 @@ -166,9 +166,9 @@ object CircuitTests extends TestSuite { assert(callbackCount == 2) } "Silent" - { - val c = circuit - var state: Model = null - var callbackCount = 0 + val c = circuit + var state: Model = null + var callbackCount = 0 def listener(cursor: ModelRO[String]): Unit = { state = c.model callbackCount += 1 @@ -293,7 +293,7 @@ object CircuitTests extends TestSuite { val zoomS = c.zoom(_.s) val zoomDataBool = c.zoom(_.data.b) - val zipped = zoomS zip zoomDataBool + val zipped = zoomS.zip(zoomDataBool) def listener(cursor: ModelRO[(String, Boolean)]): Unit = { state1 = cursor() @@ -391,7 +391,7 @@ object CircuitTests extends TestSuite { "LogState" - { val c = circuit var log = "log" - val p = new ActionProcessor[Model] { + val p = new ActionProcessor[Model] { override def process(dispatcher: Dispatcher, action: Any, next: Any => ActionResult[Model], @@ -443,7 +443,7 @@ object CircuitTests extends TestSuite { "FoldHandler" - { val c = circuit val origModel = c.model - val h1 = new ActionHandler[Model, Int](c.zoomRW(_.data.i)((m, t) => m.copy(data = m.data.copy(i = t)))) { + val h1 = new ActionHandler[Model, Int](c.zoomRW(_.data.i)((m, t) => m.copy(data = m.data.copy(i = t)))) { override protected def handle = { case SetS(newS) => updated(value + 1) diff --git a/diode-core/shared/src/test/scala/diode/EffectTests.scala b/diode-core/shared/src/test/scala/diode/EffectTests.scala index 0bca1219..b02c43f9 100644 --- a/diode-core/shared/src/test/scala/diode/EffectTests.scala +++ b/diode-core/shared/src/test/scala/diode/EffectTests.scala @@ -2,16 +2,16 @@ package diode import java.util.concurrent.atomic.AtomicInteger -import utest._ +import utest.* import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent._ -import scala.concurrent.duration._ +import scala.concurrent.* +import scala.concurrent.duration.* object EffectTests extends TestSuite { - import AnyAction._ + import AnyAction.* - def tests = TestSuite { + def tests = Tests { def efA = Effect(Future("A")) def efB = Effect(Future("B")) def efC = Effect(Future("C")) diff --git a/diode-core/shared/src/test/scala/diode/ModelRWTests.scala b/diode-core/shared/src/test/scala/diode/ModelRWTests.scala index 93aba1af..f4ec8c42 100644 --- a/diode-core/shared/src/test/scala/diode/ModelRWTests.scala +++ b/diode-core/shared/src/test/scala/diode/ModelRWTests.scala @@ -6,7 +6,7 @@ object ModelRWTests extends TestSuite { import ModelRWTestsModel._ - def tests = TestSuite { + def tests = Tests { "eq" - { var m = ComplexModel(A(999, "string"), B(Seq(1.0f, 2.0f, 3.0f), 3.0f), C(42, "c", Some(A(998, "A")))) val mr = new RootModelR(m) @@ -97,7 +97,7 @@ object ModelRWTests extends TestSuite { // normal zoomed values change reference on each call assert(rv1 ne r1.value) val rm1: ModelR[ModelOpt, Option[String]] = cr.map((c: C) => c.s) - val rmi1 = mr.zoomMap(_.c)(_.i) + val rmi1 = mr.zoomMap(_.c)(_.i) // mapped values maintain reference assert(rm1.value eq rm1.value) assert(rmi1.value eq rmi1.value) @@ -114,11 +114,11 @@ object ModelRWTests extends TestSuite { assert(rmv1 ne rm1.value) } "flatMap" - { - var m = ModelOpt(1.0f, Some(C(4242, "some", Some(A(66, "deep"))))) - val mr = new RootModelR(m) - val cr = mr.zoomFlatMap(_.c)(_.o) + var m = ModelOpt(1.0f, Some(C(4242, "some", Some(A(66, "deep"))))) + val mr = new RootModelR(m) + val cr = mr.zoomFlatMap(_.c)(_.o) val crs: ModelR[ModelOpt, Option[String]] = cr.map((a: A) => a.s) - val v1 = crs.value + val v1 = crs.value println(v1) assert(v1 eq crs.value) m = m.copy(c = m.c.map(c => c.copy(o = c.o.map(_.copy(i = 0))))) diff --git a/diode-core/shared/src/test/scala/diode/util/RetryTests.scala b/diode-core/shared/src/test/scala/diode/util/RetryTests.scala index 14f3ac15..5a3c41b1 100644 --- a/diode-core/shared/src/test/scala/diode/util/RetryTests.scala +++ b/diode-core/shared/src/test/scala/diode/util/RetryTests.scala @@ -1,17 +1,17 @@ package diode.util import diode.{AnyAction, Effect} -import utest._ +import utest.* import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent._ -import scala.concurrent.duration._ +import scala.concurrent.* +import scala.concurrent.duration.* import diode.Implicits.runAfterImpl object RetryTests extends TestSuite { - import AnyAction._ + import AnyAction.* - def tests = TestSuite { + def tests = Tests { "Immediate" - { val policy = Retry.Immediate(3) val effect = (retryPolicy: RetryPolicy) => Effect(Future("42")) diff --git a/diode-data/shared/src/main/scala/diode/data/AsyncAction.scala b/diode-data/shared/src/main/scala/diode/data/AsyncAction.scala index 5dabdb8c..1dea9e2b 100644 --- a/diode-data/shared/src/main/scala/diode/data/AsyncAction.scala +++ b/diode-data/shared/src/main/scala/diode/data/AsyncAction.scala @@ -50,7 +50,7 @@ trait AsyncAction[A, P <: AsyncAction[A, P]] extends Action { * @tparam M * @return */ - def handle[M](pf: PartialFunction[PotState, ActionResult[M]]) = + def handle[M](pf: PartialFunction[PotState, ActionResult[M]]): ActionResult[M] = pf(state) /** @@ -76,7 +76,7 @@ trait AsyncAction[A, P <: AsyncAction[A, P]] extends Action { * * @return */ - def pending = + def pending: P = next(PotState.PotPending, result) /** @@ -86,7 +86,7 @@ trait AsyncAction[A, P <: AsyncAction[A, P]] extends Action { * Result for the action. * @return */ - def ready(a: A) = + def ready(a: A): P = next(PotState.PotReady, Success(a)) /** @@ -96,7 +96,7 @@ trait AsyncAction[A, P <: AsyncAction[A, P]] extends Action { * Reason for the failure. * @return */ - def failed(ex: Throwable) = + def failed(ex: Throwable): P = next(PotState.PotFailed, Failure(ex)) /** @@ -113,7 +113,7 @@ trait AsyncAction[A, P <: AsyncAction[A, P]] extends Action { */ def effect[B](f: => Future[B])(success: B => A, failure: Throwable => Throwable = identity)(implicit ec: ExecutionContext - ) = + ): EffectSingle[P] = Effect(f.map(x => ready(success(x))).recover { case e: Throwable => failed(failure(e)) }) } @@ -163,7 +163,7 @@ trait AsyncActionRetriable[A, P <: AsyncActionRetriable[A, P]] extends AsyncActi * Updated retry policy * @return */ - def failed(ex: Throwable, nextRetryPolicy: RetryPolicy = retryPolicy) = + def failed(ex: Throwable, nextRetryPolicy: RetryPolicy = retryPolicy): P = next(PotState.PotFailed, Failure(ex), nextRetryPolicy) /** @@ -180,7 +180,9 @@ trait AsyncActionRetriable[A, P <: AsyncActionRetriable[A, P]] extends AsyncActi */ def effectWithRetry[B]( f: => Future[B] - )(success: B => A, failure: Throwable => Throwable = identity)(implicit ec: ExecutionContext) = + )(success: B => A, failure: Throwable => Throwable = identity)(implicit + ec: ExecutionContext + ): RetryPolicy => EffectSingle[P] = (nextRetryPolicy: RetryPolicy) => Effect(f.map(x => ready(success(x))).recover { case e: Throwable => failed(failure(e), nextRetryPolicy) }) } @@ -199,7 +201,9 @@ object AsyncAction { * @return * The handler function */ - def mapHandler[K, V, A <: Iterable[(K, Pot[V])], M, P <: AsyncAction[A, P]](keys: Set[K]) = { + def mapHandler[K, V, A <: Iterable[(K, Pot[V])], M, P <: AsyncAction[A, P]]( + keys: Set[K] + ): (AsyncAction[A, P], ActionHandler[M, PotMap[K, V]], Effect) => ActionResult[M] = { require(keys.nonEmpty, "AsyncAction:mapHandler - The set of keys to update can't be empty") (action: AsyncAction[A, P], handler: ActionHandler[M, PotMap[K, V]], updateEffect: Effect) => { import PotState._ @@ -238,7 +242,9 @@ object AsyncAction { * @return * The handler function */ - def vectorHandler[V, A <: Iterable[(Int, Pot[V])], M, P <: AsyncAction[A, P]](indices: Set[Int]) = { + def vectorHandler[V, A <: Iterable[(Int, Pot[V])], M, P <: AsyncAction[A, P]]( + indices: Set[Int] + ): (AsyncAction[A, P], ActionHandler[M, PotVector[V]], Effect) => ActionResult[M] = { require(indices.nonEmpty, "AsyncAction:vectorHandler - The set of indices to update can't be empty") (action: AsyncAction[A, P], handler: ActionHandler[M, PotVector[V]], updateEffect: Effect) => { import PotState._ @@ -286,7 +292,9 @@ object AsyncActionRetriable { * @return * The handler function */ - def mapHandler[K, V, A <: Iterable[(K, Pot[V])], M, P <: AsyncActionRetriable[A, P]](keys: Set[K]) = { + def mapHandler[K, V, A <: Iterable[(K, Pot[V])], M, P <: AsyncActionRetriable[A, P]]( + keys: Set[K] + ): (AsyncActionRetriable[A, P], ActionHandler[M, PotMap[K, V]], RetryPolicy => Effect) => ActionResult[M] = { require(keys.nonEmpty, "AsyncActionRetriable:mapHandler - The set of keys to update can't be empty") (action: AsyncActionRetriable[A, P], handler: ActionHandler[M, PotMap[K, V]], updateEffect: RetryPolicy => Effect) => { import PotState._ @@ -329,7 +337,9 @@ object AsyncActionRetriable { * @return * The handler function */ - def vectorHandler[V, A <: Iterable[(Int, Pot[V])], M, P <: AsyncActionRetriable[A, P]](indices: Set[Int]) = { + def vectorHandler[V, A <: Iterable[(Int, Pot[V])], M, P <: AsyncActionRetriable[A, P]]( + indices: Set[Int] + ): (AsyncActionRetriable[A, P], ActionHandler[M, PotVector[V]], RetryPolicy => Effect) => ActionResult[M] = { require(indices.nonEmpty, "AsyncActionRetriable:vectorHandler - The set of indices to update can't be empty") (action: AsyncActionRetriable[A, P], handler: ActionHandler[M, PotVector[V]], updateEffect: RetryPolicy => Effect) => { import PotState._ diff --git a/diode-data/shared/src/main/scala/diode/data/Pot.scala b/diode-data/shared/src/main/scala/diode/data/Pot.scala index b89018c4..54c9584c 100644 --- a/diode-data/shared/src/main/scala/diode/data/Pot.scala +++ b/diode-data/shared/src/main/scala/diode/data/Pot.scala @@ -40,7 +40,7 @@ sealed abstract class Pot[+A] extends Product with Serializable { self => def isPending: Boolean def isStale: Boolean def isFailed: Boolean - def isReady = !isEmpty && !isStale + def isReady: Boolean = !isEmpty && !isStale def isUnavailable: Boolean def ready[B >: A](value: B): Pot[B] = Ready(value) def pending(startTime: Long = Pot.currentTime): Pot[A] @@ -54,7 +54,7 @@ sealed abstract class Pot[+A] extends Product with Serializable { self => * @note * Implemented here to avoid the implicit conversion to Iterable. */ - final def nonEmpty = !isEmpty + final def nonEmpty: Boolean = !isEmpty @inline final def getOrElse[B >: A](default: => B): B = if (isEmpty) default else this.get @@ -110,9 +110,9 @@ sealed abstract class Pot[+A] extends Product with Serializable { self => @noinline def flatMap[B](f: A => Pot[B]): Pot[B] = map(f).flatten @noinline def flatten[B](implicit ev: A <:< Pot[B]): Pot[B] = this match { - case Empty => Empty - case Ready(x) => x - case Pending(t) => Pending(t) + case Empty => Empty + case Ready(x) => x + case Pending(t) => Pending(t) case PendingStale(x, t) => ev(x) match { case Empty => Pending(t) @@ -121,7 +121,7 @@ sealed abstract class Pot[+A] extends Product with Serializable { self => case PendingStale(y, s) => PendingStale(y, math.min(s, t)) case other => other } - case Failed(e) => Failed(e) + case Failed(e) => Failed(e) case FailedStale(x, e) => ev(x) match { case Empty => Failed(e) @@ -160,11 +160,11 @@ sealed abstract class Pot[+A] extends Product with Serializable { self => * to matter much in a collection with max size 1. */ class WithFilter(p: A => Boolean) { - def map[B](f: A => B): Pot[B] = self filter p map f + def map[B](f: A => B): Pot[B] = self `filter` p `map` f - def flatMap[B](f: A => Pot[B]): Pot[B] = self filter p flatMap f + def flatMap[B](f: A => Pot[B]): Pot[B] = self `filter` p `flatMap` f - def foreach[U](f: A => U): Unit = self filter p foreach f + def foreach[U](f: A => U): Unit = self `filter` p `foreach` f def withFilter(q: A => Boolean): WithFilter = new WithFilter(x => p(x) && q(x)) } @@ -267,7 +267,7 @@ sealed abstract class Pot[+A] extends Product with Serializable { self => */ def recover[B >: A](f: PartialFunction[Throwable, B]): Pot[B] = this - def exceptionOption = Option.empty[Throwable] + def exceptionOption: Option[Throwable] = None /** * Returns a singleton iterator returning the Pot's value if it is nonempty, or an empty iterator if the pot is empty. @@ -309,7 +309,7 @@ sealed abstract class Pot[+A] extends Product with Serializable { self => * @see * toLeft */ - @inline final def toRight[X](left: => X) = + @inline final def toRight[X](left: => X): Either[X, A] = if (isEmpty) Left(left) else Right(this.get) /** @@ -321,7 +321,7 @@ sealed abstract class Pot[+A] extends Product with Serializable { self => * @see * toRight */ - @inline final def toLeft[X](right: => X) = + @inline final def toLeft[X](right: => X): Either[A, X] = if (isEmpty) Right(right) else Left(this.get) } @@ -362,71 +362,71 @@ object Pot { } /** Default value for startTime. */ - protected[data] def currentTime = new Date().getTime + protected[data] def currentTime: Long = new Date().getTime } case object Empty extends Pot[Nothing] { - def get = throw new NoSuchElementException("Empty.get") - def isEmpty = true - def isPending = false - def isFailed = false - def isStale = false - def isUnavailable = false - def retriesLeft = 0 - def state = PotState.PotEmpty - def retryPolicy = Retry.None - - override def pending(startTime: Long = Pot.currentTime) = Pending(startTime) - override def fail(exception: Throwable) = Failed(exception) + def get: Nothing = throw new NoSuchElementException("Empty.get") + def isEmpty = true + def isPending = false + def isFailed = false + def isStale = false + def isUnavailable = false + def retriesLeft = 0 + def state: PotState = PotState.PotEmpty + def retryPolicy: Retry.None.type = Retry.None + + override def pending(startTime: Long = Pot.currentTime): Pending = Pending(startTime) + override def fail(exception: Throwable): Failed = Failed(exception) } case object Unavailable extends Pot[Nothing] { - def get = throw new NoSuchElementException("Unavailable.get") - def isEmpty = true - def isPending = false - def isFailed = true - def isStale = false - def isUnavailable = true - def retriesLeft = 0 - def state = PotState.PotUnavailable - def retryPolicy = Retry.None - - override def pending(startTime: Long = Pot.currentTime) = Pending(startTime) - override def fail(exception: Throwable) = Failed(exception) + def get: Nothing = throw new NoSuchElementException("Unavailable.get") + def isEmpty = true + def isPending = false + def isFailed = true + def isStale = false + def isUnavailable = true + def retriesLeft = 0 + def state: PotState = PotState.PotUnavailable + def retryPolicy: Retry.None.type = Retry.None + + override def pending(startTime: Long = Pot.currentTime): Pending = Pending(startTime) + override def fail(exception: Throwable): Failed = Failed(exception) } final case class Ready[+A](x: A) extends Pot[A] { - def get = x - def isEmpty = false - def isPending = false - def isFailed = false - def isStale = false - def isUnavailable = false - def retriesLeft = 0 - def state = PotState.PotReady - def retryPolicy = Retry.None - - override def pending(startTime: Long = Pot.currentTime) = PendingStale(x, startTime) - override def fail(exception: Throwable) = FailedStale(x, exception) + def get: A = x + def isEmpty = false + def isPending = false + def isFailed = false + def isStale = false + def isUnavailable = false + def retriesLeft = 0 + def state: PotState = PotState.PotReady + def retryPolicy: Retry.None.type = Retry.None + + override def pending(startTime: Long = Pot.currentTime): PendingStale[A] = PendingStale(x, startTime) + override def fail(exception: Throwable): FailedStale[A] = FailedStale(x, exception) } sealed trait PendingBase { def startTime: Long - def isPending = true - def isUnavailable = false - def state = PotState.PotPending - def duration(currentTime: Long = Pot.currentTime) = (currentTime - startTime).toInt + def isPending = true + def isUnavailable = false + def state = PotState.PotPending + def duration(currentTime: Long = Pot.currentTime): Int = (currentTime - startTime).toInt } final case class Pending(startTime: Long = Pot.currentTime) extends Pot[Nothing] with PendingBase { - def get = throw new NoSuchElementException("Pending.get") - def isEmpty = true - def isFailed = false - def isStale = false + def get: Nothing = throw new NoSuchElementException("Pending.get") + def isEmpty = true + def isFailed = false + def isStale = false - override def pending(startTime: Long = startTime) = copy(startTime) - override def fail(exception: Throwable) = Failed(exception) + override def pending(startTime: Long = startTime): Pending = copy(startTime) + override def fail(exception: Throwable): Failed = Failed(exception) } final case class PendingStale[+A](x: A, startTime: Long = Pot.currentTime) extends Pot[A] with PendingBase { @@ -435,8 +435,8 @@ final case class PendingStale[+A](x: A, startTime: Long = Pot.currentTime) exten def isFailed = false def isStale = true - override def pending(startTime: Long = startTime) = copy(x, startTime) - override def fail(exception: Throwable) = FailedStale(x, exception) + override def pending(startTime: Long = startTime): PendingStale[A] = copy(x, startTime) + override def fail(exception: Throwable): FailedStale[A] = FailedStale(x, exception) } sealed trait FailedBase { @@ -448,10 +448,10 @@ sealed trait FailedBase { } final case class Failed(exception: Throwable) extends Pot[Nothing] with FailedBase { - def get = throw new NoSuchElementException("Failed.get") - def isEmpty = true - def isStale = false - override def exceptionOption = Some(exception) + def get: Nothing = throw new NoSuchElementException("Failed.get") + def isEmpty = true + def isStale = false + override def exceptionOption: Some[Throwable] = Some(exception) override def recoverWith[B](f: PartialFunction[Throwable, Pot[B]]): Pot[B] = { if (f isDefinedAt exception) @@ -462,15 +462,15 @@ final case class Failed(exception: Throwable) extends Pot[Nothing] with FailedBa override def recover[B](f: PartialFunction[Throwable, B]): Pot[B] = this - override def pending(startTime: Long = Pot.currentTime) = Pending(startTime) - override def fail(exception: Throwable) = Failed(exception) + override def pending(startTime: Long = Pot.currentTime): Pending = Pending(startTime) + override def fail(exception: Throwable): Failed = Failed(exception) } final case class FailedStale[+A](x: A, exception: Throwable) extends Pot[A] with FailedBase { - def get = x - def isEmpty = false - def isStale = true - override def exceptionOption = Some(exception) + def get = x + def isEmpty = false + def isStale = true + override def exceptionOption: Some[Throwable] = Some(exception) override def recoverWith[B >: A](f: PartialFunction[Throwable, Pot[B]]): Pot[B] = { if (f isDefinedAt exception) @@ -481,6 +481,6 @@ final case class FailedStale[+A](x: A, exception: Throwable) extends Pot[A] with override def recover[B >: A](f: PartialFunction[Throwable, B]): Pot[B] = this - override def pending(startTime: Long = Pot.currentTime) = PendingStale(x, startTime) - override def fail(exception: Throwable) = FailedStale(x, exception) + override def pending(startTime: Long = Pot.currentTime): PendingStale[A] = PendingStale(x, startTime) + override def fail(exception: Throwable): FailedStale[A] = FailedStale(x, exception) } diff --git a/diode-data/shared/src/main/scala/diode/data/PotAction.scala b/diode-data/shared/src/main/scala/diode/data/PotAction.scala index ab8eeb5a..2e99f7c1 100644 --- a/diode-data/shared/src/main/scala/diode/data/PotAction.scala +++ b/diode-data/shared/src/main/scala/diode/data/PotAction.scala @@ -28,11 +28,11 @@ trait PotAction[A, P <: PotAction[A, P]] extends AsyncAction[A, P] { case (PotState.PotPending, _) => next(potResult.pending()) case (PotState.PotFailed, Failure(ex)) => next(potResult.fail(ex)) case (PotState.PotReady, Success(result)) => next(potResult.ready(result)) - case _ => + case _ => throw new IllegalStateException(s"PotAction is trying to enter an invalid state ($newState)") } - override def result = potResult.state match { + override def result: Try[A] = potResult.state match { case PotState.PotEmpty | PotState.PotPending => Failure(new AsyncAction.PendingException) case PotState.PotFailed => @@ -67,7 +67,7 @@ trait PotActionRetriable[A, P <: PotActionRetriable[A, P]] extends PotAction[A, case (PotState.PotPending, _) => next(potResult.pending(), newRetryPolicy) case (PotState.PotFailed, Failure(ex)) => next(potResult.fail(ex), newRetryPolicy) case (PotState.PotReady, Success(result)) => next(potResult.ready(result), newRetryPolicy) - case _ => + case _ => throw new IllegalStateException(s"PotAction is trying to enter an invalid state ($newState)") } } @@ -91,30 +91,31 @@ object PotAction { def handler[A, M, P <: PotAction[A, P]](progressInterval: FiniteDuration)(implicit runner: RunAfter, ec: ExecutionContext - ) = { (action: PotAction[A, P], handler: ActionHandler[M, Pot[A]], updateEffect: Effect) => - { - import PotState._ - import handler._ - action.state match { - case PotEmpty => - if (progressInterval > Duration.Zero) - updated(value.pending(), updateEffect + Effect.action(action.pending).after(progressInterval)) - else - updated(value.pending(), updateEffect) - case PotPending => - if (value.isPending && progressInterval > Duration.Zero) - updated(value.pending(), Effect.action(action.pending).after(progressInterval)) - else - noChange - case PotUnavailable => - updated(value.unavailable()) - case PotReady => - updated(action.potResult) - case PotFailed => - val ex = action.result.failed.get - updated(value.fail(ex)) + ): (PotAction[A, P], ActionHandler[M, Pot[A]], Effect) => ActionResult[M] = { + (action: PotAction[A, P], handler: ActionHandler[M, Pot[A]], updateEffect: Effect) => + { + import PotState._ + import handler._ + action.state match { + case PotEmpty => + if (progressInterval > Duration.Zero) + updated(value.pending(), updateEffect + Effect.action(action.pending).after(progressInterval)) + else + updated(value.pending(), updateEffect) + case PotPending => + if (value.isPending && progressInterval > Duration.Zero) + updated(value.pending(), Effect.action(action.pending).after(progressInterval)) + else + noChange + case PotUnavailable => + updated(value.unavailable()) + case PotReady => + updated(action.potResult) + case PotFailed => + val ex = action.result.failed.get + updated(value.fail(ex)) + } } - } } } @@ -137,40 +138,41 @@ object PotActionRetriable { def handler[A, M, P <: PotActionRetriable[A, P]](progressInterval: FiniteDuration)(implicit runner: RunAfter, ec: ExecutionContext - ) = { (action: PotActionRetriable[A, P], handler: ActionHandler[M, Pot[A]], updateEffect: RetryPolicy => Effect) => - { - import PotState._ - import handler._ - action.state match { - case PotEmpty => - if (progressInterval > Duration.Zero) - updated( - value.pending(), - updateEffect(action.retryPolicy) + Effect.action(action.pending).after(progressInterval) - ) - else - updated(value.pending(), updateEffect(action.retryPolicy)) - - case PotPending => - if (value.isPending && progressInterval > Duration.Zero) - updated(value.pending(), Effect.action(action.pending).after(progressInterval)) - else - noChange - case PotUnavailable => - updated(value.unavailable()) - case PotReady => - updated(action.potResult) - case PotFailed => - action.retryPolicy.retry( - action.potResult.exceptionOption.getOrElse(new IllegalStateException("Pot is not in a failed state")), - updateEffect - ) match { - case Right((_, retryEffect)) => - effectOnly(retryEffect) - case Left(ex) => - updated(value.fail(ex)) - } + ): (PotActionRetriable[A, P], ActionHandler[M, Pot[A]], RetryPolicy => Effect) => ActionResult[M] = { + (action: PotActionRetriable[A, P], handler: ActionHandler[M, Pot[A]], updateEffect: RetryPolicy => Effect) => + { + import PotState._ + import handler._ + action.state match { + case PotEmpty => + if (progressInterval > Duration.Zero) + updated( + value.pending(), + updateEffect(action.retryPolicy) + Effect.action(action.pending).after(progressInterval) + ) + else + updated(value.pending(), updateEffect(action.retryPolicy)) + + case PotPending => + if (value.isPending && progressInterval > Duration.Zero) + updated(value.pending(), Effect.action(action.pending).after(progressInterval)) + else + noChange + case PotUnavailable => + updated(value.unavailable()) + case PotReady => + updated(action.potResult) + case PotFailed => + action.retryPolicy.retry( + action.potResult.exceptionOption.getOrElse(new IllegalStateException("Pot is not in a failed state")), + updateEffect + ) match { + case Right((_, retryEffect)) => + effectOnly(retryEffect) + case Left(ex) => + updated(value.fail(ex)) + } + } } - } } } diff --git a/diode-data/shared/src/main/scala/diode/data/PotMap.scala b/diode-data/shared/src/main/scala/diode/data/PotMap.scala index 2715f721..09a6cd3b 100644 --- a/diode-data/shared/src/main/scala/diode/data/PotMap.scala +++ b/diode-data/shared/src/main/scala/diode/data/PotMap.scala @@ -41,10 +41,10 @@ class PotMap[K, V]( runAfterImpl.runAfter(0)(fetcher.fetch(keys)) } - override def clear = + override def clear: PotMap[K, V] = new PotMap(fetcher, Map.empty[K, Pot[V]]) - override def get(key: K) = { + override def get(key: K): Pot[V] = { elems.get(key) match { case Some(elem) if elem.state == PotState.PotEmpty => refresh(key) @@ -63,12 +63,12 @@ class PotMap[K, V]( def +(kv: (K, Pot[V])): PotMap[K, V] = updated(kv._1, kv._2) - def ++(xs: Iterable[(K, Pot[V])]) = updated(xs) + def ++(xs: Iterable[(K, Pot[V])]): PotMap[K, V] = updated(xs) - def -(key: K) = remove(key) + def -(key: K): PotMap[K, V] = remove(key) def get(keys: Iterable[K]): Map[K, Pot[V]] = { - var toFetch = List.empty[K] + var toFetch = List.empty[K] val values: Map[K, Pot[V]] = keys.map { key => elems.get(key) match { diff --git a/diode-data/shared/src/main/scala/diode/data/PotStream.scala b/diode-data/shared/src/main/scala/diode/data/PotStream.scala index a4dae523..494fc4c3 100644 --- a/diode-data/shared/src/main/scala/diode/data/PotStream.scala +++ b/diode-data/shared/src/main/scala/diode/data/PotStream.scala @@ -12,9 +12,9 @@ final case class StreamValue[K, V](key: K, ) { def apply() = value - def prev = stream.get(prevKey) + def prev: Option[StreamValue[K, V]] = stream.get(prevKey) - def next = stream.get(nextKey) + def next: Option[StreamValue[K, V]] = stream.get(nextKey) } class PotStream[K, V]( @@ -100,7 +100,7 @@ class PotStream[K, V]( } val reversedKvs = kvs.reverse - val newValues = + val newValues = buildStream(reversedKvs.tail.headOption.map(_._1), headKeyOption, reversedKvs.head, reversedKvs.tail, Nil) val firstKey = reversedKvs.head._1 val headKey = kvs.head._1 @@ -200,7 +200,7 @@ class PotStream[K, V]( def iterator: Iterator[(K, V)] = new Iterator[(K, V)] { private var current = headOption override def hasNext: Boolean = current.nonEmpty - override def next(): (K, V) = { + override def next(): (K, V) = { val r = current.map(sv => sv.key -> sv.value).get current = current.flatMap(_.nextKey).map(elems) r diff --git a/diode-data/shared/src/main/scala/diode/data/PotVector.scala b/diode-data/shared/src/main/scala/diode/data/PotVector.scala index 8cf97021..c6e17415 100644 --- a/diode-data/shared/src/main/scala/diode/data/PotVector.scala +++ b/diode-data/shared/src/main/scala/diode/data/PotVector.scala @@ -22,7 +22,7 @@ class PotVector[V]( newArray } - override def updated(idx: Int, value: Pot[V]) = { + override def updated(idx: Int, value: Pot[V]): PotVector[V] = { if (idx < 0 || idx >= length) throw new IndexOutOfBoundsException val newElems = if (idx >= elems.length) { @@ -96,8 +96,8 @@ class PotVector[V]( else Some(idx) } - private var current = findNext(0) - override def hasNext: Boolean = current.nonEmpty + private var current = findNext(0) + override def hasNext: Boolean = current.nonEmpty override def next(): (Int, Pot[V]) = { val idx = current.get current = findNext(idx + 1) @@ -105,7 +105,7 @@ class PotVector[V]( } } - override def remove(idx: Int) = { + override def remove(idx: Int): PotVector[V] = { elems(idx) = None this } @@ -124,7 +124,7 @@ class PotVector[V]( runAfterImpl.runAfter(0)(fetcher.fetch(indices)) } - override def clear = + override def clear: PotVector[V] = new PotVector(fetcher, length, Array.empty[Option[Pot[V]]]) override def get(idx: Int): Pot[V] = { @@ -157,7 +157,7 @@ class PotVector[V]( if (end <= start) return Seq() var missing = List.empty[Int] - val values = (start until end).map { idx => + val values = (start until end).map { idx => if (idx >= elems.length || elems(idx).isEmpty) { missing ::= idx Pending().asInstanceOf[Pot[V]] @@ -177,7 +177,7 @@ class PotVector[V]( def resized(newLength: Int) = new PotVector(fetcher, newLength, if (newLength < elems.length) util.Arrays.copyOf(elems, newLength) else elems) - def contains(idx: Int) = { + def contains(idx: Int): Boolean = { if (idx < 0 || idx >= length) throw new IndexOutOfBoundsException elems(idx).isDefined diff --git a/diode-data/shared/src/main/scala/diode/data/RefTo.scala b/diode-data/shared/src/main/scala/diode/data/RefTo.scala index 43a63b5c..6acf5dac 100644 --- a/diode-data/shared/src/main/scala/diode/data/RefTo.scala +++ b/diode-data/shared/src/main/scala/diode/data/RefTo.scala @@ -11,7 +11,7 @@ import diode._ * Function to create an update action for the value this reference points to */ class RefTo[V](val target: ModelRO[V], val updated: V => Action) { - def apply() = target() + def apply(): V = target() } object RefTo { @@ -57,6 +57,6 @@ object RefTo { @inline def stream[K, V, P](key: K, streamTarget: ModelRO[P])( updated: (K, V) => Action - )(implicit ev: P <:< PotStream[K, V], feq: FastEq[_ >: V]): RefTo[V] = + )(implicit ev: P <:< PotStream[K, V], feq: FastEq[? >: V]): RefTo[V] = new RefTo[V](streamTarget.zoom(_.apply(key)), updated(key, _: V)) } diff --git a/diode-data/shared/src/test/scala/diode/data/PotActionTests.scala b/diode-data/shared/src/test/scala/diode/data/PotActionTests.scala index 598920f6..ebc5807e 100644 --- a/diode-data/shared/src/test/scala/diode/data/PotActionTests.scala +++ b/diode-data/shared/src/test/scala/diode/data/PotActionTests.scala @@ -65,7 +65,7 @@ object PotActionTests extends TestSuite { override def fetch(keys: Iterable[String]): Unit = () } - def tests = TestSuite { + def tests = Tests { "PotAction" - { "CreateEmpty" - { val ta = TestAction() @@ -95,7 +95,7 @@ object PotActionTests extends TestSuite { } } "EffectFail" - { - val ta = TestAction() + val ta = TestAction() val eff = ta.effect(Future { if (true) throw new Exception("Oh no!") else 42 })( _.toString, @@ -157,7 +157,7 @@ object PotActionTests extends TestSuite { val model = Model(PendingStale("41")) val modelRW = new RootModelRW(model) val handlerFail = new TestFailHandler(modelRW.zoomRW(_.s)((m, v) => m.copy(s = v))) - val nextAction = handlerFail.handleAction(model, TestActionRP(Failed(new TimeoutException), Immediate(1))) match { + val nextAction = handlerFail.handleAction(model, TestActionRP(Failed(new TimeoutException), Immediate(1))) match { case Some(EffectOnly(effects)) => assert(effects.size == 1) // run effect diff --git a/diode-data/shared/src/test/scala/diode/data/PotCollectionTests.scala b/diode-data/shared/src/test/scala/diode/data/PotCollectionTests.scala index 8986f155..ae607654 100644 --- a/diode-data/shared/src/test/scala/diode/data/PotCollectionTests.scala +++ b/diode-data/shared/src/test/scala/diode/data/PotCollectionTests.scala @@ -1,11 +1,11 @@ package diode.data import diode.Implicits.runAfterImpl -import diode.data.PotState._ -import utest._ +import diode.data.PotState.* +import utest.* import scala.concurrent.ExecutionContext.Implicits.global -import scala.util._ +import scala.util.* object PotCollectionTests extends TestSuite { @@ -16,7 +16,7 @@ object PotCollectionTests extends TestSuite { override def fetch(keys: Iterable[K]): Unit = lastFetch = keys } - def tests = TestSuite { + def tests = Tests { "PotMap" - { "update" - { val fetcher = new TestFetcher[String] diff --git a/diode-data/shared/src/test/scala/diode/data/PotTests.scala b/diode-data/shared/src/test/scala/diode/data/PotTests.scala index 5d3bc00b..fbdf6fb4 100644 --- a/diode-data/shared/src/test/scala/diode/data/PotTests.scala +++ b/diode-data/shared/src/test/scala/diode/data/PotTests.scala @@ -1,9 +1,9 @@ package diode.data -import utest._ +import utest.* object PotTests extends TestSuite { - def tests = TestSuite { + def tests = Tests { "Pot" - { "mapVariants" - { val p = Pot.empty[String] diff --git a/diode-data/shared/src/test/scala/diode/data/RefToTests.scala b/diode-data/shared/src/test/scala/diode/data/RefToTests.scala index 8ff5b8bc..b8b2dbbf 100644 --- a/diode-data/shared/src/test/scala/diode/data/RefToTests.scala +++ b/diode-data/shared/src/test/scala/diode/data/RefToTests.scala @@ -1,7 +1,7 @@ package diode.data import diode.{Action, RootModelRW} -import utest._ +import utest.* object RefToTests extends TestSuite { @@ -22,12 +22,12 @@ object RefToTests extends TestSuite { case class RefAction(s: String) extends Action - def tests = TestSuite { + def tests = Tests { "refToMap" - { val fetcher = new TestFetcher[String] val root = Model(PotMap(fetcher, Map("ceoID" -> Ready(User("Ms. CEO")))), Seq()) val modelRW = new RootModelRW(root) - val m = root.copy( + val m = root.copy( employees = Seq(Employee("CEO", RefTo("ceoID", modelRW.zoom(_.users))((id, value) => RefAction(s"Update $id to $value")))) ) @@ -42,7 +42,7 @@ object RefToTests extends TestSuite { val fetcher = new TestFetcher[Int] val root = ModelV(PotVector(fetcher, 5, Vector(Ready(User("Ms. CEO")))), Seq()) val modelRW = new RootModelRW(root) - val m = + val m = root.copy( employees = Seq(Employee("CEO", RefTo(0, modelRW.zoom(_.users))((id, value) => RefAction(s"Update $id to $value")))) @@ -56,7 +56,7 @@ object RefToTests extends TestSuite { val fetcher = new TestFetcher[String] val root = ModelS(PotStream(fetcher, Seq("ceoID" -> Ready(User("Ms. CEO")))), Seq()) val modelRW = new RootModelRW(root) - val m = root.copy( + val m = root.copy( employees = Seq( Employee("CEO", RefTo.stream("ceoID", modelRW.zoom(_.users))((id, value) => RefAction(s"Update $id to $value"))) ) diff --git a/diode-devtools/js/src/main/scala/diode/dev/Hooks.scala b/diode-devtools/js/src/main/scala/diode/dev/Hooks.scala index 070035f4..f9782cc7 100644 --- a/diode-devtools/js/src/main/scala/diode/dev/Hooks.scala +++ b/diode-devtools/js/src/main/scala/diode/dev/Hooks.scala @@ -5,8 +5,8 @@ import org.scalajs.dom import org.scalajs.dom.KeyboardEvent object Hooks { - def hookPersistState(id: String, dispatch: Dispatcher) = { - def keyDown(event: KeyboardEvent) = { + def hookPersistState(id: String, dispatch: Dispatcher): Unit = { + def keyDown(event: KeyboardEvent): Unit = { if (event.ctrlKey && event.shiftKey) { val c = Character.toChars(event.keyCode)(0).toLower c match { @@ -20,6 +20,6 @@ object Hooks { } } } - dom.window.addEventListener("keydown", keyDown _) + dom.window.addEventListener("keydown", (event: KeyboardEvent) => keyDown(event)) } } diff --git a/diode-devtools/shared/src/main/scala/diode/dev/PersistState.scala b/diode-devtools/shared/src/main/scala/diode/dev/PersistState.scala index db4023c9..4e912bab 100644 --- a/diode-devtools/shared/src/main/scala/diode/dev/PersistState.scala +++ b/diode-devtools/shared/src/main/scala/diode/dev/PersistState.scala @@ -27,7 +27,7 @@ abstract class PersistState[M <: AnyRef, P] extends ActionProcessor[M] { // internal action dispatched once loading is completed private case class Loaded(newModel: M) extends Action - override def process(dispatch: Dispatcher, action: Any, next: Any => ActionResult[M], currentModel: M) = { + override def process(dispatch: Dispatcher, action: Any, next: Any => ActionResult[M], currentModel: M): ActionResult[M] = { action match { case Save(id) => // pickle and save diff --git a/diode-react/src/main/scala/diode/react/ReactConnector.scala b/diode-react/src/main/scala/diode/react/ReactConnector.scala index aa77ca43..f0f73ef6 100644 --- a/diode-react/src/main/scala/diode/react/ReactConnector.scala +++ b/diode-react/src/main/scala/diode/react/ReactConnector.scala @@ -9,8 +9,8 @@ import scala.scalajs.js /** * Wraps a model reader, dispatcher and React connector to be passed to React components in props. */ -case class ModelProxy[S](modelReader: ModelRO[S], theDispatch: Any => Unit, connector: ReactConnector[_ <: AnyRef]) { - def value = modelReader() +case class ModelProxy[S](modelReader: ModelRO[S], theDispatch: Any => Unit, connector: ReactConnector[? <: AnyRef]) { + def value: S = modelReader() /** * Perform a dispatch action in a `Callback` @@ -22,16 +22,17 @@ case class ModelProxy[S](modelReader: ModelRO[S], theDispatch: Any => Unit, conn */ def dispatchNow[A: ActionType](action: A): Unit = theDispatch(action) - def apply() = modelReader() + def apply(): S = modelReader() - def zoom[T](f: S => T)(implicit feq: FastEq[_ >: T]) = ModelProxy(modelReader.zoom(f), theDispatch, connector) + def zoom[T](f: S => T)(implicit feq: FastEq[? >: T]): ModelProxy[T] = + ModelProxy(modelReader.zoom(f), theDispatch, connector) - def wrap[T <: AnyRef, C](f: S => T)(compB: ModelProxy[T] => C)(implicit ev: C => VdomElement, feq: FastEq[_ >: T]): C = { + def wrap[T <: AnyRef, C](f: S => T)(compB: ModelProxy[T] => C)(implicit ev: C => VdomElement, feq: FastEq[? >: T]): C = { val _ = ev compB(zoom(f)) } - def connect[T <: AnyRef](f: S => T)(implicit feq: FastEq[_ >: T]): ReactConnectProxy[T] = { + def connect[T <: AnyRef](f: S => T)(implicit feq: FastEq[? >: T]): ReactConnectProxy[T] = { connector.connect(modelReader.zoom(f)) } } @@ -50,7 +51,7 @@ trait ReactConnector[M <: AnyRef] { circuit: Circuit[M] => */ def wrap[S <: AnyRef, C]( zoomFunc: M => S - )(compB: ModelProxy[S] => C)(implicit ev: C => VdomElement, feq: FastEq[_ >: S]): C = { + )(compB: ModelProxy[S] => C)(implicit ev: C => VdomElement, feq: FastEq[? >: S]): C = { wrap(circuit.zoom(zoomFunc))(compB) } @@ -81,7 +82,7 @@ trait ReactConnector[M <: AnyRef] { circuit: Circuit[M] => * @return * A ReactConnectProxy */ - def connect[S <: AnyRef](zoomFunc: M => S, key: js.Any)(implicit feq: FastEq[_ >: S]): ReactConnectProxy[S] = { + def connect[S <: AnyRef](zoomFunc: M => S, key: js.Any)(implicit feq: FastEq[? >: S]): ReactConnectProxy[S] = { connect(circuit.zoom(zoomFunc), key) } @@ -94,7 +95,7 @@ trait ReactConnector[M <: AnyRef] { circuit: Circuit[M] => * @return * A ReactConnectProxy */ - def connect[S <: AnyRef](zoomFunc: M => S)(implicit feq: FastEq[_ >: S]): ReactConnectProxy[S] = { + def connect[S <: AnyRef](zoomFunc: M => S)(implicit feq: FastEq[? >: S]): ReactConnectProxy[S] = { connect(circuit.zoom(zoomFunc)) } diff --git a/diode-react/src/main/scala/diode/react/package.scala b/diode-react/src/main/scala/diode/react/package.scala index f4c2e394..328c40eb 100644 --- a/diode-react/src/main/scala/diode/react/package.scala +++ b/diode-react/src/main/scala/diode/react/package.scala @@ -6,5 +6,5 @@ import japgolly.scalajs.react.vdom.VdomElement package object react { type ReactConnectProps[A] = ModelProxy[A] => VdomElement - type ReactConnectProxy[A] = CtorType.Props[ReactConnectProps[A], UnmountedWithRoot[ReactConnectProps[A], _, _, _]] + type ReactConnectProxy[A] = CtorType.Props[ReactConnectProps[A], UnmountedWithRoot[ReactConnectProps[A], ?, ?, ?]] } diff --git a/doc/usage/Handlers.md b/doc/usage/Handlers.md index 53bd831b..40811d56 100644 --- a/doc/usage/Handlers.md +++ b/doc/usage/Handlers.md @@ -30,20 +30,24 @@ defined as a trait and two case classes. ```scala sealed trait FileNode { def id: String + def name: String + def children: IndexedSeq[FileNode] + def isDirectory: Boolean } final case class Directory( - id: String, - name: String, - children: IndexedSeq[FileNode] = IndexedSeq.empty) extends FileNode { + id: String, + name: String, + children: IndexedSeq[FileNode] = IndexedSeq.empty) extends FileNode { override def isDirectory = true } final case class File(id: String, name: String) extends FileNode { val children = IndexedSeq.empty[FileNode] + override def isDirectory = false } ``` @@ -62,9 +66,13 @@ And some actions, too! ```scala case class ReplaceTree(newTree: Directory) extends Action + case class AddNode(path: Seq[String], node: FileNode) extends Action + case class RemoveNode(path: Seq[String]) extends Action + case class ReplaceNode(path: Seq[String], node: FileNode) extends Action + case class Select(selected: Seq[String]) extends Action ``` @@ -93,9 +101,13 @@ it's rather straightforward: ```scala abstract class ActionHandler[M, T](val modelRW: ModelRW[M, T]) { def handle: PartialFunction[AnyRef, ActionResult[M]] + def value: T = modelRW.eval(currentModel) - def updated(newValue: T): ActionResult[M] = ... - ... + + def updated(newValue: T): ActionResult[M] = + +... +... } ``` @@ -112,7 +124,7 @@ a valid path, so we should wrap the result in an `Option`. ```scala def zoomToChildren[M](path: Seq[String], rw: ModelRW[M, Directory]) - : Option[ModelRW[M, IndexedSeq[FileNode]]] = { +: Option[ModelRW[M, IndexedSeq[FileNode]]] = { if (path.isEmpty) { Some(rw.zoomTo(_.children)) } else { @@ -120,6 +132,7 @@ def zoomToChildren[M](path: Seq[String], rw: ModelRW[M, Directory]) } } ``` + In the trivial case (and at the end of the recursion) the path is empty and we'll return a `ModelRW` for the current directory's `children`. @@ -127,7 +140,7 @@ Let's take a look at the full implementation. ```scala def zoomToChildren[M](path: Seq[String], rw: ModelRW[M, Directory]) - : Option[ModelRW[M, IndexedSeq[FileNode]]] = { +: Option[ModelRW[M, IndexedSeq[FileNode]]] = { if (path.isEmpty) { Some(rw.zoomTo(_.children)) } else { @@ -150,46 +163,49 @@ First we try to find the index to the `children` sequence, where the next direct the result we either return `None` for failure, or dive deeper into the hierarchy by recursively calling the same function again with am updated reader/writer and path. The writer function copies all nodes preceding the one we are interested in, then adds a new node and then any nodes succeeding the original node. - + Now we are ready to write action handlers for rest of the tree manipulation functions. ```scala -case AddNode(path, node) => - // zoom to the directory and add new node at the end of its children list - zoomToChildren(path.tail, modelRW) match { - case Some(rw) => ModelUpdate(rw.updated(rw.value :+ node)) - case None => noChange - } +case AddNode(path, node) +=> +// zoom to the directory and add new node at the end of its children list +zoomToChildren(path.tail, modelRW) match { + case Some(rw) => ModelUpdate(rw.updated(rw.value :+ node)) + case None => noChange +} ``` With the helper function to navigate the hierarchy the actual implementation of `AddNode` turns out to be quite trivial. Since we have access to the `children` of the parent directory, we simply need to add the new node at the end of it. ```scala -case RemoveNode(path) => - if (path.init.nonEmpty) { - // zoom to parent directory and remove node from its children list - val nodeId = path.last - zoomToChildren(path.init.tail, modelRW) match { - case Some(rw) => ModelUpdate(rw.updated(rw.value.filterNot(_.id == nodeId))) - case None => noChange - } - } else { - // cannot remove root - noChange +case RemoveNode(path) +=> +if (path.init.nonEmpty) { + // zoom to parent directory and remove node from its children list + val nodeId = path.last + zoomToChildren(path.init.tail, modelRW) match { + case Some(rw) => ModelUpdate(rw.updated(rw.value.filterNot(_.id == nodeId))) + case None => noChange } -case ReplaceNode(path, node) => - if (path.init.nonEmpty) { - // zoom to parent directory and replace node in its children list with a new one - val nodeId = path.last - zoomToChildren(path.init.tail, modelRW) match { - case Some(rw) => ModelUpdate(rw.updated(rw.value.map(n => if (n.id == nodeId) node else n))) - case None => noChange - } - } else { - // cannot replace root - noChange +} else { + // cannot remove root + noChange +} +case ReplaceNode(path, node) +=> +if (path.init.nonEmpty) { + // zoom to parent directory and replace node in its children list with a new one + val nodeId = path.last + zoomToChildren(path.init.tail, modelRW) match { + case Some(rw) => ModelUpdate(rw.updated(rw.value.map(n => if (n.id == nodeId) node else n))) + case None => noChange } +} else { + // cannot replace root + noChange +} ``` Removal and replacement are almost identical, except for the final transformation of the `children` sequence. Here we @@ -217,7 +233,7 @@ function. ## Handling actions multiple times The default behaviour of `composeHandlers` combines your handler functions in such a way that only one of them gets to -handle the passed action. In some cases you'll want to divide the processing of an action into multiple handlers, for +handle the passed action. In some cases you'll want to divide the processing of an action into multiple handlers, for example when a change in model causes some secondary changes elsewhere in the model (typically related to the UI). For these purposes you can use `foldHandlers`, which runs the action through all the provided handlers, passing an updated model from one handler to the next and combining the resulting effects. @@ -230,7 +246,7 @@ val selectionHandler = new ActionHandler(zoomTo(_.tree.selected)) { case Select(sel) => updated(sel) case RemoveNode(path) => // select parent node if selected node is removed - if(path.init.nonEmpty && path == value) + if (path.init.nonEmpty && path == value) updated(path.init) else noChange @@ -255,12 +271,15 @@ your `Circuit` implementation, it's usually better to have them as separate clas ```scala class DirectoryTreeHandler[M](modelRW: ModelRW[M, Directory]) extends ActionHandler(modelRW) { - override def handle = { ... } + override def handle = { + ... + } } object AppCircuit extends Circuit[RootModel] { val treeHandler = new DirectoryTreeHandler(zoomTo(_.tree.root)) - ... + . +.. } ``` @@ -269,7 +288,7 @@ doesn't care about the root model type, usually easiest is just to use the data ```scala object DirectoryTreeHandlerTests extends TestSuite { - def tests = TestSuite { + def tests = Tests { // test data val dir = Directory("/", "/", Vector( Directory("2", "My files", Vector( @@ -283,7 +302,7 @@ object DirectoryTreeHandlerTests extends TestSuite { ``` In individual test cases - + 1. Build a handler instance. 2. Call the `handleAction` method with an appropriate action and current model. 3. Check the result. diff --git a/examples/treeview/src/test/scala/example/DirectoryTreeHandlerTests.scala b/examples/treeview/src/test/scala/example/DirectoryTreeHandlerTests.scala index 3918b52a..70cbdaae 100644 --- a/examples/treeview/src/test/scala/example/DirectoryTreeHandlerTests.scala +++ b/examples/treeview/src/test/scala/example/DirectoryTreeHandlerTests.scala @@ -5,7 +5,7 @@ import diode.RootModelRW import utest._ object DirectoryTreeHandlerTests extends TestSuite { - def tests = TestSuite { + def tests = Tests { // test data val dir = Directory( "/", diff --git a/project/plugins.sbt b/project/plugins.sbt index 1b6d80ff..554632ce 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -6,4 +6,6 @@ addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6") +addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.14.5") + addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.10") From 5bb86c0a3d8d31218c4b5aa11e857390d624dc76 Mon Sep 17 00:00:00 2001 From: amiklushou Date: Tue, 3 Feb 2026 17:21:51 +0200 Subject: [PATCH 4/5] Code review. --- diode-core/shared/src/main/scala/diode/Circuit.scala | 8 ++++---- diode-core/shared/src/main/scala/diode/util/Retry.scala | 2 +- diode-data/shared/src/main/scala/diode/data/Pot.scala | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/diode-core/shared/src/main/scala/diode/Circuit.scala b/diode-core/shared/src/main/scala/diode/Circuit.scala index 4aab0d29..0bbb0907 100644 --- a/diode-core/shared/src/main/scala/diode/Circuit.scala +++ b/diode-core/shared/src/main/scala/diode/Circuit.scala @@ -415,10 +415,10 @@ trait Circuit[M <: AnyRef] extends Dispatcher with ZoomTo[M, M] { // no-op false case ActionResult.ModelUpdate(newModel) => - update(newModel.asInstanceOf[M]) + update(newModel) false case ActionResult.ModelUpdateSilent(newModel) => - update(newModel.asInstanceOf[M]) + update(newModel) true case ActionResult.EffectOnly(effects) => // run effects @@ -429,7 +429,7 @@ trait Circuit[M <: AnyRef] extends Dispatcher with ZoomTo[M, M] { }(effects.ec) true case ActionResult.ModelUpdateEffect(newModel, effects) => - update(newModel.asInstanceOf[M]) + update(newModel) // run effects effects .run(a => dispatch(a)) @@ -438,7 +438,7 @@ trait Circuit[M <: AnyRef] extends Dispatcher with ZoomTo[M, M] { }(effects.ec) false case ActionResult.ModelUpdateSilentEffect(newModel, effects) => - update(newModel.asInstanceOf[M]) + update(newModel) // run effects effects .run(a => dispatch(a)) diff --git a/diode-core/shared/src/main/scala/diode/util/Retry.scala b/diode-core/shared/src/main/scala/diode/util/Retry.scala index 3cfffb20..cc480b49 100644 --- a/diode-core/shared/src/main/scala/diode/util/Retry.scala +++ b/diode-core/shared/src/main/scala/diode/util/Retry.scala @@ -5,7 +5,7 @@ import java.util.concurrent.TimeUnit import diode.Effect import scala.concurrent.ExecutionContext -import scala.concurrent.duration._ +import scala.concurrent.duration.* /** * Define a policy for retrying diff --git a/diode-data/shared/src/main/scala/diode/data/Pot.scala b/diode-data/shared/src/main/scala/diode/data/Pot.scala index 54c9584c..af3ecdf5 100644 --- a/diode-data/shared/src/main/scala/diode/data/Pot.scala +++ b/diode-data/shared/src/main/scala/diode/data/Pot.scala @@ -3,7 +3,7 @@ package diode.data import java.util.Date import diode.Monad -import diode.util._ +import diode.util.* import scala.util.{Failure, Success, Try} From efa2a0eb84d3f58cbc94f573379c98d6497ac266 Mon Sep 17 00:00:00 2001 From: amiklushou Date: Tue, 3 Feb 2026 17:49:58 +0200 Subject: [PATCH 5/5] Update scalajs-react to 3.0.0 --- build.sbt | 8 ++++---- .../src/main/scala/diode/react/ReactConnector.scala | 9 +++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/build.sbt b/build.sbt index 3420d26c..c0369fa3 100644 --- a/build.sbt +++ b/build.sbt @@ -1,8 +1,8 @@ -import sbt.Keys._ -import sbt._ +import sbt.Keys.* +import sbt.* // shadow sbt-scalajs' crossProject and CrossType from Scala.js 0.6.x import sbtcrossproject.CrossPlugin.autoImport.{crossProject, CrossType} -import Util._ +import Util.* ThisBuild / scalafmtOnCompile := true @@ -157,7 +157,7 @@ lazy val diodeReact: Project = project .settings( name := "diode-react", libraryDependencies ++= Seq( - "com.github.japgolly.scalajs-react" %%% "core" % "2.1.1" + "com.github.japgolly.scalajs-react" %%% "core" % "3.0.0" ) ) .dependsOn(diode.js) diff --git a/diode-react/src/main/scala/diode/react/ReactConnector.scala b/diode-react/src/main/scala/diode/react/ReactConnector.scala index f0f73ef6..6c813d26 100644 --- a/diode-react/src/main/scala/diode/react/ReactConnector.scala +++ b/diode-react/src/main/scala/diode/react/ReactConnector.scala @@ -1,7 +1,7 @@ package diode.react -import diode._ -import japgolly.scalajs.react._ +import diode.* +import japgolly.scalajs.react.* import japgolly.scalajs.react.vdom.VdomElement import scala.scalajs.js @@ -136,13 +136,14 @@ trait ReactConnector[M <: AnyRef] { circuit: Circuit[M] => (stateHasChanged >>= updateState).runNow() } - def render(s: S, compB: ReactConnectProps[S]) = wrap(modelReader)(compB) + def render(compB: ReactConnectProps[S], s: S) = wrap(modelReader)(compB) } ScalaComponent .builder[ReactConnectProps[S]]("DiodeWrapper") .initialState(modelReader()) - .renderBackend[Backend] + .backend(new Backend(_)) + .renderPS(_.backend.render(_, _)) .componentDidMount(_.backend.didMount) .componentWillUnmount(_.backend.willUnmount) .shouldComponentUpdatePure(scope => (scope.currentState ne scope.nextState) || (scope.currentProps ne scope.nextProps))