@jupyter/docprovider 4.2.1 → 4.3.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/tokens.d.ts CHANGED
@@ -101,3 +101,9 @@ export interface IForkManager extends IDisposable {
101
101
  * Token providing a fork manager instance.
102
102
  */
103
103
  export declare const IForkManagerToken: Token<IForkManager>;
104
+ export interface ISessionClosePayload {
105
+ reason: 'unknown_session' | 'version_mismatch' | 'initialization_error';
106
+ sessionId?: string;
107
+ reloadable?: boolean;
108
+ errorReason?: string;
109
+ }
package/lib/ydrive.js CHANGED
@@ -191,6 +191,13 @@ export class RtcContentProvider {
191
191
  const key = `${options.format}:${options.type}:${localPath}`;
192
192
  const provider = this._providers.get(key);
193
193
  if (provider) {
194
+ // The default jupyter-server REST content provider would ask
195
+ // for content here; this would trigger any encoding errors
196
+ // leading to fast-rejection. However we do not ask for
197
+ // content as it is synced over a websocket instead,
198
+ // this means that the errors do not propagate.
199
+ // Instead we handle it by ensuring that the `ready` promise
200
+ // rejection (due to websocket closure ahead of sync) propagates.
194
201
  // If the document doesn't exist, `super.get` will reject with an
195
202
  // error and the provider will never be resolved.
196
203
  // Use `Promise.all` to reject as soon as possible. The Context will
@@ -38,8 +38,10 @@ export declare class WebSocketProvider implements IDocumentProvider, IForkProvid
38
38
  get wsProvider(): YWebsocketProvider | null;
39
39
  private _disconnect;
40
40
  private _onUserChanged;
41
+ private _buildSessionExpiredMessage;
41
42
  private _onConnectionClosed;
42
43
  private _onSync;
44
+ private _getCloseReasonMessage;
43
45
  private _awareness;
44
46
  private _contentType;
45
47
  private _format;
@@ -51,6 +53,7 @@ export declare class WebSocketProvider implements IDocumentProvider, IForkProvid
51
53
  private _yWebsocketProvider;
52
54
  private _serverSettings;
53
55
  private _trans;
56
+ private _hasSynced;
54
57
  }
55
58
  /**
56
59
  * A namespace for WebSocketProvider statics.
package/lib/yprovider.js CHANGED
@@ -2,7 +2,7 @@
2
2
  | Copyright (c) Jupyter Development Team.
3
3
  | Distributed under the terms of the Modified BSD License.
4
4
  |----------------------------------------------------------------------------*/
5
- import { showErrorMessage, Dialog } from '@jupyterlab/apputils';
5
+ import { Dialog, showDialog } from '@jupyterlab/apputils';
6
6
  import { ServerConnection } from '@jupyterlab/services';
7
7
  import { PromiseDelegate } from '@lumino/coreutils';
8
8
  import { Signal } from '@lumino/signaling';
@@ -27,12 +27,46 @@ export class WebSocketProvider {
27
27
  */
28
28
  constructor(options) {
29
29
  var _a;
30
- this._onConnectionClosed = (event) => {
30
+ this._onConnectionClosed = async (event) => {
31
+ if ([4400, 4404, 4500].includes(event.code)) {
32
+ if (!this._hasSynced) {
33
+ // Rejecting the ready promise will close the file placeholder widget.
34
+ const reason = this._getCloseReasonMessage(event.code);
35
+ this._ready.reject(reason);
36
+ // Disposing model prevents repeated websocket reconnection attempts.
37
+ // Rejecting the ready promise will ultimately close the file,
38
+ // but the document manager takes some time to do so.
39
+ this._sharedModel.dispose();
40
+ }
41
+ }
31
42
  if (event.code === 1003) {
32
43
  console.error('Document provider closed:', event.reason);
33
- showErrorMessage(this._trans.__('Document session error'), event.reason, [
34
- Dialog.okButton()
35
- ]);
44
+ let payload;
45
+ try {
46
+ payload = JSON.parse(event.reason);
47
+ }
48
+ catch (_a) {
49
+ payload = {
50
+ reason: 'unknown_session',
51
+ sessionId: '',
52
+ reloadable: false,
53
+ errorReason: event.reason
54
+ };
55
+ }
56
+ const { title, body } = this._buildSessionExpiredMessage(payload, this._trans);
57
+ const result = await showDialog({
58
+ title,
59
+ body,
60
+ buttons: payload.reloadable
61
+ ? [
62
+ Dialog.cancelButton({ label: this._trans.__('Continue') }),
63
+ Dialog.okButton({ label: this._trans.__('Reload') })
64
+ ]
65
+ : [Dialog.okButton({ label: this._trans.__('Ok') })]
66
+ });
67
+ if (result.button.accept && payload.reloadable) {
68
+ window.location.reload();
69
+ }
36
70
  // Dispose shared model immediately. Better break the document model,
37
71
  // than overriding data on disk.
38
72
  this._sharedModel.dispose();
@@ -40,6 +74,7 @@ export class WebSocketProvider {
40
74
  };
41
75
  this._onSync = (isSynced) => {
42
76
  if (isSynced) {
77
+ this._hasSynced = true;
43
78
  if (this._yWebsocketProvider) {
44
79
  this._yWebsocketProvider.off('sync', this._onSync);
45
80
  const state = this._sharedModel.ydoc.getMap('state');
@@ -49,6 +84,7 @@ export class WebSocketProvider {
49
84
  }
50
85
  };
51
86
  this._ready = new PromiseDelegate();
87
+ this._hasSynced = false;
52
88
  this._isDisposed = false;
53
89
  this._path = options.path;
54
90
  this._contentType = options.contentType;
@@ -153,4 +189,39 @@ export class WebSocketProvider {
153
189
  _onUserChanged(user) {
154
190
  this._awareness.setLocalStateField('user', user.identity);
155
191
  }
192
+ _buildSessionExpiredMessage(payload, trans) {
193
+ switch (payload.reason) {
194
+ case 'version_mismatch':
195
+ return {
196
+ title: trans.__('Collaboration extension updated'),
197
+ body: trans.__('Reload the browser tab to load the new version.')
198
+ };
199
+ case 'initialization_error':
200
+ return {
201
+ title: trans.__('Document error'),
202
+ body: trans.__('Failed to initialize the document. Close this tab and reopen the file.')
203
+ };
204
+ case 'unknown_session':
205
+ default:
206
+ return {
207
+ title: trans.__('Session expired'),
208
+ body: payload.errorReason
209
+ ? trans.__(payload.errorReason)
210
+ : trans.__('Reload the browser tab to continue.')
211
+ };
212
+ }
213
+ }
214
+ _getCloseReasonMessage(code) {
215
+ switch (code) {
216
+ case 4400: {
217
+ return this._trans.__('Bad request for %1', this._path);
218
+ }
219
+ case 4404: {
220
+ return this._trans.__('Could not find %1', this._path);
221
+ }
222
+ case 4500: {
223
+ return this._trans.__('Internal server error when loading %1', this._path);
224
+ }
225
+ }
226
+ }
156
227
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jupyter/docprovider",
3
- "version": "4.2.1",
3
+ "version": "4.3.0-alpha.0",
4
4
  "description": "JupyterLab - Document Provider",
5
5
  "homepage": "https://github.com/jupyterlab/jupyter-collaboration",
6
6
  "bugs": {
@@ -41,7 +41,7 @@
41
41
  "watch": "tsc -b --watch"
42
42
  },
43
43
  "dependencies": {
44
- "@jupyter/collaborative-drive": "^4.2.1",
44
+ "@jupyter/collaborative-drive": "^4.3.0-alpha.0",
45
45
  "@jupyter/ydoc": "^2.1.3 || ^3.0.0",
46
46
  "@jupyterlab/apputils": "^4.5.0",
47
47
  "@jupyterlab/cells": "^4.5.0",
@@ -59,6 +59,7 @@
59
59
  },
60
60
  "devDependencies": {
61
61
  "@jupyterlab/testing": "^4.5.0",
62
+ "@jupyterlab/testutils": "^4.5.0",
62
63
  "@types/jest": "^29.2.0",
63
64
  "jest": "^29.5.0",
64
65
  "rimraf": "^4.1.2",