Skip to content

Commit

Permalink
Load/auto-save document from the back-end using y-py (jupyterlab#12360)
Browse files Browse the repository at this point in the history
* Load document from the back-end using y-py

* Load only documents metadata when collaborative

* Delay closing the room in the backend

* Update Yjs

* Fix notebook ycell initialization

* Watch file change in the back-end and overwrite Y document

* Automatically save from the back-end

* Small fixes

* Use ypy-websocket's WebsocketServer

* Poll for file changes for now, until watchfiles is fixed

* Use ypy-websocket v0.1.2

* Remove watchfiles

* Rename save_document to maybe_save_document, add collab_file_poll_interval config

* Workaround ypy bug

* Fix for new notebook

* Use jupyter_ydoc

* Rename yjs_echo_ws.py->ydoc_handler.py, YjsEchoWebSocket->YDocWebSocketHandler

* Update ypy-websocket and jupyter_ydoc minimum versions

* Use ypy-websocket>=0.1.6

* Update jupyter_ydoc>=0.1.4

* Move WEBSOCKET_SERVER global variable to YDocWebSocketHandler class attribute

* Fix tests

* Update jupyterlab/staging/package.json

* Rename collab_file_poll_interval to collaborative_file_poll_interval, update extension migration documentation

* Set room name as file_type:file_name

* Don't save file if already on disk

* Pin jupyter_ydoc>=0.1.5

* Set room name as format:type:path

* Disable save button

* Show caption only in collaborative mode

* Sync file attributes with room name

* Clear dirty flag when opening document

* Pin jupyter_ydoc>=0.1.7 which observes the dirty flag

* Don't save when dirty flag cleared

* Moves nbformat and nbformat_minor to ymeta, changes the YNotebook eve… (#2)

* Moves nbformat and nbformat_minor to ymeta, changes the YNotebook event to support the new nbformat, and adds a local dirty property

* Pin jupyter_ydoc>=0.1.8

* Adds a local dirty property in the DocumentModel (#3)

* Removes the initialization of the dirty property from the frontend (#4)

* Removes the initialization of the dirty property from the frontend

* Remove setting dirty in the SharedDocument

Co-authored-by: hbcarlos <[email protected]>
Co-authored-by: Frédéric Collonval <[email protected]>
  • Loading branch information
3 people committed May 16, 2022
1 parent 44082eb commit 6734670
Show file tree
Hide file tree
Showing 4 changed files with 17 additions and 101 deletions.
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,7 @@
"@jupyterlab/user": "^4.0.0-alpha.9",
"@lumino/coreutils": "^1.12.0",
"lib0": "^0.2.42",
"y-websocket": "^1.3.15",
"yjs": "^13.5.17"
"y-websocket": "^1.3.15"
},
"devDependencies": {
"@jupyterlab/testutils": "^4.0.0-alpha.9",
Expand Down
12 changes: 0 additions & 12 deletions src/mock.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,6 @@
import { IDocumentProvider } from './index';

export class ProviderMock implements IDocumentProvider {
requestInitialContent(): Promise<boolean> {
return Promise.resolve(false);
}
putInitializedState(): void {
/* nop */
}
acquireLock(): Promise<number> {
return Promise.resolve(0);
}
releaseLock(lock: number): void {
/* nop */
}
destroy(): void {
/* nop */
}
Expand Down
11 changes: 1 addition & 10 deletions src/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,6 @@ export const IDocumentProviderFactory = new Token<IDocumentProviderFactory>(
* An interface for a document provider.
*/
export interface IDocumentProvider {
/**
* Resolves to true if the initial content has been initialized on the server. false otherwise.
*/
requestInitialContent(): Promise<boolean>;

/**
* Put the initialized state.
*/
putInitializedState(): void;

/**
* Returns a Promise that resolves when renaming is ackownledged.
*/
Expand Down Expand Up @@ -58,6 +48,7 @@ export namespace IDocumentProviderFactory {
*/
path: string;
contentType: string;
format: string;

/**
* The YNotebook.
Expand Down
92 changes: 15 additions & 77 deletions src/yprovider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { PromiseDelegate } from '@lumino/coreutils';
import * as decoding from 'lib0/decoding';
import * as encoding from 'lib0/encoding';
import { WebsocketProvider as YWebsocketProvider } from 'y-websocket';
import * as Y from 'yjs';
import { IDocumentProvider, IDocumentProviderFactory } from './tokens';

/**
Expand All @@ -32,38 +31,19 @@ export class WebSocketProvider
constructor(options: WebSocketProvider.IOptions) {
super(
options.url,
options.contentType + ':' + options.path,
options.format + ':' + options.contentType + ':' + options.path,
options.ymodel.ydoc,
{
awareness: options.ymodel.awareness
}
);
this._path = options.path;
this._contentType = options.contentType;
this._format = options.format;
this._serverUrl = options.url;

// Message handler that receives the initial content
this.messageHandlers[127] = (
encoder,
decoder,
provider,
emitSynced,
messageType
) => {
// received initial content
const initialContent = decoding.readTailAsUint8Array(decoder);
// Apply data from server
if (initialContent.byteLength > 0) {
Y.applyUpdate(this.doc, initialContent);
}
const initialContentRequest = this._initialContentRequest;
this._initialContentRequest = null;
if (initialContentRequest) {
initialContentRequest.resolve(initialContent.byteLength > 0);
}
};
// Message handler that receives the rename acknowledge
this.messageHandlers[125] = (
this.messageHandlers[127] = (
encoder,
decoder,
provider,
Expand All @@ -74,9 +54,6 @@ export class WebSocketProvider
decoding.readTailAsUint8Array(decoder)[0] ? true : false
);
};
this._isInitialized = false;
this._onConnectionStatus = this._onConnectionStatus.bind(this);
this.on('status', this._onConnectionStatus);

const awareness = options.ymodel.awareness;
const user = options.user;
Expand All @@ -100,10 +77,12 @@ export class WebSocketProvider
this._path = newPath;
const encoder = encoding.createEncoder();
this._renameAck = new PromiseDelegate<boolean>();
encoding.write(encoder, 125);
encoding.write(encoder, 127);
// writing a utf8 string to the encoder
const escapedPath = unescape(
encodeURIComponent(this._contentType + ':' + newPath)
encodeURIComponent(
this._format + ':' + this._contentType + ':' + newPath
)
);
for (let i = 0; i < escapedPath.length; i++) {
encoding.write(
Expand All @@ -116,42 +95,18 @@ export class WebSocketProvider
this.disconnectBc();
// The next time the provider connects, we should connect through a different server url
this.bcChannel =
this._serverUrl + '/' + this._contentType + ':' + this._path;
this._serverUrl +
'/' +
this._format +
':' +
this._contentType +
':' +
this._path;
this.url = this.bcChannel;
this.connectBc();
}
}

/**
* Resolves to true if the initial content has been initialized on the server. false otherwise.
*/
requestInitialContent(): Promise<boolean> {
if (this._initialContentRequest) {
return this._initialContentRequest.promise;
}

this._initialContentRequest = new PromiseDelegate<boolean>();
this._sendMessage(new Uint8Array([127]));

// Resolve with true if the server doesn't respond for some reason.
// In case of a connection problem, we don't want the user to re-initialize the window.
// Instead wait for y-websocket to connect to the server.
// @todo maybe we should reload instead..
setTimeout(() => this._initialContentRequest?.resolve(false), 1000);
return this._initialContentRequest.promise;
}

/**
* Put the initialized state.
*/
putInitializedState(): void {
const encoder = encoding.createEncoder();
encoding.writeVarUint(encoder, 126);
encoding.writeUint8Array(encoder, Y.encodeStateAsUpdate(this.doc));
this._sendMessage(encoding.toUint8Array(encoder));
this._isInitialized = true;
}

/**
* Send a new message to WebSocket server.
*
Expand All @@ -171,27 +126,10 @@ export class WebSocketProvider
send();
}

/**
* Handle a change to the connection status.
*
* @param status The connection status.
*/
private async _onConnectionStatus(status: {
status: 'connected' | 'disconnected';
}): Promise<void> {
if (this._isInitialized && status.status === 'connected') {
const contentIsInitialized = await this.requestInitialContent();
if (!contentIsInitialized) {
this.putInitializedState();
}
}
}

private _path: string;
private _contentType: string;
private _format: string;
private _serverUrl: string;
private _isInitialized: boolean;
private _initialContentRequest: PromiseDelegate<boolean> | null = null;
private _renameAck: PromiseDelegate<boolean>;
}

Expand Down

0 comments on commit 6734670

Please sign in to comment.