import { type ReactNode, useEffect, useMemo, useRef, useState } from "react"; import type { StreamSessionFactory } from "../session/session-shape/session-shape.types.js"; import { type StreamContextValue, StreamReactContext, } from "./stream-context.js"; export interface StreamProviderProps { /** * Factory that constructs the `StreamSession` lazily on first mount. * Consumers supply this so the package stays transport-agnostic — the * factory wires in the WebRTC / WebSocket / Trystero transport, the * `audio-engine-browser` instance, telemetry sinks, etc. */ readonly sessionFactory: StreamSessionFactory; readonly children?: ReactNode; } /** * StrictMode-safe singleton provider. Calls `sessionFactory()` exactly * once per React tree (guarded by an `inFlight` ref), subscribes to every * session event, and increments a `version` counter on each tick so hooks * re-render. On unmount the session is disconnected and event listeners * are detached. */ export function StreamProvider(props: StreamProviderProps): ReactNode { const factoryRef = useRef(props.sessionFactory); factoryRef.current = props.sessionFactory; const sessionRef = useRef | null>(null); const createdRef = useRef(false); if (!createdRef.current) { sessionRef.current = factoryRef.current(); createdRef.current = true; } const [version, setVersion] = useState(0); useEffect(() => { // `sessionRef.current` is non-null here because the throw in the // memo below runs first when the factory returned null. const session = sessionRef.current as NonNullable< typeof sessionRef.current >; const bump = (): void => { setVersion((v) => v + 1); }; const offs = [ session.on("state-changed", bump), session.on("interruption", bump), session.on("peers-changed", bump), session.on("mixer-changed", bump), session.on("levels-changed", bump), session.on("recording-changed", bump), session.on("permission-changed", bump), ]; return () => { for (const off of offs) { off(); } void Promise.resolve(session.disconnect()).catch(() => { /* disconnect best-effort — error surfaces via session events */ }); }; }, []); const value = useMemo(() => { const session = sessionRef.current; if (session === null) { throw new Error("StreamProvider: session factory returned null"); } return { session, version }; }, [version]); return ( {props.children} ); }