import { useEffect } from "react"; import { socketAtom } from "@/features/websocket/atoms/socket-atom.ts"; import { useAtom, useSetAtom } from "jotai"; import { treeDataAtom } from "@/features/page/tree/atoms/tree-data-atom.ts"; import { WebSocketEvent } from "@/features/websocket/types"; import { SpaceTreeNode } from "@/features/page/tree/types.ts"; import { useQueryClient } from "@tanstack/react-query"; import { treeModel } from "@/features/page/tree/model/tree-model"; import { applyUpdateOne, applyAddTreeNode, applyMoveTreeNode, applyDeleteTreeNode, } from "@/features/websocket/tree-socket-reducers.ts"; import localEmitter from "@/lib/local-emitter.ts"; export const useTreeSocket = () => { const [socket] = useAtom(socketAtom); // Setter-only: this hook writes the tree from socket events but never reads it // reactively, so useSetAtom avoids re-rendering UserProvider (its host) on // every tree event. const setTreeData = useSetAtom(treeDataAtom); const queryClient = useQueryClient(); useEffect(() => { const updateNodeName = (event) => { if (event.payload?.title === undefined) return; setTreeData((prev) => { if (!treeModel.find(prev, event?.id)) return prev; return treeModel.update(prev, event.id, { name: event.payload.title, } as Partial); }); }; localEmitter.on("message", updateNodeName); return () => { localEmitter.off("message", updateNodeName); }; }, []); useEffect(() => { if (!socket) return; // Named handler + off() cleanup (mirrors use-notification-socket). Without // cleanup, every socket recreation / effect re-run stacked another listener, // so a single broadcast fired duplicated tree walks after each reconnect. const handleMessage = (event: WebSocketEvent) => { switch (event.operation) { case "updateOne": if (event.entity[0] === "pages") { setTreeData((prev) => applyUpdateOne(prev, event)); } break; case "addTreeNode": setTreeData((prev) => applyAddTreeNode(prev, event.payload)); break; case "moveTreeNode": setTreeData((prev) => applyMoveTreeNode(prev, event.payload)); break; case "deleteTreeNode": // The `invalidateQueries` side effect stays in the hook; the tree // transform (`applyDeleteTreeNode`) is pure. Only invalidate when the // node is actually in the tree (mirrors the pure reducer's early-out). setTreeData((prev) => { if (treeModel.find(prev, event.payload.node.id)) { queryClient.invalidateQueries({ queryKey: ["pages", event.payload.node.slugId].filter(Boolean), }); } return applyDeleteTreeNode(prev, event.payload); }); break; } }; socket.on("message", handleMessage); return () => { socket.off("message", handleMessage); }; }, [socket, queryClient, setTreeData]); };