Initial Commit
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user