diff --git a/js/components/src/livestream-store/hooks.tsx b/js/components/src/livestream-store/hooks.tsx index 2a7d3bac..81d2c3cd 100644 --- a/js/components/src/livestream-store/hooks.tsx +++ b/js/components/src/livestream-store/hooks.tsx @@ -275,7 +275,6 @@ export const useReportChatMessage = () => { export const usePinChatMessage = () => { const agent = usePDSAgent(); - const store = getStoreFromContext(); return async ( messageUri: string, diff --git a/js/web/src/components/stream/livestream-provider.test.tsx b/js/web/src/components/stream/livestream-provider.test.tsx new file mode 100644 index 00000000..71d3abd5 --- /dev/null +++ b/js/web/src/components/stream/livestream-provider.test.tsx @@ -0,0 +1,59 @@ +import type { LivestreamStore } from "@streamplace/core"; +import { act, type ReactNode } from "react"; +import { createRoot } from "react-dom/client"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { LivestreamProvider } from "./livestream-provider"; + +class MockWebSocket { + onopen: (() => void) | null = null; + onmessage: ((event: MessageEvent) => void) | null = null; + onclose: (() => void) | null = null; + onerror: (() => void) | null = null; + + close() {} +} + +describe("LivestreamProvider", () => { + let container: HTMLDivElement; + + beforeEach(() => { + container = document.createElement("div"); + document.body.appendChild(container); + vi.stubGlobal("WebSocket", MockWebSocket); + vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); + }); + + afterEach(() => { + container.remove(); + vi.unstubAllGlobals(); + }); + + it("creates fresh stream state when the user changes", async () => { + const root = createRoot(container); + let currentStore: LivestreamStore | undefined; + const captureStore = (store: LivestreamStore): ReactNode => { + currentStore = store; + return null; + }; + + await act(async () => { + root.render( + {captureStore}, + ); + }); + + const aliceStore = currentStore!; + act(() => aliceStore.setState({ viewers: 42 })); + + await act(async () => { + root.render( + {captureStore}, + ); + }); + + expect(currentStore).not.toBe(aliceStore); + expect(currentStore?.getState().viewers).toBeNull(); + + await act(async () => root.unmount()); + }); +}); diff --git a/js/web/src/components/stream/livestream-provider.tsx b/js/web/src/components/stream/livestream-provider.tsx index d3ea15eb..1e8c537e 100644 --- a/js/web/src/components/stream/livestream-provider.tsx +++ b/js/web/src/components/stream/livestream-provider.tsx @@ -4,7 +4,7 @@ import { makeLivestreamStore, type LivestreamStore, } from "@streamplace/core"; -import { useEffect, useRef, useState, type ReactNode } from "react"; +import { useEffect, useMemo, type ReactNode } from "react"; interface LivestreamProviderProps { /** The DID or handle of the user to connect to. */ @@ -22,19 +22,10 @@ export function LivestreamProvider({ user, children, }: LivestreamProviderProps) { - const store = useRef(null); - const [initialized, setInitialized] = useState(false); - - // Initialize store once - useEffect(() => { - store.current = makeLivestreamStore(); - setInitialized(true); - }, []); + const store = useMemo(() => makeLivestreamStore(), [user]); // WebSocket connection useEffect(() => { - if (!store.current) return; - const wsUrl = `${getStreamplaceUrl()}/api/websocket/${user}`; let ws: WebSocket | null = null; @@ -65,7 +56,7 @@ export function LivestreamProvider({ flushTimer = null; const batch = messageBuffer; messageBuffer = []; - store.current?.setState((s) => handleWebSocketMessages(s, batch)); + store.setState((s) => handleWebSocketMessages(s, batch)); }; ws.onmessage = (event) => { @@ -82,13 +73,13 @@ export function LivestreamProvider({ ws.onclose = () => { if (connectId !== currentConnectId) return; - store.current?.setState((s) => ({ ...s, websocketConnected: false })); + store.setState((s) => ({ ...s, websocketConnected: false })); scheduleReconnect(); }; ws.onerror = () => { if (connectId !== currentConnectId) return; - store.current?.setState((s) => ({ ...s, websocketConnected: false })); + store.setState((s) => ({ ...s, websocketConnected: false })); ws?.close(); scheduleReconnect(); }; @@ -102,15 +93,7 @@ export function LivestreamProvider({ if (reconnectTimeout) clearTimeout(reconnectTimeout); ws?.close(); }; - }, [user]); - - if (!initialized || !store.current) { - return ( -
-
-
- ); - } + }, [store, user]); - return <>{children(store.current)}; + return <>{children(store)}; } diff --git a/js/web/src/lib/store/slices/blueskySlice.ts b/js/web/src/lib/store/slices/blueskySlice.ts index 6d340875..ad893f66 100644 --- a/js/web/src/lib/store/slices/blueskySlice.ts +++ b/js/web/src/lib/store/slices/blueskySlice.ts @@ -362,7 +362,7 @@ export const createBlueskySlice: StateCreator< }, logout: async () => { - await storage.removeItem("did"); + await storage.removeItem(DID_KEY); await storage.removeItem(STORED_KEY_KEY); const state = get() as BlueskySlice; if (!state.oauthSession) {