Skip to content

Commit 2c47bc0

Browse files
Fix confirmation on manual saving (#490)
* Fix confirmation on manual saving * Please `mypy` * Use `MessageType.RAW` instead of a custom var * Update error message Co-authored-by: David Brochart <[email protected]> * Pin pre-commit `pycrdt-websocket` version * Add save ID to avoid mixing statuses --------- Co-authored-by: David Brochart <[email protected]>
1 parent 91e1645 commit 2c47bc0

File tree

4 files changed

+82
-12
lines changed

4 files changed

+82
-12
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ repos:
4444
- id: mypy
4545
exclude: "(^binder/jupyter_config\\.py$)|(^scripts/bump_version\\.py$)|(/setup\\.py$)"
4646
args: ["--config-file", "pyproject.toml"]
47-
additional_dependencies: [tornado, pytest, pycrdt-websocket]
47+
additional_dependencies: [tornado, pytest, 'pycrdt-websocket<0.16.0']
4848
stages: [manual]
4949

5050
- repo: https://github.com/sirosen/check-jsonschema

packages/docprovider/src/ydrive.ts

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
ServerConnection,
1212
User
1313
} from '@jupyterlab/services';
14+
import { PromiseDelegate } from '@lumino/coreutils';
1415
import { ISignal, Signal } from '@lumino/signaling';
1516

1617
import { DocumentChange, ISharedDocument, YDocument } from '@jupyter/ydoc';
@@ -22,20 +23,14 @@ import {
2223
} from '@jupyter/collaborative-drive';
2324
import { Awareness } from 'y-protocols/awareness';
2425
import { ISettingRegistry } from '@jupyterlab/settingregistry';
26+
import * as decoding from 'lib0/decoding';
2527
import * as encoding from 'lib0/encoding';
2628

2729
const DISABLE_RTC =
2830
PageConfig.getOption('disableRTC') === 'true' ? true : false;
2931

3032
const RAW_MESSAGE_TYPE = 2;
3133

32-
const SAVE_MESSAGE = (() => {
33-
const encoder = encoding.createEncoder();
34-
encoding.writeVarUint(encoder, RAW_MESSAGE_TYPE);
35-
encoding.writeVarString(encoder, 'save');
36-
return encoding.toUint8Array(encoder);
37-
})();
38-
3934
/**
4035
* The url for the default drive service.
4136
*/
@@ -134,9 +129,60 @@ export class RtcContentProvider
134129
if (options.format && options.type) {
135130
const key = `${options.format}:${options.type}:${localPath}`;
136131
const provider = this._providers.get(key);
132+
const saveId = ++this._saveCounter;
137133

138134
if (provider) {
139-
provider.wsProvider?.ws?.send(SAVE_MESSAGE);
135+
const ws = provider.wsProvider?.ws;
136+
if (ws) {
137+
const delegate = new PromiseDelegate<void>();
138+
const handler = (event: MessageEvent) => {
139+
const data = new Uint8Array(event.data);
140+
const decoder = decoding.createDecoder(data);
141+
try {
142+
const messageType = decoding.readVarUint(decoder);
143+
if (messageType !== RAW_MESSAGE_TYPE) {
144+
return;
145+
}
146+
} catch {
147+
return;
148+
}
149+
const rawReply = decoding.readVarString(decoder);
150+
let reply: {
151+
type: 'save';
152+
responseTo: number;
153+
status: 'success' | 'skipped' | 'failed';
154+
} | null = null;
155+
try {
156+
reply = JSON.parse(rawReply);
157+
} catch (e) {
158+
console.debug('The raw reply received was not a JSON reply');
159+
}
160+
if (
161+
reply &&
162+
reply['type'] === 'save' &&
163+
reply['responseTo'] === saveId
164+
) {
165+
if (reply.status === 'success') {
166+
delegate.resolve();
167+
} else if (reply.status === 'failed') {
168+
delegate.reject('Saving failed');
169+
} else if (reply.status === 'skipped') {
170+
delegate.reject('Saving already in progress');
171+
} else {
172+
delegate.reject('Unrecognised save reply status');
173+
}
174+
}
175+
};
176+
ws.addEventListener('message', handler);
177+
const encoder = encoding.createEncoder();
178+
encoding.writeVarUint(encoder, RAW_MESSAGE_TYPE);
179+
encoding.writeVarString(encoder, 'save');
180+
encoding.writeVarUint(encoder, saveId);
181+
const saveMessage = encoding.toUint8Array(encoder);
182+
ws.send(saveMessage);
183+
await delegate.promise;
184+
ws.removeEventListener('message', handler);
185+
}
140186
const fetchOptions: Contents.IFetchOptions = {
141187
type: options.type,
142188
format: options.format,
@@ -256,6 +302,7 @@ export class RtcContentProvider
256302
};
257303

258304
private _user: User.IManager;
305+
private _saveCounter = 0;
259306
private _trans: TranslationBundle;
260307
private _globalAwareness: Awareness | null;
261308
private _providers: Map<string, WebSocketProvider>;

projects/jupyter-server-ydoc/jupyter_server_ydoc/handlers.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from jupyter_server.base.handlers import APIHandler, JupyterHandler
1616
from jupyter_server.utils import ensure_async
1717
from jupyter_ydoc import ydocs as YDOCS
18-
from pycrdt import Doc, UndoManager
18+
from pycrdt import Doc, Encoder, UndoManager
1919
from pycrdt_websocket.yroom import YRoom
2020
from pycrdt_websocket.ystore import BaseYStore
2121
from tornado import web
@@ -273,7 +273,7 @@ async def open(self, room_id: str) -> None: # type:ignore[override]
273273
if self._room_id != "JupyterLab:globalAwareness":
274274
self._emit_awareness_event(self.current_user.username, "join")
275275

276-
async def send(self, message):
276+
async def send(self, message: bytes) -> None:
277277
"""
278278
Send a message to the client.
279279
"""
@@ -299,16 +299,38 @@ async def on_message(self, message):
299299
if header == MessageType.RAW:
300300
msg = decoder.read_var_string()
301301
if msg == "save":
302+
save_id = decoder.read_var_uint()
303+
save_reply = {
304+
"type": "save",
305+
"responseTo": save_id,
306+
}
302307
try:
303308
room = cast(DocumentRoom, self.room)
304-
room._save_to_disc()
309+
save_task = room._save_to_disc()
310+
# task may be missing if save was already in progress
311+
if save_task:
312+
await save_task
313+
await self.send(
314+
self._encode_json_message({**save_reply, "status": "success"})
315+
)
316+
else:
317+
await self.send(
318+
self._encode_json_message({**save_reply, "status": "skipped"})
319+
)
305320
except Exception:
306321
self.log.error("Couldn't save content from room: %s", self._room_id)
322+
await self.send(self._encode_json_message({**save_reply, "status": "failed"}))
307323
return
308324

309325
self._message_queue.put_nowait(message)
310326
self._websocket_server.ypatch_nb += 1
311327

328+
def _encode_json_message(self, message: dict) -> bytes:
329+
encoder = Encoder()
330+
encoder.write_var_uint(MessageType.RAW)
331+
encoder.write_var_string(json.dumps(message))
332+
return encoder.to_bytes()
333+
312334
def on_close(self) -> None:
313335
"""
314336
On connection close.

projects/jupyter-server-ydoc/jupyter_server_ydoc/rooms.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,7 @@ def _save_to_disc(self):
280280
self._saving_document = asyncio.create_task(
281281
self._maybe_save_document(self._saving_document)
282282
)
283+
return self._saving_document
283284

284285
async def _maybe_save_document(self, saving_document: asyncio.Task | None) -> None:
285286
"""

0 commit comments

Comments
 (0)