66 * LICENSE file in the root directory of this source tree.
77 */
88
9- import React , { createContext , useContext , useCallback , useMemo } from 'react' ;
9+ import React , { createContext , useContext , useCallback , useMemo , useEffect , useRef , useState } from 'react' ;
1010import type { CollaborationConfig , CollaborationPresence , CollaborationOperation } from '@object-ui/types' ;
1111
1212export interface CollaborationContextValue {
@@ -18,6 +18,10 @@ export interface CollaborationContextValue {
1818 sendOperation : ( operation : Omit < CollaborationOperation , 'id' | 'timestamp' | 'version' > ) => void ;
1919 /** Current user ID */
2020 currentUserId ?: string ;
21+ /** Connection state */
22+ connectionState : 'disconnected' | 'connecting' | 'connected' | 'error' ;
23+ /** Version history entries (when versionHistory is enabled) */
24+ versionCount : number ;
2125}
2226
2327const CollabCtx = createContext < CollaborationContextValue | null > ( null ) ;
@@ -34,50 +38,137 @@ export interface CollaborationProviderProps {
3438 } ;
3539 /** Callback when an operation is received from another user */
3640 onOperation ?: ( operation : CollaborationOperation ) => void ;
41+ /** Callback when a remote user joins or leaves */
42+ onPresenceChange ?: ( users : CollaborationPresence [ ] ) => void ;
3743 /** Children */
3844 children : React . ReactNode ;
3945}
4046
4147/**
4248 * Provider for multi-user collaborative editing.
4349 * Manages WebSocket connection, presence, and operation broadcasting.
50+ * Supports real-time collaboration via WebSocket when serverUrl is configured.
4451 */
4552export function CollaborationProvider ( {
4653 config,
4754 user,
4855 onOperation,
56+ onPresenceChange,
4957 children,
5058} : CollaborationProviderProps ) {
59+ const wsRef = useRef < WebSocket | null > ( null ) ;
60+ const [ remoteUsers , setRemoteUsers ] = useState < CollaborationPresence [ ] > ( [ ] ) ;
61+ const [ connectionState , setConnectionState ] = useState < 'disconnected' | 'connecting' | 'connected' | 'error' > ( 'disconnected' ) ;
62+ const versionRef = useRef ( 0 ) ;
63+ const reconnectTimerRef = useRef < ReturnType < typeof setTimeout > | null > ( null ) ;
64+
65+ // Connect via WebSocket when serverUrl is provided
66+ useEffect ( ( ) => {
67+ if ( ! config . enabled || ! user || ! config . serverUrl ) return ;
68+
69+ const canUseWS = typeof WebSocket !== 'undefined' ;
70+ if ( ! canUseWS ) return ;
71+
72+ function connect ( ) {
73+ const url = new URL ( config . serverUrl ! ) ;
74+ if ( config . roomId ) url . searchParams . set ( 'room' , config . roomId ) ;
75+ url . searchParams . set ( 'userId' , user ! . id ) ;
76+
77+ setConnectionState ( 'connecting' ) ;
78+ const ws = new WebSocket ( url . toString ( ) ) ;
79+
80+ ws . onopen = ( ) => {
81+ setConnectionState ( 'connected' ) ;
82+ // Send join message
83+ ws . send ( JSON . stringify ( {
84+ type : 'join' ,
85+ userId : user ! . id ,
86+ userName : user ! . name ,
87+ avatar : user ! . avatar ,
88+ } ) ) ;
89+ } ;
90+
91+ ws . onmessage = ( event ) => {
92+ try {
93+ const msg = JSON . parse ( event . data ) ;
94+ if ( msg . type === 'presence' ) {
95+ const users = ( msg . users || [ ] ) as CollaborationPresence [ ] ;
96+ setRemoteUsers ( users . filter ( ( u : CollaborationPresence ) => u . userId !== user ! . id ) ) ;
97+ onPresenceChange ?.( users ) ;
98+ } else if ( msg . type === 'operation' ) {
99+ onOperation ?.( msg . operation as CollaborationOperation ) ;
100+ }
101+ } catch {
102+ // Ignore malformed messages
103+ }
104+ } ;
105+
106+ ws . onclose = ( ) => {
107+ setConnectionState ( 'disconnected' ) ;
108+ wsRef . current = null ;
109+ // Auto-reconnect
110+ if ( config . enabled ) {
111+ reconnectTimerRef . current = setTimeout ( connect , config . autoSaveInterval ?? 3000 ) ;
112+ }
113+ } ;
114+
115+ ws . onerror = ( ) => {
116+ setConnectionState ( 'error' ) ;
117+ } ;
118+
119+ wsRef . current = ws ;
120+ }
121+
122+ connect ( ) ;
123+
124+ return ( ) => {
125+ if ( reconnectTimerRef . current ) clearTimeout ( reconnectTimerRef . current ) ;
126+ if ( wsRef . current ) {
127+ wsRef . current . onclose = null ; // Prevent reconnect on intentional close
128+ wsRef . current . close ( ) ;
129+ wsRef . current = null ;
130+ }
131+ setConnectionState ( 'disconnected' ) ;
132+ } ;
133+ } , [ config . enabled , config . serverUrl , config . roomId , config . autoSaveInterval , user , onOperation , onPresenceChange ] ) ;
134+
51135 const users = useMemo < CollaborationPresence [ ] > ( ( ) => {
52136 if ( ! config . enabled || ! user ) return [ ] ;
53- return [ {
137+ const currentUser : CollaborationPresence = {
54138 userId : user . id ,
55139 userName : user . name ,
56140 avatar : user . avatar ,
57141 color : generateColor ( user . id ) ,
58142 status : 'active' as const ,
59143 lastActivity : new Date ( ) . toISOString ( ) ,
60- } ] ;
61- } , [ config . enabled , user ] ) ;
144+ } ;
145+ return [ currentUser , ...remoteUsers ] ;
146+ } , [ config . enabled , user , remoteUsers ] ) ;
62147
63- const isConnected = config . enabled && ! ! user ;
148+ const isConnected = config . enabled && ! ! user && ( config . serverUrl ? connectionState === 'connected' : true ) ;
64149
65150 const sendOperation = useCallback (
66151 ( operation : Omit < CollaborationOperation , 'id' | 'timestamp' | 'version' > ) => {
67- if ( ! isConnected || ! user ) return ;
152+ if ( ! config . enabled || ! user ) return ;
68153
154+ versionRef . current += 1 ;
69155 const fullOp : CollaborationOperation = {
70156 ...operation ,
71- id : `op-${ Date . now ( ) } ` ,
157+ id : `op-${ Date . now ( ) } - ${ versionRef . current } ` ,
72158 userId : user . id ,
73159 timestamp : new Date ( ) . toISOString ( ) ,
74- version : Date . now ( ) ,
160+ version : versionRef . current ,
75161 } ;
76162
77- // In a real implementation, this would send via WebSocket
163+ // Send via WebSocket if connected
164+ if ( wsRef . current ?. readyState === WebSocket . OPEN ) {
165+ wsRef . current . send ( JSON . stringify ( { type : 'operation' , operation : fullOp } ) ) ;
166+ }
167+
168+ // Always notify local listeners
78169 onOperation ?.( fullOp ) ;
79170 } ,
80- [ isConnected , user , onOperation ] ,
171+ [ config . enabled , user , onOperation ] ,
81172 ) ;
82173
83174 const value = useMemo < CollaborationContextValue > (
@@ -86,8 +177,10 @@ export function CollaborationProvider({
86177 isConnected,
87178 sendOperation,
88179 currentUserId : user ?. id ,
180+ connectionState,
181+ versionCount : versionRef . current ,
89182 } ) ,
90- [ users , isConnected , sendOperation , user ?. id ] ,
183+ [ users , isConnected , sendOperation , user ?. id , connectionState ] ,
91184 ) ;
92185
93186 return < CollabCtx . Provider value = { value } > { children } </ CollabCtx . Provider > ;
0 commit comments