From 9e6464700c8b06a3ad5cf538a59085deaedb84e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mareks=20Ramp=C4=81ns?= <8796159+mr-git@users.noreply.github.com> Date: Wed, 12 Aug 2026 10:37:30 +0300 Subject: [PATCH 01/11] update CI and Release workflows --- .github/workflows/ci.yml | 41 +++++------------------------------ .github/workflows/release.yml | 10 ++++----- 2 files changed, 10 insertions(+), 41 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 450503e..54a7b8a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,41 +1,10 @@ name: CI -on: [push, pull_request] +on: + push: + branches: [ master ] + pull_request: jobs: test: - - runs-on: ubuntu-latest - - strategy: - matrix: - scala: - - 2.13.5 - - 2.12.13 - - steps: - - uses: actions/checkout@v2 - - - uses: coursier/cache-action@v5 - - - name: scala - uses: olafurpg/setup-scala@v10 - with: - java-version: openjdk@1.11 - - - name: build ${{ matrix.scala }} - run: sbt ++${{ matrix.scala }} clean coverage test - - - name: test coverage - if: success() - env: - COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} - run: sbt ++${{ matrix.scala }} coverageReport coverageAggregate coveralls - - - name: slack - uses: homoluctus/slatify@master - if: failure() && github.ref == 'refs/heads/master' - with: - type: ${{ job.status }} - job_name: Build - url: ${{ secrets.SLACK_WEBHOOK }} \ No newline at end of file + uses: evolution-gaming/scala-github-actions/.github/workflows/ci.yml@dde27b9bd793d41d5aacf8fb74403c9de5da1146 # v6.3.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a8a2f2b..3b7a7cc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,11 +1,11 @@ -name: Publish new Release +name: Publish Release on: - release: - types: [published] - branches: [master] + push: + tags: + - 'v*' jobs: release: - uses: evolution-gaming/scala-github-actions/.github/workflows/release.yml@v1 + uses: evolution-gaming/scala-github-actions/.github/workflows/release.yml@v5 secrets: inherit From 1303296a08bf07394b9d26e4f243f4b1adf8a605 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mareks=20Ramp=C4=81ns?= <8796159+mr-git@users.noreply.github.com> Date: Wed, 12 Aug 2026 10:39:36 +0300 Subject: [PATCH 02/11] update SBT to 2.0.6, also update related plugins --- build.sbt | 10 ++++------ project/build.properties | 2 +- project/plugins.sbt | 6 ++---- version.sbt | 1 - 4 files changed, 7 insertions(+), 12 deletions(-) delete mode 100644 version.sbt diff --git a/build.sbt b/build.sbt index f9e98c4..3f386f1 100644 --- a/build.sbt +++ b/build.sbt @@ -2,13 +2,13 @@ name := "serially" organization := "com.evolutiongaming" -homepage := Some(url("https://github.com/evolution-gaming/serially")) +homepage := Some(uri("https://github.com/evolution-gaming/serially")) startYear := Some(2018) organizationName := "Evolution" -organizationHomepage := Some(url("https://evolution.com")) +organizationHomepage := Some(uri("https://evolution.com")) scalaVersion := crossScalaVersions.value.head @@ -24,10 +24,8 @@ libraryDependencies ++= Seq( "com.typesafe.akka" %% "akka-testkit" % "2.6.8" % Test, "org.scalatest" %% "scalatest" % "3.2.9" % Test) -licenses := Seq(("MIT", url("https://opensource.org/licenses/MIT"))) - -releaseCrossBuild := true +licenses := Seq(("MIT", uri("https://opensource.org/licenses/MIT"))) //addCommandAlias("check", "all versionPolicyCheck Compile/doc") addCommandAlias("check", "show version") -addCommandAlias("build", "+all compile test") +addCommandAlias("build", "+all compile testFull") diff --git a/project/build.properties b/project/build.properties index 19479ba..fdcf9af 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.5.2 +sbt.version = 2.0.6 diff --git a/project/plugins.sbt b/project/plugins.sbt index 8f253da..7bba46e 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,8 +1,6 @@ -addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.7.2") +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.4.4") -addSbtPlugin("org.scoverage" % "sbt-coveralls" % "1.2.7") - -addSbtPlugin("com.github.sbt" % "sbt-release" % "1.0.15") +addSbtPlugin("com.github.sbt" % "sbt-dynver" % "5.1.1") addSbtPlugin("com.evolution" % "sbt-scalac-opts-plugin" % "0.2.0") diff --git a/version.sbt b/version.sbt deleted file mode 100644 index 2094d25..0000000 --- a/version.sbt +++ /dev/null @@ -1 +0,0 @@ -ThisBuild / version := "1.0.6-SNAPSHOT" From 82da23fdd8d9b5f4e3ff27375948685b0f8fd2fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mareks=20Ramp=C4=81ns?= <8796159+mr-git@users.noreply.github.com> Date: Wed, 12 Aug 2026 10:41:12 +0300 Subject: [PATCH 03/11] introduce sbt-version-policy plugin --- build.sbt | 7 +++++-- project/plugins.sbt | 6 +++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/build.sbt b/build.sbt index 3f386f1..54bd88d 100644 --- a/build.sbt +++ b/build.sbt @@ -1,3 +1,5 @@ +import sbtversionpolicy.Compatibility.BinaryCompatible + name := "serially" organization := "com.evolutiongaming" @@ -18,6 +20,8 @@ Compile / doc / scalacOptions ++= Seq("-groups", "-implicits", "-no-link-warning publishTo := Some(Resolver.evolutionReleases) +versionPolicyIntention := BinaryCompatible + libraryDependencies ++= Seq( "com.evolutiongaming" %% "future-helper" % "1.0.6", "com.typesafe.akka" %% "akka-actor" % "2.6.8", @@ -26,6 +30,5 @@ libraryDependencies ++= Seq( licenses := Seq(("MIT", uri("https://opensource.org/licenses/MIT"))) -//addCommandAlias("check", "all versionPolicyCheck Compile/doc") -addCommandAlias("check", "show version") +addCommandAlias("check", "all versionPolicyCheck Compile/doc") addCommandAlias("build", "+all compile testFull") diff --git a/project/plugins.sbt b/project/plugins.sbt index 7bba46e..54e584d 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -4,4 +4,8 @@ addSbtPlugin("com.github.sbt" % "sbt-dynver" % "5.1.1") addSbtPlugin("com.evolution" % "sbt-scalac-opts-plugin" % "0.2.0") -addSbtPlugin("com.evolution" % "sbt-artifactory-plugin" % "0.1.2") \ No newline at end of file +addSbtPlugin("com.evolution" % "sbt-artifactory-plugin" % "0.1.2") + +addSbtPlugin("ch.epfl.scala" % "sbt-version-policy" % "3.3.0") + +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.6.2") From 9b18213355ebf1bdbb4b694e182f78e4e29ed517 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mareks=20Ramp=C4=81ns?= <8796159+mr-git@users.noreply.github.com> Date: Wed, 12 Aug 2026 10:45:02 +0300 Subject: [PATCH 04/11] update akka to 2.6.21 --- build.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index 54bd88d..ca5dd71 100644 --- a/build.sbt +++ b/build.sbt @@ -24,8 +24,8 @@ versionPolicyIntention := BinaryCompatible libraryDependencies ++= Seq( "com.evolutiongaming" %% "future-helper" % "1.0.6", - "com.typesafe.akka" %% "akka-actor" % "2.6.8", - "com.typesafe.akka" %% "akka-testkit" % "2.6.8" % Test, + "com.typesafe.akka" %% "akka-actor" % "2.6.21", // scala-steward:off + "com.typesafe.akka" %% "akka-testkit" % "2.6.21" % Test, // scala-steward:off "org.scalatest" %% "scalatest" % "3.2.9" % Test) licenses := Seq(("MIT", uri("https://opensource.org/licenses/MIT"))) From ad4ac1d6b168b5da3d8bd91ceb47c078f31c6e11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mareks=20Ramp=C4=81ns?= <8796159+mr-git@users.noreply.github.com> Date: Wed, 12 Aug 2026 10:45:13 +0300 Subject: [PATCH 05/11] update scalatest to 3.2.20 --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index ca5dd71..9e8d9dd 100644 --- a/build.sbt +++ b/build.sbt @@ -26,7 +26,7 @@ libraryDependencies ++= Seq( "com.evolutiongaming" %% "future-helper" % "1.0.6", "com.typesafe.akka" %% "akka-actor" % "2.6.21", // scala-steward:off "com.typesafe.akka" %% "akka-testkit" % "2.6.21" % Test, // scala-steward:off - "org.scalatest" %% "scalatest" % "3.2.9" % Test) + "org.scalatest" %% "scalatest" % "3.2.20" % Test) licenses := Seq(("MIT", uri("https://opensource.org/licenses/MIT"))) From 5bc7373c21d8ff3d06b6fe03266a3e3097e1a67e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mareks=20Ramp=C4=81ns?= <8796159+mr-git@users.noreply.github.com> Date: Wed, 12 Aug 2026 10:46:59 +0300 Subject: [PATCH 06/11] update future-helper to 1.0.7 --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 9e8d9dd..5fccafa 100644 --- a/build.sbt +++ b/build.sbt @@ -23,7 +23,7 @@ publishTo := Some(Resolver.evolutionReleases) versionPolicyIntention := BinaryCompatible libraryDependencies ++= Seq( - "com.evolutiongaming" %% "future-helper" % "1.0.6", + "com.evolutiongaming" %% "future-helper" % "1.0.7", "com.typesafe.akka" %% "akka-actor" % "2.6.21", // scala-steward:off "com.typesafe.akka" %% "akka-testkit" % "2.6.21" % Test, // scala-steward:off "org.scalatest" %% "scalatest" % "3.2.20" % Test) From e8828bc4030cf6626a49bae6f43e0754521f6723 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mareks=20Ramp=C4=81ns?= <8796159+mr-git@users.noreply.github.com> Date: Wed, 12 Aug 2026 10:47:35 +0300 Subject: [PATCH 07/11] update Scala to 2.13.18 and 3.3.8, drop Scala 2.12 support --- build.sbt | 2 +- .../com/evolutiongaming/concurrent/serially/Serially.scala | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/build.sbt b/build.sbt index 5fccafa..8813ef2 100644 --- a/build.sbt +++ b/build.sbt @@ -14,7 +14,7 @@ organizationHomepage := Some(uri("https://evolution.com")) scalaVersion := crossScalaVersions.value.head -crossScalaVersions := Seq("2.13.5", "2.12.13") +crossScalaVersions := Seq("2.13.18", "3.3.8") Compile / doc / scalacOptions ++= Seq("-groups", "-implicits", "-no-link-warnings") diff --git a/src/main/scala/com/evolutiongaming/concurrent/serially/Serially.scala b/src/main/scala/com/evolutiongaming/concurrent/serially/Serially.scala index b0291c1..9892ec5 100644 --- a/src/main/scala/com/evolutiongaming/concurrent/serially/Serially.scala +++ b/src/main/scala/com/evolutiongaming/concurrent/serially/Serially.scala @@ -21,10 +21,6 @@ object Serially { def apply(name: Option[String] = None)(implicit factory: ActorRefFactory): Serially = { - case class Func(f: () => Unit) - case class StopPrepare(promise: Promise[Unit]) - case class StopCommit(promise: Promise[Unit]) - def actor() = new Actor { def receive: Receive = { case Func(f) => f() @@ -83,6 +79,9 @@ object Serially { def stop() = Future.unit } + private case class Func(f: () => Unit) + private case class StopPrepare(promise: Promise[Unit]) + private case class StopCommit(promise: Promise[Unit]) case object Stopped extends RuntimeException with NoStackTrace } \ No newline at end of file From 028e84bca65db091a037fc5086643d51133d42f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mareks=20Ramp=C4=81ns?= <8796159+mr-git@users.noreply.github.com> Date: Wed, 12 Aug 2026 10:50:19 +0300 Subject: [PATCH 08/11] introduce and apply scalafmt --- .scalafmt.conf | 96 +++++++++++++++++++ build.sbt | 10 +- .../concurrent/serially/AsyncVar.scala | 6 +- .../concurrent/serially/Serially.scala | 18 ++-- .../concurrent/serially/SeriallyAsync.scala | 9 +- .../concurrent/serially/StateVar.scala | 13 ++- .../concurrent/serially/AsyncVarSpec.scala | 6 +- .../serially/SeriallyAsyncSpec.scala | 5 +- .../concurrent/serially/SeriallySpec.scala | 5 +- .../concurrent/serially/StateVarSpec.scala | 4 +- 10 files changed, 141 insertions(+), 31 deletions(-) create mode 100644 .scalafmt.conf diff --git a/.scalafmt.conf b/.scalafmt.conf new file mode 100644 index 0000000..d31bdbb --- /dev/null +++ b/.scalafmt.conf @@ -0,0 +1,96 @@ +# Main goals: +# - nicer commit diffs (trailing commas, no alignment for pattern matching, force new lines) +# - better interop with default IntelliJ IDEA setup (matching import and modifiers sorting logic) +# - better developer experience on laptop screens (like 16' MBPs) with IntelliJ IDEA (line wraps) + +version = 3.11.5 + +runner.dialect = scala213source3 + +# only format files tracked by git +project.git = true + +maxColumn = 120 +trailingCommas = always + +preset = default +# do not align to make nicer commit diffs +align.preset = none + +indent { + # altering defnSite and extendSite to have this: + # final class MyErr extends RuntimeException( + # "super error message", + # ) + # instead of this: + # final class MyErr extends RuntimeException( + # "super error message", + # ) + defnSite = 2 + extendSite = 0 +} + +spaces { + # makes string interpolation with curlies more visually distinct + inInterpolatedStringCurlyBraces = true +} + +newlines { + # keep author new lines where possible + source = keep + # force new line after "(implicit" for multi-line arg lists + implicitParamListModifierForce = [after] + avoidForSimpleOverflow = [ + tooLong, # if the line would be too long even after newline inserted, do nothing + slc, # do nothing if overflow caused by single line comment + ] +} + +verticalMultiline { + atDefnSite = true + arityThreshold = 4 # more than 3 args in a list will be turned vertical + newlineAfterOpenParen = true # for nicer commit diffs +} + +# for nicer commit diffs - forces new line before last parenthesis: +# class MyCls( +# arg1: String, +# arg2: String, +# ) extends MyTrait { +# +# without it: +# class MyCls( +# arg1: String, +# arg2: String) extends MyTrait { +danglingParentheses.exclude = [] + +docstrings { + # easier to view diffs in IDEA on 16' MBP screen if docs max line are shorter than code + wrapMaxColumn = 100 + # next settings make it similar to the default IDEA javadoc formatting + style = Asterisk + oneline = unfold + blankFirstLine = unfold +} + +rewrite.rules = [ + Imports, + RedundantParens, + SortModifiers, + prefercurlyfors, +] + +# put visibility modifier first +rewrite.sortModifiers.preset = styleGuide + +# Import sorting as similar as possible to scalafix's "OrganizeImports.preset = INTELLIJ_2020_3". +# Scalafix is not used as its commands mess up "all .." build aliases and it takes long time to run, +# while its code semantic based features are not needed here. +# I.e. detection of unused imports is done with Scala compiler options. +rewrite.imports { + sort = ascii + groups = [ + [".*"], + ["java\\..*", "javax\\..*", "scala\\..*"], + ] +} diff --git a/build.sbt b/build.sbt index 8813ef2..a5e299c 100644 --- a/build.sbt +++ b/build.sbt @@ -24,11 +24,13 @@ versionPolicyIntention := BinaryCompatible libraryDependencies ++= Seq( "com.evolutiongaming" %% "future-helper" % "1.0.7", - "com.typesafe.akka" %% "akka-actor" % "2.6.21", // scala-steward:off - "com.typesafe.akka" %% "akka-testkit" % "2.6.21" % Test, // scala-steward:off - "org.scalatest" %% "scalatest" % "3.2.20" % Test) + "com.typesafe.akka" %% "akka-actor" % "2.6.21", // scala-steward:off + "com.typesafe.akka" %% "akka-testkit" % "2.6.21" % Test, // scala-steward:off + "org.scalatest" %% "scalatest" % "3.2.20" % Test, +) licenses := Seq(("MIT", uri("https://opensource.org/licenses/MIT"))) -addCommandAlias("check", "all versionPolicyCheck Compile/doc") +addCommandAlias("check", "+all scalafmtCheckRepo versionPolicyCheck Compile/doc") +addCommandAlias("fmt", "+all scalafmtRepo") addCommandAlias("build", "+all compile testFull") diff --git a/src/main/scala/com/evolutiongaming/concurrent/serially/AsyncVar.scala b/src/main/scala/com/evolutiongaming/concurrent/serially/AsyncVar.scala index 089fee3..2cef070 100644 --- a/src/main/scala/com/evolutiongaming/concurrent/serially/AsyncVar.scala +++ b/src/main/scala/com/evolutiongaming/concurrent/serially/AsyncVar.scala @@ -15,7 +15,11 @@ trait AsyncVar[S] extends StateVar[S] { object AsyncVar { - def apply[S](state: S)(implicit factory: ActorRefFactory): AsyncVar[S] = { + def apply[S]( + state: S, + )(implicit + factory: ActorRefFactory, + ): AsyncVar[S] = { val serially = SeriallyAsync() apply(state, serially) } diff --git a/src/main/scala/com/evolutiongaming/concurrent/serially/Serially.scala b/src/main/scala/com/evolutiongaming/concurrent/serially/Serially.scala index 9892ec5..ee1022f 100644 --- a/src/main/scala/com/evolutiongaming/concurrent/serially/Serially.scala +++ b/src/main/scala/com/evolutiongaming/concurrent/serially/Serially.scala @@ -1,9 +1,8 @@ package com.evolutiongaming.concurrent.serially -import java.util.concurrent.atomic.AtomicBoolean - import akka.actor.{Actor, ActorRef, ActorRefFactory, Props} +import java.util.concurrent.atomic.AtomicBoolean import scala.concurrent.{Future, Promise} import scala.util.Try import scala.util.control.NoStackTrace @@ -19,20 +18,24 @@ object Serially { private val StoppedFailure = Future.failed(Stopped) - def apply(name: Option[String] = None)(implicit factory: ActorRefFactory): Serially = { + def apply( + name: Option[String] = None, + )(implicit + factory: ActorRefFactory, + ): Serially = { def actor() = new Actor { def receive: Receive = { - case Func(f) => f() + case Func(f) => f() case StopPrepare(promise) => self.tell(StopCommit(promise), ActorRef.noSender) - case StopCommit(promise) => promise.success(()); context.stop(self); + case StopCommit(promise) => promise.success(()); context.stop(self); } } val props = Props(actor()) val ref = name match { - case None => factory.actorOf(props) + case None => factory.actorOf(props) case Some(name) => factory.actorOf(props, name) } @@ -71,7 +74,6 @@ object Serially { } } - def now: Serially = new Serially { def apply[T](f: => T): Future[T] = Future.fromTry(Try(f)) @@ -84,4 +86,4 @@ object Serially { private case class StopCommit(promise: Promise[Unit]) case object Stopped extends RuntimeException with NoStackTrace -} \ No newline at end of file +} diff --git a/src/main/scala/com/evolutiongaming/concurrent/serially/SeriallyAsync.scala b/src/main/scala/com/evolutiongaming/concurrent/serially/SeriallyAsync.scala index f8208e7..40b2c8a 100644 --- a/src/main/scala/com/evolutiongaming/concurrent/serially/SeriallyAsync.scala +++ b/src/main/scala/com/evolutiongaming/concurrent/serially/SeriallyAsync.scala @@ -18,7 +18,11 @@ trait SeriallyAsync extends Serially { object SeriallyAsync { - def apply(name: Option[String] = None)(implicit factory: ActorRefFactory): SeriallyAsync = { + def apply( + name: Option[String] = None, + )(implicit + factory: ActorRefFactory, + ): SeriallyAsync = { val serially = Serially(name) apply(serially) } @@ -45,11 +49,10 @@ object SeriallyAsync { } } - def now: SeriallyAsync = new SeriallyAsync { def async[T](f: => Future[T]) = f def stop() = Future.unit } -} \ No newline at end of file +} diff --git a/src/main/scala/com/evolutiongaming/concurrent/serially/StateVar.scala b/src/main/scala/com/evolutiongaming/concurrent/serially/StateVar.scala index 7819a43..bf17b70 100644 --- a/src/main/scala/com/evolutiongaming/concurrent/serially/StateVar.scala +++ b/src/main/scala/com/evolutiongaming/concurrent/serially/StateVar.scala @@ -11,8 +11,9 @@ trait StateVar[S] { def apply[SS](f: S => (S, SS)): Future[SS] /** - * @return previous value - */ + * @return + * previous value + */ final def set(value: S): Future[S] = getAndUpdate(_ => value) final def update(f: S => S): Future[S] = updateAndGet(f) @@ -41,7 +42,11 @@ trait StateVar[S] { object StateVar { - def apply[S](state: S)(implicit factory: ActorRefFactory): StateVar[S] = { + def apply[S]( + state: S, + )(implicit + factory: ActorRefFactory, + ): StateVar[S] = { val serially = Serially() apply(state, serially) } @@ -65,4 +70,4 @@ object StateVar { override def toString = s"StateVar($s)" } } -} \ No newline at end of file +} diff --git a/src/test/scala/com/evolutiongaming/concurrent/serially/AsyncVarSpec.scala b/src/test/scala/com/evolutiongaming/concurrent/serially/AsyncVarSpec.scala index 1f11ab9..bb2fa46 100644 --- a/src/test/scala/com/evolutiongaming/concurrent/serially/AsyncVarSpec.scala +++ b/src/test/scala/com/evolutiongaming/concurrent/serially/AsyncVarSpec.scala @@ -1,10 +1,10 @@ package com.evolutiongaming.concurrent.serially - -import scala.util.Success import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec +import scala.util.Success + class AsyncVarSpec extends AnyWordSpec with Matchers { "AsyncVar" should { @@ -61,4 +61,4 @@ class AsyncVarSpec extends AnyWordSpec with Matchers { val serially = SeriallyAsync.now val state = AsyncVar(0, serially) } -} \ No newline at end of file +} diff --git a/src/test/scala/com/evolutiongaming/concurrent/serially/SeriallyAsyncSpec.scala b/src/test/scala/com/evolutiongaming/concurrent/serially/SeriallyAsyncSpec.scala index ecff974..e6d1f58 100644 --- a/src/test/scala/com/evolutiongaming/concurrent/serially/SeriallyAsyncSpec.scala +++ b/src/test/scala/com/evolutiongaming/concurrent/serially/SeriallyAsyncSpec.scala @@ -2,13 +2,12 @@ package com.evolutiongaming.concurrent.serially import com.evolutiongaming.concurrent.CurrentThreadExecutionContext import org.scalatest.concurrent.ScalaFutures +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec import scala.concurrent.duration._ import scala.concurrent.{Await, Promise, TimeoutException} import scala.util.control.NoStackTrace -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpec - class SeriallyAsyncSpec extends AnyWordSpec with ActorSpec with Matchers with ScalaFutures { diff --git a/src/test/scala/com/evolutiongaming/concurrent/serially/SeriallySpec.scala b/src/test/scala/com/evolutiongaming/concurrent/serially/SeriallySpec.scala index 96e72be..1f9c218 100644 --- a/src/test/scala/com/evolutiongaming/concurrent/serially/SeriallySpec.scala +++ b/src/test/scala/com/evolutiongaming/concurrent/serially/SeriallySpec.scala @@ -1,12 +1,12 @@ package com.evolutiongaming.concurrent.serially import org.scalatest.concurrent.ScalaFutures +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec import scala.concurrent.duration._ import scala.concurrent.{Await, Promise, TimeoutException} import scala.util.control.NoStackTrace -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpec class SeriallySpec extends AnyWordSpec with ActorSpec with Matchers with ScalaFutures { @@ -59,4 +59,3 @@ class SeriallySpec extends AnyWordSpec with ActorSpec with Matchers with ScalaFu case object TestException extends RuntimeException with NoStackTrace } - diff --git a/src/test/scala/com/evolutiongaming/concurrent/serially/StateVarSpec.scala b/src/test/scala/com/evolutiongaming/concurrent/serially/StateVarSpec.scala index 2dbdf54..5aec564 100644 --- a/src/test/scala/com/evolutiongaming/concurrent/serially/StateVarSpec.scala +++ b/src/test/scala/com/evolutiongaming/concurrent/serially/StateVarSpec.scala @@ -1,10 +1,10 @@ package com.evolutiongaming.concurrent.serially - -import scala.util.Success import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec +import scala.util.Success + class StateVarSpec extends AnyWordSpec with Matchers { "StateVar" should { From 1bb5452375dc8b14ee7e306e358fa1748b7b12fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mareks=20Ramp=C4=81ns?= <8796159+mr-git@users.noreply.github.com> Date: Wed, 12 Aug 2026 10:53:29 +0300 Subject: [PATCH 09/11] improve scalacOptions --- build.sbt | 29 +++++++++++++++++++ .../concurrent/serially/SeriallyAsync.scala | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index a5e299c..59864bc 100644 --- a/build.sbt +++ b/build.sbt @@ -16,6 +16,28 @@ scalaVersion := crossScalaVersions.value.head crossScalaVersions := Seq("2.13.18", "3.3.8") +scalacOptions ++= crossSettings( + scalaVersion = scalaVersion.value, + // Good compiler options for Scala 2.13 are coming from com.evolution:sbt-scalac-opts-plugin:0.1.0, + // but its support for Scala 3 is limited, especially what concerns linting options. + // + // If Scala 3 is made the primary target, good linting scalac options for it should be added first. + if3 = Seq( + "-Ykind-projector:underscores", + + // disable new brace-less syntax: + // https://alexn.org/blog/2022/10/24/scala-3-optional-braces/ + "-no-indent", + + // improve error messages: + "-explain", + "-explain-types", + ), + if2 = Seq( + "-Xsource:3", + ), +) + Compile / doc / scalacOptions ++= Seq("-groups", "-implicits", "-no-link-warnings") publishTo := Some(Resolver.evolutionReleases) @@ -31,6 +53,13 @@ libraryDependencies ++= Seq( licenses := Seq(("MIT", uri("https://opensource.org/licenses/MIT"))) +def crossSettings[T](scalaVersion: String, if3: T, if2: T): T = { + scalaVersion match { + case version if version.startsWith("3") => if3 + case _ => if2 + } +} + addCommandAlias("check", "+all scalafmtCheckRepo versionPolicyCheck Compile/doc") addCommandAlias("fmt", "+all scalafmtRepo") addCommandAlias("build", "+all compile testFull") diff --git a/src/main/scala/com/evolutiongaming/concurrent/serially/SeriallyAsync.scala b/src/main/scala/com/evolutiongaming/concurrent/serially/SeriallyAsync.scala index 40b2c8a..318cbe9 100644 --- a/src/main/scala/com/evolutiongaming/concurrent/serially/SeriallyAsync.scala +++ b/src/main/scala/com/evolutiongaming/concurrent/serially/SeriallyAsync.scala @@ -33,7 +33,7 @@ object SeriallyAsync { implicit val ec = CurrentThreadExecutionContext val tryUnit = Success(()) - val tryToUnit = (_: Try[_]) => tryUnit + val tryToUnit = (_: Try[?]) => tryUnit new SeriallyAsync { From 4d78b5ce1cd266513931da71a212d98517f488d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mareks=20Ramp=C4=81ns?= <8796159+mr-git@users.noreply.github.com> Date: Wed, 12 Aug 2026 10:57:30 +0300 Subject: [PATCH 10/11] expose types where they are due --- .../concurrent/serially/AsyncVar.scala | 6 ++--- .../concurrent/serially/Serially.scala | 4 ++-- .../concurrent/serially/SeriallyAsync.scala | 12 +++++----- .../concurrent/serially/StateVar.scala | 2 +- .../concurrent/serially/AsyncVarSpec.scala | 13 +++++----- .../serially/SeriallyAsyncSpec.scala | 24 +++++++++---------- .../concurrent/serially/SeriallySpec.scala | 20 ++++++++-------- .../concurrent/serially/StateVarSpec.scala | 13 +++++----- 8 files changed, 48 insertions(+), 46 deletions(-) diff --git a/src/main/scala/com/evolutiongaming/concurrent/serially/AsyncVar.scala b/src/main/scala/com/evolutiongaming/concurrent/serially/AsyncVar.scala index 2cef070..4207a87 100644 --- a/src/main/scala/com/evolutiongaming/concurrent/serially/AsyncVar.scala +++ b/src/main/scala/com/evolutiongaming/concurrent/serially/AsyncVar.scala @@ -2,7 +2,7 @@ package com.evolutiongaming.concurrent.serially import akka.actor.ActorRefFactory import com.evolutiongaming.concurrent.CurrentThreadExecutionContext -import com.evolutiongaming.concurrent.FutureHelper._ +import com.evolutiongaming.concurrent.FutureHelper.* import scala.concurrent.Future @@ -26,13 +26,13 @@ object AsyncVar { def apply[S](state: S, serially: SeriallyAsync): AsyncVar[S] = { - implicit val ec = CurrentThreadExecutionContext + implicit val ec: CurrentThreadExecutionContext.type = CurrentThreadExecutionContext @volatile var s = state new AsyncVar[S] { - def value() = s + def value(): S = s def async[SS](f: S => Future[(S, SS)]): Future[SS] = { serially.async[SS] { diff --git a/src/main/scala/com/evolutiongaming/concurrent/serially/Serially.scala b/src/main/scala/com/evolutiongaming/concurrent/serially/Serially.scala index ee1022f..1c702b1 100644 --- a/src/main/scala/com/evolutiongaming/concurrent/serially/Serially.scala +++ b/src/main/scala/com/evolutiongaming/concurrent/serially/Serially.scala @@ -60,7 +60,7 @@ object Serially { } } - def stop() = { + def stop(): Future[Unit] = { running { if (stopped.compareAndSet(false, true)) { val promise = Promise[Unit]() @@ -78,7 +78,7 @@ object Serially { def apply[T](f: => T): Future[T] = Future.fromTry(Try(f)) - def stop() = Future.unit + def stop(): Future[Unit] = Future.unit } private case class Func(f: () => Unit) diff --git a/src/main/scala/com/evolutiongaming/concurrent/serially/SeriallyAsync.scala b/src/main/scala/com/evolutiongaming/concurrent/serially/SeriallyAsync.scala index 318cbe9..bb4c6dd 100644 --- a/src/main/scala/com/evolutiongaming/concurrent/serially/SeriallyAsync.scala +++ b/src/main/scala/com/evolutiongaming/concurrent/serially/SeriallyAsync.scala @@ -2,7 +2,7 @@ package com.evolutiongaming.concurrent.serially import akka.actor.ActorRefFactory import com.evolutiongaming.concurrent.CurrentThreadExecutionContext -import com.evolutiongaming.concurrent.FutureHelper._ +import com.evolutiongaming.concurrent.FutureHelper.* import scala.concurrent.Future import scala.util.{Success, Try} @@ -31,13 +31,13 @@ object SeriallyAsync { var future = Future.unit - implicit val ec = CurrentThreadExecutionContext + implicit val ec: CurrentThreadExecutionContext.type = CurrentThreadExecutionContext val tryUnit = Success(()) val tryToUnit = (_: Try[?]) => tryUnit new SeriallyAsync { - def async[T](f: => Future[T]) = { + def async[T](f: => Future[T]): Future[T] = { serially { val result = if (future.isCompleted) f else future.flatMap(_ => f) future = FutureOps(result).transform(tryToUnit) @@ -45,14 +45,14 @@ object SeriallyAsync { }.flatten } - def stop() = serially.stop() + def stop(): Future[Unit] = serially.stop() } } def now: SeriallyAsync = new SeriallyAsync { - def async[T](f: => Future[T]) = f + def async[T](f: => Future[T]): Future[T] = f - def stop() = Future.unit + def stop(): Future[Unit] = Future.unit } } diff --git a/src/main/scala/com/evolutiongaming/concurrent/serially/StateVar.scala b/src/main/scala/com/evolutiongaming/concurrent/serially/StateVar.scala index bf17b70..03dbbcf 100644 --- a/src/main/scala/com/evolutiongaming/concurrent/serially/StateVar.scala +++ b/src/main/scala/com/evolutiongaming/concurrent/serially/StateVar.scala @@ -57,7 +57,7 @@ object StateVar { new StateVar[S] { - def value() = s + def value(): S = s def apply[SS](f: S => (S, SS)): Future[SS] = { serially { diff --git a/src/test/scala/com/evolutiongaming/concurrent/serially/AsyncVarSpec.scala b/src/test/scala/com/evolutiongaming/concurrent/serially/AsyncVarSpec.scala index bb2fa46..0a0feba 100644 --- a/src/test/scala/com/evolutiongaming/concurrent/serially/AsyncVarSpec.scala +++ b/src/test/scala/com/evolutiongaming/concurrent/serially/AsyncVarSpec.scala @@ -3,6 +3,7 @@ package com.evolutiongaming.concurrent.serially import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec +import scala.concurrent.Future import scala.util.Success class AsyncVarSpec extends AnyWordSpec with Matchers { @@ -10,7 +11,7 @@ class AsyncVarSpec extends AnyWordSpec with Matchers { "AsyncVar" should { "apply" in new Scope { - val result = state { before => + val result: Future[Int] = state { before => val after = before + 1 (after, before) } @@ -23,19 +24,19 @@ class AsyncVarSpec extends AnyWordSpec with Matchers { } "update" in new Scope { - val result = state.update(_ + 1) + val result: Future[Int] = state.update(_ + 1) result.value shouldEqual Some(Success(1)) state.value() shouldEqual 1 } "updateAndGet" in new Scope { - val result = state.updateAndGet(_ + 1) + val result: Future[Int] = state.updateAndGet(_ + 1) result.value shouldEqual Some(Success(1)) state.value() shouldEqual 1 } "getAndUpdate" in new Scope { - val result = state.getAndUpdate(_ + 1) + val result: Future[Int] = state.getAndUpdate(_ + 1) result.value shouldEqual Some(Success(0)) state.value() shouldEqual 1 } @@ -47,7 +48,7 @@ class AsyncVarSpec extends AnyWordSpec with Matchers { } "set" in new Scope { - val result = state.set(1) + val result: Future[Int] = state.set(1) result.value shouldEqual Some(Success(0)) state.value() shouldEqual 1 } @@ -58,7 +59,7 @@ class AsyncVarSpec extends AnyWordSpec with Matchers { } private trait Scope { - val serially = SeriallyAsync.now + private val serially = SeriallyAsync.now val state = AsyncVar(0, serially) } } diff --git a/src/test/scala/com/evolutiongaming/concurrent/serially/SeriallyAsyncSpec.scala b/src/test/scala/com/evolutiongaming/concurrent/serially/SeriallyAsyncSpec.scala index e6d1f58..5fa2624 100644 --- a/src/test/scala/com/evolutiongaming/concurrent/serially/SeriallyAsyncSpec.scala +++ b/src/test/scala/com/evolutiongaming/concurrent/serially/SeriallyAsyncSpec.scala @@ -5,8 +5,8 @@ import org.scalatest.concurrent.ScalaFutures import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec -import scala.concurrent.duration._ -import scala.concurrent.{Await, Promise, TimeoutException} +import scala.concurrent.duration.* +import scala.concurrent.{Await, Future, Promise, TimeoutException} import scala.util.control.NoStackTrace class SeriallyAsyncSpec extends AnyWordSpec with ActorSpec with Matchers with ScalaFutures { @@ -17,7 +17,7 @@ class SeriallyAsyncSpec extends AnyWordSpec with ActorSpec with Matchers with Sc var value = 0 val promise = Promise[Int]() serially { value = promise.future.futureValue } - val future = serially { value = 2 } + val future: Future[Unit] = serially { value = 2 } intercept[TimeoutException] { Await.result(future, 100.millis) } promise.success(1) Await.result(future, timeout.duration) @@ -28,7 +28,7 @@ class SeriallyAsyncSpec extends AnyWordSpec with ActorSpec with Matchers with Sc var value = 0 val promise = Promise[Int]() serially.async { promise.future.map(value = _)(CurrentThreadExecutionContext) } - val future = serially { value = 2 } + val future: Future[Unit] = serially { value = 2 } intercept[TimeoutException] { Await.result(future, 100.millis) } promise.success(1) Await.result(future, timeout.duration) @@ -36,20 +36,20 @@ class SeriallyAsyncSpec extends AnyWordSpec with ActorSpec with Matchers with Sc } "not fail on exceptions" in new Scope { - val future1 = serially { throw TestException } + val future1: Future[Nothing] = serially { throw TestException } intercept[TestException.type] { Await.result(future1, timeout.duration) } - val future2 = serially { 1 } + val future2: Future[Int] = serially { 1 } Await.result(future2, timeout.duration) shouldEqual 1 } "fail tasks when stopped" in new Scope { val promise = Promise[Int]() - val result0 = serially { promise.future.futureValue } - val result1 = serially { 1 } - val resultStop = serially.stop() - val result2 = serially { 2 } + val result0: Future[Int] = serially { promise.future.futureValue } + val result1: Future[Int] = serially { 1 } + val resultStop: Future[Unit] = serially.stop() + val result2: Future[Int] = serially { 2 } promise.success(0) @@ -59,14 +59,14 @@ class SeriallyAsyncSpec extends AnyWordSpec with ActorSpec with Matchers with Sc intercept[Serially.Stopped.type] { Await.result(result2, timeout.duration) } - val result3 = serially { 3 } + val result3: Future[Int] = serially { 3 } intercept[Serially.Stopped.type] { Await.result(result3, timeout.duration) } } } private trait Scope extends ActorScope { - val serially = SeriallyAsync()(system) + val serially: SeriallyAsync = SeriallyAsync()(system) } case object TestException extends RuntimeException with NoStackTrace diff --git a/src/test/scala/com/evolutiongaming/concurrent/serially/SeriallySpec.scala b/src/test/scala/com/evolutiongaming/concurrent/serially/SeriallySpec.scala index 1f9c218..b892e85 100644 --- a/src/test/scala/com/evolutiongaming/concurrent/serially/SeriallySpec.scala +++ b/src/test/scala/com/evolutiongaming/concurrent/serially/SeriallySpec.scala @@ -4,8 +4,8 @@ import org.scalatest.concurrent.ScalaFutures import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec -import scala.concurrent.duration._ -import scala.concurrent.{Await, Promise, TimeoutException} +import scala.concurrent.duration.* +import scala.concurrent.{Await, Future, Promise, TimeoutException} import scala.util.control.NoStackTrace class SeriallySpec extends AnyWordSpec with ActorSpec with Matchers with ScalaFutures { @@ -16,7 +16,7 @@ class SeriallySpec extends AnyWordSpec with ActorSpec with Matchers with ScalaFu var value = 0 val promise = Promise[Int]() serially { value = promise.future.futureValue } - val future = serially { value = 2 } + val future: Future[Unit] = serially { value = 2 } intercept[TimeoutException] { Await.result(future, 100.millis) } promise.success(1) Await.result(future, timeout.duration) @@ -24,20 +24,20 @@ class SeriallySpec extends AnyWordSpec with ActorSpec with Matchers with ScalaFu } "not fail on exceptions" in new Scope { - val future1 = serially { throw TestException } + val future1: Future[Nothing] = serially { throw TestException } intercept[TestException.type] { Await.result(future1, timeout.duration) } - val future2 = serially { 1 } + val future2: Future[Int] = serially { 1 } Await.result(future2, timeout.duration) shouldEqual 1 } "fail tasks when stopped" in new Scope { val promise = Promise[Int]() - val result0 = serially { promise.future.futureValue } - val result1 = serially { 1 } - val resultStop = serially.stop() - val result2 = serially { 2 } + val result0: Future[Int] = serially { promise.future.futureValue } + val result1: Future[Int] = serially { 1 } + val resultStop: Future[Unit] = serially.stop() + val result2: Future[Int] = serially { 2 } promise.success(0) @@ -47,7 +47,7 @@ class SeriallySpec extends AnyWordSpec with ActorSpec with Matchers with ScalaFu intercept[Serially.Stopped.type] { Await.result(result2, timeout.duration) } - val result3 = serially { 3 } + val result3: Future[Int] = serially { 3 } intercept[Serially.Stopped.type] { Await.result(result3, timeout.duration) } } diff --git a/src/test/scala/com/evolutiongaming/concurrent/serially/StateVarSpec.scala b/src/test/scala/com/evolutiongaming/concurrent/serially/StateVarSpec.scala index 5aec564..a2363f9 100644 --- a/src/test/scala/com/evolutiongaming/concurrent/serially/StateVarSpec.scala +++ b/src/test/scala/com/evolutiongaming/concurrent/serially/StateVarSpec.scala @@ -3,6 +3,7 @@ package com.evolutiongaming.concurrent.serially import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec +import scala.concurrent.Future import scala.util.Success class StateVarSpec extends AnyWordSpec with Matchers { @@ -10,7 +11,7 @@ class StateVarSpec extends AnyWordSpec with Matchers { "StateVar" should { "apply" in new Scope { - val result = state { before => + val result: Future[Int] = state { before => val after = before + 1 (after, before) } @@ -23,19 +24,19 @@ class StateVarSpec extends AnyWordSpec with Matchers { } "update" in new Scope { - val result = state.update(_ + 1) + val result: Future[Int] = state.update(_ + 1) result.value shouldEqual Some(Success(1)) state.value() shouldEqual 1 } "updateAndGet" in new Scope { - val result = state.updateAndGet(_ + 1) + val result: Future[Int] = state.updateAndGet(_ + 1) result.value shouldEqual Some(Success(1)) state.value() shouldEqual 1 } "getAndUpdate" in new Scope { - val result = state.getAndUpdate(_ + 1) + val result: Future[Int] = state.getAndUpdate(_ + 1) result.value shouldEqual Some(Success(0)) state.value() shouldEqual 1 } @@ -47,7 +48,7 @@ class StateVarSpec extends AnyWordSpec with Matchers { } "set" in new Scope { - val result = state.set(1) + val result: Future[Int] = state.set(1) result.value shouldEqual Some(Success(0)) state.value() shouldEqual 1 } @@ -58,7 +59,7 @@ class StateVarSpec extends AnyWordSpec with Matchers { } private trait Scope { - val serially = Serially.now + val serially: Serially = Serially.now val state = StateVar(0, serially) } } From 250280f281d998999a13e3bfc00994122e3e7cc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mareks=20Ramp=C4=81ns?= <8796159+mr-git@users.noreply.github.com> Date: Wed, 12 Aug 2026 13:49:49 +0300 Subject: [PATCH 11/11] TEMPORARY silence the bin-compat check --- .github/workflows/ci.yml | 3 +++ build.sbt | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 54a7b8a..fd048b3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,3 +8,6 @@ on: jobs: test: uses: evolution-gaming/scala-github-actions/.github/workflows/ci.yml@dde27b9bd793d41d5aacf8fb74403c9de5da1146 # v6.3.0 + # TODO remove after first release with Scala 3 support + with: + version_policy_check: false \ No newline at end of file diff --git a/build.sbt b/build.sbt index 59864bc..08db6f9 100644 --- a/build.sbt +++ b/build.sbt @@ -51,6 +51,13 @@ libraryDependencies ++= Seq( "org.scalatest" %% "scalatest" % "3.2.20" % Test, ) +// TODO remove after first release with Scala 3 support +versionPolicyIgnored ++= Seq( + // add libraries here that are known to be binary compatible, like: + "com.typesafe.akka" %% "akka-actor", + "org.scala-lang.modules" %% "scala-java8-compat", +) + licenses := Seq(("MIT", uri("https://opensource.org/licenses/MIT"))) def crossSettings[T](scalaVersion: String, if3: T, if2: T): T = {