Skip to content

feat: add Capacitor mobile runtime support - #74

Merged
pietrodicaprio merged 3 commits into
mainfrom
docs/capacitor-mobile-release
Sep 1, 2026
Merged

pietrodicaprio merged 3 commits into
mainfrom
docs/capacitor-mobile-release

Conversation

@pietrodicaprio

Copy link
Copy Markdown
Member

Summary

Add the first opt-in Capacitor mobile runtime path for RFW without changing the browser defaults:

  • add build.delivery: "embedded" for optimized raw WASM packaged inside a native container;
  • add build.sscTransport: "capacitor" and a fail-closed Go/WASM transport selector;
  • add an unpublished iOS Capacitor SSC plugin backed by URLSessionWebSocketTask;
  • add hostclient.ResetSession() for exact authenticated-identity changes;
  • add browser, build, Swift package, physical-device, and CI coverage.

The existing defaults remain delivery: "network" and sscTransport: "browser".

Why

Capacitor local asset handlers do not reliably serve precompressed WASM with the matching Content-Encoding, so a production packaged application needs to load the optimized raw bundle it ships rather than fall back from a network compression contract.

On physical iOS, native HTTP retained the Secure host cookie while a WebSocket opened by WKWebView did not present it to the same remote host. The native SSC transport keeps cookie ownership in the shared native URLSession cookie jar and carries only protocol frames and lifecycle events across the Capacitor bridge.

Physical multi-user testing also exposed a separate lifecycle boundary: replacing the HTTP cookie did not replace an already-open SSC connection, so the shell could show Alice while host-bound variables still belonged to Bob. ResetSession makes that transition explicit and immediate.

Build contract

build.delivery is independent of build.type:

  • network preserves the existing compressed/negotiated production path;
  • embedded removes stale .br/.gz artifacts, keeps optimized app.wasm, stamps RFW_WASM_DELIVERY, and makes the loader request only that packaged file;
  • malformed delivery or SSC transport values fail the build;
  • project-owned loader overrides remain untouched and must be updated explicitly.

rfw_config.js now stamps RFW_SSC_TRANSPORT. Selecting capacitor never silently falls back to a browser WebSocket when Capacitor or the plugin is missing.

Native iOS trust boundary

The local mobile/capacitor-ssc package:

  • requires an exact WSS origin allowlist;
  • rejects credentials, remote cleartext WS, redirects, malformed origins, unsafe queue sizes, and unsafe frame limits;
  • permits loopback WS only behind an explicit development option;
  • sets the configured native Origin and uses HTTPCookieStorage.shared;
  • never returns cookie values over the JavaScript bridge;
  • bounds the outbound queue and terminates the connection on overflow;
  • bounds inbound frames;
  • closes the socket when the application enters the background and blocks connects while inactive.

There is deliberately no Android implementation in this PR.

Authenticated identity reset

hostclient.ResetSession():

  • advances a connection generation before closing the old transport;
  • prevents old-generation frames from reaching replacement handlers;
  • clears SSC session ID, resume token, inbound/outbound sequence state, replay outbox, queued messages, and pending calls;
  • returns hostclient.ErrSessionReset to interrupted typed calls;
  • preserves registrations belonging to owners that remain mounted, so app-lifetime feeds can initialize on the fresh connection;
  • includes the generation in send-dedup keys so a new user is not suppressed by the previous user's cache entry.

The consumer contract is: fail guards closed and unmount user-owned scopes, release app feeds and wipe user stores, call ResetSession, then install the new claims and mount the replacement route.

Dependency on PR #72

This is a stacked PR based on #72 at 07566500ba03a7fdbc0143bcda886059ad48bfd1.

The reset contract relies on #72's cleanup-capable host registrations and router.Revalidate() behavior: user-owned scopes must be removed before reset so only genuinely app-lifetime registrations survive. After #72 merges, this PR can be retargeted to main without carrying an application workaround.

Verification

Post-rebase gates on the exact pushed commits:

  • go test ./...
  • go test -race ./...
  • go vet ./...
  • GOOS=js GOARCH=wasm go build ./...
  • GOOS=js GOARCH=wasm go vet ./...
  • full serial Chrome/WASM suite through .github/scripts/wasmbrowsertest-retry
  • GOOS=js GOARCH=wasm go test -count=20 -exec wasmbrowsertest ./hostclient
  • npm run verify:ios: 8/8 iOS Simulator tests
  • signed physical iPhone build with byte-identical packaged app.wasm

Physical iOS observations:

  • cookie-authenticated native WSS initialization and host-component delivery;
  • ping over native SSC;
  • same-session background/resume with authorized replay;
  • revocation while backgrounded, resume denial, protected-owner teardown, and revoked route;
  • cold /deep/alpha App API payload with the guarded owner and route parameter mounted;
  • Alice to Bob to Alice without backgrounding: each transition unmounted the old owner, closed SSC, opened a fresh bound session, and ended with matching shell and host users.

The workflow adds a macOS Test (iOS native SSC) job and makes it a release dependency.

Publication boundary

The full Capacitor release guide remains local and is intentionally excluded from this PR. The package README contains only the minimum integration and verification contract. External-app Universal Link resolution and Android runtime/device validation remain separate gates.

@pietrodicaprio
pietrodicaprio requested a review from a team as a code owner September 1, 2026 12:43
Copilot AI lite review requested due to automatic review settings September 1, 2026 12:43

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Base automatically changed from feat/multiuser-ownership-contracts to main September 1, 2026 13:01
Add embedded WASM delivery, an opt-in native iOS SSC transport with exact-origin and bounded-queue controls, browser/iOS tests, and a macOS CI gate. Browser WebSocket remains the compatibility default.
Close and generation-fence the previous transport, clear resume and delivery state, fail pending calls, and preserve only registrations owned by components that remain mounted.
@pietrodicaprio
pietrodicaprio force-pushed the docs/capacitor-mobile-release branch from 2a57d94 to bd39268 Compare September 1, 2026 13:01

@mirkobrombin mirkobrombin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good.

@pietrodicaprio
pietrodicaprio merged commit ca94b7c into main Sep 1, 2026
8 checks passed
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.

3 participants