Skip to content

Commit a3dac7a

Browse files
committed
add observe_jupyter_ydoc() method for consumers
1 parent aff427f commit a3dac7a

File tree

1 file changed

+52
-7
lines changed

1 file changed

+52
-7
lines changed

jupyter_server_documents/rooms/yroom.py

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from ..websockets import YjsClientGroup
66

77
import pycrdt
8+
import uuid
89
from pycrdt import YMessageType, YSyncMessageType as YSyncMessageSubtype
910
from jupyter_server_documents.ydocs import ydocs as jupyter_ydoc_classes
1011
from 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

Comments
 (0)