Skip to content

Compile BSWFoundation for WebAssembly (browser) + unified BSWLogger - #184

Open
piercifani wants to merge 18 commits into
developfrom
feature/wasm-port
Open

Compile BSWFoundation for WebAssembly (browser) + unified BSWLogger#184
piercifani wants to merge 18 commits into
developfrom
feature/wasm-port

Conversation

@piercifani

@piercifani piercifani commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Why

This is the browser WebAssembly support pass for BSWFoundation. The goal is to let the package compile for wasm32-unknown-wasip1 and run inside a browser-hosted SwiftWasm app, while preserving the existing Apple and Android behavior.

The main platform split is networking and persistence: browsers do not expose URLSession or Keychain-equivalent secure storage to SwiftWasm, so this PR adds browser-specific runtime hooks, fetch-backed APIClient networking, and a narrow localStorage-backed UserDefaultsBacked path for non-sensitive values.

What changed

  • Browser runtime: adds BSWBrowserRuntime.installJavaScriptEventLoop() so JavaScript hosts can install JavaScriptKit's event-loop executor before starting Swift async work.
  • APIClient on WASM: adds FetchNetworkFetcher, backed by JavaScript fetch, and uses it as the default WASM network fetcher while keeping URLSession defaults on platforms that support it.
  • WASM persistence: keeps KeychainBacked unavailable on WASM with a clear diagnostic, and backs UserDefaultsBacked / CodableUserDefaultsBacked with browser localStorage.
  • Unified logging: consolidates package logging around BSWLogger, using OSLog on Apple platforms, AndroidLogging on Android, and swift-log for WASM.
  • Android cleanup: adds the AndroidLogging dependency directly through swift-android-native; keeps Skip-only storage integrations behind SKIP_ENABLED=1 with targeted unavailable stubs when those modules are missing.
  • Package conditioning: scopes JavaScriptKit, JavaScriptEventLoop, JavaScriptFoundationCompat, swift-log, HTTPTypesFoundation, Crypto, Skip, and Android dependencies to the platforms that can actually use them.
  • Tests and docs: expands WASM-safe tests, documents platform boundaries, adds browser/Node harness docs, and updates README guidance for WebAssembly, Android, and production WASM size notes.
  • CI: adds a wasm-build job alongside the existing macOS, iOS, and Android jobs.

Platform behavior

  • Apple platforms keep URLSession networking, OSLog logging, Keychain-backed storage, and UserDefaults-backed storage.
  • Android supports the plain Swift Android build for package compilation and logging. KeychainBacked, CodableKeychainBacked, UserDefaultsBacked, and CodableUserDefaultsBacked require Skip storage modules, so consumers should build with SKIP_ENABLED=1 when using those wrappers.
  • Browser WASM uses JavaScript fetch and browser localStorage. Browser fetch keeps normal browser security behavior for TLS, CORS, and mixed content; Environment.shouldAllowInsecureConnections is not emulated.
  • Keychain-backed storage remains unavailable on WASM. String.hmac remains available because swift-crypto is still linked for the target.

Verification

Current PR checks are green:

  • macos-build
  • ios-build
  • android-build
  • wasm-build

Also smoke-tested locally during the port:

  • swift test
  • swift build --swift-sdk swift-6.3.2-RELEASE_android
  • swift build --swift-sdk swift-6.3.2-RELEASE_wasm

piercifani and others added 4 commits July 5, 2026 09:47
Second step of the WASM effort (after the swift-http-types adoption). The
whole package now builds for wasm32 (swift build --swift-sdk ...wasm) and
gains a browser network fetcher.

- Package.swift: JavaScriptKit (+ JavaScriptEventLoop, JavaScriptFoundationCompat)
  and swift-log linked WASI-only; HTTPTypesFoundation restricted to the platforms
  where URLSession exists (its URLSession bridge doesn't compile for wasm).
- APIClient+FetchFetcher.swift: FetchNetworkFetcher, an APIClientNetworkFetcher
  backed by the browser fetch API via JavaScriptKit, wired as the default fetcher
  on wasm (replacing the fatalError stub).
- Logging: OSLog is Apple-only; on wasm a small Logger/OSLogType shim backed by
  swift-log keeps the existing OSLog-shaped call sites compiling unchanged.
- Deferred off wasm for now (follow-up PR): KeychainBacked/UserDefaultsBacked
  (need localStorage backing) and Observable.stream(for:) (needs a
  DispatchQueue-free, concurrency-correct reschedule).

Platform detection uses `#if os(WASI)` (per swift.org's WASM guide). Router and
JSONParser needed no changes; this SDK's Foundation provides
JSONSerialization/CharacterSet/date formatters. Apple/Android builds unchanged
(guards are additive; Apple build + RouterTests verified green).

Compile-verified only; browser runtime validation (a real fetch) is a follow-up
via a JavaScriptKit harness.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the per-file `#if os(Android) import AndroidLogging #elseif !os(WASI)
import OSLog #endif` dance (which relied on every platform happening to vend a
`Logger`/`OSLogType` symbol) with a single, owned, public `BSWLogger`.

It forwards to each platform's native backend behind one set of guards:
- Apple: OSLog (Console.app / unified logging)
- Android: AndroidLogging (logcat)
- WebAssembly: swift-log (browser console via a LogHandler)
- other: print

`APIClient+Logging`, `JSONParser` and `FailableCodableArray` now use `BSWLogger`
and no longer import logging modules directly, and apps built on BSWFoundation
get one consistent logging surface. Apple + wasm builds and RouterTests are green;
the Android branch uses only the AndroidLogging API the code already exercised.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The self-hosted `mobile` runners don't have the WebAssembly Swift SDK, so the
new wasm job runs on ubuntu-latest using the official `swift:6.3.3` image as the
toolchain, installs the matching wasm Swift SDK, and compiles the package with
`swift build --swift-sdk swift-6.3.3-RELEASE_wasm`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
KeychainBacked / CodableKeychainBacked / UserDefaultsBacked /
CodableUserDefaultsBacked now work on WebAssembly (previously excluded), backed
by a new WASMKeyValueStore that wraps window.localStorage via JavaScriptKit.

- New WASMKeyValueStore (#if os(WASI)): string/data get + set (nil removes),
  no-ops gracefully if localStorage is unavailable.
- The property wrappers gain #if os(WASI) branches and are now included on wasm
  (guard relaxed from `!os(Linux) && !os(WASI)` to `!os(Linux)`); the
  KeychainAccess import is tightened to `canImport(Darwin)`.

Documented warning: localStorage is NOT secure, so KeychainBacked values are
not encrypted at rest on wasm.

AuthStorage stays Apple-only for now (it isn't cross-platform even on Android);
porting it is a separate follow-up.

Verified green on Apple (build + UserDefaultsBackedTests), wasm
(swift build --swift-sdk), and Android (SKIP_ENABLED=1 skip android build).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Base automatically changed from feature/http-types-adoption to develop July 5, 2026 08:46
piercifani and others added 4 commits July 5, 2026 10:52
- FetchNetworkFetcher now surfaces the response's HTTP header fields, read from
  the JS Headers object (Array.from → [[name, value], …]) into the returned
  APIClient.Response's HTTPResponse.
- Observable.stream(for:) is no longer excluded on wasm. Reworked into a
  coordinator whose reschedule captures a Sendable `self` instead of a recursive
  local closure (which Swift 6 region-based isolation rejected as a `sending`
  data-race risk). It reschedules with a Task on wasm (the JavaScriptKit event
  loop) and DispatchQueue.main elsewhere.

Verified green: Apple (build + ObservationTests), wasm (swift build --swift-sdk),
and Android (SKIP_ENABLED=1 skip android build).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A small executable package that proves BSWFoundation's WebAssembly paths work at
runtime, not just at compile time:
- a real `fetch` GET through FetchNetworkFetcher, decoded by JSONParser, and
- a `localStorage` round-trip through KeychainBacked → WASMKeyValueStore.

Bundled with JavaScriptKit's PackageToJS (`swift package … js`). Runs in Node
(main.mjs shims localStorage, which node lacks; fetch is built in) and in a
browser (index.html; both APIs native there). The built bundle (.wasm) and
node_modules are gitignored.

Verified locally in Node against swift-6.3.2-RELEASE_wasm:
  ✅ fetch GET https://httpbingo.org/ip → origin = …
  ✅ KeychainBacked localStorage round-trip → 'hello-from-wasm'

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a "WebAssembly / Browser Support" section to the top-level README (parallel
to Android Support) with a from-scratch harness runbook: install the wasm Swift
SDK, build via the JavaScriptKit `js` plugin, and run in Node or a browser, incl.
expected output and the localStorage-not-secure caveat. Add a matching
prerequisite pointer in WASMHarness/README.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The test target now builds and runs on wasm — in Node via JavaScriptKit's
`js test` — with 44 tests across 12 suites passing.

- Link JavaScriptEventLoopTestSupport (wasi-only) so async tests (Task.sleep,
  observation streams) get the JS event-loop executor instead of hitting an
  unsupported WASI async-io syscall.
- Guard the network / URLSession / file tests in APIClientTests off wasm; the
  deterministic mock-backed tests run there. Extend APIClientErrorTests'
  localization expectations to os(WASI) (plain "could not", as on Android/Linux).
- Add an `isWASI` test flag and disable the Apple-coupled UserDefaultsBackedTests
  on wasm (it asserts against UserDefaults.standard; the wasm storage path is
  covered by WASMHarness).
- .github/wasm-prelude.js shims `localStorage` for Node.
- CI: the wasm-build job now installs Node + the WASI shim and runs `js test`
  after the compile gate.

Apple/Android builds are unchanged (the test dependency is wasi-conditioned;
Apple `swift build --build-tests` verified green).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@piercifani
piercifani force-pushed the feature/wasm-port branch from 6bc1ae2 to 8b21d08 Compare July 5, 2026 10:04
piercifani and others added 10 commits July 5, 2026 12:33
Explain that the ~76 MB figure is a debug artifact: for deployment, build in release
with Binaryen's wasm-opt on PATH (so PackageToJS runs its size pass) and serve the
wasm brotli/gzip-compressed. Include reference sizes for the WASMHarness bundle
(release + wasm-opt ≈ 45 MB raw / ~12 MB brotli) and note the reflection-metadata
stripping option and the Foundation baseline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds two consumer-facing subsections to the WebAssembly docs:

- Building a browser app on BSWFoundation: the JavaScriptKit event-loop
  dependencies, installGlobalExecutor(), the PackageToJS `js` plugin,
  serving, and the UserDefaultsBacked<T> -> CodableUserDefaultsBacked caveat.
- Reusing a Swift @observable ViewModel from a JS framework such as React:
  the globalThis bridge contract plus the Vite /public loading gotcha.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts:
#	Sources/BSWFoundation/APIClient/APIClient+Logging.swift
#	Sources/BSWFoundation/Extensions/Observation+Ext.swift
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