55import json
66from logging import Logger
77from pathlib import Path
8- from typing import Any , Dict , Optional , Tuple
8+ from typing import Any , Dict , Optional , Tuple , Type
99
1010from jupyter_server .auth import authorized
1111from jupyter_server .base .handlers import APIHandler , JupyterHandler
1212from jupyter_server .utils import ensure_async
1313from jupyter_ydoc import ydocs as YDOCS # type: ignore
1414from tornado import web
1515from tornado .websocket import WebSocketHandler
16- from ypy_websocket import WebsocketServer , YMessageType , YRoom # type: ignore
16+ from ypy_websocket . websocket_server import WebsocketServer , YRoom # type: ignore
1717from ypy_websocket .ystore import ( # type: ignore
1818 BaseYStore ,
1919 SQLiteYStore ,
2020 TempFileYStore ,
2121 YDocNotFound ,
2222)
23+ from ypy_websocket .yutils import YMessageType # type: ignore
2324
2425YFILE = YDOCS ["file" ]
2526
@@ -28,16 +29,22 @@ class JupyterTempFileYStore(TempFileYStore):
2829 prefix_dir = "jupyter_ystore_"
2930
3031
31- class JupyterSQLiteYStore (SQLiteYStore ):
32- db_path = ".jupyter_ystore.db"
33- document_ttl = 24 * 60 * 60
32+ def sqlite_ystore_factory (
33+ db_path : str = ".jupyter_ystore.db" , document_ttl : Optional [int ] = None
34+ ) -> Type [SQLiteYStore ]:
35+ _db_path = db_path
36+ _document_ttl = document_ttl
37+
38+ class JupyterSQLiteYStore (SQLiteYStore ):
39+ db_path = _db_path
40+ document_ttl = _document_ttl
41+
42+ return JupyterSQLiteYStore
3443
3544
3645class DocumentRoom (YRoom ):
3746 """A Y room for a possibly stored document (e.g. a notebook)."""
3847
39- is_transient = False
40-
4148 def __init__ (self , type : str , ystore : BaseYStore , log : Optional [Logger ]):
4249 super ().__init__ (ready = False , ystore = ystore , log = log )
4350 self .type = type
@@ -49,8 +56,6 @@ def __init__(self, type: str, ystore: BaseYStore, log: Optional[Logger]):
4956class TransientRoom (YRoom ):
5057 """A Y room for sharing state (e.g. awareness)."""
5158
52- is_transient = True
53-
5459 def __init__ (self , log : Optional [Logger ]):
5560 super ().__init__ (log = log )
5661
@@ -132,6 +137,7 @@ async def __anext__(self):
132137
133138 def get_file_info (self ) -> Tuple [str , str , str ]:
134139 assert self .websocket_server is not None
140+ assert isinstance (self .room , DocumentRoom )
135141 room_name = self .websocket_server .get_room_name (self .room )
136142 file_format : str
137143 file_type : str
@@ -175,10 +181,10 @@ async def open(self, path):
175181 asyncio .create_task (self .websocket_server .serve (self ))
176182
177183 # cancel the deletion of the room if it was scheduled
178- if not self .room . is_transient and self .room .cleaner is not None :
184+ if isinstance ( self .room , DocumentRoom ) and self .room .cleaner is not None :
179185 self .room .cleaner .cancel ()
180186
181- if not self .room . is_transient and not self .room .ready :
187+ if isinstance ( self .room , DocumentRoom ) and not self .room .ready :
182188 file_format , file_type , file_path = self .get_file_info ()
183189 self .log .debug ("Opening Y document from disk: %s" , file_path )
184190 model = await ensure_async (
@@ -188,26 +194,30 @@ async def open(self, path):
188194 # check again if ready, because loading the file can be async
189195 if not self .room .ready :
190196 # try to apply Y updates from the YStore for this document
191- try :
192- await self .room .ystore .apply_updates (self .room .ydoc )
193- read_from_source = False
194- except YDocNotFound :
195- # YDoc not found in the YStore, create the document from the source file (no change history)
196- read_from_source = True
197+ read_from_source = True
198+ if self .room .ystore is not None :
199+ try :
200+ await self .room .ystore .apply_updates (self .room .ydoc )
201+ read_from_source = False
202+ except YDocNotFound :
203+ # YDoc not found in the YStore, create the document from the source file (no change history)
204+ pass
197205 if not read_from_source :
198206 # if YStore updates and source file are out-of-sync, resync updates with source
199207 if self .room .document .source != model ["content" ]:
200208 read_from_source = True
201209 if read_from_source :
202210 self .room .document .source = model ["content" ]
203- await self .room .ystore .encode_state_as_update (self .room .ydoc )
211+ if self .room .ystore :
212+ await self .room .ystore .encode_state_as_update (self .room .ydoc )
204213 self .room .document .dirty = False
205214 self .room .ready = True
206215 self .room .watcher = asyncio .create_task (self .watch_file ())
207216 # save the document when changed
208217 self .room .document .observe (self .on_document_change )
209218
210219 async def watch_file (self ):
220+ assert isinstance (self .room , DocumentRoom )
211221 poll_interval = self .settings ["collaborative_file_poll_interval" ]
212222 if not poll_interval :
213223 self .room .watcher = None
@@ -217,6 +227,7 @@ async def watch_file(self):
217227 await self .maybe_load_document ()
218228
219229 async def maybe_load_document (self ):
230+ assert isinstance (self .room , DocumentRoom )
220231 file_format , file_type , file_path = self .get_file_info ()
221232 async with self .lock :
222233 model = await ensure_async (
@@ -267,7 +278,7 @@ def on_message(self, message):
267278 # filter out message depending on changes
268279 if skip :
269280 self .log .debug (
270- "Filtered out Y message of type: %s" , YMessageType (message_type ).raw_str ()
281+ "Filtered out Y message of type: %s" , YMessageType (message_type ).name
271282 )
272283 return skip
273284 self ._message_queue .put_nowait (message )
@@ -276,12 +287,13 @@ def on_message(self, message):
276287 def on_close (self ) -> None :
277288 # stop serving this client
278289 self ._message_queue .put_nowait (b"" )
279- if not self .room . is_transient and self .room .clients == [self ]:
290+ if isinstance ( self .room , DocumentRoom ) and self .room .clients == [self ]:
280291 # no client in this room after we disconnect
281292 # keep the document for a while in case someone reconnects
282293 self .room .cleaner = asyncio .create_task (self .clean_room ())
283294
284295 async def clean_room (self ) -> None :
296+ assert isinstance (self .room , DocumentRoom )
285297 seconds = self .settings ["collaborative_document_cleanup_delay" ]
286298 if seconds is None :
287299 return
@@ -309,6 +321,7 @@ def on_document_change(self, event):
309321 self .saving_document = asyncio .create_task (self .maybe_save_document ())
310322
311323 async def maybe_save_document (self ):
324+ assert isinstance (self .room , DocumentRoom )
312325 seconds = self .settings ["collaborative_document_save_delay" ]
313326 if seconds is None :
314327 return
0 commit comments