Skip to content

Commit 3e4607b

Browse files
committed
first version receiving Yjs messages
1 parent 09b4ee2 commit 3e4607b

File tree

7 files changed

+92
-36
lines changed

7 files changed

+92
-36
lines changed

jupyter_rtc_core/app.py

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@
22
from traitlets.config import Config
33
import asyncio
44

5-
from traitlets import Instance
6-
from traitlets import Type
7-
from .handlers import RouteHandler
5+
from traitlets import Instance, Type
6+
from .handlers import RouteHandler, YRoomSessionHandler
87
from .websockets import GlobalAwarenessWebsocket, YRoomWebsocket
98
from .rooms.yroom_manager import YRoomManager
109

@@ -20,7 +19,9 @@ class RtcExtensionApp(ExtensionApp):
2019
# global awareness websocket
2120
# (r"api/collaboration/room/JupyterLab:globalAwareness/?", GlobalAwarenessWebsocket),
2221
# # ydoc websocket
23-
(r"api/collaboration/room/(.*)", YRoomWebsocket)
22+
(r"api/collaboration/room/(.*)", YRoomWebsocket),
23+
# handler that just adds compatibility with Jupyter Collaboration's frontend
24+
(r"api/collaboration/session/(.*)", YRoomSessionHandler)
2425
]
2526

2627
yroom_manager_class = Type(
@@ -44,18 +45,25 @@ def initialize_settings(self):
4445
# We cannot access the 'file_id_manager' key immediately because server
4546
# extensions initialize in alphabetical order. 'jupyter_rtc_core' <
4647
# 'jupyter_server_fileid'.
47-
get_fileid_manager = lambda: self.settings["file_id_manager"]
48-
contents_manager = self.serverapp.contents_manager
49-
loop = asyncio.get_event_loop_policy().get_event_loop()
50-
log = self.log
48+
# def get_fileid_manager():
49+
# self.log.info("IN GETTER")
50+
# for k, v in self.settings.items():
51+
# print(f"{k}: {v}")
52+
# print(len(self.settings.items()))
53+
# print(id(self.settings))
54+
# return self.settings["file_id_manager"]
55+
# contents_manager = self.serverapp.contents_manager
56+
# loop = asyncio.get_event_loop_policy().get_event_loop()
57+
# log = self.log
5158

52-
# Initialize YRoomManager
53-
self.settings["yroom_manager"] = YRoomManager(
54-
get_fileid_manager=get_fileid_manager,
55-
contents_manager=contents_manager,
56-
loop=loop,
57-
log=log
58-
)
59+
# # Initialize YRoomManager
60+
# self.settings["yroom_manager"] = YRoomManager(
61+
# get_fileid_manager=get_fileid_manager,
62+
# contents_manager=contents_manager,
63+
# loop=loop,
64+
# log=log
65+
# )
66+
pass
5967

6068

6169
def _link_jupyter_server_extension(self, server_app):

jupyter_rtc_core/handlers.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
import uuid
23

34
from jupyter_server.base.handlers import APIHandler
45
import tornado
@@ -14,4 +15,33 @@ def get(self):
1415
}))
1516

1617

18+
# TODO: remove this by v1.0.0 if deemed unnecessary. Just adding this for
19+
# compatibility with the `jupyter_collaboration` frontend.
20+
class YRoomSessionHandler(APIHandler):
21+
SESSION_ID = str(uuid.uuid4())
22+
23+
@tornado.web.authenticated
24+
def put(self, path):
25+
body = json.loads(self.request.body)
26+
format = body["format"]
27+
content_type = body["type"]
28+
# self.log.info("IN HANDLER")
29+
# for k, v in self.settings.items():
30+
# print(f"{k}: {v}")
31+
# print(len(self.settings.items()))
32+
# print(id(self.settings))
33+
34+
file_id_manager = self.settings["file_id_manager"]
35+
file_id = file_id_manager.index(path)
36+
37+
data = json.dumps(
38+
{
39+
"format": format,
40+
"type": content_type,
41+
"fileId": file_id,
42+
"sessionId": self.SESSION_ID,
43+
}
44+
)
45+
self.set_status(200)
46+
self.finish(data)
1747

jupyter_rtc_core/rooms/yroom.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from .yroom_file_api import YRoomFileAPI
1313

1414
if TYPE_CHECKING:
15-
from typing import Literal, Tuple
15+
from typing import Literal, Tuple, Any
1616
from jupyter_server_fileid.manager import BaseFileIdManager
1717
from jupyter_server.services.contents.manager import AsyncContentsManager, ContentsManager
1818

@@ -22,7 +22,11 @@ class YRoom:
2222
log: Logger
2323
"""Log object"""
2424
room_id: str
25-
"""Room Id"""
25+
"""
26+
The ID of the room. This is a composite ID following the format:
27+
28+
room_id := "{file_type}:{file_format}
29+
"""
2630

2731
_jupyter_ydoc: YBaseDoc
2832
"""JupyterYDoc"""
@@ -60,9 +64,10 @@ def __init__(
6064
self._client_group = YjsClientGroup(room_id=room_id, log=self.log, loop=self._loop)
6165
self._ydoc = pycrdt.Doc()
6266
self._awareness = pycrdt.Awareness(ydoc=self._ydoc)
67+
_, file_type, _ = self.room_id.split(":")
6368
JupyterYDocClass = cast(
6469
type[YBaseDoc],
65-
jupyter_ydoc_classes.get(self.file_type, jupyter_ydoc_classes["file"])
70+
jupyter_ydoc_classes.get(file_type, jupyter_ydoc_classes["file"])
6671
)
6772
self.jupyter_ydoc = JupyterYDocClass(ydoc=self._ydoc, awareness=self._awareness)
6873

@@ -307,11 +312,11 @@ def write_sync_update(self, message_payload: bytes, client_id: str | None = None
307312
def handle_awareness_update(self, client_id: str, message: bytes) -> None:
308313
# Apply the AwarenessUpdate message
309314
try:
310-
message_payload = message[1:]
315+
message_payload = pycrdt.read_message(message[1:])
311316
self._awareness.apply_awareness_update(message_payload, origin=self)
312317
except Exception as e:
313318
self.log.error(
314-
"An exception occurred when applying an AwarenessUpdate"
319+
"An exception occurred when applying an AwarenessUpdate "
315320
f"message from client '{client_id}':"
316321
)
317322
self.log.exception(e)

jupyter_rtc_core/rooms/yroom_manager.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,25 +18,20 @@ class YRoomManager():
1818
def __init__(
1919
self,
2020
*,
21-
get_fileid_manager: Callable[[], BaseFileIdManager],
21+
fileid_manager: BaseFileIdManager,
2222
contents_manager: AsyncContentsManager | ContentsManager,
2323
loop: asyncio.AbstractEventLoop,
2424
log: logging.Logger,
2525
):
2626
# Bind instance attributes
27-
self._get_fileid_manager = get_fileid_manager
27+
self.fileid_manager = fileid_manager
2828
self.contents_manager = contents_manager
2929
self.loop = loop
3030
self.log = log
3131
self._rooms_by_id = {}
3232
# Initialize dictionary of YRooms, keyed by room ID
3333

3434

35-
@property
36-
def fileid_manager(self) -> BaseFileIdManager:
37-
return self._get_fileid_manager()
38-
39-
4035
def get_room(self, room_id: str) -> YRoom | None:
4136
"""
4237
Retrieves a YRoom given a room ID. If the YRoom does not exist, this
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
from .global_awareness_ws import GlobalAwarenessWebsocket
2-
from .yroom_ws import YRoomWebsocket
32
from .clients import YjsClient, YjsClientGroup
3+
from .yroom_ws import YRoomWebsocket

jupyter_rtc_core/websockets/clients.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
"""
66

77
from __future__ import annotations
8-
from datetime import timedelta, timezone
9-
import datetime
8+
from datetime import timedelta, timezone, datetime
109
from logging import Logger
1110
from typing import TYPE_CHECKING
1211
import uuid
@@ -43,7 +42,7 @@ def synced(self):
4342

4443
@synced.setter
4544
def synced(self, v: bool):
46-
self.synced = v
45+
self._synced = v
4746
self.last_modified = datetime.now(timezone.utc)
4847

4948
class YjsClientGroup:
@@ -67,7 +66,7 @@ class YjsClientGroup:
6766
"""Log object"""
6867
loop: asyncio.AbstractEventLoop
6968
"""Event loop"""
70-
poll_interval_seconds: int
69+
_poll_interval_seconds: int
7170
"""The poll time interval used while auto removing desynced clients"""
7271
desynced_timeout_seconds: int
7372
"""The max time period in seconds that a desynced client does not become synced before get auto removed from desynced dict"""
@@ -79,7 +78,7 @@ def __init__(self, *, room_id: str, log: Logger, loop: asyncio.AbstractEventLoop
7978
self.log = log
8079
self.loop = loop
8180
self.loop.create_task(self._clean_desynced())
82-
self.poll_interval_seconds = poll_interval_seconds
81+
self._poll_interval_seconds = poll_interval_seconds
8382
self.desynced_timeout_seconds = desynced_timeout_seconds
8483

8584
def add(self, websocket: WebSocketHandler) -> str:

jupyter_rtc_core/websockets/yroom_ws.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22
from tornado.httpclient import HTTPError
33
from tornado.websocket import WebSocketHandler
44
from typing import TYPE_CHECKING
5+
import asyncio
6+
from ..rooms import YRoomManager
7+
import logging
58

69
if TYPE_CHECKING:
710
from jupyter_server_fileid.manager import BaseFileIdManager
8-
from ..rooms import YRoom, YRoomManager
11+
from jupyter_server.services.contents.manager import AsyncContentsManager, ContentsManager
12+
from ..rooms import YRoom
913

1014
class YRoomWebsocket(WebSocketHandler):
1115
yroom: YRoom
@@ -14,12 +18,26 @@ class YRoomWebsocket(WebSocketHandler):
1418

1519
@property
1620
def yroom_manager(self) -> YRoomManager:
21+
if "yroom_manager" not in self.settings:
22+
self.settings["yroom_manager"] = YRoomManager(
23+
fileid_manager=self.fileid_manager,
24+
contents_manager=self.contents_manager,
25+
loop=asyncio.get_event_loop_policy().get_event_loop(),
26+
# TODO: change this. we should pass `self.log` from our
27+
# `ExtensionApp` to log messages w/ "RtcCoreExtension" prefix
28+
log=logging.Logger("TEMP")
29+
30+
)
1731
return self.settings["yroom_manager"]
18-
1932

2033
@property
2134
def fileid_manager(self) -> BaseFileIdManager:
2235
return self.settings["file_id_manager"]
36+
37+
38+
@property
39+
def contents_manager(self) -> AsyncContentsManager | ContentsManager:
40+
return self.settings["contents_manager"]
2341

2442

2543
def prepare(self):
@@ -38,7 +56,7 @@ def prepare(self):
3856
raise HTTPError(404, f"No file with ID '{fileid}'.")
3957

4058

41-
def open(self):
59+
def open(self, *_, **__):
4260
# Create the YRoom
4361
yroom = self.yroom_manager.get_room(self.room_id)
4462
if not yroom:
@@ -51,6 +69,7 @@ def open(self):
5169

5270
def on_message(self, message: bytes):
5371
# Route all messages to the YRoom for processing
72+
print(message)
5473
self.yroom.add_message(self.client_id, message)
5574

5675

0 commit comments

Comments
 (0)