Skip to content

Commit 16cba94

Browse files
dmonadecharleshbcarlosjtpio
authored
Shared editing with collaborative notebook model. (jupyterlab#10118)
* initial skeleton for a shared model package * integrity * more shared types * use shared namespace * rename shared-model to nbmodel * rename shared-model to nbmodel * add lumino signals with delta content * add lumino signals with delta content * better signals + utils skeleton * Implement nbmodel.SharedModel with Yjs * add tests and revamp ymodel API * nbmodel integration - sync text source * implement suggestions * nbmodel integration - sync codemirror with codemirrorbinding * implement suggestions * only dispose ycodemirror binding if enabled * pin yjs versions * yeditorBinding field can be null * pin y-codemirror version * add nbmodel path to tsconfig * Fix yjs imports * fix cdemirror package tests * Add more test and factore the cell factory into a namespace * use static factory methods for cell creation * add updateMetadata on notebook interface * Sync notebook celllist with nbmodel * docprovider package skeleton * docprovider package skeleton * test for the docprovider skeleton * fix issues when syncing remote changes * add yjs ws server * integrate docprovider into docregistry * make notebook-context collaborative * test services * Document.IModel: rename attributed nbmodel to nbnotebook as it is has a ISharedNotebook type * rename nbmodelSwitched to nbcellSwitched to be more specific on the effective action * nbmodel: remove unused utils * make cell metadata shareable * fix package.json * fix tsconfig.json * more typings for cell metadata * ensure with manage metadata not defined in nbformat * debug issue * Check if notebook is initialized befor creating an empty cell * Adds documentation * reverts jupyterlab#20 * fix tests by reverting yjs_echo_server, metadata, and cell-duplication fix * add awareness & yjs_ws_server * enable back the shared cell metadata * Add docprovider and nbmodel to singletons * Y.UndoManager integration * fix completer * lint * fix switching cell-type * fix type issue * Added new optional argument to IModelFactory * Docstrings * Review and lint * Quick pass on the lab code style * More style formatting * Prefix remote-caret CSS class * Lint * Docstrings ISharedNotebook * docstring api * Improve documentation for isStandalone * docstring ymodel * docstring create cell methods * Unobserve on dispose * Added ymodel to YNotebook * Add source to nbcell * upgrade nbmodel and docprovider package to 3.1.0-alpha.4 * temporary disable splice tests * better comment out splice_source test * Added file content type to the guid of the provider * Make ycellMapping private in nbmodel * add docs for nbmodel & docprovider * add cell id and fix undo-tests * rebase to upstream * re-enable services tests * fix moving cells * refactor to use shared-models package to implement different document models * initialize * separate FileModel/DocumentModel from Notebook model * rework import of es modules * fix notebook tests * rename nbmodel imports to models * Modify YCodeCell * Changed ymodel * Changed CodeCellModel * review * fix remove cell * fix output move and display issues * fix overwriting remote content when loading another window * fix syncing outputs initially * Add copyright header to yjs_echo_ws.py * Fix None checks * Add --collaborative option to activate collaboration * Default outputs to [] * bump version of shared-models & docprovider to match other packages * package.json integrity update Co-authored-by: Eric Charles <[email protected]> Co-authored-by: hbcarlos <[email protected]> Co-authored-by: Jeremy Tuloup <[email protected]>
1 parent b9ee4f6 commit 16cba94

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

67 files changed

+3290
-98
lines changed

binder/environment.yml

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ name: example-environment
22
channels:
33
- conda-forge
44
dependencies:
5+
- jupyterlab-link-share=0.2
56
- jupyter-server-proxy
67
- matplotlib-base
78
- nodejs=14

builder/src/webpack.config.base.ts

+12
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,18 @@ const rules = [
4040
use: {
4141
loader: 'raw-loader'
4242
}
43+
},
44+
{
45+
test: /\.m?js/,
46+
resolve: {
47+
fullySpecified: false
48+
}
49+
},
50+
{
51+
test: /\.c?js/,
52+
resolve: {
53+
fullySpecified: false
54+
}
4355
}
4456
];
4557

dev_mode/package.json

+6
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"@jupyterlab/debugger-extension": "~3.1.0-alpha.6",
3838
"@jupyterlab/docmanager": "~3.1.0-alpha.6",
3939
"@jupyterlab/docmanager-extension": "~3.1.0-alpha.6",
40+
"@jupyterlab/docprovider": "~3.1.0-alpha.6",
4041
"@jupyterlab/docregistry": "~3.1.0-alpha.6",
4142
"@jupyterlab/documentsearch": "~3.1.0-alpha.6",
4243
"@jupyterlab/documentsearch-extension": "~3.1.0-alpha.6",
@@ -84,6 +85,7 @@
8485
"@jupyterlab/settingeditor": "~3.1.0-alpha.6",
8586
"@jupyterlab/settingeditor-extension": "~3.1.0-alpha.6",
8687
"@jupyterlab/settingregistry": "~3.1.0-alpha.6",
88+
"@jupyterlab/shared-models": "~3.1.0-alpha.6",
8789
"@jupyterlab/shortcuts-extension": "~3.1.0-alpha.6",
8890
"@jupyterlab/statedb": "~3.1.0-alpha.6",
8991
"@jupyterlab/statusbar": "~3.1.0-alpha.6",
@@ -257,6 +259,7 @@
257259
"@jupyterlab/coreutils",
258260
"@jupyterlab/debugger",
259261
"@jupyterlab/docmanager",
262+
"@jupyterlab/docprovider",
260263
"@jupyterlab/documentsearch",
261264
"@jupyterlab/extensionmanager",
262265
"@jupyterlab/filebrowser",
@@ -273,6 +276,7 @@
273276
"@jupyterlab/services",
274277
"@jupyterlab/settingeditor",
275278
"@jupyterlab/settingregistry",
279+
"@jupyterlab/shared-models",
276280
"@jupyterlab/statedb",
277281
"@jupyterlab/statusbar",
278282
"@jupyterlab/terminal",
@@ -317,6 +321,7 @@
317321
"@jupyterlab/debugger-extension": "../packages/debugger-extension",
318322
"@jupyterlab/docmanager": "../packages/docmanager",
319323
"@jupyterlab/docmanager-extension": "../packages/docmanager-extension",
324+
"@jupyterlab/docprovider": "../packages/docprovider",
320325
"@jupyterlab/docregistry": "../packages/docregistry",
321326
"@jupyterlab/documentsearch": "../packages/documentsearch",
322327
"@jupyterlab/documentsearch-extension": "../packages/documentsearch-extension",
@@ -364,6 +369,7 @@
364369
"@jupyterlab/settingeditor": "../packages/settingeditor",
365370
"@jupyterlab/settingeditor-extension": "../packages/settingeditor-extension",
366371
"@jupyterlab/settingregistry": "../packages/settingregistry",
372+
"@jupyterlab/shared-models": "../packages/shared-models",
367373
"@jupyterlab/shortcuts-extension": "../packages/shortcuts-extension",
368374
"@jupyterlab/statedb": "../packages/statedb",
369375
"@jupyterlab/statusbar": "../packages/statusbar",

jupyterlab/handlers/yjs_echo_ws.py

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
"""Echo WebSocket handler for real time collaboration with Yjs"""
2+
3+
# Copyright (c) Jupyter Development Team.
4+
# Distributed under the terms of the Modified BSD License.
5+
6+
import uuid
7+
import time
8+
9+
from tornado.ioloop import IOLoop
10+
from tornado.websocket import WebSocketHandler
11+
12+
acquireLockMessageType = 127
13+
releaseLockMessageType = 126
14+
requestInitializedContentMessageType = 125
15+
putInitializedContentMessageType = 124
16+
17+
class YjsRoom:
18+
def __init__(self):
19+
self.lock = None
20+
self.clients = {}
21+
self.content = bytes([])
22+
23+
class YJSEchoWS(WebSocketHandler):
24+
rooms = {}
25+
26+
def open(self, guid):
27+
#print("[YJSEchoWS]: open", guid)
28+
cls = self.__class__
29+
self.id = str(uuid.uuid4())
30+
self.room_id = guid
31+
room = cls.rooms.get(self.room_id)
32+
if room is None:
33+
room = YjsRoom()
34+
cls.rooms[self.room_id] = room
35+
room.clients[self.id] = ( IOLoop.current(), self.hook_send_message )
36+
# Send SyncStep1 message (based on y-protocols)
37+
self.write_message(bytes([0, 0, 1, 0]), binary=True)
38+
39+
def on_message(self, message):
40+
#print("[YJSEchoWS]: message, ", message)
41+
cls = self.__class__
42+
room = cls.rooms.get(self.room_id)
43+
if message[0] == acquireLockMessageType: # tries to acquire lock
44+
now = int(time.time())
45+
if room.lock is None or now - room.lock > 15: # no lock or timeout
46+
room.lock = now
47+
# print('Acquired new lock: ', room.lock)
48+
# return acquired lock
49+
self.write_message(bytes([acquireLockMessageType]) + room.lock.to_bytes(4, byteorder = 'little'), binary=True)
50+
elif message[0] == releaseLockMessageType:
51+
releasedLock = int.from_bytes(message[1:], byteorder = 'little')
52+
# print("trying release lock: ", releasedLock)
53+
if room.lock == releasedLock:
54+
# print('released lock: ', room.lock)
55+
room.lock = None
56+
elif message[0] == requestInitializedContentMessageType:
57+
# print("client requested initial content")
58+
self.write_message(bytes([requestInitializedContentMessageType]) + room.content, binary=True)
59+
elif message[0] == putInitializedContentMessageType:
60+
# print("client put initialized content")
61+
room.content = message[1:]
62+
elif room:
63+
for client_id, (loop, hook_send_message) in room.clients.items() :
64+
if self.id != client_id :
65+
loop.add_callback(hook_send_message, message)
66+
67+
def on_close(self):
68+
# print("[YJSEchoWS]: close")
69+
cls = self.__class__
70+
room = cls.rooms.get(self.room_id)
71+
room.clients.pop(self.id)
72+
if len(room.clients) == 0 :
73+
cls.rooms.pop(self.room_id)
74+
# print("[YJSEchoWS]: close room " + self.room_id)
75+
76+
return True
77+
78+
def check_origin(self, origin):
79+
#print("[YJSEchoWS]: check origin")
80+
return True
81+
82+
def hook_send_message(self, msg):
83+
self.write_message(msg, binary=True)

jupyterlab/labapp.py

+13
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from .handlers.build_handler import Builder, BuildHandler, build_path
3131
from .handlers.error_handler import ErrorHandler
3232
from .handlers.extension_manager_handler import ExtensionHandler, ExtensionManager, extensions_handler_path
33+
from .handlers.yjs_echo_ws import YJSEchoWS
3334

3435
DEV_NOTE = """You're running JupyterLab from source.
3536
If you're working on the TypeScript sources of JupyterLab, try running
@@ -496,6 +497,10 @@ class LabApp(NBClassicConfigShimMixin, LabServerApp):
496497
{'LabApp': {'extensions_in_dev_mode': True}},
497498
"Load prebuilt extensions in dev-mode."
498499
)
500+
flags['collaborative'] = (
501+
{'LabApp': {'collaborative': True}},
502+
"Whether to enable collaborative mode."
503+
)
499504

500505
subcommands = dict(
501506
build=(LabBuildApp, LabBuildApp.description.splitlines()[0]),
@@ -552,6 +557,9 @@ class LabApp(NBClassicConfigShimMixin, LabServerApp):
552557

553558
expose_app_in_browser = Bool(False, config=True,
554559
help="Whether to expose the global app instance to browser via window.jupyterlab")
560+
561+
collaborative = Bool(False, config=True,
562+
help="Whether to enable collaborative mode.")
555563

556564
@default('app_dir')
557565
def _default_app_dir(self):
@@ -660,6 +668,7 @@ def initialize_handlers(self):
660668
page_config['token'] = self.serverapp.token
661669
page_config['exposeAppInBrowser'] = self.expose_app_in_browser
662670
page_config['quitButton'] = self.serverapp.quit_button
671+
page_config['collaborative'] = self.collaborative
663672

664673
# Client-side code assumes notebookVersion is a JSON-encoded string
665674
page_config['notebookVersion'] = json.dumps(jpserver_version_info)
@@ -672,6 +681,10 @@ def initialize_handlers(self):
672681
build_handler = (build_path, BuildHandler, {'builder': builder})
673682
handlers.append(build_handler)
674683

684+
#YJS_Echo WS Handler
685+
yjs_echo_handler = (r"/api/yjs/(.*)", YJSEchoWS)
686+
handlers.append(yjs_echo_handler)
687+
675688
errored = False
676689

677690
if self.core_mode:

jupyterlab/tests/test_jupyterlab.py

+1
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,7 @@ def test_build(self):
448448
assert self.pkg_names['extension'] in data
449449

450450
@pytest.mark.slow
451+
@pytest.mark.skip(reason="TODO temporary ci skip - enable when shared-models and docprovider packages are published")
451452
def test_build_splice_packages(self):
452453
app_options = AppOptions(splice_source=True)
453454
assert install_extension(self.mock_extension) is True

packages/cells/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
"@jupyterlab/outputarea": "^3.1.0-alpha.6",
5555
"@jupyterlab/rendermime": "^3.1.0-alpha.6",
5656
"@jupyterlab/services": "^6.1.0-alpha.6",
57+
"@jupyterlab/shared-models": "^3.1.0-alpha.6",
5758
"@jupyterlab/ui-components": "^3.1.0-alpha.6",
5859
"@lumino/algorithm": "^1.3.3",
5960
"@lumino/coreutils": "^1.5.3",

0 commit comments

Comments
 (0)