@jupyter/docprovider 4.1.1 → 4.2.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/ydrive.d.ts +10 -5
- package/lib/ydrive.js +98 -13
- package/package.json +9 -9
package/lib/ydrive.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { TranslationBundle } from '@jupyterlab/translation';
|
|
2
|
-
import { Contents, IContentProvider,
|
|
2
|
+
import { Contents, IContentProvider, ServerConnection, User } from '@jupyterlab/services';
|
|
3
3
|
import { ISignal } from '@lumino/signaling';
|
|
4
4
|
import { IDocumentProvider, ISharedModelFactory } from '@jupyter/collaborative-drive';
|
|
5
5
|
import { Awareness } from 'y-protocols/awareness';
|
|
@@ -11,14 +11,17 @@ export interface IForkProvider {
|
|
|
11
11
|
format: string;
|
|
12
12
|
}
|
|
13
13
|
declare namespace RtcContentProvider {
|
|
14
|
-
interface IOptions
|
|
14
|
+
interface IOptions {
|
|
15
15
|
user: User.IManager;
|
|
16
16
|
trans: TranslationBundle;
|
|
17
17
|
globalAwareness: Awareness | null;
|
|
18
|
+
serverSettings: ServerConnection.ISettings;
|
|
18
19
|
docmanagerSettings: ISettingRegistry.ISettings | null;
|
|
20
|
+
currentDrive: Contents.IDrive;
|
|
21
|
+
fileChanged?: ISignal<Contents.IDrive, Contents.IChangedArgs>;
|
|
19
22
|
}
|
|
20
23
|
}
|
|
21
|
-
export declare class RtcContentProvider
|
|
24
|
+
export declare class RtcContentProvider implements IContentProvider {
|
|
22
25
|
constructor(options: RtcContentProvider.IOptions);
|
|
23
26
|
/**
|
|
24
27
|
* SharedModel factory for the content provider.
|
|
@@ -45,7 +48,7 @@ export declare class RtcContentProvider extends RestContentProvider implements I
|
|
|
45
48
|
* @returns A promise which resolves with the file content model when the
|
|
46
49
|
* file is saved.
|
|
47
50
|
*/
|
|
48
|
-
save(localPath: string, options?: Partial<Contents.IModel>): Promise<Contents.IModel>;
|
|
51
|
+
save(localPath: string, options?: Partial<Contents.IModel> & Contents.IContentProvisionOptions): Promise<Contents.IModel>;
|
|
49
52
|
/**
|
|
50
53
|
* A signal emitted when a file operation takes place.
|
|
51
54
|
*/
|
|
@@ -53,10 +56,12 @@ export declare class RtcContentProvider extends RestContentProvider implements I
|
|
|
53
56
|
private _onCreate;
|
|
54
57
|
private _user;
|
|
55
58
|
private _saveCounter;
|
|
59
|
+
private _currentDrive;
|
|
56
60
|
private _trans;
|
|
57
61
|
private _globalAwareness;
|
|
58
62
|
private _providers;
|
|
59
|
-
private
|
|
63
|
+
private _providerFileChanged;
|
|
64
|
+
private _driveFileChanged?;
|
|
60
65
|
private _serverSettings;
|
|
61
66
|
private _docmanagerSettings;
|
|
62
67
|
}
|
package/lib/ydrive.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// Copyright (c) Jupyter Development Team.
|
|
2
2
|
// Distributed under the terms of the Modified BSD License.
|
|
3
3
|
import { PageConfig, URLExt } from '@jupyterlab/coreutils';
|
|
4
|
-
import { RestContentProvider } from '@jupyterlab/services';
|
|
5
4
|
import { PromiseDelegate } from '@lumino/coreutils';
|
|
6
5
|
import { Signal } from '@lumino/signaling';
|
|
7
6
|
import { WebSocketProvider } from './yprovider';
|
|
@@ -13,11 +12,10 @@ const RAW_MESSAGE_TYPE = 2;
|
|
|
13
12
|
* The url for the default drive service.
|
|
14
13
|
*/
|
|
15
14
|
const DOCUMENT_PROVIDER_URL = 'api/collaboration/room';
|
|
16
|
-
export class RtcContentProvider
|
|
15
|
+
export class RtcContentProvider {
|
|
17
16
|
constructor(options) {
|
|
18
|
-
super(options);
|
|
19
17
|
this._onCreate = (options, sharedModel) => {
|
|
20
|
-
var _a, _b, _c, _d, _e, _f;
|
|
18
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
21
19
|
if (typeof options.format !== 'string') {
|
|
22
20
|
return;
|
|
23
21
|
}
|
|
@@ -47,13 +45,80 @@ export class RtcContentProvider extends RestContentProvider {
|
|
|
47
45
|
documents.push(options.path);
|
|
48
46
|
(_f = this._globalAwareness) === null || _f === void 0 ? void 0 : _f.setLocalStateField('documents', documents);
|
|
49
47
|
}
|
|
50
|
-
|
|
48
|
+
let path = options.path;
|
|
49
|
+
let key = `${options.format}:${options.contentType}:${path}`;
|
|
51
50
|
this._providers.set(key, provider);
|
|
51
|
+
const handlePathChange = (pathChange) => {
|
|
52
|
+
var _a, _b;
|
|
53
|
+
const oldPath = pathChange.oldValue;
|
|
54
|
+
const newPath = pathChange.newValue;
|
|
55
|
+
if (!oldPath || !newPath) {
|
|
56
|
+
// This is expected when shared model initializes and the path is first populated
|
|
57
|
+
console.debug('New or old path not given', pathChange);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const oldKey = `${options.format}:${options.contentType}:${oldPath}`;
|
|
61
|
+
if (oldKey !== key) {
|
|
62
|
+
console.error('The computed old provider key is different from the current key');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const newKey = `${options.format}:${options.contentType}:${newPath}`;
|
|
66
|
+
// Check if the provider is still registered (it may have been disposed if document was closed)
|
|
67
|
+
const provider = this._providers.get(oldKey);
|
|
68
|
+
if (!provider) {
|
|
69
|
+
console.warn(`Could not find a provider to update after rename ${oldKey}, ${newKey}`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// Re-register the provider under the new key
|
|
73
|
+
this._providers.set(newKey, provider);
|
|
74
|
+
this._providers.delete(oldKey);
|
|
75
|
+
// Update the provider key so that it can be disposed correctly when shared document closes
|
|
76
|
+
key = newKey;
|
|
77
|
+
path = newPath;
|
|
78
|
+
// Update the documents field
|
|
79
|
+
const state = ((_a = this._globalAwareness) === null || _a === void 0 ? void 0 : _a.getLocalState()) || {};
|
|
80
|
+
const documents = state.documents || [];
|
|
81
|
+
const oldPathIndex = documents.indexOf(oldPath);
|
|
82
|
+
if (documents.includes(oldPath) && !documents.includes(newPath)) {
|
|
83
|
+
documents.splice(oldPathIndex, 1);
|
|
84
|
+
documents.push(newPath);
|
|
85
|
+
(_b = this._globalAwareness) === null || _b === void 0 ? void 0 : _b.setLocalStateField('documents', documents);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
// The information about file being renamed can come from two places:
|
|
89
|
+
// - from the sharedModel via changed signal with documentChange
|
|
90
|
+
// - from the fileChanged signal of the drive
|
|
91
|
+
// Neither method is foolproof:
|
|
92
|
+
// - the shared model approach can be delayed as the change needs to be
|
|
93
|
+
// reflected by the server and come back, in which case we get a race condition
|
|
94
|
+
// - the fileChanged signal is emitted with a larger delay for renames of collaborators
|
|
95
|
+
// Thus we need both.
|
|
96
|
+
const handleFileChangedSignal = (_, change) => {
|
|
97
|
+
var _a, _b;
|
|
98
|
+
if (change.type !== 'rename') {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const oldPath = (_a = change.oldValue) === null || _a === void 0 ? void 0 : _a.path;
|
|
102
|
+
const newPath = (_b = change.newValue) === null || _b === void 0 ? void 0 : _b.path;
|
|
103
|
+
if (oldPath !== path) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
handlePathChange({
|
|
107
|
+
oldValue: oldPath,
|
|
108
|
+
newValue: newPath,
|
|
109
|
+
name: 'path'
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
(_g = this._driveFileChanged) === null || _g === void 0 ? void 0 : _g.connect(handleFileChangedSignal);
|
|
52
113
|
sharedModel.changed.connect(async (_, change) => {
|
|
53
114
|
var _a;
|
|
54
115
|
if (!change.stateChange) {
|
|
55
116
|
return;
|
|
56
117
|
}
|
|
118
|
+
const pathChanges = change.stateChange.filter(change => change.name === 'path');
|
|
119
|
+
for (const pathChange of pathChanges) {
|
|
120
|
+
handlePathChange(pathChange);
|
|
121
|
+
}
|
|
57
122
|
const hashChanges = change.stateChange.filter(change => change.name === 'hash');
|
|
58
123
|
if (hashChanges.length === 0) {
|
|
59
124
|
return;
|
|
@@ -67,7 +132,7 @@ export class RtcContentProvider extends RestContentProvider {
|
|
|
67
132
|
// observers about this change so that they can store the new hash value.
|
|
68
133
|
const newPath = (_a = sharedModel.state.path) !== null && _a !== void 0 ? _a : options.path;
|
|
69
134
|
const model = await this.get(newPath, { content: false });
|
|
70
|
-
this.
|
|
135
|
+
this._providerFileChanged.emit({
|
|
71
136
|
type: 'save',
|
|
72
137
|
newValue: { ...model, hash: hashChange.newValue },
|
|
73
138
|
// we do not have the old model because it was discarded when server made the change,
|
|
@@ -76,7 +141,7 @@ export class RtcContentProvider extends RestContentProvider {
|
|
|
76
141
|
});
|
|
77
142
|
});
|
|
78
143
|
sharedModel.disposed.connect(() => {
|
|
79
|
-
var _a, _b;
|
|
144
|
+
var _a, _b, _c;
|
|
80
145
|
const provider = this._providers.get(key);
|
|
81
146
|
if (provider) {
|
|
82
147
|
provider.dispose();
|
|
@@ -85,11 +150,13 @@ export class RtcContentProvider extends RestContentProvider {
|
|
|
85
150
|
// Remove the document path from the list of opened ones for this user.
|
|
86
151
|
const state = ((_a = this._globalAwareness) === null || _a === void 0 ? void 0 : _a.getLocalState()) || {};
|
|
87
152
|
const documents = state.documents || [];
|
|
88
|
-
const index = documents.indexOf(
|
|
153
|
+
const index = documents.indexOf(path);
|
|
89
154
|
if (index > -1) {
|
|
90
155
|
documents.splice(index, 1);
|
|
91
156
|
}
|
|
92
157
|
(_b = this._globalAwareness) === null || _b === void 0 ? void 0 : _b.setLocalStateField('documents', documents);
|
|
158
|
+
// Disconnect signal
|
|
159
|
+
(_c = this._driveFileChanged) === null || _c === void 0 ? void 0 : _c.disconnect(handleFileChangedSignal);
|
|
93
160
|
});
|
|
94
161
|
}
|
|
95
162
|
catch (error) {
|
|
@@ -99,14 +166,17 @@ export class RtcContentProvider extends RestContentProvider {
|
|
|
99
166
|
}
|
|
100
167
|
};
|
|
101
168
|
this._saveCounter = 0;
|
|
102
|
-
|
|
169
|
+
// This is for emitting signals to be proxied to `Drive.fileChanged`
|
|
170
|
+
this._providerFileChanged = new Signal(this);
|
|
103
171
|
this._user = options.user;
|
|
104
172
|
this._trans = options.trans;
|
|
105
173
|
this._globalAwareness = options.globalAwareness;
|
|
106
174
|
this._serverSettings = options.serverSettings;
|
|
175
|
+
this._currentDrive = options.currentDrive;
|
|
107
176
|
this.sharedModelFactory = new SharedModelFactory(this._onCreate);
|
|
108
177
|
this._providers = new Map();
|
|
109
178
|
this._docmanagerSettings = options.docmanagerSettings;
|
|
179
|
+
this._driveFileChanged = options.fileChanged;
|
|
110
180
|
}
|
|
111
181
|
get providers() {
|
|
112
182
|
return this._providers;
|
|
@@ -130,7 +200,12 @@ export class RtcContentProvider extends RestContentProvider {
|
|
|
130
200
|
// Use `Promise.all` to reject as soon as possible. The Context will
|
|
131
201
|
// show a dialog to the user.
|
|
132
202
|
const [model] = await Promise.all([
|
|
133
|
-
|
|
203
|
+
// Not calling get() with options.contentProviderId otherwise it's an infinite loop
|
|
204
|
+
this._currentDrive.get(localPath, {
|
|
205
|
+
...options,
|
|
206
|
+
content: false,
|
|
207
|
+
contentProviderId: undefined
|
|
208
|
+
}),
|
|
134
209
|
provider.ready
|
|
135
210
|
]);
|
|
136
211
|
// The server doesn't return a model with a format when content is false,
|
|
@@ -138,7 +213,11 @@ export class RtcContentProvider extends RestContentProvider {
|
|
|
138
213
|
return { ...model, format: options.format };
|
|
139
214
|
}
|
|
140
215
|
}
|
|
141
|
-
|
|
216
|
+
// Not calling get() with options.contentProviderId otherwise it's an infinite loop
|
|
217
|
+
return this._currentDrive.get(localPath, {
|
|
218
|
+
...options,
|
|
219
|
+
contentProviderId: undefined
|
|
220
|
+
});
|
|
142
221
|
}
|
|
143
222
|
/**
|
|
144
223
|
* Save a file.
|
|
@@ -215,14 +294,20 @@ export class RtcContentProvider extends RestContentProvider {
|
|
|
215
294
|
};
|
|
216
295
|
return this.get(localPath, fetchOptions);
|
|
217
296
|
}
|
|
297
|
+
else {
|
|
298
|
+
console.warn(`Could not find a provider for ${localPath}, falling back to REST API save`);
|
|
299
|
+
}
|
|
218
300
|
}
|
|
219
|
-
return
|
|
301
|
+
return this._currentDrive.save(localPath, {
|
|
302
|
+
...options,
|
|
303
|
+
contentProviderId: undefined
|
|
304
|
+
});
|
|
220
305
|
}
|
|
221
306
|
/**
|
|
222
307
|
* A signal emitted when a file operation takes place.
|
|
223
308
|
*/
|
|
224
309
|
get fileChanged() {
|
|
225
|
-
return this.
|
|
310
|
+
return this._providerFileChanged;
|
|
226
311
|
}
|
|
227
312
|
}
|
|
228
313
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jupyter/docprovider",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.2.0-alpha.0",
|
|
4
4
|
"description": "JupyterLab - Document Provider",
|
|
5
5
|
"homepage": "https://github.com/jupyterlab/jupyter-collaboration",
|
|
6
6
|
"bugs": {
|
|
@@ -41,14 +41,14 @@
|
|
|
41
41
|
"watch": "tsc -b --watch"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@jupyter/collaborative-drive": "^4.
|
|
44
|
+
"@jupyter/collaborative-drive": "^4.2.0-alpha.0",
|
|
45
45
|
"@jupyter/ydoc": "^2.1.3 || ^3.0.0",
|
|
46
|
-
"@jupyterlab/apputils": "^4.
|
|
47
|
-
"@jupyterlab/cells": "^4.
|
|
48
|
-
"@jupyterlab/coreutils": "^6.
|
|
49
|
-
"@jupyterlab/notebook": "^4.
|
|
50
|
-
"@jupyterlab/services": "^7.
|
|
51
|
-
"@jupyterlab/translation": "^4.
|
|
46
|
+
"@jupyterlab/apputils": "^4.5.0",
|
|
47
|
+
"@jupyterlab/cells": "^4.5.0",
|
|
48
|
+
"@jupyterlab/coreutils": "^6.5.0",
|
|
49
|
+
"@jupyterlab/notebook": "^4.5.0",
|
|
50
|
+
"@jupyterlab/services": "^7.5.0",
|
|
51
|
+
"@jupyterlab/translation": "^4.5.0",
|
|
52
52
|
"@lumino/coreutils": "^2.2.1",
|
|
53
53
|
"@lumino/disposable": "^2.1.4",
|
|
54
54
|
"@lumino/signaling": "^2.1.4",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"yjs": "^13.5.40"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
|
-
"@jupyterlab/testing": "^4.
|
|
61
|
+
"@jupyterlab/testing": "^4.5.0",
|
|
62
62
|
"@types/jest": "^29.2.0",
|
|
63
63
|
"jest": "^29.5.0",
|
|
64
64
|
"rimraf": "^4.1.2",
|