@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 CHANGED
@@ -1,5 +1,5 @@
1
1
  import { TranslationBundle } from '@jupyterlab/translation';
2
- import { Contents, IContentProvider, RestContentProvider, User } from '@jupyterlab/services';
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 extends RestContentProvider.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 extends RestContentProvider implements IContentProvider {
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 _ydriveFileChanged;
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 extends RestContentProvider {
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
- const key = `${options.format}:${options.contentType}:${options.path}`;
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._ydriveFileChanged.emit({
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(options.path);
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
- this._ydriveFileChanged = new Signal(this);
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
- super.get(localPath, { ...options, content: false }),
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
- return super.get(localPath, options);
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 super.save(localPath, options);
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._ydriveFileChanged;
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.1.1",
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.1.1",
44
+ "@jupyter/collaborative-drive": "^4.2.0-alpha.0",
45
45
  "@jupyter/ydoc": "^2.1.3 || ^3.0.0",
46
- "@jupyterlab/apputils": "^4.4.0",
47
- "@jupyterlab/cells": "^4.4.0",
48
- "@jupyterlab/coreutils": "^6.4.0",
49
- "@jupyterlab/notebook": "^4.4.0",
50
- "@jupyterlab/services": "^7.4.0",
51
- "@jupyterlab/translation": "^4.4.0",
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.4.0",
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",