Skip to content

Commit c6c9393

Browse files
author
Jialin Zhang
committed
adding global awareness support
1 parent 3600dab commit c6c9393

File tree

7 files changed

+101
-56
lines changed

7 files changed

+101
-56
lines changed

jupyter_rtc_core/app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class RtcExtensionApp(ExtensionApp):
1818
# # this can be deleted prior to initial release.
1919
(r"jupyter-rtc-core/get-example/?", RouteHandler),
2020
# global awareness websocket
21-
# (r"api/collaboration/room/JupyterLab:globalAwareness/?", GlobalAwarenessWebsocket),
21+
(r"api/collaboration/room/JupyterLab:globalAwareness/?", GlobalAwarenessWebsocket),
2222
# # ydoc websocket
2323
(r"api/collaboration/room/(.*)", YRoomWebsocket),
2424
# # handler that just adds compatibility with Jupyter Collaboration's frontend

jupyter_rtc_core/rooms/yroom.py

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class YRoom:
2929
room_id := "{file_type}:{file_format}:{file_id}"
3030
"""
3131

32-
_jupyter_ydoc: YBaseDoc
32+
_jupyter_ydoc: YBaseDoc | None
3333
"""JupyterYDoc"""
3434
_ydoc: pycrdt.Doc
3535
"""Ydoc"""
@@ -53,8 +53,8 @@ def __init__(
5353
room_id: str,
5454
log: Logger,
5555
loop: asyncio.AbstractEventLoop,
56-
fileid_manager: BaseFileIdManager,
57-
contents_manager: AsyncContentsManager | ContentsManager,
56+
fileid_manager: BaseFileIdManager | None = None,
57+
contents_manager: AsyncContentsManager | ContentsManager | None = None,
5858
):
5959
# Bind instance attributes
6060
self.log = log
@@ -65,23 +65,29 @@ def __init__(
6565
self._client_group = YjsClientGroup(room_id=room_id, log=self.log, loop=self._loop)
6666
self._ydoc = pycrdt.Doc()
6767
self._awareness = pycrdt.Awareness(ydoc=self._ydoc)
68-
_, file_type, _ = self.room_id.split(":")
69-
JupyterYDocClass = cast(
70-
type[YBaseDoc],
71-
jupyter_ydoc_classes.get(file_type, jupyter_ydoc_classes["file"])
72-
)
73-
self._jupyter_ydoc = JupyterYDocClass(ydoc=self._ydoc, awareness=self._awareness)
74-
75-
# Initialize YRoomFileAPI and begin loading content
76-
self.file_api = YRoomFileAPI(
77-
room_id=self.room_id,
78-
jupyter_ydoc=self._jupyter_ydoc,
79-
log=self.log,
80-
loop=self._loop,
81-
fileid_manager=fileid_manager,
82-
contents_manager=contents_manager
83-
)
84-
self.file_api.load_ydoc_content()
68+
69+
self.file_api = None
70+
self._jupyter_ydoc = None
71+
if fileid_manager and contents_manager:
72+
_, file_type, _ = self.room_id.split(":")
73+
JupyterYDocClass = cast(
74+
type[YBaseDoc],
75+
jupyter_ydoc_classes.get(file_type, jupyter_ydoc_classes["file"])
76+
)
77+
self._jupyter_ydoc = JupyterYDocClass(ydoc=self._ydoc, awareness=self._awareness)
78+
79+
# Initialize YRoomFileAPI and begin loading content
80+
self.file_api = YRoomFileAPI(
81+
room_id=self.room_id,
82+
jupyter_ydoc=self._jupyter_ydoc,
83+
log=self.log,
84+
loop=self._loop,
85+
fileid_manager=fileid_manager,
86+
contents_manager=contents_manager
87+
)
88+
self.file_api.load_ydoc_content()
89+
self._jupyter_ydoc.observe(self._on_jupyter_ydoc_update)
90+
8591

8692
# Start observers on `self.ydoc` and `self.awareness` to ensure new
8793
# updates are broadcast to all clients and saved to disk.
@@ -91,7 +97,6 @@ def __init__(
9197
self._ydoc_subscription = self._ydoc.observe(
9298
self._on_ydoc_update
9399
)
94-
self._jupyter_ydoc.observe(self._on_jupyter_ydoc_update)
95100

96101
# Initialize message queue and start background task that routes new
97102
# messages in the message queue to the appropriate handler method.
@@ -118,7 +123,8 @@ async def get_jupyter_ydoc(self):
118123
(`jupyter_ydoc.ybasedoc.YBaseDoc`) after waiting for its content to be
119124
loaded from the ContentsManager.
120125
"""
121-
await self.file_api.ydoc_content_loaded
126+
if self.file_api:
127+
await self.file_api.ydoc_content_loaded
122128
return self._jupyter_ydoc
123129

124130

@@ -127,7 +133,8 @@ async def get_ydoc(self):
127133
Returns a reference to the room's YDoc (`pycrdt.Doc`) after
128134
waiting for its content to be loaded from the ContentsManager.
129135
"""
130-
await self.file_api.ydoc_content_loaded
136+
if self.file_api:
137+
await self.file_api.ydoc_content_loaded
131138
return self._ydoc
132139

133140

@@ -155,7 +162,8 @@ async def _process_message_queue(self) -> None:
155162
"""
156163
# Wait for content to be loaded before processing any messages in the
157164
# message queue
158-
await self.file_api.ydoc_content_loaded
165+
if self.file_api:
166+
await self.file_api.ydoc_content_loaded
159167

160168
# Begin processing messages from the message queue
161169
while True:
@@ -448,7 +456,8 @@ async def stop(self) -> None:
448456
# Remove all observers, as updates no longer need to be broadcast
449457
self._ydoc.unobserve(self._ydoc_subscription)
450458
self._awareness.unobserve(self._awareness_subscription)
451-
self._jupyter_ydoc.unobserve()
459+
if self._jupyter_ydoc:
460+
self._jupyter_ydoc.unobserve()
452461

453462
# Finish processing all messages, then stop the queue to end the
454463
# `_process_message_queue()` background task.
@@ -457,4 +466,5 @@ async def stop(self) -> None:
457466

458467
# Finally, stop FileAPI and return. This saves the final content of the
459468
# JupyterYDoc in the process.
460-
await self.file_api.stop()
469+
if self.file_api:
470+
await self.file_api.stop()

jupyter_rtc_core/rooms/yroom_manager.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,18 @@ def get_room(self, room_id: str) -> YRoom | None:
4848
# Otherwise, create a new room
4949
try:
5050
self.log.info(f"Initializing room '{room_id}'.")
51-
yroom = YRoom(
52-
room_id=room_id,
53-
log=self.log,
54-
loop=self.loop,
55-
fileid_manager=self.fileid_manager,
56-
contents_manager=self.contents_manager,
57-
)
51+
if room_id == "JupyterLab:globalAwareness":
52+
yroom = YRoom(room_id=room_id,
53+
log=self.log,
54+
loop=self.loop)
55+
else:
56+
yroom = YRoom(
57+
room_id=room_id,
58+
log=self.log,
59+
loop=self.loop,
60+
fileid_manager=self.fileid_manager,
61+
contents_manager=self.contents_manager,
62+
)
5863
self._rooms_by_id[room_id] = yroom
5964
return yroom
6065
except Exception as e:
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
from .global_awareness_ws import GlobalAwarenessWebsocket
21
from .clients import YjsClient, YjsClientGroup
32
from .yroom_ws import YRoomWebsocket
3+
from .global_awareness_ws import GlobalAwarenessWebsocket
Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,49 @@
1+
from __future__ import annotations
2+
from tornado.httpclient import HTTPError
13
from tornado.websocket import WebSocketHandler
4+
from typing import TYPE_CHECKING
5+
from ..rooms import YRoomManager
6+
import logging
7+
8+
if TYPE_CHECKING:
9+
from ..rooms import YRoom
210

311
class GlobalAwarenessWebsocket(WebSocketHandler):
4-
pass
12+
yroom: YRoom
13+
client_id: str | None
14+
# TODO: change this. we should pass `self.log` from our
15+
# `ExtensionApp` to log messages w/ "RtcCoreExtension" prefix
16+
log = logging.Logger("TEMP")
17+
18+
@property
19+
def yroom_manager(self) -> YRoomManager:
20+
return self.settings["yroom_manager"]
21+
22+
23+
def prepare(self):
24+
# Create the YRoom
25+
yroom = self.yroom_manager.get_room("JupyterLab:globalAwareness")
26+
if not yroom:
27+
raise HTTPError(500, f"Unable to initialize Global Awareness Room.")
28+
self.yroom = yroom
29+
30+
31+
def open(self, *_, **__):
32+
# Add self as a client to the YRoom.
33+
# If `client_id is None`, then the YRoom is being stopped, and this WS
34+
# should be closed immediately w/ close code 1001.
35+
self.client_id = self.yroom.clients.add(self)
36+
if not self.client_id:
37+
self.close(code=1001)
38+
return
39+
40+
41+
def on_message(self, message: bytes):
42+
# Route all messages to the YRoom for processing
43+
self.yroom.add_message(self.client_id, message)
44+
45+
def on_close(self):
46+
if self.client_id:
47+
self.log.info(f"Closed Global Awareness Websocket to client '{self.client_id}'.")
48+
self.yroom.clients.remove(self.client_id)
49+

jupyter_rtc_core/websockets/yroom_ws.py

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,6 @@ def prepare(self):
4040
request_path: str = self.request.path
4141
self.room_id = request_path.strip("/").split("/")[-1]
4242

43-
# TODO: remove this once globalawareness is implemented
44-
if self.room_id == "JupyterLab:globalAwareness":
45-
self.close(1011)
46-
return
47-
4843
# Verify the file ID contained in the room ID points to a valid file.
4944
fileid = self.room_id.split(":")[-1]
5045
path = self.fileid_manager.get_path(fileid)
@@ -53,11 +48,6 @@ def prepare(self):
5348

5449

5550
def open(self, *_, **__):
56-
# TODO: remove this later
57-
if self.room_id == "JupyterLab:globalAwareness":
58-
self.close(1011)
59-
return
60-
6151
# Create the YRoom
6252
yroom = self.yroom_manager.get_room(self.room_id)
6353
if not yroom:
@@ -74,10 +64,6 @@ def open(self, *_, **__):
7464

7565

7666
def on_message(self, message: bytes):
77-
# TODO: remove this later
78-
if self.room_id == "JupyterLab:globalAwareness":
79-
return
80-
8167
if not self.client_id:
8268
self.close(code=1001)
8369
return
@@ -87,10 +73,6 @@ def on_message(self, message: bytes):
8773

8874

8975
def on_close(self):
90-
# TODO: remove this later
91-
if self.room_id == "JupyterLab:globalAwareness":
92-
return
93-
9476
if self.client_id:
9577
self.log.info(f"Closed Websocket to client '{self.client_id}'.")
9678
self.yroom.clients.remove(self.client_id)

src/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ import { IAwareness } from '@jupyter/ydoc';
4646
import {
4747
AwarenessKernelStatus
4848
} from './kernelstatus';
49+
import { ServerConnection } from '@jupyterlab/services';
50+
import { WebSocketAwarenessProvider } from './docprovider/awareness';
51+
import { URLExt } from '@jupyterlab/coreutils';
4952
/**
5053
* Initialization data for the @jupyter/rtc-core extension.
5154
*/
@@ -102,15 +105,15 @@ export const rtcGlobalAwarenessPlugin: JupyterFrontEndPlugin<IAwareness> = {
102105
const awareness = new Awareness(ydoc);
103106

104107
// TODO: Uncomment once global awareness is working
105-
/*const server = ServerConnection.makeSettings();
108+
const server = ServerConnection.makeSettings();
106109
const url = URLExt.join(server.wsUrl, 'api/collaboration/room');
107110

108111
new WebSocketAwarenessProvider({
109112
url: url,
110113
roomID: 'JupyterLab:globalAwareness',
111114
awareness: awareness,
112115
user: user
113-
});*/
116+
});
114117

115118
state.changed.connect(async () => {
116119
const data: any = await state.toJSON();

0 commit comments

Comments
 (0)