Skip to content

Commit

Permalink
Shared editing with collaborative notebook model. (#10118)
Browse files Browse the repository at this point in the history
* 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 #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 <eric@datalayer.io>
Co-authored-by: hbcarlos <carlosherrerocontact@gmail.com>
Co-authored-by: Jeremy Tuloup <jeremy.tuloup@gmail.com>
  • Loading branch information
4 people committed May 10, 2021
1 parent b9ee4f6 commit 16cba94
Show file tree
Hide file tree
Showing 67 changed files with 3,290 additions and 98 deletions.
1 change: 1 addition & 0 deletions binder/environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ name: example-environment
channels:
- conda-forge
dependencies:
- jupyterlab-link-share=0.2
- jupyter-server-proxy
- matplotlib-base
- nodejs=14
Expand Down
12 changes: 12 additions & 0 deletions builder/src/webpack.config.base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,18 @@ const rules = [
use: {
loader: 'raw-loader'
}
},
{
test: /\.m?js/,
resolve: {
fullySpecified: false
}
},
{
test: /\.c?js/,
resolve: {
fullySpecified: false
}
}
];

Expand Down
6 changes: 6 additions & 0 deletions dev_mode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"@jupyterlab/debugger-extension": "~3.1.0-alpha.6",
"@jupyterlab/docmanager": "~3.1.0-alpha.6",
"@jupyterlab/docmanager-extension": "~3.1.0-alpha.6",
"@jupyterlab/docprovider": "~3.1.0-alpha.6",
"@jupyterlab/docregistry": "~3.1.0-alpha.6",
"@jupyterlab/documentsearch": "~3.1.0-alpha.6",
"@jupyterlab/documentsearch-extension": "~3.1.0-alpha.6",
Expand Down Expand Up @@ -84,6 +85,7 @@
"@jupyterlab/settingeditor": "~3.1.0-alpha.6",
"@jupyterlab/settingeditor-extension": "~3.1.0-alpha.6",
"@jupyterlab/settingregistry": "~3.1.0-alpha.6",
"@jupyterlab/shared-models": "~3.1.0-alpha.6",
"@jupyterlab/shortcuts-extension": "~3.1.0-alpha.6",
"@jupyterlab/statedb": "~3.1.0-alpha.6",
"@jupyterlab/statusbar": "~3.1.0-alpha.6",
Expand Down Expand Up @@ -257,6 +259,7 @@
"@jupyterlab/coreutils",
"@jupyterlab/debugger",
"@jupyterlab/docmanager",
"@jupyterlab/docprovider",
"@jupyterlab/documentsearch",
"@jupyterlab/extensionmanager",
"@jupyterlab/filebrowser",
Expand All @@ -273,6 +276,7 @@
"@jupyterlab/services",
"@jupyterlab/settingeditor",
"@jupyterlab/settingregistry",
"@jupyterlab/shared-models",
"@jupyterlab/statedb",
"@jupyterlab/statusbar",
"@jupyterlab/terminal",
Expand Down Expand Up @@ -317,6 +321,7 @@
"@jupyterlab/debugger-extension": "../packages/debugger-extension",
"@jupyterlab/docmanager": "../packages/docmanager",
"@jupyterlab/docmanager-extension": "../packages/docmanager-extension",
"@jupyterlab/docprovider": "../packages/docprovider",
"@jupyterlab/docregistry": "../packages/docregistry",
"@jupyterlab/documentsearch": "../packages/documentsearch",
"@jupyterlab/documentsearch-extension": "../packages/documentsearch-extension",
Expand Down Expand Up @@ -364,6 +369,7 @@
"@jupyterlab/settingeditor": "../packages/settingeditor",
"@jupyterlab/settingeditor-extension": "../packages/settingeditor-extension",
"@jupyterlab/settingregistry": "../packages/settingregistry",
"@jupyterlab/shared-models": "../packages/shared-models",
"@jupyterlab/shortcuts-extension": "../packages/shortcuts-extension",
"@jupyterlab/statedb": "../packages/statedb",
"@jupyterlab/statusbar": "../packages/statusbar",
Expand Down
83 changes: 83 additions & 0 deletions jupyterlab/handlers/yjs_echo_ws.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""Echo WebSocket handler for real time collaboration with Yjs"""

# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.

import uuid
import time

from tornado.ioloop import IOLoop
from tornado.websocket import WebSocketHandler

acquireLockMessageType = 127
releaseLockMessageType = 126
requestInitializedContentMessageType = 125
putInitializedContentMessageType = 124

class YjsRoom:
def __init__(self):
self.lock = None
self.clients = {}
self.content = bytes([])

class YJSEchoWS(WebSocketHandler):
rooms = {}

def open(self, guid):
#print("[YJSEchoWS]: open", guid)
cls = self.__class__
self.id = str(uuid.uuid4())
self.room_id = guid
room = cls.rooms.get(self.room_id)
if room is None:
room = YjsRoom()
cls.rooms[self.room_id] = room
room.clients[self.id] = ( IOLoop.current(), self.hook_send_message )
# Send SyncStep1 message (based on y-protocols)
self.write_message(bytes([0, 0, 1, 0]), binary=True)

def on_message(self, message):
#print("[YJSEchoWS]: message, ", message)
cls = self.__class__
room = cls.rooms.get(self.room_id)
if message[0] == acquireLockMessageType: # tries to acquire lock
now = int(time.time())
if room.lock is None or now - room.lock > 15: # no lock or timeout
room.lock = now
# print('Acquired new lock: ', room.lock)
# return acquired lock
self.write_message(bytes([acquireLockMessageType]) + room.lock.to_bytes(4, byteorder = 'little'), binary=True)
elif message[0] == releaseLockMessageType:
releasedLock = int.from_bytes(message[1:], byteorder = 'little')
# print("trying release lock: ", releasedLock)
if room.lock == releasedLock:
# print('released lock: ', room.lock)
room.lock = None
elif message[0] == requestInitializedContentMessageType:
# print("client requested initial content")
self.write_message(bytes([requestInitializedContentMessageType]) + room.content, binary=True)
elif message[0] == putInitializedContentMessageType:
# print("client put initialized content")
room.content = message[1:]
elif room:
for client_id, (loop, hook_send_message) in room.clients.items() :
if self.id != client_id :
loop.add_callback(hook_send_message, message)

def on_close(self):
# print("[YJSEchoWS]: close")
cls = self.__class__
room = cls.rooms.get(self.room_id)
room.clients.pop(self.id)
if len(room.clients) == 0 :
cls.rooms.pop(self.room_id)
# print("[YJSEchoWS]: close room " + self.room_id)

return True

def check_origin(self, origin):
#print("[YJSEchoWS]: check origin")
return True

def hook_send_message(self, msg):
self.write_message(msg, binary=True)
13 changes: 13 additions & 0 deletions jupyterlab/labapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from .handlers.build_handler import Builder, BuildHandler, build_path
from .handlers.error_handler import ErrorHandler
from .handlers.extension_manager_handler import ExtensionHandler, ExtensionManager, extensions_handler_path
from .handlers.yjs_echo_ws import YJSEchoWS

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

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

expose_app_in_browser = Bool(False, config=True,
help="Whether to expose the global app instance to browser via window.jupyterlab")

collaborative = Bool(False, config=True,
help="Whether to enable collaborative mode.")

@default('app_dir')
def _default_app_dir(self):
Expand Down Expand Up @@ -660,6 +668,7 @@ def initialize_handlers(self):
page_config['token'] = self.serverapp.token
page_config['exposeAppInBrowser'] = self.expose_app_in_browser
page_config['quitButton'] = self.serverapp.quit_button
page_config['collaborative'] = self.collaborative

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

#YJS_Echo WS Handler
yjs_echo_handler = (r"/api/yjs/(.*)", YJSEchoWS)
handlers.append(yjs_echo_handler)

errored = False

if self.core_mode:
Expand Down
1 change: 1 addition & 0 deletions jupyterlab/tests/test_jupyterlab.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,7 @@ def test_build(self):
assert self.pkg_names['extension'] in data

@pytest.mark.slow
@pytest.mark.skip(reason="TODO temporary ci skip - enable when shared-models and docprovider packages are published")
def test_build_splice_packages(self):
app_options = AppOptions(splice_source=True)
assert install_extension(self.mock_extension) is True
Expand Down
1 change: 1 addition & 0 deletions packages/cells/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"@jupyterlab/outputarea": "^3.1.0-alpha.6",
"@jupyterlab/rendermime": "^3.1.0-alpha.6",
"@jupyterlab/services": "^6.1.0-alpha.6",
"@jupyterlab/shared-models": "^3.1.0-alpha.6",
"@jupyterlab/ui-components": "^3.1.0-alpha.6",
"@lumino/algorithm": "^1.3.3",
"@lumino/coreutils": "^1.5.3",
Expand Down

0 comments on commit 16cba94

Please sign in to comment.