refactor(call): Session-backed WebRTC — delegate topology to SDK - #159
Open
HexaField wants to merge 18 commits into
Open
refactor(call): Session-backed WebRTC — delegate topology to SDK#159HexaField wants to merge 18 commits into
HexaField wants to merge 18 commits into
Conversation
✅ Deploy Preview for coasys-we ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Add an SFU adapter that wraps any SfuManager-compatible backend behind the existing CallMesh interface. The store branches on an optional sfuBackend in deps — when provided, the call connects through the SFU relay instead of building N-1 direct peer connections. New files: sfu.ts — SfuBackend structural interface + createCallSfu() adapter Store changes: - sfuBackend?: SfuBackend in CallStoreDeps - topology signal (mesh/sfu) for the template to surface mode - qualityPreference signal + setQualityPreference action (high/med/low) - SFU join with automatic mesh fallback on failure - Proper teardown of SFU state Exports: - createCallSfu, SfuBackend, SfuQuality, CallTopology The SfuBackend interface matches SfuManager from @coasys/ad4m without importing it — the host constructs the manager and passes it in. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
HexaField
force-pushed
the
feat/sfu-integration
branch
from
August 27, 2026 02:04
8b01954 to
216ec28
Compare
…ckend Delete sfu.ts entirely — the SDK's Session (coasys/ad4m#712) now unifies mesh and SFU topologies behind a single interface, making the adapter redundant. The store accepts an optional CallBackend (structurally satisfied by Session from @coasys/ad4m) through deps. When provided: - Session handles topology resolution, signalling, and roster internally - Store calls session.join/leave/destroy for lifecycle - Store calls session.replaceTrack for camera↔screen swap (no renego) - Store reads session.participants for tile streams - Store delegates setQualityPreference to session - Roster effect skips (Session polls onlineAgents internally) When no backend: falls back to WE's built-in peer-to-peer mesh unchanged. Net -163 lines. Depends on coasys/ad4m#712 (feat/embedded-sfu). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add BackendDataMessage type for data channel messages - Add sendData/onData to CallBackend interface - Wire backend.onData() subscription in backend join path - Expose sendData/onData in store return value - Clean up data subscription on teardown - Export BackendDataMessage from call module Enables reactions, raised hands, collaborative pointers through the Session relay when a backend handles the call. Mesh-only calls return silently from sendData (no server relay available). Zero new type errors (46 pre-existing monorepo cross-ref errors). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add automatic quality adjustment and relay availability signals to the call store for the UI to consume: RelayInfo interface: - relayActive: whether a relay server handles the call - participantCount: total participants including self - meshLimitReached: true when mesh call exceeds 6 participants Auto quality (SFU only): - Adjusts simulcast layer based on participant count: 5-8 participants → medium, 9+ → low - Only active when user has not explicitly set a preference - qualityIsManual flag tracks explicit user choice - Resets on call teardown Mesh limit warning: - relayInfo().meshLimitReached signals the UI to show a quality hint when a peer-to-peer call grows beyond 6 participants Zero new type errors. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…/billing Export bundledModules from shared barrel so platform deployments can register their own modules before PlatformProvider runs initializeIntegrations. Add Settings.schema.ts with Account and Billing sub-routes. Account shows DID, email, and plan via $store resolution (modules.platform.*). Billing shows current plan, credits, upgrade/buy buttons. Both gated on modules.platform.email signal — hidden for guest sessions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… account/billing" This reverts commit a0c14ff.
# Conflicts: # packages/module-system/call/src/index.ts # packages/module-system/call/src/store.ts
…dicator Gap 1 — Session construction and backend injection: - callSessionAdapter.ts: factory bridging NeighbourhoodProxy.createSession to the call module's CallBackend interface (late-binding getters) - moduleHostServices.ts: createCallBackend service + getter forwarding - DatasetStore.tsx: binds factory via provideModuleHostServices - module-shared/module.ts: createBackend on ModuleStoreDeps (neutral port) - store.ts: join() resolves backend from createBackend factory or static backend, with error handling and fallback to mesh Gap 2 — Quality selector UI: - store.ts: cycleQuality action (high → medium → low → high) - index.ts: qualitySelector fragment — cycling button with signal-strength icon, only visible when topology is SFU Gap 3 — Topology indicator UI: - index.ts: topologyIndicator fragment — broadcast icon for SFU, graph icon for mesh, with tooltips Also: pnpm overrides migrated from package.json to pnpm-workspace.yaml (pnpm 10.18 no longer reads the pnpm field in package.json). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ssionBackend signal Topology indicator previously rendered even without a Session backend (pure mesh fallback). Wraps the indicator in an outer $if on hasSessionBackend — only shows mesh/SFU state when a backend exists. Adds hasSessionBackend boolean signal to the call store, set when createBackend succeeds, reset on leave. Exported for schema bindings. Verified: 8/8 Playwright E2E tests pass on Macbook Electron (CDP). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The pnpm override pointed to `link:../ad4m-sfu/core` — a local checkout that does not exist in CI. Reverts package management to match dev: overrides in package.json, lockfile from dev. Restructures callSessionAdapter to compile against the published `@coasys/ad4m` (0.13.0-test-model-layer) which does not export Session or createSession. The adapter now: - Imports only `type PerspectiveProxy` (exists in published build) - Gets the NeighbourhoodProxy via `PerspectiveProxy.getNeighbourhoodProxy()` rather than constructing directly (avoids constructor signature diff) - Checks for `createSession` at runtime and throws a clear error when paired with an executor that lacks SFU support - Defines a local `SessionCapableProxy` interface for the cast Once `@coasys/ad4m` publishes the SFU types, the local interface can be replaced with direct imports. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CallStoreDeps.createBackend returned Promise<CallBackend> while ModuleStoreDeps.createBackend (the shared contract) returned Promise<unknown>. TypeScript rejected the assignment at createStore. Widens the return type to Promise<unknown> and casts at the call site. The structural contract still holds at runtime. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Accidentally dropped in 0b1dcdb when testing dev build — the git checkout origin/dev test wiped the staged export line. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The WE expression DSL supports == and != but not === or !==. The strict-equality operators caused ExpressionSyntaxError at import time, failing index.test.ts in CI. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two settings surfaces for moderators to control call topology: 1. Space Settings → Features → Call topology section - Topology mode selector (mesh / designated / gateway / cascaded) - Conditional fields for designated peer DID, cascade params - Fallback mode selector - Max mesh participants before SFU escalation - Available SFU node scanner - Gated on callConfigSupported && canAdminister 2. Per-call settings popover (gear icon in call bar) - Read-only topology indicator (mesh / SFU icon) - Quality preference selector (High / Medium / Low) - Connection summary with mesh limit warning Backend adapter (callConfigAdapter.ts): - Reads/writes SFU config via NeighbourhoodProxy Social DNA - Runtime capability gating for published @coasys/ad4m compat - Discovers SFU-capable nodes via presence scan Session adapter now reads moderator's topology choice from Social DNA before creating a session, instead of hardcoding 'auto'. Module host services wired through ModuleStoreDeps (getCallConfig, setCallConfig, getAvailableSfuNodes, callConfigSupported). Call store exposes reactive signals for config state, saving state, available nodes, and connection info summary. Tests: 29 new tests across 3 files - callConfig.test.ts (11): store config signals, load/save/refresh - callConfigAdapter.test.ts (11): adapter read/write/probe/fallback - callSessionAdapter.test.ts (7): topology resolution, fallbacks Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two bugs prevented callConfigSupported (and other late-bound service ports)
from resolving at runtime:
1. moduleHostServices: deps closures like `() => services.dataset?.()` read
nothing reactive when services have not mounted yet. Effects that call them
first establish zero Solid dependencies and never re-fire. Add a revision
signal that every closure reads; bump it in provideModuleHostServices so
effects re-run when a new service slice arrives.
2. moduleRegistry: `{ ...storeDeps }` evaluates every getter at spread time,
copying undefined values as plain properties. Switch to
`Object.assign(Object.create(storeDeps), ...)` so prototypal delegation
preserves getters — reads at any later time still reach the real service.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The adapter declared `(config: CallConfig)` but ModuleHostServices uses `(config: unknown)` because the module-system boundary has no access to the CallConfig type. TypeScript rejects the narrower parameter type (contravariance). Widen the adapter signature to `unknown` and cast internally where the concrete type reaches the nhProxy call. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
HexaField
marked this pull request as ready for review
September 10, 2026 06:10
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
This PR replaces the
SfuBackendadapter with a structuralCallBackendinterface that the AD4M SDKSessionsatisfies directly. The call module now delegates topology resolution (mesh / SFU / auto), signalling, roster polling, and peer connection management to the SDK. Two runtime bugs that prevented the call settings UI from rendering have also been fixed.Why
The previous
SfuBackendadapter (211 lines) duplicated logic that now lives in the SDKSession— topology selection, ICE negotiation, renegotiation handling, and participant tracking. Removing the adapter eliminates that duplication and lets the call module drive lifecycle through one stable interface. The settings UI bugs (dead Solid effects and spread-killed getters) blocked runtime verification of the call topology configuration surface.How
CallBackendinterface (store.ts)CallBackenddefines a structural contract that SDKSessionsatisfies without an explicitimplements:join(localStream)/leave()/destroy()— lifecyclereplaceTrack(kind, track)— camera/screen swap without renegotiationsetQualityPreference(quality)— delegates to SFU quality signallingparticipants— reactive participant map with streamsparticipant-joined,participant-left,stream-added,stream-removed,topology-changed,errorThe store subscribes to backend events and reads
participantsfor tile streams. The mesh fallback path remains unchanged for when no backend exists.Late-binding fixes (
moduleHostServices.ts,moduleRegistry.ts)Two bugs prevented
callConfigSupportedfrom returningtrueat runtime:Dead effects — deps closures like
() => services.dataset?.()read nothing reactive whenservices.datasetremainedundefined(module stores mount before host stores). The first effect run tracked zero Solid dependencies and never re-fired. Fix: added a revision signal that every closure reads;provideModuleHostServicesbumps it afterObject.assign.Spread kills getters —
{ ...storeDeps, onDispose, settings }evaluated every getter at spread time, copyingundefinedas plain properties. Fix:Object.assign(Object.create(storeDeps), { onDispose, settings })preserves getters via prototypal delegation.Type alignment (
callConfigAdapter.ts)Widened
setCallConfigparameter fromCallConfigtounknownto match theModuleHostServicesinterface boundary (the module system has no access to theCallConfigtype). Internal cast at thenhProxy.setSfuConfigcall site.Files changed
module-system/callstore.ts,index.tsCallBackendinterface, backend event wiring,SfuBackendremovalmodule-system/callsfu.tsapp-shellmoduleHostServices.tsapp-shellmoduleRegistry.tsObject.createprototypal delegation for depsbackend-system/ad4mcallConfigAdapter.tssetCallConfigparameter widened tounknownReferences
feat/embedded-sfu) for the SDKSessioninterface