|
4 | 4 | import * as services from '@jupyterlab/services';
|
5 | 5 | import * as widgets from '@jupyter-widgets/base';
|
6 | 6 |
|
7 |
| -import { JSONObject, PartialJSONObject } from '@lumino/coreutils'; |
| 7 | +import { |
| 8 | + JSONObject, |
| 9 | + PartialJSONObject, |
| 10 | + PromiseDelegate, |
| 11 | +} from '@lumino/coreutils'; |
8 | 12 |
|
9 | 13 | import {
|
10 | 14 | DOMWidgetView,
|
@@ -32,6 +36,21 @@ import sanitize from 'sanitize-html';
|
32 | 36 |
|
33 | 37 | const PROTOCOL_MAJOR_VERSION = PROTOCOL_VERSION.split('.', 1)[0];
|
34 | 38 |
|
| 39 | +/** |
| 40 | + * The control comm target name. |
| 41 | + */ |
| 42 | +export const CONTROL_COMM_TARGET = 'jupyter.widget.control'; |
| 43 | + |
| 44 | +/** |
| 45 | + * The supported version for the control comm channel. |
| 46 | + */ |
| 47 | +export const CONTROL_COMM_PROTOCOL_VERSION = '1.0.0'; |
| 48 | + |
| 49 | +/** |
| 50 | + * Time (in ms) after which we consider the control comm target not responding. |
| 51 | + */ |
| 52 | +export const CONTROL_COMM_TIMEOUT = 4000; |
| 53 | + |
35 | 54 | /**
|
36 | 55 | * Sanitize HTML-formatted descriptions.
|
37 | 56 | */
|
@@ -342,6 +361,197 @@ export abstract class ManagerBase implements IWidgetManager {
|
342 | 361 | return await modelPromise;
|
343 | 362 | }
|
344 | 363 |
|
| 364 | + /** |
| 365 | + * Fetch all widgets states from the kernel using the control comm channel |
| 366 | + * If this fails (control comm handler not implemented kernel side), |
| 367 | + * it will fallback to `_loadFromKernelSlow`. |
| 368 | + */ |
| 369 | + protected async _loadFromKernel(): Promise<void> { |
| 370 | + // Try fetching all widget states through the control comm |
| 371 | + let data: any; |
| 372 | + let buffers: any; |
| 373 | + try { |
| 374 | + const initComm = await this._create_comm( |
| 375 | + CONTROL_COMM_TARGET, |
| 376 | + uuid(), |
| 377 | + { widgets: null }, |
| 378 | + { version: CONTROL_COMM_PROTOCOL_VERSION } |
| 379 | + ); |
| 380 | + |
| 381 | + await new Promise((resolve, reject) => { |
| 382 | + initComm.on_msg((msg: any) => { |
| 383 | + data = msg['content']['data']; |
| 384 | + |
| 385 | + if (data.method !== 'update_states') { |
| 386 | + console.warn(` |
| 387 | + Unknown ${data.method} message on the Control channel |
| 388 | + `); |
| 389 | + return; |
| 390 | + } |
| 391 | + |
| 392 | + buffers = (msg.buffers || []).map((b: any) => { |
| 393 | + if (b instanceof DataView) { |
| 394 | + return b; |
| 395 | + } else { |
| 396 | + return new DataView(b instanceof ArrayBuffer ? b : b.buffer); |
| 397 | + } |
| 398 | + }); |
| 399 | + |
| 400 | + resolve(null); |
| 401 | + }); |
| 402 | + |
| 403 | + initComm.on_close(() => reject('Control comm was closed too early')); |
| 404 | + |
| 405 | + // Send a states request msg |
| 406 | + initComm.send({ method: 'request_states' }, {}); |
| 407 | + |
| 408 | + // Reject if we didn't get a response in time |
| 409 | + setTimeout( |
| 410 | + () => reject('Control comm did not respond in time'), |
| 411 | + CONTROL_COMM_TIMEOUT |
| 412 | + ); |
| 413 | + }); |
| 414 | + |
| 415 | + initComm.close(); |
| 416 | + } catch (error) { |
| 417 | + console.warn( |
| 418 | + 'Failed to fetch widgets through the "jupyter.widget.control" comm channel, fallback to slow fetching of widgets. Reason:', |
| 419 | + error |
| 420 | + ); |
| 421 | + // Fallback to the old implementation for old ipywidgets backend versions (<=7.6) |
| 422 | + return this._loadFromKernelSlow(); |
| 423 | + } |
| 424 | + |
| 425 | + const states: any = data.states; |
| 426 | + |
| 427 | + // Extract buffer paths |
| 428 | + const bufferPaths: any = {}; |
| 429 | + for (const bufferPath of data.buffer_paths) { |
| 430 | + if (!bufferPaths[bufferPath[0]]) { |
| 431 | + bufferPaths[bufferPath[0]] = []; |
| 432 | + } |
| 433 | + bufferPaths[bufferPath[0]].push(bufferPath.slice(1)); |
| 434 | + } |
| 435 | + |
| 436 | + // Start creating all widgets |
| 437 | + await Promise.all( |
| 438 | + Object.keys(states).map(async (widget_id) => { |
| 439 | + try { |
| 440 | + const state = states[widget_id]; |
| 441 | + const comm = await this._create_comm('jupyter.widget', widget_id); |
| 442 | + |
| 443 | + // Put binary buffers |
| 444 | + if (widget_id in bufferPaths) { |
| 445 | + const nBuffers = bufferPaths[widget_id].length; |
| 446 | + put_buffers( |
| 447 | + state, |
| 448 | + bufferPaths[widget_id], |
| 449 | + buffers.splice(0, nBuffers) |
| 450 | + ); |
| 451 | + } |
| 452 | + |
| 453 | + await this.new_model( |
| 454 | + { |
| 455 | + model_name: state.model_name, |
| 456 | + model_module: state.model_module, |
| 457 | + model_module_version: state.model_module_version, |
| 458 | + model_id: widget_id, |
| 459 | + comm: comm, |
| 460 | + }, |
| 461 | + state.state |
| 462 | + ); |
| 463 | + } catch (error) { |
| 464 | + // Failed to create a widget model, we continue creating other models so that |
| 465 | + // other widgets can render |
| 466 | + console.error(error); |
| 467 | + } |
| 468 | + }) |
| 469 | + ); |
| 470 | + } |
| 471 | + |
| 472 | + /** |
| 473 | + * Old implementation of fetching widgets one by one using |
| 474 | + * the request_state message on each comm. |
| 475 | + */ |
| 476 | + protected async _loadFromKernelSlow(): Promise<void> { |
| 477 | + const comm_ids = await this._get_comm_info(); |
| 478 | + |
| 479 | + // For each comm id that we do not know about, create the comm, and request the state. |
| 480 | + const widgets_info = await Promise.all( |
| 481 | + Object.keys(comm_ids).map(async (comm_id) => { |
| 482 | + try { |
| 483 | + const model = this.get_model(comm_id); |
| 484 | + // TODO Have the same this.get_model implementation for |
| 485 | + // the widgetsnbextension and labextension, the one that |
| 486 | + // throws an error if the model is not found instead of |
| 487 | + // returning undefined |
| 488 | + if (model === undefined) { |
| 489 | + throw new Error('widget model not found'); |
| 490 | + } |
| 491 | + await model; |
| 492 | + // If we successfully get the model, do no more. |
| 493 | + return; |
| 494 | + } catch (e) { |
| 495 | + // If we have the widget model not found error, then we can create the |
| 496 | + // widget. Otherwise, rethrow the error. We have to check the error |
| 497 | + // message text explicitly because the get_model function in this |
| 498 | + // class throws a generic error with this specific text. |
| 499 | + if (e.message !== 'widget model not found') { |
| 500 | + throw e; |
| 501 | + } |
| 502 | + const comm = await this._create_comm(this.comm_target_name, comm_id); |
| 503 | + |
| 504 | + let msg_id = ''; |
| 505 | + const info = new PromiseDelegate<Private.ICommUpdateData>(); |
| 506 | + comm.on_msg((msg: services.KernelMessage.ICommMsgMsg) => { |
| 507 | + if ( |
| 508 | + (msg.parent_header as any).msg_id === msg_id && |
| 509 | + msg.header.msg_type === 'comm_msg' && |
| 510 | + msg.content.data.method === 'update' |
| 511 | + ) { |
| 512 | + const data = msg.content.data as any; |
| 513 | + const buffer_paths = data.buffer_paths || []; |
| 514 | + const buffers = msg.buffers || []; |
| 515 | + put_buffers(data.state, buffer_paths, buffers); |
| 516 | + info.resolve({ comm, msg }); |
| 517 | + } |
| 518 | + }); |
| 519 | + msg_id = comm.send( |
| 520 | + { |
| 521 | + method: 'request_state', |
| 522 | + }, |
| 523 | + this.callbacks(undefined) |
| 524 | + ); |
| 525 | + |
| 526 | + return info.promise; |
| 527 | + } |
| 528 | + }) |
| 529 | + ); |
| 530 | + |
| 531 | + // We put in a synchronization barrier here so that we don't have to |
| 532 | + // topologically sort the restored widgets. `new_model` synchronously |
| 533 | + // registers the widget ids before reconstructing their state |
| 534 | + // asynchronously, so promises to every widget reference should be available |
| 535 | + // by the time they are used. |
| 536 | + await Promise.all( |
| 537 | + widgets_info.map(async (widget_info) => { |
| 538 | + if (!widget_info) { |
| 539 | + return; |
| 540 | + } |
| 541 | + const content = widget_info.msg.content as any; |
| 542 | + await this.new_model( |
| 543 | + { |
| 544 | + model_name: content.data.state._model_name, |
| 545 | + model_module: content.data.state._model_module, |
| 546 | + model_module_version: content.data.state._model_module_version, |
| 547 | + comm: widget_info.comm, |
| 548 | + }, |
| 549 | + content.data.state |
| 550 | + ); |
| 551 | + }) |
| 552 | + ); |
| 553 | + } |
| 554 | + |
345 | 555 | async _make_model(
|
346 | 556 | options: RequiredSome<IModelOptions, 'model_id'>,
|
347 | 557 | serialized_state: any = {}
|
@@ -690,3 +900,13 @@ export function serialize_state(
|
690 | 900 | });
|
691 | 901 | return { version_major: 2, version_minor: 0, state: state };
|
692 | 902 | }
|
| 903 | + |
| 904 | +namespace Private { |
| 905 | + /** |
| 906 | + * Data promised when a comm info request resolves. |
| 907 | + */ |
| 908 | + export interface ICommUpdateData { |
| 909 | + comm: IClassicComm; |
| 910 | + msg: services.KernelMessage.ICommMsgMsg; |
| 911 | + } |
| 912 | +} |
0 commit comments