Skip to content

feat(wasm): java.time formatting, ofPattern and region zones on Wasm components - #48

Draft
sideeffffect wants to merge 6 commits into
feat/wasm-componentfrom
feat/wasm-java-time
Draft

sideeffffect wants to merge 6 commits into
feat/wasm-componentfrom
feat/wasm-java-time

Conversation

@sideeffffect

@sideeffffect sideeffffect commented Oct 17, 2026 •

Copy link
Copy Markdown
Owner

Stacked on #44 (base feat/wasm-component).

What

Shared code on the Wasm-component path can now use java.time formatting and parsing. With an unreleased scala-java-time (built from source), also DateTimeFormatter.ofPattern and region zones.

  • dapr4s-wasm (wallWasm) depends on scala-java-time 2.8.0 (version from the root build.sbt) and scala-java-locales 1.6.0. Both are in its POM, so every app gets them, in source and release mode.
  • scripts/wasm/build-scala-java-time.sh builds scala-java-time from source and publishLocal's it as 2.8.1-dapr4s-local (scalaJavaTimeWasm in wasm/build.sbt). It is cquiroz/scala-java-time main at 48cf7a6, the merge commit of Compute the raw offset of TimeZone.getTimeZone lazily cquiroz/scala-java-time#618 (merged 2026-10-17; no merge step in the script), with the tzdb module compiled by a Scala 3 nightly. CI runs it with a JDK 17 and caches ~/.ivy2/local/io.github.cquiroz by the script's hash.
  • Only the test components use the source build: counter and the consumer check depend on scala-java-time + scala-java-time-tzdb 2.8.1-dapr4s-local. The dapr4s-wasm POM stays on 2.8.0, because Central must have every version it names (wasm publishing is ON).
  • test/wasm/counter has a java-time route. scripts/wasm/e2e.sh asserts its exact answer under daprd (source mode). The consumer check asserts it too (release mode).
  • scripts/wasm/e2e.sh warms the component up until an invocation succeeds (counter is now ~4 MB with the tz database; on the CI runner the first call after "up and running" failed), and every failed invoke now reaches fail() with dapr's answer instead of a silent set -e exit.
  • AGENTS.md, docs/WASM-COMPONENTS-PROPOSAL.md, docs/DESIGN.md and the wiki (article, index.md, log.md) state the new rule.

The two scala-java-time gaps and their fixes

  • DateTimeFormatter.ofPattern (any pattern) did not link. Any pattern can contain z, so the linker keeps ZoneTextPrinterParser. Its print/parse call TimeZone.getTimeZone, and the JS TimeZone.getTimeZone(zoneId) computed the raw offset at Instant.now (the wall clock, js.Date.now). Fix: Compute the raw offset of TimeZone.getTimeZone lazily cquiroz/scala-java-time#618 computes the raw offset lazily (merged 2026-10-17, waits for a scala-java-time release > 2.8.0).
  • Region zones (ZoneId.of("Europe/Prague"), scala-java-time-tzdb) did not link. The provider's @EnableReflectiveInstantiation registration is a static initializer, and the linker always keeps static initializers. Scala 3 <= 3.10.0 emits it with the JS-backed Reflect.registerInstantiatableClass(..., js.Array(...)). This is a compiler gap, not a scala-java-time one: Scala 3 emits the Scala-typed registerInstantiatableClassV2 since scala/scala3@3bb676972 (on release-3.10.1, not in 3.10.0). So the script compiles only the tzdb module with nightly 3.10.2-RC1-bin-20261017-ae374e9-NIGHTLY (core stays on 3.9.0). It rewrites the nightly scala3-library version in the tzdb POM back to 3.9.0, so it does not evict the app's scala3-library.

Both failures are reproduced here with the released 2.8.0 (link errors: Instant$.now called from TimeZone$.getTimeZone called from ZoneTextPrinterParser.parse; Reflect$.registerInstantiatableClass called from TzdbZoneRulesProvider.<stinit>, "called from core module analyzer").

Retires with a scala-java-time release > 2.8.0 (has #618); region zones also need that release built with Scala 3 >= 3.10.1. Then pin it in dapr4s-wasm and drop scalaJavaTimeWasm, the script and its CI steps.

Earlier, without a source build

scala-java-locales 1.6.0 (released 2026-10-17) contains cquiroz/scala-java-locales#640. scala-java-time 2.8.0 contains cquiroz/scala-java-time#616 and cquiroz/scala-java-time#617. With scala-java-locales 1.5.4 the base java.time code fails to link (DefaultLocale.scala: global:window, through DateTimeFormatter.ISO_INSTANT).

What works on the wasm path (link AND run)

Answer from the component under wasmtime + daprd (the same as on the JVM):
instant=2026-10-17T10:15:30Z;date=2026-10-18;zoned=2026-10-17T12:15:30+02:00;same=true;iso=2026-10-17T05:15:30-05:00;rfc=Sat, 17 Oct 2026 12:15:30 +0200;day=Saturday;default=UTC;utc=2026-10-17T10:15:30Z[UTC];duration=PT1H45M;prague=2026-10-17T12:15:30+02:00[Europe/Prague];winter=2026-12-24T18:30+01:00[Europe/Prague];newyork=-04:00;pattern=17.10.2026 12:15 Europe/Prague;parsed=2026-12-24T18:30+01:00[Europe/Prague];text=Saturday 17 October 2026

  • Parsing and toString: Instant, LocalDate, ZonedDateTime, Duration.
  • Predefined formatters: ISO_OFFSET_DATE_TIME, RFC_1123_DATE_TIME.
  • ofPattern format and parse, also with a region zone (VV) and month/day names (source build).
  • Localized text: DayOfWeek.getDisplayName(FULL, ENGLISH).
  • Zones: ZoneId.systemDefault() = UTC, ZoneId.of("UTC"), ZoneOffsets; Europe/Prague (summer and winter time) and America/New_York (source build).

What still does not link

These are documented in AGENTS.md, with how each one goes away:

  • Wall-clock (Instant.now, Clock.system*, currentTimeMillis, TimeZone.getRawOffset): the scala-wasm javalib calls js.Date.now. This is the same on 1.22.1-wasm.6 and on its scala-wasm branch today. Out of reach for dapr4s.
  • With the released scala-java-time 2.8.0: ofPattern and region zones (see above).
  • println and UUID.randomUUID: not re-tested, so the old rule stays.

How verified (locally, as CI does)

  • scripts/wasm/build-scala-java-time.sh (fresh clone, JDK 17): OK. The tzdb IR calls registerInstantiatableClassV2; its POM names scala3-library 3.9.0.
  • Root: sbt scalafmtCheckAll scalafmtSbtCheck: OK.
  • wasm/: scalafmtCheckAll scalafmtSbtCheck -Ddapr4s.wasm.core=source wallWasm/test: 15/15.
  • SBT_ARGS=-Ddapr4s.wasm.core=source scripts/wasm/e2e.sh: OK (counterRaw, counter incl. java-time, importsProbe).
  • sbt wallWasm/test publishLocal + scripts/wasm/consumer-check.sh: OK. The dapr4s-wasm POM lists scala-java-time 2.8.0 and scala-java-locales 1.6.0.
  • wasm/ publishLocal with the source build removed from ~/.ivy2/local: OK, so the publish job does not need it.

🤖 Generated with Claude Code

sideeffffect and others added 4 commits October 17, 2026 17:55
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dapr4s-wasm now depends on scala-java-time 2.8.0 (default zone UTC without a JS host)
and scala-java-locales 1.6.0 (Locale.getDefault no longer reads `window` without a JS
host, cquiroz/scala-java-locales#640). Both are on Central, so no source build.

counter gets a `java-time` route (parse/toString, ISO and RFC formatters, localized
day name, default zone, UTC/offset zones, Duration); e2e.sh and the consumer check
assert its answer under daprd.

Still blocked, documented in AGENTS.md with how each retires: the wall-clock
(scala-wasm javalib js.Date.now; also reached by every DateTimeFormatter.ofPattern via
TimeZone) and tzdb region zones (@EnableReflectiveInstantiation -> Reflect js.Array).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…onents

Both need an unreleased scala-java-time. scripts/wasm/build-scala-java-time.sh
builds it from source (main + cquiroz/scala-java-time#618, tzdb compiled with a
Scala 3 nightly) and publishLocal's it as 2.8.1-dapr4s-local; CI runs it with a
JDK 17, cached by the script's hash.

- ofPattern: any pattern kept ZoneTextPrinterParser reachable, whose
  TimeZone.getTimeZone computed the raw offset at Instant.now (the wall clock,
  absent without a JS host). #618 computes it lazily.
- Region zones: the tzdb provider's @EnableReflectiveInstantiation registration
  is a static initializer (always linked). Scala 3 <= 3.10.0 emits it with the
  JS-backed Reflect.registerInstantiatableClass; scala/scala3@3bb676972 (3.10.1)
  emits the Scala-typed V2.

Only counter and the consumer check use the source build; dapr4s-wasm's POM stays
on scala-java-time 2.8.0 (Central must have it). Counter's java-time route now
converts to Europe/Prague and New York and formats/parses with ofPattern; e2e
asserts the exact answer. The wall clock stays blocked (scala-wasm javalib).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sideeffffect sideeffffect changed the title feat(wasm): java.time formatting and parsing on Wasm components feat(wasm): java.time formatting, ofPattern and region zones on Wasm components Oct 17, 2026
sideeffffect and others added 2 commits October 17, 2026 19:11
…failed invokes

counter now carries the tz database (~4 MB). On the CI runner the first
invocation after "up and running" failed, and set -e ended the script without
a message. Warm up until an invocation succeeds, reset the counter, and let
every failed invoke reach fail() with dapr's answer and the logs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cquiroz/scala-java-time#618 is merged
(48cf7a6). The script now builds that main commit, without a merge
step. It waits for a scala-java-time release > 2.8.0; region zones also
need that release built with Scala 3 >= 3.10.1.

Co-Authored-By: Claude Opus 5.5 <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