Skip to content

Share one localhost WS listener across native product executions - #600

Open
decrypto21 wants to merge 1 commit into
mainfrom
355-shared-ws-listener
Open

Share one localhost WS listener across native product executions#600
decrypto21 wants to merge 1 commit into
mainfrom
355-shared-ws-listener

Conversation

@decrypto21

@decrypto21 decrypto21 commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

What

Every product execution (App, Widget, Chat/Worker) connects through one shared localhost WebSocket listener instead of its own, each with an independent token.

  • One shared listener. Owned by NativeTrUApiHostRuntime, started lazily on the first execution's start_ws_bridge call and kept alive for the runtime's lifetime.
  • Independent tokens. Each execution registers a fresh cryptographically random token against a shared registry; a token only ever resolves to its own execution, checked in constant time with no early exit, including across duplicated t= query pairs, so timing can't reveal which token, if any, matched.
  • Isolated handshakes. Each connection's handshake runs in its own task, so a stalled or hostile peer only ever blocks its own connection, never a sibling's or the shared accept loop.
  • Clean revocation. Closing an execution removes its token and aborts only its own connections. Aborting now disposes the connection's ProductRuntime too, so host-core subscriptions and chat state are torn down the same as on a graceful close.
  • Bounded resources. Connection limits are enforced both per-execution and across the whole shared listener.
  • No API changes. start_ws_bridge/stop_ws_bridge keep their existing signatures, so iOS/Android need no source changes. Confirmed by regenerating the UniFFI Swift bindings, where only doc-comment text and the API checksum moved.
  • Legacy pattern unaffected. NativeTrUApiCore, the combined single-host/single-execution wrapper the issue's acceptance criteria call the legacy API, was already removed from this repo in refactor(native): remove legacy single-execution core #508, before this PR started. Only NativeTrUApiHostRuntime/NativeProductExecution remain, and opening exactly one execution from one host runtime through them keeps working end-to-end unchanged: native.rs's existing pending_permission_decision_does_not_stall_bridge and start_ws_bridge_twice_returns_already_running tests cover a real connect, a real request/response round-trip, and a clean stop through exactly that path.

Why

Every execution binding its own listener means its own port, accept loop, and connection cap: unnecessary overhead that matters more as a host runs multiple product executions concurrently (App, Widget, Chat side by side, not just one at a time). One shared listener removes the duplication while keeping every execution's traffic, queues, and backpressure as isolated as they were before.

How

A connection's token match happens inside the WebSocket handshake, before its task is spawned or registered against its execution, so in principle a token could be revoked in the gap between a sibling's handshake resolving and its registration landing. Both sides serialize on the same per-execution lock: revoke marks the execution revoked and aborts whatever is already registered under one lock acquisition, and registration checks that same flag before adding itself under the same lock. Whichever happens first is what the other observes, so a connection admitted in that window is never left running with no owner.

Handshakes run in independent tasks rather than serialized through the accept loop, so the connection-count caps are reserved with a compare-and-swap loop instead of a plain read-then-increment. Only the first execution to register against an idle bridge can choose bind_port; a later caller's request is logged (truapi.ws_bridge.bind_port_ignored) rather than silently dropped if it differs from the port already running.

Downstream compatibility

Checked against a real consumer, polkadot-ios-community, which already opens two executions (SPA/App and Chat) from one TrUAPIHostRuntime, the exact multi-execution shape this change targets. Its startWsBridge/stopWsBridge call sites are untouched, so it keeps working unchanged once it picks up a release that includes this PR:

host-rust-core                        polkadot-ios-community
──────────────                        ───────────────────────
main ──●────●────●──[this PR]         Package.resolved pins to
       │    │    │                    a released tag (e.g. v0.12.0)
    v0.7.1 v0.12.0 v0.13.1                      │
                                                 ▼
                                  TrUAPIHostRuntime (one process-wide core)
                                    │                        │
                            makeSPAExecution          makeChatExecution
                                    │                        │
                             startWsBridge()           startWsBridge()
                                    │                        │
                             before: own port          before: own port
                             after:  one shared port, independent tokens

Verification

  • cargo build --workspace
  • cargo +nightly fmt --check
  • cargo clippy --workspace --all-targets --all-features -- -D warnings
  • cargo test --workspace
  • cargo build -p truapi-server --target wasm32-unknown-unknown --no-default-features
  • make uniffi && ./ios/truapi-host/scripts/sync-bindings.sh --check

@decrypto21
decrypto21 requested a review from a team September 4, 2026 06:39
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