@@ -14,7 +14,12 @@ import {
1414import { PromiseDelegate } from '@lumino/coreutils' ;
1515import { ISignal , Signal } from '@lumino/signaling' ;
1616
17- import { DocumentChange , ISharedDocument , YDocument } from '@jupyter/ydoc' ;
17+ import {
18+ DocumentChange ,
19+ ISharedDocument ,
20+ StateChange ,
21+ YDocument
22+ } from '@jupyter/ydoc' ;
1823
1924import { WebSocketProvider } from './yprovider' ;
2025import {
@@ -49,6 +54,7 @@ namespace RtcContentProvider {
4954 trans : TranslationBundle ;
5055 globalAwareness : Awareness | null ;
5156 docmanagerSettings : ISettingRegistry . ISettings | null ;
57+ fileChanged ?: ISignal < Contents . IDrive , Contents . IChangedArgs > ;
5258 }
5359}
5460
@@ -65,6 +71,7 @@ export class RtcContentProvider
6571 this . sharedModelFactory = new SharedModelFactory ( this . _onCreate ) ;
6672 this . _providers = new Map < string , WebSocketProvider > ( ) ;
6773 this . _docmanagerSettings = options . docmanagerSettings ;
74+ this . _driveFileChanged = options . fileChanged ;
6875 }
6976
7077 /**
@@ -189,6 +196,10 @@ export class RtcContentProvider
189196 content : false
190197 } ;
191198 return this . get ( localPath , fetchOptions ) ;
199+ } else {
200+ console . warn (
201+ `Could not find a provider for ${ localPath } , falling back to REST API save`
202+ ) ;
192203 }
193204 }
194205
@@ -199,7 +210,7 @@ export class RtcContentProvider
199210 * A signal emitted when a file operation takes place.
200211 */
201212 get fileChanged ( ) : ISignal < this, Contents . IChangedArgs > {
202- return this . _ydriveFileChanged ;
213+ return this . _providerFileChanged ;
203214 }
204215
205216 private _onCreate = (
@@ -241,13 +252,99 @@ export class RtcContentProvider
241252 this . _globalAwareness ?. setLocalStateField ( 'documents' , documents ) ;
242253 }
243254
244- const key = `${ options . format } :${ options . contentType } :${ options . path } ` ;
255+ let path = options . path ;
256+ let key = `${ options . format } :${ options . contentType } :${ path } ` ;
245257 this . _providers . set ( key , provider ) ;
246258
259+ const handlePathChange = (
260+ pathChange : StateChange < string | undefined >
261+ ) => {
262+ const oldPath = pathChange . oldValue ;
263+ const newPath = pathChange . newValue ;
264+ if ( ! oldPath || ! newPath ) {
265+ // This is expected when shared model initializes and the path is first populated
266+ console . debug ( 'New or old path not given' , pathChange ) ;
267+ return ;
268+ }
269+
270+ const oldKey = `${ options . format } :${ options . contentType } :${ oldPath } ` ;
271+ if ( oldKey !== key ) {
272+ console . error (
273+ 'The computed old provider key is different from the current key'
274+ ) ;
275+ return ;
276+ }
277+ const newKey = `${ options . format } :${ options . contentType } :${ newPath } ` ;
278+
279+ // Check if the provider is still registered (it may have been disposed if document was closed)
280+ const provider = this . _providers . get ( oldKey ) ;
281+ if ( ! provider ) {
282+ console . warn (
283+ `Could not find a provider to update after rename ${ oldKey } , ${ newKey } `
284+ ) ;
285+ return ;
286+ }
287+
288+ // Re-register the provider under the new key
289+ this . _providers . set ( newKey , provider ) ;
290+ this . _providers . delete ( oldKey ) ;
291+
292+ // Update the provider key so that it can be disposed correctly when shared document closes
293+ key = newKey ;
294+ path = newPath ;
295+
296+ // Update the documents field
297+ const state = this . _globalAwareness ?. getLocalState ( ) || { } ;
298+ const documents : string [ ] = state . documents || [ ] ;
299+ const oldPathIndex = documents . indexOf ( oldPath ) ;
300+ if ( documents . includes ( oldPath ) && ! documents . includes ( newPath ) ) {
301+ documents . splice ( oldPathIndex , 1 ) ;
302+ documents . push ( newPath ) ;
303+ this . _globalAwareness ?. setLocalStateField ( 'documents' , documents ) ;
304+ }
305+ } ;
306+
307+ // The information about file being renamed can come from two places:
308+ // - from the sharedModel via changed signal with documentChange
309+ // - from the fileChanged signal of the drive
310+ // Neither method is foolproof:
311+ // - the shared model approach can be delayed as the change needs to be
312+ // reflected by the server and come back, in which case we get a race condition
313+ // - the fileChanged signal is emitted with a larger delay for renames of collaborators
314+ // Thus we need both.
315+ const handleFileChangedSignal = (
316+ _ : Contents . IDrive ,
317+ change : Contents . IChangedArgs
318+ ) => {
319+ if ( change . type !== 'rename' ) {
320+ return ;
321+ }
322+ const oldPath = change . oldValue ?. path ;
323+ const newPath = change . newValue ?. path ;
324+ if ( oldPath !== path ) {
325+ return ;
326+ }
327+ handlePathChange ( {
328+ oldValue : oldPath ,
329+ newValue : newPath ,
330+ name : 'path'
331+ } ) ;
332+ } ;
333+
334+ this . _driveFileChanged ?. connect ( handleFileChangedSignal ) ;
335+
247336 sharedModel . changed . connect ( async ( _ , change ) => {
248337 if ( ! change . stateChange ) {
249338 return ;
250339 }
340+
341+ const pathChanges = change . stateChange . filter (
342+ change => change . name === 'path'
343+ ) ;
344+ for ( const pathChange of pathChanges ) {
345+ handlePathChange ( pathChange ) ;
346+ }
347+
251348 const hashChanges = change . stateChange . filter (
252349 change => change . name === 'hash'
253350 ) ;
@@ -267,7 +364,7 @@ export class RtcContentProvider
267364 const newPath = sharedModel . state . path ?? options . path ;
268365 const model = await this . get ( newPath as string , { content : false } ) ;
269366
270- this . _ydriveFileChanged . emit ( {
367+ this . _providerFileChanged . emit ( {
271368 type : 'save' ,
272369 newValue : { ...model , hash : hashChange . newValue } ,
273370 // we do not have the old model because it was discarded when server made the change,
@@ -285,12 +382,15 @@ export class RtcContentProvider
285382
286383 // Remove the document path from the list of opened ones for this user.
287384 const state = this . _globalAwareness ?. getLocalState ( ) || { } ;
288- const documents : any [ ] = state . documents || [ ] ;
289- const index = documents . indexOf ( options . path ) ;
385+ const documents : string [ ] = state . documents || [ ] ;
386+ const index = documents . indexOf ( path ) ;
290387 if ( index > - 1 ) {
291388 documents . splice ( index , 1 ) ;
292389 }
293390 this . _globalAwareness ?. setLocalStateField ( 'documents' , documents ) ;
391+
392+ // Disconnect signal
393+ this . _driveFileChanged ?. disconnect ( handleFileChangedSignal ) ;
294394 } ) ;
295395 } catch ( error ) {
296396 // Falling back to the contents API if opening the websocket failed
@@ -306,7 +406,10 @@ export class RtcContentProvider
306406 private _trans : TranslationBundle ;
307407 private _globalAwareness : Awareness | null ;
308408 private _providers : Map < string , WebSocketProvider > ;
309- private _ydriveFileChanged = new Signal < this, Contents . IChangedArgs > ( this ) ;
409+ // This is for emitting signals to be proxied to `Drive.fileChanged`
410+ private _providerFileChanged = new Signal < this, Contents . IChangedArgs > ( this ) ;
411+ // This is for listening to `Drive.fileChanged` signal
412+ private _driveFileChanged ?: ISignal < Contents . IDrive , Contents . IChangedArgs > ;
310413 private _serverSettings : ServerConnection . ISettings ;
311414 private _docmanagerSettings : ISettingRegistry . ISettings | null ;
312415}
0 commit comments