@jupyter/docprovider 1.0.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @packageDocumentation
3
+ * @module docprovider
4
+ */
5
+ export * from './ydrive';
6
+ export * from './yprovider';
package/lib/index.js ADDED
@@ -0,0 +1,10 @@
1
+ /* -----------------------------------------------------------------------------
2
+ | Copyright (c) Jupyter Development Team.
3
+ | Distributed under the terms of the Modified BSD License.
4
+ |----------------------------------------------------------------------------*/
5
+ /**
6
+ * @packageDocumentation
7
+ * @module docprovider
8
+ */
9
+ export * from './ydrive';
10
+ export * from './yprovider';
@@ -0,0 +1,77 @@
1
+ import { Contents, Drive, User } from '@jupyterlab/services';
2
+ /**
3
+ * A Collaborative implementation for an `IDrive`, talking to the
4
+ * server using the Jupyter REST API and a WebSocket connection.
5
+ */
6
+ export declare class YDrive extends Drive {
7
+ /**
8
+ * Construct a new drive object.
9
+ *
10
+ * @param user - The user manager to add the identity to the awareness of documents.
11
+ */
12
+ constructor(user: User.IManager);
13
+ /**
14
+ * SharedModel factory for the YDrive.
15
+ */
16
+ readonly sharedModelFactory: Contents.ISharedFactory;
17
+ /**
18
+ * Delete a file.
19
+ *
20
+ * @param localPath - The path to the file.
21
+ *
22
+ * @returns A promise which resolves when the file is deleted.
23
+ *
24
+ * #### Notes
25
+ * Uses the [Jupyter Notebook API](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/notebook/master/notebook/services/api/api.yaml#!/contents).
26
+ */
27
+ delete(localPath: string): Promise<void>;
28
+ /**
29
+ * Dispose of the resources held by the manager.
30
+ */
31
+ dispose(): void;
32
+ /**
33
+ * Get a file or directory.
34
+ *
35
+ * @param localPath: The path to the file.
36
+ *
37
+ * @param options: The options used to fetch the file.
38
+ *
39
+ * @returns A promise which resolves with the file content.
40
+ *
41
+ * Uses the [Jupyter Notebook API](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/notebook/master/notebook/services/api/api.yaml#!/contents) and validates the response model.
42
+ */
43
+ get(localPath: string, options?: Contents.IFetchOptions): Promise<Contents.IModel>;
44
+ /**
45
+ * Rename a file or directory.
46
+ *
47
+ * @param oldLocalPath - The original file path.
48
+ *
49
+ * @param newLocalPath - The new file path.
50
+ *
51
+ * @returns A promise which resolves with the new file contents model when
52
+ * the file is renamed.
53
+ *
54
+ * #### Notes
55
+ * Uses the [Jupyter Notebook API](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/notebook/master/notebook/services/api/api.yaml#!/contents) and validates the response model.
56
+ */
57
+ rename(oldLocalPath: string, newLocalPath: string): Promise<Contents.IModel>;
58
+ /**
59
+ * Save a file.
60
+ *
61
+ * @param localPath - The desired file path.
62
+ *
63
+ * @param options - Optional overrides to the model.
64
+ *
65
+ * @returns A promise which resolves with the file content model when the
66
+ * file is saved.
67
+ *
68
+ * #### Notes
69
+ * Ensure that `model.content` is populated for the file.
70
+ *
71
+ * Uses the [Jupyter Notebook API](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/notebook/master/notebook/services/api/api.yaml#!/contents) and validates the response model.
72
+ */
73
+ save(localPath: string, options?: Partial<Contents.IModel>): Promise<Contents.IModel>;
74
+ private _onCreate;
75
+ private _user;
76
+ private _providers;
77
+ }
package/lib/ydrive.js ADDED
@@ -0,0 +1,182 @@
1
+ // Copyright (c) Jupyter Development Team.
2
+ // Distributed under the terms of the Modified BSD License.
3
+ import { YFile, YNotebook } from '@jupyter/ydoc';
4
+ import { URLExt } from '@jupyterlab/coreutils';
5
+ import { Drive } from '@jupyterlab/services';
6
+ import { WebSocketProvider } from './yprovider';
7
+ /**
8
+ * The url for the default drive service.
9
+ */
10
+ const Y_DOCUMENT_PROVIDER_URL = 'api/yjs';
11
+ /**
12
+ * A Collaborative implementation for an `IDrive`, talking to the
13
+ * server using the Jupyter REST API and a WebSocket connection.
14
+ */
15
+ export class YDrive extends Drive {
16
+ /**
17
+ * Construct a new drive object.
18
+ *
19
+ * @param user - The user manager to add the identity to the awareness of documents.
20
+ */
21
+ constructor(user) {
22
+ super({ name: 'YDrive' });
23
+ this._onCreate = (options, sharedModel) => {
24
+ if (typeof options.format !== 'string') {
25
+ return;
26
+ }
27
+ try {
28
+ const provider = new WebSocketProvider({
29
+ url: URLExt.join(this.serverSettings.wsUrl, Y_DOCUMENT_PROVIDER_URL),
30
+ path: options.path,
31
+ format: options.format,
32
+ contentType: options.contentType,
33
+ model: sharedModel,
34
+ user: this._user
35
+ });
36
+ const key = `${options.contentType}:${options.format}:${options.path}`;
37
+ this._providers.set(key, provider);
38
+ sharedModel.disposed.connect(() => {
39
+ const provider = this._providers.get(key);
40
+ if (provider) {
41
+ provider.dispose();
42
+ this._providers.delete(key);
43
+ }
44
+ });
45
+ }
46
+ catch (error) {
47
+ // Falling back to the contents API if opening the websocket failed
48
+ // This may happen if the shared document is not a YDocument.
49
+ console.error(`Failed to open websocket connection for ${options.path}.\n:${error}`);
50
+ }
51
+ };
52
+ this._user = user;
53
+ this._providers = new Map();
54
+ this.sharedModelFactory = new SharedModelFactory(this._onCreate);
55
+ }
56
+ /**
57
+ * Delete a file.
58
+ *
59
+ * @param localPath - The path to the file.
60
+ *
61
+ * @returns A promise which resolves when the file is deleted.
62
+ *
63
+ * #### Notes
64
+ * Uses the [Jupyter Notebook API](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/notebook/master/notebook/services/api/api.yaml#!/contents).
65
+ */
66
+ async delete(localPath) {
67
+ await super.delete(localPath);
68
+ // FIXME
69
+ // We are not removing the path from `sharedPaths` as multiple providers of the same file (with different model) may exist.
70
+ //this._sharedPaths.delete(localPath);
71
+ }
72
+ /**
73
+ * Dispose of the resources held by the manager.
74
+ */
75
+ dispose() {
76
+ if (this.isDisposed) {
77
+ return;
78
+ }
79
+ this._providers.forEach(p => p.dispose());
80
+ this._providers.clear();
81
+ super.dispose();
82
+ }
83
+ /**
84
+ * Get a file or directory.
85
+ *
86
+ * @param localPath: The path to the file.
87
+ *
88
+ * @param options: The options used to fetch the file.
89
+ *
90
+ * @returns A promise which resolves with the file content.
91
+ *
92
+ * Uses the [Jupyter Notebook API](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/notebook/master/notebook/services/api/api.yaml#!/contents) and validates the response model.
93
+ */
94
+ async get(localPath, options) {
95
+ if (options && options.format && options.type) {
96
+ const key = `${options.type}:${options.format}:${localPath}`;
97
+ const provider = this._providers.get(key);
98
+ if (provider) {
99
+ const model = super.get(localPath, { ...options, content: false });
100
+ await provider.ready;
101
+ return model;
102
+ }
103
+ }
104
+ return super.get(localPath, options);
105
+ }
106
+ /**
107
+ * Rename a file or directory.
108
+ *
109
+ * @param oldLocalPath - The original file path.
110
+ *
111
+ * @param newLocalPath - The new file path.
112
+ *
113
+ * @returns A promise which resolves with the new file contents model when
114
+ * the file is renamed.
115
+ *
116
+ * #### Notes
117
+ * Uses the [Jupyter Notebook API](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/notebook/master/notebook/services/api/api.yaml#!/contents) and validates the response model.
118
+ */
119
+ async rename(oldLocalPath, newLocalPath) {
120
+ return await super.rename(oldLocalPath, newLocalPath);
121
+ }
122
+ /**
123
+ * Save a file.
124
+ *
125
+ * @param localPath - The desired file path.
126
+ *
127
+ * @param options - Optional overrides to the model.
128
+ *
129
+ * @returns A promise which resolves with the file content model when the
130
+ * file is saved.
131
+ *
132
+ * #### Notes
133
+ * Ensure that `model.content` is populated for the file.
134
+ *
135
+ * Uses the [Jupyter Notebook API](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/notebook/master/notebook/services/api/api.yaml#!/contents) and validates the response model.
136
+ */
137
+ async save(localPath, options = {}) {
138
+ // Save is done from the backend
139
+ return this.get(localPath, { ...options, content: false });
140
+ }
141
+ }
142
+ /**
143
+ * Yjs sharedModel factory for real-time collaboration.
144
+ */
145
+ class SharedModelFactory {
146
+ constructor(_onCreate) {
147
+ this._onCreate = _onCreate;
148
+ /**
149
+ * Whether the IDrive supports real-time collaboration or not.
150
+ */
151
+ this.collaborative = true;
152
+ }
153
+ /**
154
+ * Create a new `ISharedDocument` instance.
155
+ *
156
+ * It should return `undefined` if the factory is not able to create a `ISharedDocument`.
157
+ */
158
+ createNew(options) {
159
+ if (typeof options.format !== 'string') {
160
+ console.warn(`Only defined format are supported; got ${options.format}.`);
161
+ return;
162
+ }
163
+ if (!options.collaborative) {
164
+ return;
165
+ }
166
+ let sharedModel;
167
+ switch (options.contentType) {
168
+ case 'file':
169
+ sharedModel = new YFile();
170
+ break;
171
+ case 'notebook':
172
+ sharedModel = new YNotebook();
173
+ break;
174
+ //default:
175
+ // FIXME we should request a registry for the proper sharedModel
176
+ }
177
+ if (sharedModel) {
178
+ this._onCreate(options, sharedModel);
179
+ }
180
+ return sharedModel;
181
+ }
182
+ }
@@ -0,0 +1,82 @@
1
+ import { User } from '@jupyterlab/services';
2
+ import { DocumentChange, YDocument } from '@jupyter/ydoc';
3
+ import { IDisposable } from '@lumino/disposable';
4
+ /**
5
+ * An interface for a document provider.
6
+ */
7
+ export interface IDocumentProvider extends IDisposable {
8
+ /**
9
+ * Returns a Promise that resolves when the document provider is ready.
10
+ */
11
+ readonly ready: Promise<void>;
12
+ }
13
+ /**
14
+ * A class to provide Yjs synchronization over WebSocket.
15
+ *
16
+ * We specify custom messages that the server can interpret. For reference please look in yjs_ws_server.
17
+ *
18
+ */
19
+ export declare class WebSocketProvider implements IDocumentProvider {
20
+ /**
21
+ * Construct a new WebSocketProvider
22
+ *
23
+ * @param options The instantiation options for a WebSocketProvider
24
+ */
25
+ constructor(options: WebSocketProvider.IOptions);
26
+ /**
27
+ * Test whether the object has been disposed.
28
+ */
29
+ get isDisposed(): boolean;
30
+ /**
31
+ * A promise that resolves when the document provider is ready.
32
+ */
33
+ get ready(): Promise<void>;
34
+ /**
35
+ * Dispose of the resources held by the object.
36
+ */
37
+ dispose(): void;
38
+ private _onUserChanged;
39
+ private _awareness;
40
+ private _contentType;
41
+ private _format;
42
+ private _isDisposed;
43
+ private _path;
44
+ private _ready;
45
+ private _serverUrl;
46
+ private _ydoc;
47
+ private _yWebsocketProvider;
48
+ }
49
+ /**
50
+ * A namespace for WebSocketProvider statics.
51
+ */
52
+ export declare namespace WebSocketProvider {
53
+ /**
54
+ * The instantiation options for a WebSocketProvider.
55
+ */
56
+ interface IOptions {
57
+ /**
58
+ * The server URL
59
+ */
60
+ url: string;
61
+ /**
62
+ * The document file path
63
+ */
64
+ path: string;
65
+ /**
66
+ * Content type
67
+ */
68
+ contentType: string;
69
+ /**
70
+ * The source format
71
+ */
72
+ format: string;
73
+ /**
74
+ * The shared model
75
+ */
76
+ model: YDocument<DocumentChange>;
77
+ /**
78
+ * The user data
79
+ */
80
+ user: User.IManager;
81
+ }
82
+ }
@@ -0,0 +1,92 @@
1
+ /* -----------------------------------------------------------------------------
2
+ | Copyright (c) Jupyter Development Team.
3
+ | Distributed under the terms of the Modified BSD License.
4
+ |----------------------------------------------------------------------------*/
5
+ import { URLExt } from '@jupyterlab/coreutils';
6
+ import { ServerConnection } from '@jupyterlab/services';
7
+ import { PromiseDelegate } from '@lumino/coreutils';
8
+ import { Signal } from '@lumino/signaling';
9
+ import { WebsocketProvider as YWebsocketProvider } from 'y-websocket';
10
+ /**
11
+ * Room Id endpoint provided by `jupyter_collaboration`
12
+ * See https://github.com/jupyterlab/jupyter_collaboration
13
+ */
14
+ const FILE_PATH_TO_ROOM_ID_URL = 'api/yjs/roomid';
15
+ /**
16
+ * A class to provide Yjs synchronization over WebSocket.
17
+ *
18
+ * We specify custom messages that the server can interpret. For reference please look in yjs_ws_server.
19
+ *
20
+ */
21
+ export class WebSocketProvider {
22
+ /**
23
+ * Construct a new WebSocketProvider
24
+ *
25
+ * @param options The instantiation options for a WebSocketProvider
26
+ */
27
+ constructor(options) {
28
+ this._ready = new PromiseDelegate();
29
+ this._isDisposed = false;
30
+ this._path = options.path;
31
+ this._contentType = options.contentType;
32
+ this._format = options.format;
33
+ this._serverUrl = options.url;
34
+ this._ydoc = options.model.ydoc;
35
+ this._awareness = options.model.awareness;
36
+ this._yWebsocketProvider = null;
37
+ const user = options.user;
38
+ user.ready
39
+ .then(() => {
40
+ this._onUserChanged(user);
41
+ })
42
+ .catch(e => console.error(e));
43
+ user.userChanged.connect(this._onUserChanged, this);
44
+ const serverSettings = ServerConnection.makeSettings();
45
+ const url = URLExt.join(serverSettings.baseUrl, FILE_PATH_TO_ROOM_ID_URL, encodeURIComponent(this._path));
46
+ const data = {
47
+ method: 'PUT',
48
+ body: JSON.stringify({ format: this._format, type: this._contentType })
49
+ };
50
+ ServerConnection.makeRequest(url, data, serverSettings)
51
+ .then(response => {
52
+ if (response.status !== 200 && response.status !== 201) {
53
+ throw new ServerConnection.ResponseError(response);
54
+ }
55
+ return response.text();
56
+ })
57
+ .then(roomid => {
58
+ this._yWebsocketProvider = new YWebsocketProvider(this._serverUrl, roomid, this._ydoc, {
59
+ awareness: this._awareness
60
+ });
61
+ })
62
+ .then(() => this._ready.resolve())
63
+ .catch(reason => console.warn(reason));
64
+ }
65
+ /**
66
+ * Test whether the object has been disposed.
67
+ */
68
+ get isDisposed() {
69
+ return this._isDisposed;
70
+ }
71
+ /**
72
+ * A promise that resolves when the document provider is ready.
73
+ */
74
+ get ready() {
75
+ return this._ready.promise;
76
+ }
77
+ /**
78
+ * Dispose of the resources held by the object.
79
+ */
80
+ dispose() {
81
+ var _a;
82
+ if (this.isDisposed) {
83
+ return;
84
+ }
85
+ this._isDisposed = true;
86
+ (_a = this._yWebsocketProvider) === null || _a === void 0 ? void 0 : _a.destroy();
87
+ Signal.clearData(this);
88
+ }
89
+ _onUserChanged(user) {
90
+ this._awareness.setLocalStateField('user', user.identity);
91
+ }
92
+ }
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@jupyter/docprovider",
3
+ "version": "1.0.0-alpha.0",
4
+ "description": "JupyterLab - Document Provider",
5
+ "homepage": "https://github.com/jupyterlab/jupyter_collaboration",
6
+ "bugs": {
7
+ "url": "https://github.com/jupyterlab/jupyter_collaboration/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/jupyterlab/jupyter_collaboration.git"
12
+ },
13
+ "license": "BSD-3-Clause",
14
+ "author": "Project Jupyter",
15
+ "sideEffects": [
16
+ "style/**/*"
17
+ ],
18
+ "main": "lib/index.js",
19
+ "types": "lib/index.d.ts",
20
+ "directories": {
21
+ "lib": "lib/"
22
+ },
23
+ "files": [
24
+ "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}",
25
+ "schema/*.json",
26
+ "style/**/*.{css,eot,gif,html,jpg,json,png,svg,woff2,ttf}",
27
+ "style/index.js"
28
+ ],
29
+ "scripts": {
30
+ "build": "tsc -b",
31
+ "build:prod": "jlpm run build",
32
+ "build:test": "tsc --build tsconfig.test.json",
33
+ "clean": "rimraf lib tsconfig.tsbuildinfo",
34
+ "clean:lib": "jlpm run clean:all",
35
+ "clean:all": "rimraf lib tsconfig.tsbuildinfo node_modules",
36
+ "install:extension": "jlpm run build",
37
+ "test": "jest",
38
+ "test:cov": "jest --collect-coverage",
39
+ "test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand",
40
+ "test:debug:watch": "node --inspect-brk node_modules/.bin/jest --runInBand --watch",
41
+ "watch": "tsc -b --watch"
42
+ },
43
+ "dependencies": {
44
+ "@jupyter/ydoc": "^0.3.1",
45
+ "@jupyterlab/coreutils": "^6.0.0-alpha.18",
46
+ "@jupyterlab/services": "^7.0.0-alpha.18",
47
+ "@lumino/coreutils": "^2.0.0-alpha.6",
48
+ "@lumino/disposable": "^2.0.0-alpha.6",
49
+ "@lumino/signaling": "^2.0.0-alpha.6",
50
+ "y-protocols": "^1.0.5",
51
+ "y-websocket": "^1.3.15",
52
+ "yjs": "^13.5.40"
53
+ },
54
+ "devDependencies": {
55
+ "@jupyterlab/testing": "^4.0.0-alpha.18",
56
+ "@types/jest": "^29.2.0",
57
+ "rimraf": "~3.0.0",
58
+ "typescript": "~4.7.3"
59
+ },
60
+ "publishConfig": {
61
+ "access": "public"
62
+ },
63
+ "typedoc": {
64
+ "entryPoint": "./src/index.ts",
65
+ "displayName": "@jupyter/docprovider",
66
+ "tsconfig": "./tsconfig.json"
67
+ }
68
+ }