mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
218 lines
6.2 KiB
TypeScript
218 lines
6.2 KiB
TypeScript
/**
|
|
* useRealtimeService Hook
|
|
* Initializes and manages WebSocket connection for real-time collaboration
|
|
*
|
|
* Migrated from workflowui - requires Redux store and realtime service
|
|
*
|
|
* Features:
|
|
* - WebSocket connection initialization with JWT authentication
|
|
* - Real-time user presence tracking and cursor positions
|
|
* - Collaborative item locking/unlocking during editing
|
|
* - Live canvas update broadcasting
|
|
* - Automatic reconnection with exponential backoff
|
|
*/
|
|
|
|
import { useEffect, useCallback, useRef } from 'react';
|
|
|
|
/** Connected user interface */
|
|
export interface ConnectedUser {
|
|
userId: string;
|
|
userName: string;
|
|
userColor: string;
|
|
cursorPosition?: { x: number; y: number };
|
|
}
|
|
|
|
/** Current user interface */
|
|
export interface CurrentUser {
|
|
id: string;
|
|
name: string;
|
|
}
|
|
|
|
/** Realtime service interface */
|
|
export interface RealtimeService {
|
|
connect: (projectId: string, userId: string, userName: string, userColor: string) => void;
|
|
disconnect: () => void;
|
|
subscribe: (event: string, callback: (data: any) => void) => void;
|
|
broadcastCanvasUpdate: (itemId: string, position: { x: number; y: number }, size: { width: number; height: number }) => void;
|
|
broadcastCursorPosition: (x: number, y: number) => void;
|
|
lockItem: (itemId: string) => void;
|
|
releaseItem: (itemId: string) => void;
|
|
}
|
|
|
|
/** Redux actions interface */
|
|
export interface RealtimeActions {
|
|
setConnected: (connected: boolean) => any;
|
|
addConnectedUser: (user: ConnectedUser) => any;
|
|
removeConnectedUser: (userId: string) => any;
|
|
updateRemoteCursor: (data: { userId: string; position: { x: number; y: number } }) => any;
|
|
lockItem: (data: { itemId: string; userId: string }) => any;
|
|
releaseItem: (itemId: string) => any;
|
|
}
|
|
|
|
export interface UseRealtimeServiceOptions {
|
|
projectId: string;
|
|
enabled?: boolean;
|
|
onError?: (error: Error) => void;
|
|
/** Redux dispatch function */
|
|
dispatch: (action: any) => void;
|
|
/** Current user from Redux state */
|
|
user: CurrentUser | null;
|
|
/** Connected users from Redux state */
|
|
connectedUsers: ConnectedUser[];
|
|
/** Realtime service instance */
|
|
realtimeService: RealtimeService;
|
|
/** Redux action creators */
|
|
actions: RealtimeActions;
|
|
}
|
|
|
|
/**
|
|
* Hook to manage realtime service connection
|
|
* Automatically connects on mount and disconnects on unmount
|
|
*/
|
|
export function useRealtimeService(options: UseRealtimeServiceOptions) {
|
|
const {
|
|
projectId,
|
|
enabled = true,
|
|
onError,
|
|
dispatch,
|
|
user,
|
|
connectedUsers,
|
|
realtimeService,
|
|
actions
|
|
} = options;
|
|
|
|
const connectionRef = useRef<RealtimeService | null>(null);
|
|
|
|
// Initialize realtime connection on mount
|
|
useEffect(() => {
|
|
if (!enabled || !user || !projectId) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Get user color (generate deterministic color from user ID)
|
|
const hash = user.id.split('').reduce((acc, char) => {
|
|
return ((acc << 5) - acc) + char.charCodeAt(0);
|
|
}, 0);
|
|
const hue = Math.abs(hash) % 360;
|
|
const userColor = `hsl(${hue}, 70%, 50%)`;
|
|
|
|
// Connect to realtime service
|
|
realtimeService.connect(
|
|
projectId,
|
|
user.id,
|
|
user.name,
|
|
userColor
|
|
);
|
|
|
|
// Store connection reference
|
|
connectionRef.current = realtimeService;
|
|
|
|
// Set up event listeners
|
|
realtimeService.subscribe('connected', () => {
|
|
dispatch(actions.setConnected(true));
|
|
});
|
|
|
|
realtimeService.subscribe('disconnected', () => {
|
|
dispatch(actions.setConnected(false));
|
|
});
|
|
|
|
realtimeService.subscribe('user_joined', (data: any) => {
|
|
dispatch(
|
|
actions.addConnectedUser({
|
|
userId: data.userId,
|
|
userName: data.userName,
|
|
userColor: data.userColor || '#999',
|
|
cursorPosition: undefined
|
|
})
|
|
);
|
|
});
|
|
|
|
realtimeService.subscribe('user_left', (data: any) => {
|
|
dispatch(actions.removeConnectedUser(data.userId));
|
|
});
|
|
|
|
realtimeService.subscribe('cursor_moved', (data: any) => {
|
|
dispatch(
|
|
actions.updateRemoteCursor({
|
|
userId: data.userId,
|
|
position: data.position
|
|
})
|
|
);
|
|
});
|
|
|
|
realtimeService.subscribe('item_locked', (data: any) => {
|
|
dispatch(
|
|
actions.lockItem({
|
|
itemId: data.itemId,
|
|
userId: data.userId
|
|
})
|
|
);
|
|
});
|
|
|
|
realtimeService.subscribe('item_released', (data: any) => {
|
|
dispatch(actions.releaseItem(data.itemId));
|
|
});
|
|
|
|
return () => {
|
|
// Disconnect on unmount
|
|
realtimeService.disconnect();
|
|
dispatch(actions.setConnected(false));
|
|
};
|
|
} catch (error) {
|
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
console.error('Failed to initialize realtime service:', err);
|
|
onError?.(err);
|
|
}
|
|
}, [projectId, user, enabled, dispatch, onError, realtimeService, actions]);
|
|
|
|
// Broadcast canvas item updates
|
|
const broadcastCanvasUpdate = useCallback(
|
|
(itemId: string, position: { x: number; y: number }, size: { width: number; height: number }) => {
|
|
if (connectionRef.current) {
|
|
realtimeService.broadcastCanvasUpdate(itemId, position, size);
|
|
}
|
|
},
|
|
[realtimeService]
|
|
);
|
|
|
|
// Broadcast cursor position
|
|
const broadcastCursorPosition = useCallback((x: number, y: number) => {
|
|
if (connectionRef.current) {
|
|
realtimeService.broadcastCursorPosition(x, y);
|
|
}
|
|
}, [realtimeService]);
|
|
|
|
// Lock item during editing
|
|
const lockCanvasItem = useCallback((itemId: string) => {
|
|
if (connectionRef.current && user) {
|
|
realtimeService.lockItem(itemId);
|
|
dispatch(
|
|
actions.lockItem({
|
|
itemId,
|
|
userId: user.id
|
|
})
|
|
);
|
|
}
|
|
}, [dispatch, user, realtimeService, actions]);
|
|
|
|
// Release item after editing
|
|
const releaseCanvasItem = useCallback((itemId: string) => {
|
|
if (connectionRef.current) {
|
|
realtimeService.releaseItem(itemId);
|
|
dispatch(actions.releaseItem(itemId));
|
|
}
|
|
}, [dispatch, realtimeService, actions]);
|
|
|
|
return {
|
|
isConnected: connectionRef.current !== null,
|
|
connectedUsers,
|
|
broadcastCanvasUpdate,
|
|
broadcastCursorPosition,
|
|
lockCanvasItem,
|
|
releaseCanvasItem
|
|
};
|
|
}
|
|
|
|
export default useRealtimeService;
|