SwitchBoard
Every Synapse app has a single SwitchBoard. It is the only shared
object in the architecture — every Node, Coordinator, and Provider talks
to the rest of the app through it. Nothing else holds a reference to
anything else.
The SwitchBoard interface
Section titled “The SwitchBoard interface”SwitchBoard is an interface with three groups of methods:
| Group | Methods | Purpose |
|---|---|---|
| Produce | broadcastState, triggerImpulse, handleRequest | Emit data upstream through interceptors into the appropriate channel flow. |
| Consume | stateFlow, impulseFlow | Return downstream-intercepted SharedFlows for a given type. |
| Raw access | getRawStateFlow, getRawImpulseFlow | Return the backing SharedFlow without downstream interception — for advanced composition where the caller manages interception. |
| Intercept | addInterceptor | Register an interceptor at a specific intercept point. Returns a Registration handle. |
Every method takes a KClass token to identify the type on the bus.
Reified extension functions let call sites omit the token:
// These are equivalent:switchBoard.broadcastState(SessionState::class, session)switchBoard.broadcastState(session) // reified — infers KClass from TA convenience extension installs logging interceptors across all six intercept points at once:
switchBoard.addLoggingInterceptors<AnalyticsEvent> { event -> analytics.track(event)}DefaultSwitchBoard
Section titled “DefaultSwitchBoard”DefaultSwitchBoard is the concrete implementation, designed for
constructor injection via Hilt or any DI container.
class DefaultSwitchBoard @Inject constructor( @SwitchBoardScope scope: CoroutineScope, providerRegistry: ProviderRegistry, @SwitchBoardWorkerContext workerContext: CoroutineContext = Dispatchers.IO, @SwitchBoardStopTimeout stopTimeoutMillis: Long = 3_000, @SwitchBoardReplayExpiration replayExpirationMillis: Long = 3_000,) : SwitchBoard| Parameter | Purpose | Default |
|---|---|---|
scope | Parent CoroutineScope. Canceling it tears down every flow the bus manages. | (required) |
providerRegistry | Immutable registry mapping each DataImpulse type to its provider factory. | (required) |
workerContext | CoroutineContext for provider work. | Dispatchers.IO |
stopTimeoutMillis | How long a shared flow keeps emitting after its last subscriber disconnects. | 3000 (3 s) |
replayExpirationMillis | How long a replay cache is retained after all subscribers disconnect. | 3000 (3 s) |
The four qualifier annotations (@SwitchBoardScope,
@SwitchBoardWorkerContext, @SwitchBoardStopTimeout,
@SwitchBoardReplayExpiration) let Hilt distinguish each parameter
without ambiguity. See Hilt Integration for the module
wiring.
Internal flow layout
Section titled “Internal flow layout”Flows are stored in a ConcurrentHashMap per channel and created lazily
on first access — a type that is never broadcast or triggered allocates
nothing.
| Channel | Flow type | Replay | Buffer |
|---|---|---|---|
| State | MutableSharedFlow | 1 | DROP_OLDEST |
| Reaction | MutableSharedFlow | 0 | extraBufferCapacity = 64, DROP_OLDEST |
| Request | SharedFlow via ProviderManager | 1 | — |
Sharing strategy
Section titled “Sharing strategy”Downstream flows are shared via SharingStarted.WhileSubscribed:
- Emission continues for
stopTimeoutMillisafter the last subscriber disconnects, avoiding unnecessary restarts during brief configuration changes. - The replay cache is retained for
replayExpirationMillisafter emission stops, so a subscriber that reconnects quickly still sees the latest value without a re-fetch.
For tests, call setEagerSharing() before any flow is consumed to
switch to SharingStarted.Eagerly, so flows are hot from the start and
values are never missed due to timing.
Thread safety
Section titled “Thread safety”All internal maps are ConcurrentHashMap. All flows are thread-safe by
Kotlin coroutines contract. The global logger and trace listener use
AtomicReference. Every member of SwitchBoard is safe to call from
any coroutine or thread.
Lifecycle
Section titled “Lifecycle”The SwitchBoard has no dispose() method. Its lifetime is controlled
entirely by the CoroutineScope you inject:
- App-scoped (typical) — lives for the process. Flows survive configuration changes. State replay works across activity recreations.
- Test-scoped —
SynapseTestRulecreates a fresh scope per test and cancels it on teardown, so nothing leaks between tests.
When the scope is canceled, all shared flows stop, all active provider jobs are canceled, and all interceptor pipelines shut down. No manual cleanup is needed.
- Hilt Integration — wiring the switchboard, registry, and coordinators in a real app