Skip to content

Commit b06410d

Browse files
authored
Fix document example (#272)
* Fix linter on document example * Fix ExampleModel * Bump ydoc and collaboration * Fix README * Bump base deps * Update snapshot --------- Co-authored-by: Frédéric Collonval <[email protected]>
1 parent 6a166f1 commit b06410d

File tree

9 files changed

+101
-75
lines changed

9 files changed

+101
-75
lines changed

documents/README.md

+39-36
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,18 @@ Developers can provide new extensions to support additional documents (or replac
2828
The model, the shared model and the view will be provided through new factories and the file type will be registered directly.
2929
For that you will need to access the [`DocumentRegistry`](https://jupyterlab.readthedocs.io/en/latest/api/classes/docregistry.DocumentRegistry-1.html) to register new [`FileType`s](https://jupyterlab.readthedocs.io/en/latest/api/interfaces/rendermime_interfaces.IRenderMime.IFileType.html), models and views. This way, when opening a new file, the [`DocumentManager`](https://jupyterlab.readthedocs.io/en/latest/api/classes/docmanager.DocumentManager-1.html) will look into the file metadata and create an instance of `Context` with the right model for this file. To register new documents, you can create factories, either a [`IModelFactory`](https://jupyterlab.readthedocs.io/en/latest/api/interfaces/docregistry.DocumentRegistry.IModelFactory.html) for the model and/or a [`IWidgetFactory`](https://jupyterlab.readthedocs.io/en/latest/api/interfaces/docregistry.DocumentRegistry.IWidgetFactory.html) for the view.
3030

31-
The shared model needs to be registered only if your file must be collaborative. For that you will need to register it in the [`ICollaborativeDrive`](https://jupyterlab-realtime-collaboration.readthedocs.io/en/latest/api/interfaces/docprovider.ICollaborativeDrive.html) token provided by the `@jupyter/docprovider` package.
31+
The shared model needs to be registered only if your file must be collaborative. For that you will need to register it in the [`ICollaborativeDrive`](https://jupyterlab-realtime-collaboration.readthedocs.io/en/latest/api/interfaces/collaborative_drive.ICollaborativeDrive.html) token provided by the `@jupyter/collaborative-drive` package.
3232

3333
> Packaging note: when using an optional external extension (here
34-
> `@jupyter/docprovider` from `jupyter-collaboration`), you must
34+
> `@jupyter/collaborative-drive` from `jupyter-collaboration`), you must
3535
> tell JupyterLab to include that package in the current extension by
3636
> adding the following configuration in `package.json`.:
3737
3838
```json5
3939
// package.json#L108-L113
4040

4141
"sharedPackages": {
42-
"@jupyter/docprovider": {
42+
"@jupyter/collaborative-drive": {
4343
"bundled": true,
4444
"singleton": true
4545
}
@@ -229,7 +229,7 @@ The `DocumentModel` represents the file content in the frontend. Through the mod
229229
230230
## Make it collaborative
231231

232-
In JupyterLab v3.1, we introduced the package `@jupyterlab/shared-models` to swap `ModelDB` as a data storage to make the notebooks collaborative. We implemented these shared models using [Yjs](https://yjs.dev), a high-performance CRDT for building collaborative applications that automatically sync. You can find all the documentation of Yjs [here](https://docs.yjs.dev).
232+
In JupyterLab v3.1, we switched from `ModelDB` as a data storage to shared models. We implemented these shared models using [Yjs](https://yjs.dev), a high-performance CRDT for building collaborative applications that automatically sync. You can find all the documentation of Yjs [here](https://docs.yjs.dev).
233233

234234
Yjs documents (`Y.Doc`) are the main class of Yjs. They represent a shared document between clients and hold multiple shared objects. Yjs documents enable you to share different [data types like text, Array, Map or set](https://docs.yjs.dev/getting-started/working-with-shared-types), which makes it possible to create not only collaborative text editors but also diagrams, drawings,... .
235235

@@ -255,7 +255,7 @@ In this extension, we created:
255255

256256
<!-- prettier-ignore-start -->
257257
```ts
258-
// src/model.ts#L354-L354
258+
// src/model.ts#L344-L344
259259

260260
export class ExampleDoc extends YDocument<ExampleDocChange> {
261261
```
@@ -265,7 +265,7 @@ To create a new shared object, you have to use the `ydoc`. The new attribute wil
265265
266266
<!-- prettier-ignore-start -->
267267
```ts
268-
// src/model.ts#L358-L359
268+
// src/model.ts#L348-L349
269269

270270
this._content = this.ydoc.getMap('content');
271271
this._content.observe(this._contentObserver);
@@ -278,7 +278,7 @@ we provide helpers `get` and `set` to hide the complexity of `position` being st
278278
279279
<!-- prettier-ignore-start -->
280280
```ts
281-
// src/model.ts#L390-L399
281+
// src/model.ts#L408-L417
282282

283283
get(key: 'content'): string;
284284
get(key: 'position'): Position;
@@ -288,12 +288,12 @@ get(key: string): any {
288288
? data
289289
? JSON.parse(data)
290290
: { x: 0, y: 0 }
291-
: data ?? '';
291+
: (data ?? '');
292292
}
293293
```
294294
295295
```ts
296-
// src/model.ts#L407-L411
296+
// src/model.ts#L425-L429
297297

298298
set(key: 'content', value: string): void;
299299
set(key: 'position', value: PartialJSONObject): void;
@@ -315,7 +315,7 @@ this.sharedModel.awareness.setLocalStateField('mouse', pos);
315315
```
316316
317317
```ts
318-
// src/model.ts#L289-L289
318+
// src/model.ts#L279-L279
319319

320320
const clients = this.sharedModel.awareness.getStates();
321321
```
@@ -335,11 +335,11 @@ Every time you modify a shared property, this property triggers an event in all
335335
336336
<!-- prettier-ignore-start -->
337337
```ts
338-
// src/model.ts#L233-L236
338+
// src/model.ts#L376-L379
339339

340-
this.sharedModel.transact(() => {
341-
this.sharedModel.set('position', { x: obj.x, y: obj.y });
342-
this.sharedModel.set('content', obj.content);
340+
this.transact(() => {
341+
this.set('position', { x: obj.x, y: obj.y });
342+
this.set('content', obj.content);
343343
});
344344
```
345345
<!-- prettier-ignore-end -->
@@ -350,13 +350,13 @@ That client is responsible for loading, saving and watching the file on disk and
350350
to propagate all changes to all clients. This makes collaboration much more robust
351351
in case of flaky connection, file rename,... .
352352
353-
In Python, Yjs protocol is implemented in the library [`y-py`](https://github.com/y-crdt/ypy). But as we provide `@jupyterlab/shared-models` helpers for the frontend, we
353+
In Python, Yjs protocol is implemented in the library [`ycrdt`](https://github.com/jupyter-server/pycrdt). But as we provide `@jupyter/ydoc` helpers for the frontend, we
354354
provide [`jupyter-ydoc`](https://github.com/jupyter-server/jupyter_ydoc) helpers for Python.
355355
356356
A shared model must inherit from `YBaseDoc`, here:
357357
358358
```py
359-
# jupyterlab_examples_documents/document.py#L4-L7
359+
# jupyterlab_examples_documents/document.py#L6-L9
360360

361361
from jupyter_ydoc.ybasedoc import YBaseDoc
362362

@@ -368,9 +368,9 @@ The shared map is added to the model like this:
368368

369369
<!-- prettier-ignore-start -->
370370
```py
371-
# jupyterlab_examples_documents/document.py#L10-L10
371+
# jupyterlab_examples_documents/document.py#L12-L12
372372

373-
self._content = self._ydoc.get_map('content')
373+
self._content = self._ydoc.get("content", type=pycrdt.Map)
374374
```
375375
<!-- prettier-ignore-end -->
376376

@@ -379,23 +379,19 @@ must be defined:
379379

380380
<!-- prettier-ignore-start -->
381381
```py
382-
# jupyterlab_examples_documents/document.py#L16-L49
382+
# jupyterlab_examples_documents/document.py#L18-L49
383383

384384
def get(self) -> str:
385385
"""
386386
Returns the content of the document as saved by the contents manager.
387387

388388
:return: Document's content.
389389
"""
390-
data = json.loads(self._content.to_json())
390+
data = self._content.to_py()
391391
position = json.loads(data["position"])
392392
return json.dumps(
393-
{
394-
"x": position["x"],
395-
"y": position["y"],
396-
"content": data["content"]
397-
},
398-
indent=2
393+
{"x": position["x"], "y": position["y"], "content": data["content"]},
394+
indent=2,
399395
)
400396

401397
def set(self, raw_value: str) -> None:
@@ -405,15 +401,17 @@ def set(self, raw_value: str) -> None:
405401
:param raw_value: The content of the document.
406402
"""
407403
value = json.loads(raw_value)
408-
with self._ydoc.begin_transaction() as t:
404+
with self._ydoc.transaction():
409405
# clear document
410-
for key in self._content:
411-
self._content.pop(t, key)
412-
for key in [k for k in self._ystate if k not in ("dirty", "path")]:
413-
self._ystate.pop(t, key)
406+
for key in self._content.keys():
407+
self._content.pop(key)
408+
for key in [k for k in self._ystate.keys() if k not in ("dirty", "path")]:
409+
self._ystate.pop(key)
410+
411+
self._content["position"] = {"x": value["x"], "y": value["y"]}
412+
413+
self._content["content"] = value["content"]
414414

415-
self._content.set(t, "position", json.dumps({"x": value["x"], "y": value["y"]}))
416-
self._content.set(t, "content", value["content"])
417415
#
418416
```
419417
<!-- prettier-ignore-end -->
@@ -423,7 +421,7 @@ reacting to a document changes:
423421
424422
<!-- prettier-ignore-start -->
425423
```py
426-
# jupyterlab_examples_documents/document.py#L51-L60
424+
# jupyterlab_examples_documents/document.py#L51-L65
427425

428426
def observe(self, callback: "Callable[[str, Any], None]") -> None:
429427
"""
@@ -432,8 +430,13 @@ def observe(self, callback: "Callable[[str, Any], None]") -> None:
432430
:param callback: Callback that will be called when the document changes.
433431
"""
434432
self.unobserve()
435-
self._subscriptions[self._ystate] = self._ystate.observe(partial(callback, "state"))
436-
self._subscriptions[self._content] = self._content.observe(partial(callback, "content"))
433+
self._subscriptions[self._ystate] = self._ystate.observe(
434+
partial(callback, "state")
435+
)
436+
self._subscriptions[self._content] = self._content.observe(
437+
partial(callback, "content")
438+
)
439+
437440
#
438441
```
439442
<!-- prettier-ignore-end -->
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,31 @@
11
import json
22
from functools import partial
3+
from typing import Any, Callable
34

5+
import pycrdt
46
from jupyter_ydoc.ybasedoc import YBaseDoc
57

68

79
class YExampleDoc(YBaseDoc):
810
def __init__(self, *args, **kwargs):
911
super().__init__(*args, **kwargs)
10-
self._content = self._ydoc.get_map('content')
12+
self._content = self._ydoc.get("content", type=pycrdt.Map)
1113

1214
@property
1315
def version(self) -> str:
14-
return '0.1.0'
16+
return "0.1.0"
1517

1618
def get(self) -> str:
1719
"""
1820
Returns the content of the document as saved by the contents manager.
1921
2022
:return: Document's content.
2123
"""
22-
data = json.loads(self._content.to_json())
24+
data = self._content.to_py()
2325
position = json.loads(data["position"])
2426
return json.dumps(
25-
{
26-
"x": position["x"],
27-
"y": position["y"],
28-
"content": data["content"]
29-
},
30-
indent=2
27+
{"x": position["x"], "y": position["y"], "content": data["content"]},
28+
indent=2,
3129
)
3230

3331
def set(self, raw_value: str) -> None:
@@ -37,15 +35,17 @@ def set(self, raw_value: str) -> None:
3735
:param raw_value: The content of the document.
3836
"""
3937
value = json.loads(raw_value)
40-
with self._ydoc.begin_transaction() as t:
38+
with self._ydoc.transaction():
4139
# clear document
42-
for key in self._content:
43-
self._content.pop(t, key)
44-
for key in [k for k in self._ystate if k not in ("dirty", "path")]:
45-
self._ystate.pop(t, key)
40+
for key in self._content.keys():
41+
self._content.pop(key)
42+
for key in [k for k in self._ystate.keys() if k not in ("dirty", "path")]:
43+
self._ystate.pop(key)
44+
45+
self._content["position"] = {"x": value["x"], "y": value["y"]}
46+
47+
self._content["content"] = value["content"]
4648

47-
self._content.set(t, "position", json.dumps({"x": value["x"], "y": value["y"]}))
48-
self._content.set(t, "content", value["content"])
4949
#
5050

5151
def observe(self, callback: "Callable[[str, Any], None]") -> None:
@@ -55,6 +55,11 @@ def observe(self, callback: "Callable[[str, Any], None]") -> None:
5555
:param callback: Callback that will be called when the document changes.
5656
"""
5757
self.unobserve()
58-
self._subscriptions[self._ystate] = self._ystate.observe(partial(callback, "state"))
59-
self._subscriptions[self._content] = self._content.observe(partial(callback, "content"))
58+
self._subscriptions[self._ystate] = self._ystate.observe(
59+
partial(callback, "state")
60+
)
61+
self._subscriptions[self._content] = self._content.observe(
62+
partial(callback, "content")
63+
)
64+
6065
#

documents/package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@
5252
"watch:labextension": "jupyter labextension watch ."
5353
},
5454
"dependencies": {
55-
"@jupyter/docprovider": "^1.0.0",
56-
"@jupyter/ydoc": "^1.0.0",
55+
"@jupyter/collaborative-drive": "^3.0.0",
56+
"@jupyter/ydoc": "^3.0.0",
5757
"@jupyterlab/application": "^4.0.0",
5858
"@jupyterlab/apputils": "^4.0.0",
5959
"@jupyterlab/coreutils": "^6.0.0",
@@ -106,7 +106,7 @@
106106
"extension": true,
107107
"outputDir": "jupyterlab_examples_documents/labextension",
108108
"sharedPackages": {
109-
"@jupyter/docprovider": {
109+
"@jupyter/collaborative-drive": {
110110
"bundled": true,
111111
"singleton": true
112112
}

documents/pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ classifiers = [
2323
"Programming Language :: Python :: 3.12",
2424
]
2525
dependencies = [
26-
"jupyter_ydoc>=1.0.1,<2.0.0"
26+
"jupyter_ydoc>=3.0.0,<4.0.0"
2727
]
2828
dynamic = ["version", "description", "authors", "urls", "keywords"]
2929

documents/src/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ICollaborativeDrive } from '@jupyter/docprovider';
1+
import { ICollaborativeDrive } from '@jupyter/collaborative-drive';
22

33
import {
44
JupyterFrontEnd,

documents/src/model.ts

+31-13
Original file line numberDiff line numberDiff line change
@@ -212,13 +212,7 @@ export class ExampleDocModel implements DocumentRegistry.IModel {
212212
* @returns The data
213213
*/
214214
toString(): string {
215-
const pos = this.sharedModel.get('position');
216-
const obj = {
217-
x: pos?.x ?? 10,
218-
y: pos?.y ?? 10,
219-
content: this.sharedModel.get('content') ?? ''
220-
};
221-
return JSON.stringify(obj, null, 2);
215+
return this.sharedModel.getSource();
222216
}
223217

224218
/**
@@ -229,11 +223,7 @@ export class ExampleDocModel implements DocumentRegistry.IModel {
229223
* @param data Serialized data
230224
*/
231225
fromString(data: string): void {
232-
const obj = JSON.parse(data);
233-
this.sharedModel.transact(() => {
234-
this.sharedModel.set('position', { x: obj.x, y: obj.y });
235-
this.sharedModel.set('content', obj.content);
236-
});
226+
this.sharedModel.setSource(data);
237227
}
238228

239229
/**
@@ -361,6 +351,34 @@ export class ExampleDoc extends YDocument<ExampleDocChange> {
361351

362352
readonly version: string = '1.0.0';
363353

354+
/**
355+
* Get the document source
356+
*
357+
* @returns The source
358+
*/
359+
getSource(): string {
360+
const pos = this.get('position');
361+
const obj = {
362+
x: pos?.x ?? 10,
363+
y: pos?.y ?? 10,
364+
content: this.get('content') ?? ''
365+
};
366+
return JSON.stringify(obj, null, 2);
367+
}
368+
369+
/**
370+
* Set the document source
371+
*
372+
* @param value The source to set
373+
*/
374+
setSource(value: string): void {
375+
const obj = JSON.parse(value);
376+
this.transact(() => {
377+
this.set('position', { x: obj.x, y: obj.y });
378+
this.set('content', obj.content);
379+
});
380+
}
381+
364382
/**
365383
* Dispose of the resources.
366384
*/
@@ -395,7 +413,7 @@ export class ExampleDoc extends YDocument<ExampleDocChange> {
395413
? data
396414
? JSON.parse(data)
397415
: { x: 0, y: 0 }
398-
: data ?? '';
416+
: (data ?? '');
399417
}
400418

401419
/**
Loading

0 commit comments

Comments
 (0)