Skip to content

feat: cross-compile dapr4s to Scala.js — ScalablyTyped facades over the Dapr JS SDK, Wasm+JSPI direct style#38

Merged
sideeffffect merged 18 commits into
masterfrom
feat/scala-js-cross-build
Jun 13, 2026
Merged

feat: cross-compile dapr4s to Scala.js — ScalablyTyped facades over the Dapr JS SDK, Wasm+JSPI direct style#38
sideeffffect merged 18 commits into
masterfrom
feat/scala-js-cross-build

Conversation

@sideeffffect

@sideeffffect sideeffffect commented Jun 11, 2026

Copy link
Copy Markdown
Owner

What

dapr4s now cross-compiles to Scala.js, publishing dapr4s_sjs1_3 alongside dapr4s_3. The public API is source-identical on both platforms — same capability traits, same DaprApp, same derivation macros, same direct (synchronous-looking) style. On JS the internal layer is backed by the Dapr JavaScript SDK (@dapr/dapr 3.18.0) through ScalablyTyped-generated facades.

How direct style survives on JavaScript

JS cannot block, and the project's design forbids an async/Future API fork (it would also fork the whole derivation layer). The one mechanism that preserves def get(key): Option[T] is the experimental WebAssembly backend + JSPI: every capability call suspends on the SDK promise via an orphan js.await (single site: src/js/internal/JsAwait.scala) — the exact analogue of a virtual thread parking in CompletableFuture.get(). Inbound requests each re-enter js.async (one "virtual thread" per request). JS consumers link with jsEmitWasm + ES modules on Node 25+; on the plain JS backend the pure parts (models, codecs, derivation) still link, and capability code fails at link time.

Layout

src/shared    src/jvm    src/js       — production code (shared as much as possible)
test/shared   test/jvm   test/js      — same split for tests

Platform-specific dependencies are target.platform-scoped filesjvm-deps.scala (Dapr Java SDK), jvm-test-deps.test.scala (testcontainers), js-deps.scala (ScalablyTyped facades). A using dep in a platform-tagged file applies only to that platform's build and POM, so there are no --exclude flags for dependency scoping and scala-cli compile --js . just works. (There is no --include in scala-cli — positional re-include of excluded files is silently ignored; the target.platform scoping turned out strictly better than both.)

ScalablyTyped facades (generated in the build, never committed)

scripts/generate-st-facades.sh runs the ScalablyTyped converter (cli_3:1.0.0-beta45, --scala 3.3.6 --scalajs 1.21.0 -s es2022) over the npm packages pinned in package.json/package-lock.json and publishes typed facades to ~/.ivy2/local; js-deps.scala pins the resulting coordinates (digests are deterministic from lockfile + converter version + flags). CI generates them with a cache; one hand-written shim survives (ExpressModule.scala — ST's express() is not callable under Node ESM).

Consumer note: the published dapr4s_sjs1_3 POM references org.scalablytyped:* coordinates that are not on Maven Central — downstream Scala.js users run the same generation script (reproduces identical digests) before linking. Publishing the facades under our own coordinates is a possible follow-up.

Platform-diverging surfaces: compile-time absence, not runtime exceptions

DaprCapability (shared) extends DaprCapabilityPlatform (per-platform); its companion extends DaprCapabilityCompanionPlatform. On the JVM the platform trait contributes jobs/conversation; on JS those members don't exist (the JS SDK has no jobs/conversation API), so misuse is a compile error instead of an UnsupportedOperationException. JobsCapability/ConversationCapability, their models, Jobs.derive, and the job forwarders live under src/jvm. The same technique (Forwarders extends ForwardersPlatform) covers the derivation layer.

What's implemented on JS

Building block Backing
state, pubsub publish, invocation, bindings, secrets, lock SDK DaprClient (HTTP)
configuration, crypto lazy gRPC DaprClient
workflow client DaprWorkflowClient (gRPC)
actor client raw sidecar HTTP (fetch) — the SDK doesn't export its low-level actor client
serve(): subscriptions, invoke routes, bindings, job routes, actor hosting express-based DaprAppServer twin (the JVM also hand-rolls the app channel)
serve(): workflow + activity hosting WorkflowRuntime + a hand-written AsyncGenerator coroutine bridge (Task.await() yields the SDK task, JSPI suspends the fiber); deterministic newUuid mirrors the Java SDK's v5/SHA-1
jobs, conversation not on this platform — compile-time absent

Testing (all in CI via a platform matrix)

ci.yml has one test matrix job (jvm, js — Scala Native someday is one more include entry). Each leg runs compile + unit + integration:

  • JVM: 166 unit tests; testcontainers integration suite (unchanged).
  • JS unit: 132 shared unit tests on the plain JS backend on Node.
  • JS integration: 8 munit suites (26 tests) running on the Wasm+JSPI backend (Node 25) against a real daprd 1.17 (redis + placement + scheduler, scripts/test-js-integration.sh): state incl. genuine stale-etag conflicts, pubsub round-trip through a served subscription, invoke echo, secrets, lock, configuration (gRPC), actor client→hosted actor→actor state, workflows (activity output + raiseEvent through a gated workflow). Verified stable across repeated local runs.

Harness facts worth knowing (all documented in AGENTS.md/wiki): the plain-JS linker wedges on orphan-await test sources (hence --exclude test/js/integration on the unit leg — the one remaining exclude, a linker workaround); scala-cli 1.14 exits 1 after green Wasm test runs (cleanup bug, tolerated precisely by scripts/wasm-test.sh); scala-cli runs the linked ES module from /tmp, where bare npm specifiers don't resolve — fixed with a NODE_OPTIONS --import resolution hook; --test-only is ineffective on the JS runner; UUID.randomUUID() doesn't link on Scala.js.

Verification beyond CI

  • Two adversarial multi-agent review rounds (43 + 15 agents); all confirmed findings fixed — including a real high-severity one (the JS SDK's if (params?.body) silently drops falsy JSON payloads 0/false/""/null; dapr4s routes those through raw sidecar HTTP, delivery re-verified via redis stream contents).
  • Cross-publish verified with publish local: dapr4s_sjs1_3 POM contains zero io.dapr/testcontainers coordinates; jar contents correctly split.
  • Workflow hosting proven by a 30/30 fake-executor harness plus live-sidecar e2e across replay episodes.

Known limitations / follow-ups

  • ST facades not on Central (consumer note above).
  • Crypto on JS compiles (gRPC client) but isn't e2e-exercised (needs a crypto component with local keys).
  • A bind failure leaves the workflow runtime running on the JVM twin too (fixed on JS here; JVM fix is a follow-up).
  • Upstream issue candidates: scala-cli Wasm-test cleanup bug (DirectoryNotEmptyException), plain-JS linker wedge on orphan awaits, @dapr/dapr task-hub-grpc-worker isFirstAttempt bug (daprd restart permanently kills the workflow worker).

The repo wiki gained a scala-js topic (cross-building, async/JSPI/Wasm, capture-checking-on-JS, ScalablyTyped-with-scala-cli) and a dapr/dapr-js-sdk.md API map with field notes from this port.

🤖 Generated with Claude Code

sideeffffect and others added 10 commits June 11, 2026 01:53
- project.scala: declare platforms jvm + scala-js (jvm default), jsEsVersionStr
  es2017 (required by js.async/js.await), scala-java-time (java.time on JS)
- jvm-deps.scala: JVM-only deps (Dapr Java SDK, testcontainers) split out so JS
  invocations can --exclude them (scala-cli cannot platform-scope dep directives)
- tag src/internal/**, src/jvm/Dapr.scala (moved from src/Dapr.scala) and
  JVM-bound tests with //> using target.platform jvm
- SidecarConfig TLS material: java.nio.file.Path -> new PemPath opaque type
  (java.nio.file does not exist on Scala.js)
- package.json: pin @dapr/dapr 3.18.0 for the upcoming JS internal layer

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- src/internal/js/facade/: @jsimport facades for @dapr/dapr (DaprClient +
  sub-clients, DaprWorkflowClient, enums, options) and Node-global fetch
- src/internal/js/JsAwait.scala: the single orphan-js.await site — JSPI suspends
  the Wasm stack instead of parking a virtual thread; plain-JS backend fails at
  link time by design
- src/internal/js/*CapabilityImpl.scala: state, publish, invoke, secrets,
  configuration (gRPC), bindings, lock, crypto (gRPC), actor (raw sidecar HTTP,
  HttpActorContext precedent), workflow (DaprWorkflowClient); jobs/conversation
  throw UnsupportedOperationException (absent from the JS SDK)
- src/js/Dapr.scala: run/runAsync with the JVM tryClose+suppression teardown;
  serve throws until the server phase lands
- test/TestCodecsJs.scala: ujson-based twin of the Jackson test codecs
- wiki: new scala-js topic (cross-building, async/JSPI/Wasm, CC-on-JS) +
  dapr/dapr-js-sdk.md API map, raw captures, index + log updates

Both platforms compile; JVM unit tests stay green (166); scalafmt clean
(2 new CC-syntax files added to excludeFilters).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mirrors the JVM design exactly: the JVM hand-rolls the app-channel protocol on
com.sun.net.httpserver (the Java SDK server is not used either), so the JS twin
hand-rolls the same protocol on express 4 (an @dapr/dapr dependency):

- facade/Express.scala: typed express facade (JSImport.Default proven correct
  for CJS+ESM interop by runtime smoke tests)
- HttpActorContext.scala: fetch-based twin (same routes, bodies, semantics;
  also sends dapr-api-token, which the JVM twin omits)
- DaprAppServer.scala: /dapr/subscribe, /dapr/config, CloudEvent subscription
  dispatch, all-verb invoke routes, binding/job routes, actor method/timer/
  reminder/deactivate routes; every handler re-enters js.async per request;
  SIGINT/SIGTERM drain with shutdownGrace
- WorkflowHost.scala: seam for the workflow-hosting phase (throws for now)
- src/js/Dapr.scala: serve() wired up, scaladoc updated

Verified end-to-end on Wasm+JSPI with Node 25.5 against curl: subscribe/config
JSON shape, SUCCESS/RETRY/DROP mapping, invoke/binding/job/actor dispatch,
base64 timer payload unwrap, SIGTERM clean exit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Dapr JS SDK drives workflows as async generators yielding Task objects;
dapr4s workflows are direct-style with blocking Task.await(). The bridge:

- WorkflowCoroutine.scala: hand-written AsyncGenerator (next/throw +
  Symbol.asyncIterator) over a js.async fiber; Task.await() yields the SDK
  Task and suspends via JSPI until next(v)/throw(e) resumes it. Safety: the
  executor awaits every step, so fiber and generator strictly alternate on
  the single JS thread
- WorkflowContextImpl.scala (JS twin): callActivity/createTimer/
  waitForExternalEvent(+timeout)/complete/continueAsNew/getInput; newUuid
  mirrors the Java SDK's deterministic v5/SHA-1 algorithm (same namespace,
  instanceId-timestamp-counter) via node:crypto
- WorkflowHost.scala: registers workflows (getClass.getSimpleName) and
  activities (activityName) with WorkflowRuntime; activity callbacks re-enter
  js.async; close() is synchronous fire-and-forget (signal-handler frame)
- DaprCapabilityImpl: fix gRPC endpoint mapping — grpc:// schemes render as
  'grpc:host:port' which grpc-js cannot resolve; plaintext now passes bare
  host, TLS passes https://host (the only spelling that sets tls)

Proven by a 30/30 fake-executor harness (Node 25 Wasm+JSPI) and a real
daprd 1.17 e2e: typed callActivity + callActivityByName workflow completed
across multiple replay episodes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- ci.yml: new test-js job (compile + unit tests on the plain JS backend);
  publish now needs it and publishes both dapr4s_3 and dapr4s_sjs1_3
- README: platforms section with the JS consumer recipe (Wasm+JSPI, Node 25+,
  @dapr/dapr), corrected requirements, --js build/test commands
- DESIGN.md: JS internal-layer diagram, per-platform async model, new
  'Scala.js platform' section (capability matrix, coroutine bridge, build
  pattern, divergence table), project structure refreshed to on-disk reality
- AGENTS.md: JS wall rules (facades in dapr4s.internal.facade, JsAwait-only
  orphan await, per-JS-frame js.async re-entry), JS commands, stale
  WorkflowCapability non-goal replaced
- SPEC.allium: platform note on the external client entity
- DaprConfig scaladoc: accurate JS-honoured knob list
- wiki: implementation field notes (express CJS interop, GrpcEndpoint scheme
  bug, executor driving rules, deterministic v5 UUID gap) + log entry

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- publish/invoke: falsy JSON scalar payloads (0, false, "", null) were
  silently dropped by the SDK's 'if (params?.body)' truthiness check — raw
  sidecar fetch fallback carries them; verified against daprd (redis XRANGE
  shows the exact documents arriving)
- waitForCompletion: pass fractional seconds (sub-second timeouts no longer
  reject immediately)
- bulkPublish: whole-request failures rethrow like the JVM instead of
  fabricating 'all entries failed'
- URL-encode every interpolated path segment in dapr4s-owned raw-fetch URLs
  (actor invoke, getWithETag, HttpActorContext, new raw publish/invoke)
- DaprAppServer: bind failure now closes the started WorkflowRuntime (leak
  kept the event loop alive and executed activities against a torn-down
  capability); WHY-SAFE comments softened accordingly
- HttpActorContext.get: consume the response body on early returns
- PemPath: JVM-only companion extension PemPath(java.nio.file.Path)
- package.json: intentional private manifest, @dapr/dapr pinned exactly
- AGENTS.md/DESIGN.md: correct the integration-tagging claim (apps/ fixtures
  cross-compile on purpose), JS-wall carve-outs for the entry points
- scripts/k8s-test.sh: fix stale pre-rename main-class package names
- comment/doc polish: HealthClient claim, empty-string-document caveat,
  TestCodecs assumeSafe rationale

JS unit tests now also RUN green on Node: 142/142 (JVM: 166/166).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…files

- src/{shared,jvm,js} + test/{shared,jvm,js} layout (git mv, history kept);
  packages unchanged; per-file target.platform directives still do the scoping
- jobs/conversation: compile-time absence instead of UnsupportedOperationException
  — DaprCapability extends DaprCapabilityPlatform (jvm: jobs+conversation with
  ^{this}; js: empty), companion extends DaprCapabilityCompanionPlatform;
  JobsCapability/ConversationCapability + their models + Jobs.derive engine +
  job forwarders moved to src/jvm; calling them from JS code no longer compiles
- platform-scoped dependency files replace --exclude entirely: a using dep in a
  target.platform-tagged file applies only to that platform (empirically
  verified incl. clean POMs and --cross publish); testcontainers moved to
  jvm-test-deps.test.scala (.test.scala gives test scope; test.dep is NOT
  platform-scoped); js-deps.scala placeholder for the ScalablyTyped deps
- test split: jobs/conversation model+derivation tests → test/jvm
  (JVM 166/166 green, JS 132/132 green; both platforms compile clean from
  scratch, JS classpath free of dapr-sdk/testcontainers)

Known nightly compiler bug worked around: importing language.experimental.safe
in the split-out jvm model files breaks unrelated @assumeSafe enums on clean
builds; the files stay out of safe mode with documented rationale.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Scala facade over @dapr/dapr is now generated by the ScalablyTyped
converter as a build step (never committed):

- scripts/generate-st-facades.sh: pinned stc invocation
  (org.scalablytyped.converter:cli_3:1.0.0-beta45, --scala 3.3.6
  --scalajs 1.21.0 -s es2022) publishing to ivy2Local; idempotent warm skip;
  guards that its digests agree with js-deps.scala
- js-deps.scala: org.scalablytyped::dapr__dapr::3.18.0-d1e27c + express +
  node deps (platform-scoped via target.platform); digests are deterministic
  from package-lock.json + converter version + flags
- package.json: @types/express + @types/node as top-level deps (converter
  input), typescript devDep; package-lock.json now committed (digest
  determinism + CI cache key)
- src/js rewritten against typings.*: all 16 internal impls + Dapr.scala;
  5 hand facades deleted; the one survivor is the express default-import
  shim (ST's express() is not callable under Node ESM)
- documented TS-vs-wire mismatches preserved with casts (IEtag {value} vs
  plain string, stop(): void returning a Promise, getBulk row shape) and the
  ESM rule: deep @dapr/dapr module specifiers don't resolve under Node ESM —
  values only via root re-exports (LockStatus pinned numerically)

Verified: clean-build compile on both platforms (zero warnings), JVM
166/166, JS unit 132/132, and a 9/9 Wasm+JSPI smoke against daprd 1.17
(state/etag/publish incl. falsy fallback/invoke/workflow/configuration-gRPC).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ecar

8 munit suites (test/js/integration/, 26 tests) exercising the real
@dapr/dapr SDK on Node 25: state (incl. genuine stale-etag conflict — Redis
etags are integers, fabricated strings get 400 not 409), pubsub roundtrip
through a served subscription, invoke echo, secrets, lock, configuration
(gRPC), actors (client invoke -> hosted actor -> actor state), workflows
(activity-doubled output + raiseEvent through a gated workflow).

Harness:
- scripts/js-integration-env.sh: redis + placement + scheduler + daprd 1.17
  (host network, non-default ports) + the Wasm-packaged JsTestServer; down()
  also pkills orphaned servers by dist path (a stale server holds the app
  port, the new one dies EADDRINUSE, and workflow tests then time out
  against the stale server's dead workflow worker)
- scripts/wasm-test.sh: tolerates exactly the known scala-cli wasm cleanup
  bug (DirectoryNotEmptyException after green runs)
- scripts/test-js-integration.sh: one-command up -> test -> down
- scripts/js-it/node-resolve-hook.mjs (+delegate): NODE_OPTIONS --import
  module-resolution hook — scala-cli runs the linked ES module from /tmp,
  where bare specifiers can't reach the repo's node_modules (ESM ignores
  NODE_PATH/CWD); the hook retries them against the repo root

Notes: JsItEnv.uniqueId avoids java.util.UUID.randomUUID (does not link on
Scala.js — needs SecureRandom); the plain-JS unit leg must exclude
test/js/integration (orphan-await suites wedge the plain-JS linker);
--test-only is ineffective on the JS test runner (unit suites run too,
harmlessly).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CI (.github/workflows/ci.yml):
- one 'test' matrix job (legs jvm + js; Scala Native someday = one more
  include entry), each leg running compile + unit + integration tests
- js leg: Node 25 (JSPI), npm ci, ScalablyTyped generation with an
  actions/cache keyed on package-lock.json + the generation script
- publish needs [format, test], generates ST facades, publishes both
  platforms — no --exclude flags anywhere (target.platform-scoped deps)

Review fixes (focused adversarial round, 11 confirmed findings):
- rawInvoke: metadata headers now REPLACE colliding base headers (pairs-array
  HeadersInit appends and combines duplicates as 'v1, v2', which would corrupt
  Content-Type/dapr-api-token)
- queryState: restored the absent-results guard (the SDK only substitutes
  {results: []} for an EMPTY body; a results-less JSON body passes through)
- generate-st-facades.sh: skip guard now requires all three root jars (an
  interrupted run no longer wedges the build behind a single-jar marker)
- js-integration-env.sh: failure diagnostics (server.log + docker logs tails)
  before dying — CI runners are discarded with the evidence otherwise
- docs truthfulness: stale internal/js path, '--exclude not needed anywhere'
  overclaims qualified, wiki test --cross caveat, README scala-cli >= 1.14
  note for the JS harness, broken wiki index link dropped (+ lint log entry)

Docs: README (layout, ST consumer recipe, four test legs), DESIGN.md
(platform-trait technique, ST pipeline, test architecture, refreshed
structure tree), AGENTS.md (reconciled with ci.yml; platform-trait pattern
documented), SPEC.allium path fix, wiki (new scalablytyped-with-scala-cli
article; corrected the dep-leak claim — only test.dep leaks; JSPI field
notes; isFirstAttempt worker bug; Redis integer-etag behaviour).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sideeffffect sideeffffect changed the title feat: cross-compile dapr4s to Scala.js over the Dapr JS SDK feat: cross-compile dapr4s to Scala.js — ScalablyTyped facades over the Dapr JS SDK, Wasm+JSPI direct style Jun 12, 2026
sideeffffect and others added 8 commits June 12, 2026 23:52
Addresses the Phase-3 review follow-ups:

- Move the Scala.js-only deps/settings out of project.scala into js-deps.scala
  (scala-java-time, jsEsVersionStr "es2017") and add the symmetric empty
  js-test-deps.test.scala placeholder, so project.scala holds cross deps only.

- Make the published dapr4s_sjs1_3 self-contained. The ScalablyTyped facades are
  generated into a dapr4s-specific `dapr4styped.*` package (renamed from the
  default `typings.*` so they can never collide with a consumer's own ST
  output), declared `compileOnly` so they never enter the POM, and embedded
  into the jar at publish time via scripts/embed-st-facades.sh + `--resource-dirs`.
  Verified by a local publish: the POM references only Maven Central, and the jar
  carries all 12,119 dapr4styped/*.sjsir — consumers need no ST regeneration.

- Close the two integration-test coverage gaps so every JS-supported capability
  is exercised against a live sidecar on BOTH platforms: add the JVM
  ConfigurationCapabilityServerTest (configuration.redis, the twin of the JS
  configuration suite) and the JS CryptoJsIntegrationTest (crypto.dapr.localstorage
  with a per-run RSA key, the twin of the JVM crypto suite). jobs/conversation
  remain JS-absent at compile time (not untested); bindings is the lone symmetric
  gap (derivation + unit only). The in-code (JVM, testcontainers-dapr) vs YAML
  (JS) component-config mechanism — and why there is no scripts/jvm-it/components/
  — is documented under "Integration-test coverage parity" in docs/DESIGN.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The facades are `compileOnly.dep` (js-deps.scala) so the ivy-local-only
org.scalablytyped coordinates stay out of the published POM. compileOnly keeps
them on the compile and TEST classpaths — so `scala-cli compile --js` and
`scala-cli test --js` link fine (the JS unit leg, incl. CapabilityDerivationTest
which reaches the gRPC client path, is green) — but NOT on the runtime classpath
that `scala-cli package` links against. Building JsTestServer with
`package --test --js-emit-wasm` therefore failed at link time with
"Referring to non-existent class dapr4styped..." / "Cannot access module for
non-module ...", which surfaced only now because this is the first CI build using
the compileOnly facades (the previous green run predated the
typings.* -> dapr4styped.* + compileOnly rework).

Fix: scripts/st-link-jars.sh resolves the exact transitive org.scalablytyped jar
set (the same one embed-st-facades.sh embeds at publish) and emits `--jar` flags;
js-integration-env.sh passes them to the JsTestServer package step. The linker
de-duplicates against the compileOnly deps (verified: no duplicate-class errors)
and `--jar` never touches the POM. Reproduced the failure and the fix locally
(package exit 1 -> 0). Documented in docs/DESIGN.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…age`

The previous commit fixed the JsTestServer `package` link but the suite run
(`scala-cli test --js-emit-wasm` via wasm-test.sh) hit the same
"Referring to non-existent class dapr4styped…" link errors — from
StateJsIntegrationTest / WorkflowJsIntegrationTest reaching IRequest / IEtag /
OperationType / WorkflowRuntimeStatus. So it is the Wasm backend's link
(both `package` and `test --js-emit-wasm`) that excludes compileOnly deps, not
`package` specifically; the plain-JS unit `test` leg links without the facades,
which is what misled the first fix.

test-js-integration.sh now passes the same scripts/st-link-jars.sh `--jar` set to
the Wasm test invocation. Verified locally: the suite link now succeeds (it
proceeds to the run phase, failing only on the absent local sidecar/NODE hook,
which CI provides). Also sanity-checked the new crypto suite's key material:
`openssl genpkey -algorithm RSA` emits the PKCS#8 PEM crypto.dapr.localstorage
expects. DESIGN.md note corrected to cover both Wasm link commands.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Single source of truth scripts/it/components/*.yaml + scripts/it/secrets.json,
rendered per topology by scripts/it/render-components.sh (only redisHost differs:
JS host-network renders localhost:6391). The JS harness now assembles a per-run
resource dir (rendered components + shared secrets.json + fresh RSA key) mounted
into daprd at /dapr4s-it, replacing the JS-only scripts/js-it/components set.

Backends unified toward 'redis everywhere': state/pubsub/lock/configuration =
redis, secrets = local.file, crypto = localstorage. JS test seeds aligned
(it-secret-a/b, dapr4s-it-cfg-a/b). JVM suites move onto the same shared set in a
follow-up commit. Spec: docs/JVM-JS-PARITY.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Unify state/secrets/lock/crypto/configuration across platforms: each capability's
calls+assertions live once as a shared scenario trait in test/shared/scenarios
(self: munit.Assertions =>, shared API + given DaprCapability). JVM and JS suites
are now thin shells that own only bring-up and the sync/Future boundary and call
the same scenarios — equal coverage, one set of assertions.

JVM: new SharedDaprItSuite mirrors the JS harness's single all-components sidecar —
one daprd on a real Redis loading the canonical scripts/it/components set via
DaprContainer.withComponent(Path), with shared secrets.json + a fresh RSA key
mounted at /dapr4s-it (JvmItComponents renders with redisHost redis:6379). This
moves JVM state in-memory->redis and secrets local.env->local.file, matching JS.

Removed the now-redundant server-routed/direct JVM twins (State/Secrets/Lock/
Crypto/Configuration *CapabilityServerTest + *IntegrationTest); route dispatch is
covered by the unit ServerRouteDerivationTest. ETag scenarios use a stale-but-real
etag (Redis rejects non-numeric etags with 400 rather than a conflict).

Verified locally against daprd 1.17 + redis: 20/20 across the 5 new JVM ItTest
suites. JS twins compile; run on the live sidecar in CI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…h gap

InvokeScenarios (test/shared) holds the caller side — echo, falsy-0, the derived
EchoService facade, and the non-existent-app error path — with two per-platform
hooks: serverAppId and a retrying strategy (identity on JVM where sidecar health is
polled up front; retryUntilSuccess on JS for app-channel warmup). The server
bring-up stays platform-specific (a host DaprAppServer on JVM, JsItServerApp on JS)
since the server runtimes differ.

Both platforms now run the SAME four invoke scenarios — including the
non-existent-app error path, which previously had no JS twin (it was the JVM-only
InvokeIntegrationTest). Replaces InvokeCapabilityServerTest + InvokeIntegrationTest
with InvokeItTest; JS InvokeJsIntegrationTest gains the error-path test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Finish the 'redis everywhere' conversion: every JVM integration suite now runs on
the shared scripts/it/components set instead of state.in-memory/pubsub.in-memory,
matching the JS harness.

- New RedisFixture: stands up a Redis container (managed outside testcontainers-scala's
  Containers, so suites keep their Containers type and unchanged test bodies) and renders
  the shared components, for the bespoke server-delivery suites that cannot use
  SharedDaprItSuite (host DaprAppServer the sidecar calls back into; two-phase actor/
  workflow startup).
- Converted PubSub/Publish/Actor/Workflow + the app-level Order/Inventory/EndToEnd suites
  to feed daprd the shared rendered manifests via DaprContainer.withComponent(Path);
  actor/workflow state.redis (actorStateStore) on the SHARED network.
- JvmItComponents.render now keys components by name (rendered.component("statestore")).

Verified: all 15 JVM integration suites pass on redis (Actor 14, Workflow 5, Publish 6,
PubSub 3, Order/Inventory/EndToEnd 7 each, + the direct-call ItTests). JS unchanged
(already on redis). Docs: DESIGN.md parity section + docs/JVM-JS-PARITY.md (done) + wiki.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@sideeffffect sideeffffect merged commit 0317d09 into master Jun 13, 2026
8 checks passed
sideeffffect added a commit that referenced this pull request Jun 13, 2026
The cross-build split (v0.20.0, #38) moved the JVM-only conversation models into
src/jvm/ConversationModels.scala, which is deliberately NOT compiled in safe mode
(its safe import tripped a 3.10.0-RC1 capture-checker bug on unrelated optypes
enums). As a side effect the object ConversationMessage's explicitly-declared
factory `def`s (system/user/assistant/tool/developer) became unreferable from
downstream safe-mode code — breaking the dapr4s-examples conversation demo, which
constructs messages from a safe PureModule.

Annotate object ConversationMessage @scala.caps.assumeSafe (the same escape hatch
ConversationCapability and the other src/jvm objects already use). The factories
are pure case-class wrappers, so erasing their empty capture set is sound.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant