4
4
import * as utils from './utils' ;
5
5
import * as services from '@jupyterlab/services' ;
6
6
7
+ import {
8
+ PromiseDelegate ,
9
+ } from '@lumino/coreutils' ;
10
+
7
11
import {
8
12
DOMWidgetView , WidgetModel , WidgetView , DOMWidgetModel
9
13
} from './widget' ;
@@ -18,6 +22,21 @@ import {
18
22
19
23
const PROTOCOL_MAJOR_VERSION = PROTOCOL_VERSION . split ( '.' , 1 ) [ 0 ] ;
20
24
25
+ /**
26
+ * The control comm target name.
27
+ */
28
+ export const CONTROL_COMM_TARGET = 'jupyter.widget.control' ;
29
+
30
+ /**
31
+ * The supported version for the control comm channel.
32
+ */
33
+ export const CONTROL_COMM_PROTOCOL_VERSION = '1.0.0' ;
34
+
35
+ /**
36
+ * Time (in ms) after which we consider the control comm target not responding.
37
+ */
38
+ export const CONTROL_COMM_TIMEOUT = 4000 ;
39
+
21
40
/**
22
41
* The options for a model.
23
42
*
@@ -361,7 +380,236 @@ abstract class ManagerBase<T> {
361
380
widget_model . name = options . model_name ;
362
381
widget_model . module = options . model_module ;
363
382
return widget_model ;
383
+ }
384
+
385
+ /**
386
+ * Fetch all widgets states from the kernel using the control comm channel
387
+ * If this fails (control comm handler not implemented kernel side),
388
+ * it will fall back to `_loadFromKernelModels`.
389
+ *
390
+ * This is a utility function that can be used in subclasses.
391
+ */
392
+ protected async _loadFromKernel ( ) : Promise < void > {
393
+ // Try fetching all widget states through the control comm
394
+ let data : any ;
395
+ let buffers : any ;
396
+ try {
397
+ const initComm = await this . _create_comm (
398
+ CONTROL_COMM_TARGET ,
399
+ utils . uuid ( ) ,
400
+ { } ,
401
+ { version : CONTROL_COMM_PROTOCOL_VERSION }
402
+ ) ;
403
+
404
+ await new Promise ( ( resolve , reject ) => {
405
+ initComm . on_msg ( ( msg : any ) => {
406
+ data = msg [ 'content' ] [ 'data' ] ;
407
+
408
+ if ( data . method !== 'update_states' ) {
409
+ console . warn ( `
410
+ Unknown ${ data . method } message on the Control channel
411
+ ` ) ;
412
+ return ;
413
+ }
414
+
415
+ buffers = ( msg . buffers || [ ] ) . map ( ( b : any ) => {
416
+ if ( b instanceof DataView ) {
417
+ return b ;
418
+ } else {
419
+ return new DataView ( b instanceof ArrayBuffer ? b : b . buffer ) ;
420
+ }
421
+ } ) ;
422
+
423
+ resolve ( null ) ;
424
+ } ) ;
425
+
426
+ initComm . on_close ( ( ) => reject ( 'Control comm was closed too early' ) ) ;
427
+
428
+ // Send a states request msg
429
+ initComm . send ( { method : 'request_states' } , { } ) ;
430
+
431
+ // Reject if we didn't get a response in time
432
+ setTimeout (
433
+ ( ) => reject ( 'Control comm did not respond in time' ) ,
434
+ CONTROL_COMM_TIMEOUT
435
+ ) ;
436
+ } ) ;
437
+
438
+ initComm . close ( ) ;
439
+ } catch ( error ) {
440
+ console . warn (
441
+ 'Failed to fetch ipywidgets through the "jupyter.widget.control" comm channel, fallback to fetching individual model state. Reason:' ,
442
+ error
443
+ ) ;
444
+ // Fall back to the old implementation for old ipywidgets backend versions (ipywidgets<=7.6)
445
+ return this . _loadFromKernelModels ( ) ;
446
+ }
447
+
448
+ const states : any = data . states ;
449
+ const bufferPaths : any = { } ;
450
+ const bufferGroups : any = { } ;
451
+
452
+ // Group buffers and buffer paths by widget id
453
+ for ( let i = 0 ; i < data . buffer_paths . length ; i ++ ) {
454
+ const [ widget_id , ...path ] = data . buffer_paths [ i ] ;
455
+ const b = buffers [ i ] ;
456
+ if ( ! bufferPaths [ widget_id ] ) {
457
+ bufferPaths [ widget_id ] = [ ] ;
458
+ bufferGroups [ widget_id ] = [ ] ;
459
+ }
460
+ bufferPaths [ widget_id ] . push ( path ) ;
461
+ bufferGroups [ widget_id ] . push ( b ) ;
462
+ }
463
+
464
+ // Create comms for all new widgets.
465
+ let widget_comms = await Promise . all (
466
+ Object . keys ( states ) . map ( async ( widget_id ) => {
467
+ let comm = undefined ;
468
+ let modelPromise = undefined ;
469
+ try {
470
+ modelPromise = this . get_model ( widget_id ) ;
471
+ if ( modelPromise === undefined ) {
472
+ comm = await this . _create_comm ( 'jupyter.widget' , widget_id ) ;
473
+ } else {
474
+ // For JLab, the promise is rejected, so we have to await to
475
+ // find out if it is actually a model.
476
+ await modelPromise ;
477
+ }
478
+ } catch ( e ) {
479
+ // The JLab widget manager will throw an error with this specific error message.
480
+ if ( e . message !== 'widget model not found' ) {
481
+ throw e ;
482
+ }
483
+ comm = await this . _create_comm ( 'jupyter.widget' , widget_id ) ;
484
+ }
485
+ return { widget_id, comm}
486
+ } )
487
+ )
488
+
489
+ await Promise . all ( widget_comms . map ( async ( { widget_id, comm} ) => {
490
+ const state = states [ widget_id ] ;
491
+ // Put binary buffers
492
+ if ( widget_id in bufferPaths ) {
493
+ utils . put_buffers (
494
+ state ,
495
+ bufferPaths [ widget_id ] ,
496
+ bufferGroups [ widget_id ]
497
+ ) ;
498
+ }
499
+ try {
364
500
501
+ if ( comm === undefined ) {
502
+ // model already exists here
503
+ const model = await this . get_model ( widget_id ) ;
504
+ model ! . set_state ( state . state ) ;
505
+ } else {
506
+ // This must be the first await in the code path that
507
+ // reaches here so that registering the model promise in
508
+ // new_model can register the widget promise before it may
509
+ // be required by other widgets.
510
+ await this . new_model (
511
+ {
512
+ model_name : state . model_name ,
513
+ model_module : state . model_module ,
514
+ model_module_version : state . model_module_version ,
515
+ model_id : widget_id ,
516
+ comm : comm ,
517
+ } ,
518
+ state . state
519
+ ) ;
520
+ }
521
+
522
+ } catch ( error ) {
523
+ // Failed to create a widget model, we continue creating other models so that
524
+ // other widgets can render
525
+ console . error ( error ) ;
526
+ }
527
+ } ) ) ;
528
+ }
529
+
530
+ /**
531
+ * Old implementation of fetching widget models one by one using
532
+ * the request_state message on each comm.
533
+ *
534
+ * This is a utility function that can be used in subclasses.
535
+ */
536
+ protected async _loadFromKernelModels ( ) : Promise < void > {
537
+ const comm_ids = await this . _get_comm_info ( ) ;
538
+
539
+ // For each comm id that we do not know about, create the comm, and request the state.
540
+ const widgets_info = await Promise . all (
541
+ Object . keys ( comm_ids ) . map ( async ( comm_id ) => {
542
+ try {
543
+ const model = this . get_model ( comm_id ) ;
544
+ // TODO Have the same this.get_model implementation for
545
+ // the widgetsnbextension and labextension, the one that
546
+ // throws an error if the model is not found instead of
547
+ // returning undefined
548
+ if ( model === undefined ) {
549
+ throw new Error ( 'widget model not found' ) ;
550
+ }
551
+ await model ;
552
+ // If we successfully get the model, do no more.
553
+ return ;
554
+ } catch ( e ) {
555
+ // If we have the widget model not found error, then we can create the
556
+ // widget. Otherwise, rethrow the error. We have to check the error
557
+ // message text explicitly because the get_model function in this
558
+ // class throws a generic error with this specific text.
559
+ if ( e . message !== 'widget model not found' ) {
560
+ throw e ;
561
+ }
562
+ const comm = await this . _create_comm ( this . comm_target_name , comm_id ) ;
563
+
564
+ let msg_id = '' ;
565
+ const info = new PromiseDelegate < Private . ICommUpdateData > ( ) ;
566
+ comm . on_msg ( ( msg ) => {
567
+ if (
568
+ ( msg . parent_header as any ) . msg_id === msg_id &&
569
+ msg . header . msg_type === 'comm_msg' &&
570
+ msg . content . data . method === 'update'
571
+ ) {
572
+ const data = msg . content . data as any ;
573
+ const buffer_paths = data . buffer_paths || [ ] ;
574
+ const buffers = msg . buffers || [ ] ;
575
+ utils . put_buffers ( data . state , buffer_paths , buffers ) ;
576
+ info . resolve ( { comm, msg } ) ;
577
+ }
578
+ } ) ;
579
+ msg_id = comm . send (
580
+ {
581
+ method : 'request_state' ,
582
+ } ,
583
+ this . callbacks ( undefined )
584
+ ) ;
585
+
586
+ return info . promise ;
587
+ }
588
+ } )
589
+ ) ;
590
+
591
+ // We put in a synchronization barrier here so that we don't have to
592
+ // topologically sort the restored widgets. `new_model` synchronously
593
+ // registers the widget ids before reconstructing their state
594
+ // asynchronously, so promises to every widget reference should be available
595
+ // by the time they are used.
596
+ await Promise . all (
597
+ widgets_info . map ( async ( widget_info ) => {
598
+ if ( ! widget_info ) {
599
+ return ;
600
+ }
601
+ const content = widget_info . msg . content as any ;
602
+ await this . new_model (
603
+ {
604
+ model_name : content . data . state . _model_name ,
605
+ model_module : content . data . state . _model_module ,
606
+ model_module_version : content . data . state . _model_module_version ,
607
+ comm : widget_info . comm ,
608
+ } ,
609
+ content . data . state
610
+ ) ;
611
+ } )
612
+ ) ;
365
613
}
366
614
367
615
/**
@@ -586,3 +834,13 @@ function serialize_state(models: WidgetModel[], options: IStateOptions = {}) {
586
834
} ) ;
587
835
return { version_major : 2 , version_minor : 0 , state : state } ;
588
836
}
837
+
838
+ namespace Private {
839
+ /**
840
+ * Data promised when a comm info request resolves.
841
+ */
842
+ export interface ICommUpdateData {
843
+ comm : IClassicComm ;
844
+ msg : services . KernelMessage . ICommMsgMsg ;
845
+ }
846
+ }
0 commit comments