Skip to content

Commit

Permalink
Create a new endpoint for document sessions, changes tranlator proper…
Browse files Browse the repository at this point in the history
…ty and deprecates old endpoints
  • Loading branch information
hbcarlos committed Feb 28, 2023
1 parent 626339f commit 7d9e5bf
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 64 deletions.
11 changes: 10 additions & 1 deletion jupyter_collaboration/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
from traitlets import Float, Int, Type
from ypy_websocket.ystore import BaseYStore

from .handlers import SQLiteYStore, YDocRoomIdHandler, YDocWebSocketHandler
from .handlers import (
DocSessionHandler,
SQLiteYStore,
YDocRoomIdHandler,
YDocWebSocketHandler,
)


class YDocExtension(ExtensionApp):
Expand Down Expand Up @@ -57,7 +62,11 @@ def initialize_settings(self):
def initialize_handlers(self):
self.handlers.extend(
[
# Deprecated - to remove for 1.0.0
(r"/api/yjs/roomid/(.*)", YDocRoomIdHandler),
# Deprecated - to remove for 1.0.0
(r"/api/yjs/(.*)", YDocWebSocketHandler),
(r"/api/collaboration/room/(.*)", YDocWebSocketHandler),
(r"/api/collaboration/session/(.*)", DocSessionHandler),
]
)
53 changes: 46 additions & 7 deletions jupyter_collaboration/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

YFILE = YDOCS["file"]

DOCUMENT_SESSION = str(uuid.uuid4())
SERVER_SESSION = str(uuid.uuid4())


class TempFileYStore(_TempFileYStore):
Expand Down Expand Up @@ -205,9 +205,9 @@ async def open(self, path):
task.add_done_callback(self.websocket_server.background_tasks.discard)

# Close the connection if the document session expired
session = self.get_query_argument("session", "")
if isinstance(self.room, DocumentRoom) and DOCUMENT_SESSION != session:
self.close(1, "Document session expired")
session_id = self.get_query_argument("sessionId", "")
if isinstance(self.room, DocumentRoom) and SERVER_SESSION != session_id:
self.close(1003, f"Document session {session_id} expired")

# cancel the deletion of the room if it was scheduled
if isinstance(self.room, DocumentRoom) and self.room.cleaner is not None:
Expand Down Expand Up @@ -394,14 +394,51 @@ class YDocRoomIdHandler(APIHandler):
@web.authenticated
@authorized
async def put(self, path):
body = json.loads(self.request.body)
ws_url = f"{body['format']}:{body['type']}:"

file_id_manager = self.settings["file_id_manager"]

idx = file_id_manager.get_id(path)
if idx is not None:
# index already exists
self.set_status(200)
ws_url += str(idx)
self.log.info("Request for Y document '%s' with room ID: %s", path, ws_url)
return self.finish(ws_url)

# try indexing
idx = file_id_manager.index(path)
if idx is None:
# file does not exists
raise web.HTTPError(404, f"File {path!r} does not exist")

# index successfully created
self.set_status(201)
ws_url += str(idx)
self.log.info("Request for Y document '%s' with room ID: %s", path, ws_url)
return self.finish(ws_url)


class DocSessionHandler(APIHandler):
auth_resource = "contents"

@web.authenticated
@authorized
async def put(self, path):
body = json.loads(self.request.body)
format = body["format"]
content_type = body["type"]
file_id_manager = self.settings["file_id_manager"]

idx = file_id_manager.get_id(path)
if idx is not None:
# index already exists
self.log.info("Request for Y document '%s' with room ID: %s", path, idx)
data = json.dumps({"file_id": idx, "session": DOCUMENT_SESSION})
data = json.dumps(
{"format": format, "type": content_type, "fileId": idx, "sessionId": SERVER_SESSION}
)
self.set_status(200)
return self.finish(data)

# try indexing
Expand All @@ -411,7 +448,9 @@ async def put(self, path):
raise web.HTTPError(404, f"File {path!r} does not exist")

# index successfully created
self.set_status(201)
self.log.info("Request for Y document '%s' with room ID: %s", path, idx)
data = json.dumps({"file_id": idx, "session": DOCUMENT_SESSION})
data = json.dumps(
{"format": format, "type": content_type, "fileId": idx, "sessionId": SERVER_SESSION}
)
self.set_status(201)
return self.finish(data)
47 changes: 47 additions & 0 deletions packages/docprovider/src/requests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/* -----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified BSD License.
|----------------------------------------------------------------------------*/

import { URLExt } from '@jupyterlab/coreutils';
import { ServerConnection, Contents } from '@jupyterlab/services';

/**
* Document session endpoint provided by `jupyter_collaboration`
* See https://github.com/jupyterlab/jupyter_collaboration
*/
const DOC_SESSION_URL = 'api/collaboration/session';

export interface ISessionModel {
format: Contents.FileFormat;
type: Contents.ContentType;
fileId: string;
sessionId: string;
}

export async function requestDocSession(
format: string,
type: string,
path: string
): Promise<ISessionModel> {
const { makeSettings, makeRequest, ResponseError } = ServerConnection;

const settings = makeSettings();
const url = URLExt.join(
settings.baseUrl,
DOC_SESSION_URL,
encodeURIComponent(path)
);
const data = {
method: 'PUT',
body: JSON.stringify({ format, type })
};

const response = await makeRequest(url, data, settings);

if (response.status !== 200 && response.status !== 201) {
throw new ResponseError(response);
}

return response.json();
}
14 changes: 7 additions & 7 deletions packages/docprovider/src/ydrive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { WebSocketProvider } from './yprovider';
/**
* The url for the default drive service.
*/
const Y_DOCUMENT_PROVIDER_URL = 'api/yjs';
const DOCUMENT_PROVIDER_URL = 'api/collaboration/room';

/**
* A Collaborative implementation for an `IDrive`, talking to the
Expand All @@ -31,7 +31,7 @@ export class YDrive extends Drive {
constructor(user: User.IManager, translator: TranslationBundle) {
super({ name: 'YDrive' });
this._user = user;
this._translator = translator;
this._trans = translator;
this._providers = new Map<string, WebSocketProvider>();

this.sharedModelFactory = new SharedModelFactory(this._onCreate);
Expand Down Expand Up @@ -87,7 +87,7 @@ export class YDrive extends Drive {
options?: Contents.IFetchOptions
): Promise<Contents.IModel> {
if (options && options.format && options.type) {
const key = `${options.type}:${options.format}:${localPath}`;
const key = `${options.format}:${options.type}:${localPath}`;
const provider = this._providers.get(key);

if (provider) {
Expand Down Expand Up @@ -152,16 +152,16 @@ export class YDrive extends Drive {
}
try {
const provider = new WebSocketProvider({
url: URLExt.join(this.serverSettings.wsUrl, Y_DOCUMENT_PROVIDER_URL),
url: URLExt.join(this.serverSettings.wsUrl, DOCUMENT_PROVIDER_URL),
path: options.path,
format: options.format,
contentType: options.contentType,
model: sharedModel,
user: this._user,
translator: this._translator
translator: this._trans
});

const key = `${options.contentType}:${options.format}:${options.path}`;
const key = `${options.format}:${options.contentType}:${options.path}`;
this._providers.set(key, provider);

sharedModel.disposed.connect(() => {
Expand All @@ -181,7 +181,7 @@ export class YDrive extends Drive {
};

private _user: User.IManager;
private _translator: TranslationBundle;
private _trans: TranslationBundle;
private _providers: Map<string, WebSocketProvider>;
}

Expand Down
86 changes: 37 additions & 49 deletions packages/docprovider/src/yprovider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@
| Distributed under the terms of the Modified BSD License.
|----------------------------------------------------------------------------*/

import { URLExt } from '@jupyterlab/coreutils';
import { showErrorMessage, Dialog } from '@jupyterlab/apputils';
import { ServerConnection, User } from '@jupyterlab/services';
import { User } from '@jupyterlab/services';
import { TranslationBundle } from '@jupyterlab/translation';

import { PromiseDelegate } from '@lumino/coreutils';
Expand All @@ -17,11 +16,7 @@ import { DocumentChange, YDocument } from '@jupyter/ydoc';
import { Awareness } from 'y-protocols/awareness';
import { WebsocketProvider as YWebsocketProvider } from 'y-websocket';

/**
* Room Id endpoint provided by `jupyter_collaboration`
* See https://github.com/jupyterlab/jupyter_collaboration
*/
const FILE_PATH_TO_ROOM_ID_URL = 'api/yjs/roomid';
import { ISessionModel, requestDocSession } from './requests';

/**
* An interface for a document provider.
Expand Down Expand Up @@ -54,7 +49,7 @@ export class WebSocketProvider implements IDocumentProvider {
this._sharedModel = options.model;
this._awareness = options.model.awareness;
this._yWebsocketProvider = null;
this._translator = options.translator;
this._trans = options.translator;

const user = options.user;

Expand All @@ -65,39 +60,7 @@ export class WebSocketProvider implements IDocumentProvider {
.catch(e => console.error(e));
user.userChanged.connect(this._onUserChanged, this);

const serverSettings = ServerConnection.makeSettings();
const url = URLExt.join(
serverSettings.baseUrl,
FILE_PATH_TO_ROOM_ID_URL,
encodeURIComponent(this._path)
);
const data = { method: 'PUT' };
ServerConnection.makeRequest(url, data, serverSettings)
.then(response => {
if (response.status !== 200 && response.status !== 201) {
throw new ServerConnection.ResponseError(response);
}
return response.json();
})
.then(resp => {
this._yWebsocketProvider = new YWebsocketProvider(
this._serverUrl,
`${this._format}:${this._contentType}:${resp['file_id']}`,
this._sharedModel.ydoc,
{
disableBc: true,
params: { session: resp['session'] },
awareness: this._awareness
}
);

this._yWebsocketProvider.on(
'connection-close',
this._onConnectionClosed
);
})
.then(() => this._ready.resolve())
.catch(reason => console.warn(reason));
this._connect();
}

/**
Expand All @@ -122,32 +85,57 @@ export class WebSocketProvider implements IDocumentProvider {
return;
}
this._isDisposed = true;
this._yWebsocketProvider?.off('connection-close', this._onConnectionClosed);
this._yWebsocketProvider?.destroy();
Signal.clearData(this);
}

private _connect(): void {
requestDocSession(this._format, this._contentType, this._path)
.then((session: ISessionModel) => {
this._yWebsocketProvider = new YWebsocketProvider(
this._serverUrl,
`${session.format}:${session.type}:${session.fileId}`,
this._sharedModel.ydoc,
{
disableBc: true,
params: { sessionId: session.sessionId },
awareness: this._awareness
}
);

this._yWebsocketProvider.on(
'connection-close',
this._onConnectionClosed
);
})
.then(r => this._ready.resolve())
.catch(e => console.warn(e));
}

private _onUserChanged(user: User.IManager): void {
this._awareness.setLocalStateField('user', user.identity);
}

private _onConnectionClosed(event: any): void {
if (event.code === 1) {
private _onConnectionClosed = (event: any): void => {
if (event.code === 1003) {
console.error('Document provider closed:', event.reason);

showErrorMessage(
this._translator.__('Session expired'),
this._translator.__(
this._trans.__('Session expired'),
this._trans.__(
'The document session expired. You need to reload this browser tab.'
),
[Dialog.okButton({ label: this._translator.__('Reload') })]
[Dialog.okButton({ label: this._trans.__('Reload') })]
)
.then(r => window.location.reload())
.catch(e => window.location.reload());

// Dispose shared model immediately. Better break the document model,
// than overriding data on disk
// than overriding data on disk.
this._sharedModel.dispose();
}
}
};

private _awareness: Awareness;
private _contentType: string;
Expand All @@ -158,7 +146,7 @@ export class WebSocketProvider implements IDocumentProvider {
private _serverUrl: string;
private _sharedModel: YDocument<DocumentChange>;
private _yWebsocketProvider: YWebsocketProvider | null;
private _translator: TranslationBundle;
private _trans: TranslationBundle;
}

/**
Expand Down

0 comments on commit 7d9e5bf

Please sign in to comment.