4
4
from __future__ import annotations
5
5
6
6
import asyncio
7
+ import uuid
7
8
from logging import Logger
8
9
from typing import Any
9
10
10
11
from jupyter_events import EventLogger
11
12
from jupyter_ydoc import ydocs as YDOCS
12
13
from ypy_websocket .websocket_server import YRoom
13
14
from ypy_websocket .ystore import BaseYStore , YDocNotFound
15
+ from ypy_websocket .yutils import write_var_uint
14
16
15
17
from .loaders import FileLoader
16
- from .utils import JUPYTER_COLLABORATION_EVENTS_URI , LogLevel , OutOfBandChanges
18
+ from .utils import (
19
+ JUPYTER_COLLABORATION_EVENTS_URI ,
20
+ LogLevel ,
21
+ MessageType ,
22
+ OutOfBandChanges ,
23
+ RoomMessages ,
24
+ )
17
25
18
26
YFILE = YDOCS ["file" ]
19
27
@@ -45,9 +53,11 @@ def __init__(
45
53
self ._save_delay = save_delay
46
54
47
55
self ._update_lock = asyncio .Lock ()
56
+ self ._outofband_lock = asyncio .Lock ()
48
57
self ._initialization_lock = asyncio .Lock ()
49
58
self ._cleaner : asyncio .Task | None = None
50
59
self ._saving_document : asyncio .Task | None = None
60
+ self ._messages : dict [str , asyncio .Lock ] = {}
51
61
52
62
# Listen for document changes
53
63
self ._document .observe (self ._on_document_change )
@@ -149,6 +159,41 @@ async def initialize(self) -> None:
149
159
self .ready = True
150
160
self ._emit (LogLevel .INFO , "initialize" , "Room initialized" )
151
161
162
+ async def handle_msg (self , data : bytes ) -> None :
163
+ msg_type = data [0 ]
164
+ msg_id = data [2 :].decode ()
165
+
166
+ # Use a lock to prevent handling responses from multiple clients
167
+ # at the same time
168
+ async with self ._messages [msg_id ]:
169
+ # Check whether the previous client resolved the conflict
170
+ if msg_id not in self ._messages :
171
+ return
172
+
173
+ try :
174
+ ans = None
175
+ if msg_type == RoomMessages .RELOAD :
176
+ # Restore the room with the content from disk
177
+ await self ._load_document ()
178
+ ans = RoomMessages .DOC_OVERWRITTEN
179
+
180
+ elif msg_type == RoomMessages .OVERWRITE :
181
+ # Overwrite the file with content from the room
182
+ await self ._save_document ()
183
+ ans = RoomMessages .FILE_OVERWRITTEN
184
+
185
+ if ans is not None :
186
+ # Remove the lock and broadcast the resolution
187
+ self ._messages .pop (msg_id )
188
+ data = msg_id .encode ()
189
+ self ._outofband_lock .release ()
190
+ await self ._broadcast_msg (
191
+ bytes ([MessageType .ROOM , ans ]) + write_var_uint (len (data )) + data
192
+ )
193
+
194
+ except Exception :
195
+ return
196
+
152
197
def _emit (self , level : LogLevel , action : str | None = None , msg : str | None = None ) -> None :
153
198
data = {"level" : level .value , "room" : self ._room_id , "path" : self ._file .path }
154
199
if action :
@@ -187,24 +232,24 @@ async def _on_content_change(self, event: str, args: dict[str, Any]) -> None:
187
232
event (str): Type of change.
188
233
args (dict): A dictionary with format, type, last_modified.
189
234
"""
235
+ if self ._outofband_lock .locked ():
236
+ return
237
+
190
238
if event == "metadata" and (
191
239
self ._last_modified is None or self ._last_modified < args ["last_modified" ]
192
240
):
193
241
self .log .info ("Out-of-band changes. Overwriting the content in room %s" , self ._room_id )
194
242
self ._emit (LogLevel .INFO , "overwrite" , "Out-of-band changes. Overwriting the room." )
195
243
196
- try :
197
- model = await self ._file .load_content (self ._file_format , self ._file_type , True )
198
- except Exception as e :
199
- msg = f"Error loading content from file: { self ._file .path } \n { e !r} "
200
- self .log .error (msg , exc_info = e )
201
- self ._emit (LogLevel .ERROR , None , msg )
202
- return None
203
-
204
- async with self ._update_lock :
205
- self ._document .source = model ["content" ]
206
- self ._last_modified = model ["last_modified" ]
207
- self ._document .dirty = False
244
+ msg_id = str (uuid .uuid4 ())
245
+ self ._messages [msg_id ] = asyncio .Lock ()
246
+ await self ._outofband_lock .acquire ()
247
+ data = msg_id .encode ()
248
+ await self ._broadcast_msg (
249
+ bytes ([MessageType .ROOM , RoomMessages .FILE_CHANGED ])
250
+ + write_var_uint (len (data ))
251
+ + data
252
+ )
208
253
209
254
def _on_document_change (self , target : str , event : Any ) -> None :
210
255
"""
@@ -231,6 +276,45 @@ def _on_document_change(self, target: str, event: Any) -> None:
231
276
232
277
self ._saving_document = asyncio .create_task (self ._maybe_save_document ())
233
278
279
+ async def _load_document (self ) -> None :
280
+ try :
281
+ model = await self ._file .load_content (self ._file_format , self ._file_type , True )
282
+ except Exception as e :
283
+ msg = f"Error loading content from file: { self ._file .path } \n { e !r} "
284
+ self .log .error (msg , exc_info = e )
285
+ self ._emit (LogLevel .ERROR , None , msg )
286
+ return None
287
+
288
+ async with self ._update_lock :
289
+ self ._document .source = model ["content" ]
290
+ self ._last_modified = model ["last_modified" ]
291
+ self ._document .dirty = False
292
+
293
+ async def _save_document (self ) -> None :
294
+ """
295
+ Saves the content of the document to disk.
296
+ """
297
+ try :
298
+ self .log .info ("Saving the content from room %s" , self ._room_id )
299
+ model = await self ._file .save_content (
300
+ {
301
+ "format" : self ._file_format ,
302
+ "type" : self ._file_type ,
303
+ "last_modified" : self ._last_modified ,
304
+ "content" : self ._document .source ,
305
+ }
306
+ )
307
+ self ._last_modified = model ["last_modified" ]
308
+ async with self ._update_lock :
309
+ self ._document .dirty = False
310
+
311
+ self ._emit (LogLevel .INFO , "save" , "Content saved." )
312
+
313
+ except Exception as e :
314
+ msg = f"Error saving file: { self ._file .path } \n { e !r} "
315
+ self .log .error (msg , exc_info = e )
316
+ self ._emit (LogLevel .ERROR , None , msg )
317
+
234
318
async def _maybe_save_document (self ) -> None :
235
319
"""
236
320
Saves the content of the document to disk.
@@ -248,7 +332,7 @@ async def _maybe_save_document(self) -> None:
248
332
249
333
try :
250
334
self .log .info ("Saving the content from room %s" , self ._room_id )
251
- model = await self ._file .save_content (
335
+ model = await self ._file .maybe_save_content (
252
336
{
253
337
"format" : self ._file_format ,
254
338
"type" : self ._file_type ,
@@ -284,6 +368,10 @@ async def _maybe_save_document(self) -> None:
284
368
self .log .error (msg , exc_info = e )
285
369
self ._emit (LogLevel .ERROR , None , msg )
286
370
371
+ async def _broadcast_msg (self , msg : bytes ) -> None :
372
+ for client in self .clients :
373
+ await client .send (msg )
374
+
287
375
288
376
class TransientRoom (YRoom ):
289
377
"""A Y room for sharing state (e.g. awareness)."""
0 commit comments