@@ -57,6 +57,12 @@ class YDocWebSocketHandler(WebSocketHandler, JupyterHandler):
5757 _serve_task : asyncio .Task | None
5858 _message_queue : asyncio .Queue [Any ]
5959 _background_tasks : set [asyncio .Task ]
60+ _room_locks : dict [str , asyncio .Lock ] = {}
61+
62+ def _room_lock (self , room_id : str ) -> asyncio .Lock :
63+ if room_id not in self ._room_locks :
64+ self ._room_locks [room_id ] = asyncio .Lock ()
65+ return self ._room_locks [room_id ]
6066
6167 def create_task (self , aw ):
6268 task = asyncio .create_task (aw )
@@ -71,38 +77,38 @@ async def prepare(self):
7177 # Get room
7278 self ._room_id : str = self .request .path .split ("/" )[- 1 ]
7379
74- if self ._websocket_server .room_exists (self ._room_id ):
75- self .room : YRoom = await self ._websocket_server .get_room (self ._room_id )
76-
77- else :
78- if self ._room_id .count (":" ) >= 2 :
79- # DocumentRoom
80- file_format , file_type , file_id = decode_file_path (self ._room_id )
81- if file_id in self ._file_loaders :
82- self ._emit (
83- LogLevel .WARNING ,
84- None ,
85- "There is another collaborative session accessing the same file.\n The synchronization between rooms is not supported and you might lose some of your changes." ,
80+ async with self ._room_lock (self ._room_id ):
81+ if self ._websocket_server .room_exists (self ._room_id ):
82+ self .room : YRoom = await self ._websocket_server .get_room (self ._room_id )
83+ else :
84+ if self ._room_id .count (":" ) >= 2 :
85+ # DocumentRoom
86+ file_format , file_type , file_id = decode_file_path (self ._room_id )
87+ if file_id in self ._file_loaders :
88+ self ._emit (
89+ LogLevel .WARNING ,
90+ None ,
91+ "There is another collaborative session accessing the same file.\n The synchronization between rooms is not supported and you might lose some of your changes." ,
92+ )
93+
94+ file = self ._file_loaders [file_id ]
95+ updates_file_path = f".{ file_type } :{ file_id } .y"
96+ ystore = self ._ystore_class (path = updates_file_path , log = self .log )
97+ self .room = DocumentRoom (
98+ self ._room_id ,
99+ file_format ,
100+ file_type ,
101+ file ,
102+ self .event_logger ,
103+ ystore ,
104+ self .log ,
105+ self ._document_save_delay ,
86106 )
87107
88- file = self ._file_loaders [file_id ]
89- updates_file_path = f".{ file_type } :{ file_id } .y"
90- ystore = self ._ystore_class (path = updates_file_path , log = self .log )
91- self .room = DocumentRoom (
92- self ._room_id ,
93- file_format ,
94- file_type ,
95- file ,
96- self .event_logger ,
97- ystore ,
98- self .log ,
99- self ._document_save_delay ,
100- )
101-
102- else :
103- # TransientRoom
104- # it is a transient document (e.g. awareness)
105- self .room = TransientRoom (self ._room_id , self .log )
108+ else :
109+ # TransientRoom
110+ # it is a transient document (e.g. awareness)
111+ self .room = TransientRoom (self ._room_id , self .log )
106112
107113 await self ._websocket_server .start_room (self .room )
108114 self ._websocket_server .add_room (self ._room_id , self .room )
@@ -191,7 +197,8 @@ async def open(self, room_id):
191197
192198 try :
193199 # Initialize the room
194- await self .room .initialize ()
200+ async with self ._room_lock (self ._room_id ):
201+ await self .room .initialize ()
195202 self ._emit_awareness_event (self .current_user .username , "join" )
196203 except Exception as e :
197204 _ , _ , file_id = decode_file_path (self ._room_id )
@@ -319,29 +326,31 @@ async def _clean_room(self) -> None:
319326 contains a copy of the document. In addition, we remove the file if there is no rooms
320327 subscribed to it.
321328 """
322- assert isinstance (self .room , DocumentRoom )
329+ async with self ._room_lock (self ._room_id ):
330+ assert isinstance (self .room , DocumentRoom )
323331
324- if self ._cleanup_delay is None :
325- return
332+ if self ._cleanup_delay is None :
333+ return
326334
327- await asyncio .sleep (self ._cleanup_delay )
335+ await asyncio .sleep (self ._cleanup_delay )
328336
329- # Remove the room from the websocket server
330- self .log .info ("Deleting Y document from memory: %s" , self .room .room_id )
331- self ._websocket_server .delete_room (room = self .room )
337+ # Remove the room from the websocket server
338+ self .log .info ("Deleting Y document from memory: %s" , self .room .room_id )
339+ self ._websocket_server .delete_room (room = self .room )
332340
333- # Clean room
334- del self .room
335- self .log .info ("Room %s deleted" , self ._room_id )
336- self ._emit (LogLevel .INFO , "clean" , "Room deleted." )
341+ # Clean room
342+ del self .room
343+ self .log .info ("Room %s deleted" , self ._room_id )
344+ self ._emit (LogLevel .INFO , "clean" , "Room deleted." )
337345
338- # Clean the file loader if there are not rooms using it
339- _ , _ , file_id = decode_file_path (self ._room_id )
340- file = self ._file_loaders [file_id ]
341- if file .number_of_subscriptions == 0 :
342- self .log .info ("Deleting file %s" , file .path )
343- await self ._file_loaders .remove (file_id )
344- self ._emit (LogLevel .INFO , "clean" , "Loader deleted." )
346+ # Clean the file loader if there are not rooms using it
347+ _ , _ , file_id = decode_file_path (self ._room_id )
348+ file = self ._file_loaders [file_id ]
349+ if file .number_of_subscriptions == 0 :
350+ self .log .info ("Deleting file %s" , file .path )
351+ await self ._file_loaders .remove (file_id )
352+ self ._emit (LogLevel .INFO , "clean" , "Loader deleted." )
353+ del self ._room_locks [self ._room_id ]
345354
346355 def check_origin (self , origin ):
347356 """
0 commit comments