Skip to content

Wire StreamClient into StreamVideoClient as coordinator owner#1738

Draft
aleksandar-apostolov wants to merge 6 commits into
gsd/phase-2-01-adaptersfrom
gsd/phase-2-02-streamclient-rewrite
Draft

Wire StreamClient into StreamVideoClient as coordinator owner#1738
aleksandar-apostolov wants to merge 6 commits into
gsd/phase-2-01-adaptersfrom
gsd/phase-2-02-streamclient-rewrite

Conversation

@aleksandar-apostolov

@aleksandar-apostolov aleksandar-apostolov commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Goal

Closes AND-1282

Replaces our custom coordinator socket with stream-android-core's StreamClient as the owner of the coordinator WebSocket connection. StreamClient now handles the socket lifecycle, token refresh, HTTP interceptors, and reconnection. Guest and authenticated user handling is unified through a single token-provider abstraction.

Depends on #1737 (adapter bridging). Merge after parent.

Merge order: adapters (#1737) → this PR → ConnectionState rewrite (#1739).

Implementation

StreamVideoClient now owns a StreamClient instance.
Constructed eagerly in the primary constructor and disposed in cleanup(). Tests inject a mock via a constructor parameter — no more Lazy<> workarounds needed.

Event flow now goes through core: StreamClient fires WebSocket events, our listener forwards them to the existing fireEvent(...) pipeline. Existing subscribers keep working unchanged.

StreamVideoBuilder picks the right token provider based on user type.
Regular users get IntegrationStreamTokenProvider (wrapping the integration's TokenProvider). Guest users get GuestStreamTokenProvider (which calls createGuest internally). Anonymous users get a REST-only client that never opens a coordinator socket; explicit connect attempts fail fast.

Guest and anonymous flows mirror the iOS SDK (StreamVideo.loadGuestUserInfo / initialConnectIfRequired):

  • Guests auto-connect at build time like authenticated users; core's token manager drives createGuest inside connect().
  • The guest JWT is synced into the legacy Retrofit auth path (CoordinatorAuthInterceptor) the moment it is issued, and auth type flips anonymousjwt — REST and WS stay on one token until Phase 4 unifies HTTP through core.
  • The server-issued guest identity is adopted into ClientState.user, including iOS's name-preservation quirk for decorated display names.
  • Guest token refresh = createGuest again, single-flighted by StreamTokenManager.

Guest user machinery deleted.
setupGuestUser, createGuestUser, guestUserJob: Deferred<Unit>?, and every apiCall { guestUserJob?.await() } synchronization site are gone. Guest token acquisition happens inside GuestStreamTokenProvider.loadToken(userId) — first invocation calls createGuest, subsequent calls return the cached JWT via core's StreamTokenManager (which provides single-flight dedup automatically). This removes the race conditions we've been patching in the guest flow over the last few weeks.

Coordinator module surgery.
CoordinatorConnectionModule.socketConnection field is removed. The legacy CoordinatorSocketConnection class stays on disk (nothing references it), scheduled for deletion in a future cleanup PR.

StreamLifecycleObserver moved to lifecycle/legacy/ package.
SfuSocket still references it, so it can't be deleted yet — the SFU migration will remove it.

Tests:

  • StreamVideoClientTest extended with assertions that StreamClient is subscribed at init, connect() invoked on connectAsync(), disconnect() invoked on cleanup().
  • New StreamVideoBuilderDispatchTest covers the token-provider dispatch: Anonymous → REST-only provider (no throw), Authenticated → Integration provider, Guest → Guest provider, localCoordinatorAddressws://....
  • GuestStreamTokenProviderTest covers token sync into the REST auth path and the iOS name-preservation quirk; BindableWritableUserRepositoryTest covers pre-bind buffering.
  • Anonymous fail-fast covered in StreamVideoClientTest.
  • The four unit tests introduced in the parent PR are all passing now.

Testing

  • Whole-project compile passes (all consumer modules still build).
  • Full stream-video-android-core:testDebugUnitTest suite green in ~2m30s. @Ignore count unchanged at 21.
  • spotlessCheck green.
  • BCV apiCheck shows zero public API diff — the ConnectionState breaking change is in the follow-up PR.

Heads-up for future test authors: StreamVideoBuilder.build() constructs the connection module before the token-provider dispatch, and the module hits getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager — under mockk<Context>(relaxed = true) that throws ClassCastException. Tests using the builder in-process must stub CONNECTIVITY_SERVICE. See StreamVideoBuilderDispatchTest.builderFor(...) for the pattern.

Intermediate-state note

On this PR alone, ClientState.connection only updates via ConnectedEvent; disconnect transitions become visible when #1739 wires handleStreamState. Merge the stack together.

Task 1 of Plan 02-02. Eagerly inject core v5 StreamClient into
StreamVideoClient's primary constructor (D-03), replace the four
coordinatorSocket.{events,state,errors,state} collectors with a single
StreamClientListener subscription that casts to VideoEvent and forwards to
the existing fireEvent(...) dispatch pipeline. connectAsync() delegates to
streamClient.connect(); cleanup() calls streamClient.disconnect() (bridged
via runBlocking) before scope.cancel(). Deletes setupGuestUser,
createGuestUser, guestUserJob, and the corresponding await() sites. Also
lands the minimum StreamVideoBuilder wiring (streamTokenProvider dispatch
on user.type + streamClient factory seam) so the two files compile as one
unit; Task 2 extends the builder tests separately.

Rule 3 blocker: Call.kt referenced clientImpl.guestUserJob?.await() at the
join call site. Replaced the await with a KDoc comment noting that
StreamTokenManager now single-flights the guest JWT transparently through
core's auth interceptor.

TODO(02-03): streamClientListener.onState currently no-ops; the
handleStreamState routing lands in Plan 02-03 alongside the ConnectionState
sealed-interface reshape.
Task 2 of Plan 02-02. New StreamVideoBuilderDispatchTest exercises the
`when (user.type)` dispatch in StreamVideoBuilder.build() through the
streamClientFactory seam:

- Authenticated -> IntegrationStreamTokenProvider (D-05)
- Guest -> GuestStreamTokenProvider (D-06, COORD-09)
- Anonymous -> IllegalStateException with a D-07 reference
- localCoordinatorAddress "10.0.2.2:3030" threads through into
  args.resolvedWssUrl as "ws://10.0.2.2:3030/video/connect" (COORD-08)

The pre-existing StreamVideoBuilderTest stays @ignore'd (integration-style
suite depending on network authData). The new lightweight class runs on the
JVM with mockk Context + ConnectivityManager stubs.
Task 3 of Plan 02-02. Three surgical edits that finish unwiring the legacy
coordinator socket from the live code path:

1. CoordinatorConnectionModule narrows its ConnectionModuleDeclaration
   generic to Unit and turns `socketConnection` into a `Unit = Unit`
   vestigial override. The CoordinatorSocketConnection field is deleted;
   the class file itself stays on disk for Phase 8 (COORD-07).

2. StreamLifecycleObserver.kt moved from `lifecycle/` to `lifecycle/legacy/`
   (Plan B in the plan text). Coordinator-side lifecycle observation now
   flows through core's StreamLifecycleMonitor per D-10 / PROC-07. Legacy
   consumers (Coordinator/SFU socket files kept for Phase 8) have their
   imports updated to `lifecycle.legacy.StreamLifecycleObserver`. SFU is
   Phase 3 territory; the moved file survives until then.

3. IntegrationTestBase updated to read the connection ID from
   streamClient.connectionState instead of the deleted socketConnection.

Legacy coordinator test @ignore roster:
- CoordinatorSocketTest.kt / CoordinatorSocketStateServiceTest.kt do NOT
  exist under `src/test/.../socket/coordinator/` on this branch — nothing
  to annotate.
- TokenManagerImplTest.kt exercises TokenManagerImpl behaviours shared with
  SFU (still in-tree per RESEARCH line 1153). Preserved unannotated so SFU
  coverage is not lost.

@ignore baseline unchanged at 21.
@aleksandar-apostolov aleksandar-apostolov added the pr:new-feature Adds new functionality label Jul 1, 2026
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

PR checklist ✅

All required conditions are satisfied:

  • Title length is OK (or ignored by label).
  • At least one pr: label exists.
  • Sections ### Goal, ### Implementation, and ### Testing are filled, or the PR is bot-authored.
  • An issue is linked (Linear ticket or GitHub issue), or the PR is bot-authored.

🎉 Great job! This PR is ready for review.

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

SDK Size Comparison 📏

SDK Before After Difference Status
stream-video-android-core 12.05 MB 12.35 MB 0.30 MB 🟡
stream-video-android-ui-xml 5.68 MB 5.73 MB 0.05 MB 🟢
stream-video-android-ui-compose 6.28 MB 5.84 MB -0.44 MB 🚀

Core's default StreamClient factory touches WifiManager during eager
construction, which layoutlib's BridgeContext does not implement. All
Compose snapshot tests were failing at setup with
"AssertionError: Unsupported Service: wifi".

Expose the streamClientFactory seam on StreamVideoBuilder via a public
@InternalStreamVideoApi setter and have StreamPreviewDataUtils install
a NoOpStreamClient before build().
Guest users (iOS loadGuestUserInfo parity):
- GuestStreamTokenProvider now syncs the createGuest JWT into the legacy
  Retrofit auth path (CoordinatorAuthInterceptor) via onTokenIssued and
  flips the auth type anonymous->jwt, matching the legacy setupGuestUser
  sequence. Previously the token only reached core's WS path, leaving
  every guest REST call unauthenticated (401 loop).
- Builder now auto-connects guests too (only anonymous never connects);
  core's token manager drives createGuest inside connect().
- Server-issued guest identity is adopted into ClientState.user through
  BindableWritableUserRepository, including iOS's name-preservation
  quirk for server-decorated display names.

Anonymous users (D-07, iOS connectUser parity):
- build() no longer throws; anonymous clients are REST-only with
  stream-auth-type anonymous restored.
- connectAsync()/connectIfNotAlreadyConnected() fail fast without
  touching the network.
@sonarqubecloud

sonarqubecloud Bot commented Jul 3, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
70.3% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr:new-feature Adds new functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant