6
6
import asyncio
7
7
import json
8
8
import time
9
- import uuid
10
9
from typing import Any
11
10
12
11
from jupyter_server .auth import authorized
28
27
29
28
YFILE = YDOCS ["file" ]
30
29
31
- SERVER_SESSION = str (uuid .uuid4 ())
32
-
33
30
34
31
class YDocWebSocketHandler (WebSocketHandler , JupyterHandler ):
35
32
"""`YDocWebSocketHandler` uses the singleton pattern for ``WebsocketServer``,
@@ -62,6 +59,7 @@ def initialize(
62
59
room_manager : RoomManager ,
63
60
document_cleanup_delay : float | None = 60.0 ,
64
61
) -> None :
62
+ super ().initialize ()
65
63
# File ID manager cannot be passed as argument as the extension may load after this one
66
64
self ._file_id_manager = self .settings ["file_id_manager" ]
67
65
@@ -124,13 +122,6 @@ async def open(self, room_id):
124
122
"""
125
123
self ._room_id = self .request .path .split ("/" )[- 1 ]
126
124
127
- # Close the connection if the document session expired
128
- session_id = self .get_query_argument ("sessionId" , None )
129
- if session_id and SERVER_SESSION != session_id :
130
- self .close (
131
- 1003 ,
132
- f"Document session { session_id } expired. You need to reload this browser tab." ,
133
- )
134
125
try :
135
126
# Get room
136
127
self .room = await self ._room_manager .get_room (self ._room_id )
@@ -154,10 +145,22 @@ async def open(self, room_id):
154
145
self ._serve_task .cancel ()
155
146
await self ._room_manager .remove_room (self ._room_id )
156
147
157
- self ._emit (LogLevel .INFO , "initialize" , "New client connected." )
148
+ return
149
+
150
+ # Close the connection if the document session expired
151
+ session_id = self .get_query_argument ("sessionId" , None )
152
+ if session_id and session_id != self .room .session_id :
153
+ self .log .error (
154
+ f"Client tried to connect to { self ._room_id } with an expired session ID { session_id } ."
155
+ )
156
+ self .close (
157
+ 1003 ,
158
+ f"Document session { session_id } expired. You need to reload this browser tab." ,
159
+ )
158
160
159
161
# Start processing messages in the room
160
162
self ._serve_task = asyncio .create_task (self .room .serve (self ))
163
+ self ._emit (LogLevel .INFO , "initialize" , "New client connected." )
161
164
162
165
async def send (self , message ):
163
166
"""
@@ -207,14 +210,20 @@ def on_close(self) -> None:
207
210
if self ._serve_task is not None and not self ._serve_task .cancelled ():
208
211
self ._serve_task .cancel ()
209
212
210
- if self .room is not None and self .room .clients == [self ]:
211
- # no client in this room after we disconnect
212
- # Remove the room with a delay in case someone reconnects
213
- IOLoop .current ().add_callback (
214
- self ._room_manager .remove_room , self ._room_id , self ._cleanup_delay
215
- )
213
+ if self .room is not None :
214
+ # Remove it self from the list of clients
215
+ self .room .clients .remove (self )
216
+ if len (self .room .clients ) == 0 :
217
+ # no client in this room after we disconnect
218
+ # Remove the room with a delay in case someone reconnects
219
+ IOLoop .current ().add_callback (
220
+ self ._room_manager .remove_room , self ._room_id , self ._cleanup_delay
221
+ )
216
222
217
223
def _emit (self , level : LogLevel , action : str | None = None , msg : str | None = None ) -> None :
224
+ if self ._room_id .count (":" ) < 2 :
225
+ return
226
+
218
227
_ , _ , file_id = decode_file_path (self ._room_id )
219
228
path = self ._file_id_manager .get_path (file_id )
220
229
@@ -240,6 +249,22 @@ class DocSessionHandler(APIHandler):
240
249
241
250
auth_resource = "contents"
242
251
252
+ def initialize (self , store : BaseYStore , room_manager : RoomManager ) -> None :
253
+ super ().initialize ()
254
+ self ._store = store
255
+ self ._room_manager = room_manager
256
+
257
+ async def prepare (self ):
258
+ # NOTE: Initialize in the ExtensionApp.start_extension once
259
+ # https://github.com/jupyter-server/jupyter_server/issues/1329
260
+ # is done.
261
+ # We are temporarily initializing the store here because the
262
+ # initialization is async
263
+ if not self ._store .initialized :
264
+ await self ._store .initialize ()
265
+
266
+ return await super ().prepare ()
267
+
243
268
@web .authenticated
244
269
@authorized
245
270
async def put (self , path ):
@@ -251,26 +276,35 @@ async def put(self, path):
251
276
content_type = body ["type" ]
252
277
file_id_manager = self .settings ["file_id_manager" ]
253
278
279
+ status = 200
254
280
idx = file_id_manager .get_id (path )
255
- if idx is not None :
256
- # index already exists
257
- self .log .info ("Request for Y document '%s' with room ID: %s" , path , idx )
258
- data = json .dumps (
259
- {"format" : format , "type" : content_type , "fileId" : idx , "sessionId" : SERVER_SESSION }
260
- )
261
- self .set_status (200 )
262
- return self .finish (data )
263
-
264
- # try indexing
265
- idx = file_id_manager .index (path )
266
281
if idx is None :
267
- # file does not exists
268
- raise web .HTTPError (404 , f"File { path !r} does not exist" )
282
+ # try indexing
283
+ status = 201
284
+ idx = file_id_manager .index (path )
285
+ if idx is None :
286
+ # file does not exists
287
+ raise web .HTTPError (404 , f"File { path !r} does not exist" )
288
+
289
+ session_id = await self ._get_session_id (f"{ format } :{ content_type } :{ idx } " )
269
290
270
- # index successfully created
271
291
self .log .info ("Request for Y document '%s' with room ID: %s" , path , idx )
272
292
data = json .dumps (
273
- {"format" : format , "type" : content_type , "fileId" : idx , "sessionId" : SERVER_SESSION }
293
+ {"format" : format , "type" : content_type , "fileId" : idx , "sessionId" : session_id }
274
294
)
275
- self .set_status (201 )
295
+ self .set_status (status )
276
296
return self .finish (data )
297
+
298
+ async def _get_session_id (self , room_id : str ) -> str | None :
299
+ # If the room exists and it is ready, return the session_id from the room.
300
+ if self ._room_manager .has_room (room_id ):
301
+ room = await self ._room_manager .get_room (room_id )
302
+ if room .ready :
303
+ return room .session_id
304
+
305
+ if await self ._store .exists (room_id ):
306
+ doc = await self ._store .get (room_id )
307
+ if doc is not None and "session_id" in doc :
308
+ return doc ["session_id" ]
309
+
310
+ return None
0 commit comments