| CI | Release |
|---|---|
A Scala 3 library that exposes every Dapr building block as a
capture-checked capability. Dapr effects — state, pub/sub, service invocation,
secrets, configuration, bindings, distributed locks, actors, workflows,
cryptography, jobs, and conversation (LLM) — are
modelled as scala.caps.Capability values that the compiler tracks. User code
compiles under import language.experimental.safe, so the compiler statically
guarantees that a Dapr resource can never escape the scope that owns it. The Dapr
SDKs (Java SDK on the JVM, @dapr/dapr on Scala.js) are hidden entirely; users
see only Scala types.
- Scala
3.10.0-RC1-…-NIGHTLY(capture checking / safe mode; exact version pinned inproject.scala) - scala-cli
>= 1.13.0(older versions cannot build the Scala.js platform;>= 1.14to run the Scala.js integration harness,scripts/test-js-integration.sh) - JVM 25 (for the JVM platform)
- For Scala.js builds:
npm ci+scripts/generate-st-facades.sh(once per machine — see below); Node 25+ to run capability-touching code - Docker (only for the integration tests, which exercise a real
daprdsidecar + Redis)
Sources live in src/{shared,jvm,js} and test/{shared,jvm,js} (platform-specific files also
carry a per-file //> using target.platform directive). Platform-specific dependencies live in
jvm-deps.scala / jvm-test-deps.test.scala / js-deps.scala, each scoped by its own
target.platform directive — so plain --js invocations just work, with no --exclude flags
for dependency scoping.
scala-cli compile . # JVM
scala-cli test . --test-only 'dapr4s.test.unit.*' # JVM unit tests
scala-cli test . --test-only 'dapr4s.test.integration.*' # JVM integration tests (needs Docker)
scripts/generate-st-facades.sh # once per machine, before any --js build
scala-cli compile --js . # Scala.js
scala-cli test --js . --exclude test/js/integration --test-only 'dapr4s.test.unit.*' # Scala.js unit tests
PATH=<node25-bin>:$PATH scripts/test-js-integration.sh # Scala.js integration tests (Docker + Node 25)The one remaining --exclude (of test/js/integration) exists only because those Wasm-only
suites use orphan js.await, which wedges the plain-JS linker; the integration script runs
them on the Wasm backend instead, against a real daprd 1.17 + Redis environment.
//> using dep "com.github.sideeffffect::dapr4s::<version>"(The :: before the version resolves the right platform artifact — dapr4s_3 on
the JVM, dapr4s_sjs1_3 on Scala.js.)
import dapr4s.*
DaprCapability.state(StateStoreName("statestore")): // StateCapability^{cap} in scope
StateCapability.save(key, value)
// Using StateCapability here — outside the block — is a compile error.The public API is identical on the JVM and on Scala.js — synchronous, direct style on both. On the JVM, blocking calls park virtual threads; on Scala.js, the same calls suspend the WebAssembly stack via JSPI (JavaScript Promise Integration) while the Node event loop keeps running.
Scala.js supports the full capability matrix — state, pub/sub, invocation,
bindings, secrets, configuration, locks, crypto, actors, workflows, and
serve() with full app-channel parity (subscriptions, invoke routes, input
bindings, job routes, actor hosting, workflow hosting) — except jobs and
conversation, which the Dapr JS SDK has no API for. On Scala.js those two
methods simply don't exist at compile time (they live on a JVM-only platform
trait), so using them is a compile error, not a runtime exception — use the JVM
platform for those.
JS consumers of capabilities must link with the experimental WebAssembly backend
and run on Node 25+ (or Node 23/24 with --experimental-wasm-jspi); the pure
parts of dapr4s (models, codecs, derivation) also link on the plain JS backend:
//> using platform "scala-js"
//> using jsEmitWasm true
//> using jsModuleKind "es"
//> using jsEsVersionStr "es2017"
//> using dep "com.github.sideeffffect::dapr4s::<version>"Run npm install @dapr/dapr so the SDK is resolvable from the directory Node
executes in, then enter js.async { ... } once at the program edge (or use the
JS-only runAsync/serveAsync, which wrap it for you):
import dapr4s.*
import scala.scalajs.js
// one-shot request/response, with the single js.async entry at the program edge:
def main(args: Array[String]): Unit =
js.async {
Dapr().run:
summon[DaprCapability].state(StateStoreName("statestore")).get(StateStoreKey("k"))
}: UnitThe dapr4s_sjs1_3 artifact is self-contained: the ScalablyTyped-generated facades of
@dapr/dapr/express/Node that the JS interop layer is written against ship inside the
dapr4s jar (under the dapr4s-specific dapr4styped.* package, so they can never collide
with a typings.* generation of your own), and the POM references only Maven-Central
artifacts. Using dapr4s from Scala.js is therefore an ordinary dependency — no converter, no
local ivy repository, nothing beyond the using dep above plus npm install @dapr/dapr.
Building dapr4s itself (this repository) is different: the facades are compile-only
dependencies resolved from ~/.ivy2/local, so run npm ci followed by
scripts/generate-st-facades.sh once per machine before the first scala-cli compile --js ..
The facade digests are deterministic in (package-lock.json, converter version, converter
flags), the script is idempotent and skips instantly when the jars already exist, and at
publish time scripts/embed-st-facades.sh packs the generated classes into the jar.
See DESIGN.md for the architecture, the two-layer
(safe / @assumeSafe shell) model, and the Scala.js platform details, and
dapr4s-examples for runnable examples.
This work has been sponsored by Chili Piper.
Apache-2.0