55from ..websockets import YjsClientGroup
66
77import pycrdt
8+ import uuid
89from pycrdt import YMessageType , YSyncMessageType as YSyncMessageSubtype
910from jupyter_server_documents .ydocs import ydocs as jupyter_ydoc_classes
1011from jupyter_ydoc .ybasedoc import YBaseDoc
@@ -53,6 +54,14 @@ class YRoom:
5354 _jupyter_ydoc : YBaseDoc | None
5455 """JupyterYDoc"""
5556
57+ _jupyter_ydoc_observers : dict [str , callable [[str , Any ], Any ]]
58+ """
59+ Dictionary of JupyterYDoc observers added by consumers of this room.
60+
61+ Added to via `observe_jupyter_ydoc()`. Removed from via
62+ `unobserve_jupyter_ydoc()`.
63+ """
64+
5665 _ydoc : pycrdt .Doc
5766 """Ydoc"""
5867 _awareness : pycrdt .Awareness
@@ -107,6 +116,7 @@ def __init__(
107116 self ._loop = loop
108117 self ._fileid_manager = fileid_manager
109118 self ._contents_manager = contents_manager
119+ self ._jupyter_ydoc_observers = {}
110120 self ._stopped = False
111121 self ._updated = False
112122
@@ -482,6 +492,37 @@ def _on_ydoc_update(self, event: TransactionEvent) -> None:
482492 self ._broadcast_message (message , message_type = "SyncUpdate" )
483493
484494
495+ def observe_jupyter_ydoc (self , observer : callable [[str , Any ], Any ]) -> str :
496+ """
497+ Adds an observer callback to the JupyterYDoc that fires on change.
498+ The callback should accept 2 arguments:
499+
500+ 1. `updated_key: str`: the key of the shared type that was updated, e.g.
501+ "cells", "state", or "metadata".
502+
503+ 2. `event: Any`: The `pycrdt` event corresponding to the shared type.
504+ For example, if "state" refers to a `pycrdt.Map`, `event` will take the
505+ type `pycrdt.MapEvent`.
506+
507+ Consumers should use this method instead of calling `observe()` directly
508+ on the `jupyter_ydoc.YBaseDoc` instance, because JupyterYDocs generally
509+ only allow for a single observer.
510+
511+ Returns an `observer_id: str` that can be passed to
512+ `unobserve_jupyter_ydoc()` to remove the observer.
513+ """
514+ observer_id = uuid .uuid4 ()
515+ self ._jupyter_ydoc_observers [observer_id ] = observer
516+
517+
518+ def unobserve_jupyter_ydoc (self , observer_id : str ):
519+ """
520+ Removes an observer from the JupyterYDoc previously added by
521+ `observe_jupyter_ydoc()`, given the returned `observer_id`.
522+ """
523+ self ._jupyter_ydoc_observers .pop (observer_id , None )
524+
525+
485526 def _on_jupyter_ydoc_update (self , updated_key : str , event : Any ) -> None :
486527 """
487528 This method is an observer on `self._jupyter_ydoc` which saves the file
@@ -518,10 +559,15 @@ def _on_jupyter_ydoc_update(self, updated_key: str, event: Any) -> None:
518559 if should_ignore_state_update (map_event ):
519560 return
520561
521- # Otherwise, a change was made. Set `updated=True` and save the file
562+ # Otherwise, a change was made.
563+ # Call all observers added by consumers first.
564+ for observer in self ._jupyter_ydoc_observers .values ():
565+ observer (updated_key , event )
566+
567+ # Then set `updated=True` and save the file.
522568 self ._updated = True
523569 self .file_api .schedule_save ()
524-
570+
525571
526572 def handle_awareness_update (self , client_id : str , message : bytes ) -> None :
527573 # Apply the AwarenessUpdate message
@@ -707,14 +753,14 @@ def stop(self, close_code: int = 1001, immediately: bool = False):
707753 self ._loop .create_task (
708754 self .file_api .save (prev_jupyter_ydoc )
709755 )
710- self ._clear_ydoc ()
756+ self ._reset_ydoc ()
711757 self ._stopped = True
712758
713759
714- def _clear_ydoc (self ):
760+ def _reset_ydoc (self ):
715761 """
716- Clears the YDoc, awareness, and JupyterYDoc, freeing their memory to the
717- server. This deletes the YDoc history .
762+ Deletes and re-initializes the YDoc, awareness, and JupyterYDoc. This
763+ frees the memory occupied by their histories .
718764 """
719765 self ._ydoc = self ._init_ydoc ()
720766 self ._awareness = self ._init_awareness (ydoc = self ._ydoc )
@@ -723,7 +769,6 @@ def _clear_ydoc(self):
723769 awareness = self ._awareness
724770 )
725771
726-
727772 @property
728773 def stopped (self ) -> bool :
729774 """
0 commit comments