88from pycrdt import YMessageType , YSyncMessageType as YSyncMessageSubtype
99from jupyter_server_documents .ydocs import ydocs as jupyter_ydoc_classes
1010from jupyter_ydoc .ybasedoc import YBaseDoc
11+ from jupyter_events import EventLogger
1112from tornado .websocket import WebSocketHandler
1213from .yroom_file_api import YRoomFileAPI
14+ from .yroom_events_api import YRoomEventsAPI
1315
1416if TYPE_CHECKING :
1517 from typing import Literal , Tuple , Any
@@ -32,7 +34,7 @@ class YRoom:
3234
3335 file_api : YRoomFileAPI | None
3436 """
35- The `YRoomFileAPI` instance for this room. This is set to `None` if & only
37+ The `YRoomFileAPI` instance for this room. This is set to `None` only
3638 if `self.room_id == "JupyterLab:globalAwareness"`.
3739
3840 The file API provides `load_ydoc_content()` for loading the YDoc content
@@ -41,8 +43,16 @@ class YRoom:
4143 out-of-band changes.
4244 """
4345
46+ events_api : YRoomEventsAPI | None
47+ """
48+ A `YRoomEventsAPI` instance for this room that provides methods for emitting
49+ events through the `jupyter_events.EventLogger` singleton. This is set to
50+ `None` only if `self.room_id == "JupyterLab:globalAwareness"`.
51+ """
52+
4453 _jupyter_ydoc : YBaseDoc | None
4554 """JupyterYDoc"""
55+
4656 _ydoc : pycrdt .Doc
4757 """Ydoc"""
4858 _awareness : pycrdt .Awareness
@@ -83,7 +93,8 @@ def __init__(
8393 loop : asyncio .AbstractEventLoop ,
8494 fileid_manager : BaseFileIdManager ,
8595 contents_manager : AsyncContentsManager | ContentsManager ,
86- on_stop : callable [[], Any ] | None = None
96+ on_stop : callable [[], Any ] | None = None ,
97+ event_logger : EventLogger
8798 ):
8899 # Bind instance attributes
89100 self .room_id = room_id
@@ -98,14 +109,18 @@ def __init__(
98109 self ._ydoc = pycrdt .Doc ()
99110 self ._awareness = pycrdt .Awareness (ydoc = self ._ydoc )
100111
101- # If this room is providing global awareness, set `file_api` and
102- # `_jupyter_ydoc` to `None` as the YDoc is unused .
112+ # If this room is providing global awareness, set unused optional
113+ # attributes to `None`.
103114 if self .room_id == "JupyterLab:globalAwareness" :
104115 self .file_api = None
105116 self ._jupyter_ydoc = None
117+ self .events_api = None
106118 else :
107- # Otherwise, initialize `_jupyter_ydoc` and `file_api`.
119+ # Otherwise, initialize optional attributes for document rooms
120+ # Initialize JupyterYDoc
108121 self ._jupyter_ydoc = self ._init_jupyter_ydoc ()
122+
123+ # Initialize YRoomFileAPI, start loading content
109124 self .file_api = YRoomFileAPI (
110125 room_id = self .room_id ,
111126 jupyter_ydoc = self ._jupyter_ydoc ,
@@ -117,12 +132,18 @@ def __init__(
117132 on_outofband_move = self .handle_outofband_move ,
118133 on_inband_deletion = self .handle_inband_deletion
119134 )
120-
121- # Load the YDoc content after initializing
122135 self .file_api .load_ydoc_content ()
123136
124137 # Attach Jupyter YDoc observer to automatically save on change
125138 self ._jupyter_ydoc .observe (self ._on_jupyter_ydoc_update )
139+
140+ # Initialize YRoomEventsAPI
141+ self .events_api = YRoomEventsAPI (
142+ event_logger = event_logger ,
143+ fileid_manager = fileid_manager ,
144+ room_id = self .room_id ,
145+ log = self .log ,
146+ )
126147
127148 # Start observers on `self.ydoc` and `self.awareness` to ensure new
128149 # updates are always broadcast to all clients.
@@ -140,6 +161,18 @@ def __init__(
140161
141162 # Log notification that room is ready
142163 self .log .info (f"Room '{ self .room_id } ' initialized." )
164+
165+ # Emit events if defined
166+ if self .events_api :
167+ # Emit 'initialize' event
168+ self .events_api .emit_room_event ("initialize" )
169+
170+ # Emit 'load' event once content is loaded
171+ assert self .file_api
172+ async def emit_load_event ():
173+ await self .file_api .ydoc_content_loaded .wait ()
174+ self .events_api .emit_room_event ("load" )
175+ self ._loop .create_task (emit_load_event ())
143176
144177
145178 def _init_jupyter_ydoc (self ) -> YBaseDoc :
@@ -163,7 +196,7 @@ def _init_jupyter_ydoc(self) -> YBaseDoc:
163196 jupyter_ydoc_classes .get (file_type , jupyter_ydoc_classes ["file" ])
164197 )
165198
166- # Initialize Jupyter YDoc, add an observer to save it on change, return
199+ # Initialize Jupyter YDoc and return it
167200 jupyter_ydoc = JupyterYDocClass (ydoc = self ._ydoc , awareness = self ._awareness )
168201 return jupyter_ydoc
169202
@@ -613,6 +646,10 @@ def reload_ydoc(self) -> None:
613646 )
614647 self ._jupyter_ydoc .observe (self ._on_jupyter_ydoc_update )
615648
649+ # Emit 'overwrite' event as the YDoc content has been overwritten
650+ if self .events_api :
651+ self .events_api .emit_room_event ("overwrite" )
652+
616653
617654 def handle_outofband_move (self ) -> None :
618655 """
0 commit comments