smart scene sync

This commit is contained in:
Philipinho
2026-01-17 14:39:27 +00:00
parent ab81903299
commit c1cfe158cd
@@ -42,6 +42,9 @@ export function useExcalidrawCollab(
const collaboratorsRef = useRef<Map<string, Collaborator>>(new Map()); const collaboratorsRef = useRef<Map<string, Collaborator>>(new Map());
const [isCollaborating, setIsCollaborating] = useState(false); const [isCollaborating, setIsCollaborating] = useState(false);
// Track broadcasted element versions for bandwidth optimization
const broadcastedElementVersions = useRef<Map<string, number>>(new Map());
const roomId = pageId ? `excalidraw-${pageId}` : null; const roomId = pageId ? `excalidraw-${pageId}` : null;
const username = currentUser?.user?.name || "Anonymous"; const username = currentUser?.user?.name || "Anonymous";
@@ -77,32 +80,52 @@ export function useExcalidrawCollab(
[socket, roomId, username, excalidrawAPI], [socket, roomId, username, excalidrawAPI],
); );
// Broadcast scene changes // Broadcast scene changes with bandwidth optimization
const broadcastScene = useMemo( const broadcastScene = useCallback(
() => (elements: readonly ExcalidrawElement[], syncAll = false) => {
throttle((elements: readonly ExcalidrawElement[]) => { if (!socket || !roomId || !isInitialized.current) {
if (!socket || !roomId || !isInitialized.current) { return;
return; }
}
const sceneVersion = getSceneVersion(elements); const sceneVersion = getSceneVersion(elements);
if (sceneVersion <= lastBroadcastedVersion.current) { if (sceneVersion <= lastBroadcastedVersion.current) {
return; return;
} }
const data: SceneUpdateMessage = { // Filter to only send elements that changed since last broadcast
type: "SCENE_UPDATE", const changedElements = elements.filter((element) => {
payload: { elements }, const lastVersion = broadcastedElementVersions.current.get(element.id);
}; return syncAll || lastVersion === undefined || element.version > lastVersion;
});
const json = JSON.stringify(data); if (changedElements.length === 0) {
socket.emit("server-broadcast", [roomId, json, null]); return;
lastBroadcastedVersion.current = sceneVersion; }
}, 100),
const data: SceneUpdateMessage = {
type: "SCENE_UPDATE",
payload: { elements: changedElements },
};
// Update tracking map
for (const element of changedElements) {
broadcastedElementVersions.current.set(element.id, element.version);
}
const json = JSON.stringify(data);
socket.emit("server-broadcast", [roomId, json, null]);
lastBroadcastedVersion.current = sceneVersion;
},
[socket, roomId], [socket, roomId],
); );
// Throttled version for onChange handler
const throttledBroadcastScene = useMemo(
() => throttle((elements: readonly ExcalidrawElement[]) => broadcastScene(elements, false), 100),
[broadcastScene],
);
// Handle incoming broadcasts // Handle incoming broadcasts
const handleClientBroadcast = useCallback( const handleClientBroadcast = useCallback(
(jsonData: string, _iv: Uint8Array | null) => { (jsonData: string, _iv: Uint8Array | null) => {
@@ -207,7 +230,8 @@ export function useExcalidrawCollab(
socket.on("new-user", (socketId: string) => { socket.on("new-user", (socketId: string) => {
console.log("New user joined:", socketId); console.log("New user joined:", socketId);
if (excalidrawAPI) { if (excalidrawAPI) {
broadcastScene(excalidrawAPI.getSceneElements()); // Send full scene to new user (syncAll = true)
broadcastScene(excalidrawAPI.getSceneElements(), true);
} }
}); });
@@ -220,6 +244,7 @@ export function useExcalidrawCollab(
socket.off("new-user"); socket.off("new-user");
isInitialized.current = false; isInitialized.current = false;
lastBroadcastedVersion.current = -1; lastBroadcastedVersion.current = -1;
broadcastedElementVersions.current = new Map();
collaboratorsRef.current = new Map(); collaboratorsRef.current = new Map();
setIsCollaborating(false); setIsCollaborating(false);
}; };
@@ -234,7 +259,7 @@ export function useExcalidrawCollab(
]); ]);
return { return {
broadcastScene, broadcastScene: throttledBroadcastScene,
broadcastPointer, broadcastPointer,
isCollaborating, isCollaborating,
}; };