81 lines
2.8 KiB
TypeScript
81 lines
2.8 KiB
TypeScript
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<ReturnType<StreamSessionFactory> | 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<StreamContextValue>(() => {
|
|
const session = sessionRef.current;
|
|
if (session === null) {
|
|
throw new Error("StreamProvider: session factory returned null");
|
|
}
|
|
return { session, version };
|
|
}, [version]);
|
|
|
|
return (
|
|
<StreamReactContext.Provider value={value}>
|
|
{props.children}
|
|
</StreamReactContext.Provider>
|
|
);
|
|
}
|