@@ -54,13 +54,23 @@ export class IPyWidgetMessageDispatcher implements IIPyWidgetMessageDispatcher {
54
54
private totalWaitTime : number = 0 ;
55
55
private totalWaitedMessages : number = 0 ;
56
56
private hookCount : number = 0 ;
57
- private fullHandleMessage ?: { id : string ; promise : Deferred < void > } ;
58
57
/**
59
- * This will be true if user has executed something that has resulted in the use of ipywidgets.
60
- * We make this determinination based on whether we see messages coming from backend kernel of a specific shape.
61
- * E.g. if it contains ipywidget mime type, then widgets are in use.
58
+ * The Output widget's model can set up or tear down a kernel message hook on state change.
59
+ * We need to wait until the kernel message hook has been connected before it's safe to send
60
+ * more messages to the UI kernel.
61
+ *
62
+ * To do this we:
63
+ * - Keep track of the id of all the Output widget models in the outputWidgetIds instance variable.
64
+ * We add/remove these ids by inspecting messages in onKernelSocketMessage.
65
+ * - When a state update message is sent to one of these widgets, we synchronize with the UI and
66
+ * stop sending messages until we receive a reply indicating that the state change has been fully handled.
67
+ * We keep track of the message we're waiting for in the fullHandleMessage instance variable.
68
+ * We start waiting for the state change to finish processing in onKernelSocketMessage,
69
+ * and we stop waiting in iopubMessageHandled.
62
70
*/
63
- private isUsingIPyWidgets ?: boolean ;
71
+ private outputWidgetIds = new Set < string > ( ) ;
72
+ private fullHandleMessage ?: { id : string ; promise : Deferred < void > } ;
73
+ private isUsingIPyWidgets = false ;
64
74
private readonly deserialize : ( data : string | ArrayBuffer ) => KernelMessage . IMessage < KernelMessage . MessageType > ;
65
75
66
76
constructor ( private readonly kernelProvider : IKernelProvider , public readonly document : NotebookDocument ) {
@@ -251,14 +261,12 @@ export class IPyWidgetMessageDispatcher implements IIPyWidgetMessageDispatcher {
251
261
// fully handled on both the UI and extension side before we process the next message incoming
252
262
private messageNeedsFullHandle ( message : any ) {
253
263
// We only get a handled callback for iopub messages, so this channel must be iopub
254
- if ( message . channel === 'iopub' ) {
255
- if ( message . header ?. msg_type === 'comm_msg' ) {
256
- // IOPub comm messages need to be fully handled
257
- return true ;
258
- }
259
- }
260
-
261
- return false ;
264
+ return (
265
+ message . channel === 'iopub' &&
266
+ message . header ?. msg_type === 'comm_msg' &&
267
+ message . content ?. data ?. method === 'update' &&
268
+ this . outputWidgetIds . has ( message . content ?. comm_id )
269
+ ) ;
262
270
}
263
271
264
272
// Callback from the UI kernel when an iopubMessage has been fully handled
@@ -272,46 +280,11 @@ export class IPyWidgetMessageDispatcher implements IIPyWidgetMessageDispatcher {
272
280
}
273
281
private async onKernelSocketMessage ( data : WebSocketData ) : Promise < void > {
274
282
// Hooks expect serialized data as this normally comes from a WebSocket
275
- let message : undefined | KernelMessage . ICommOpenMsg ; // = this.deserialize(data as any) as any;
276
- if ( ! this . isUsingIPyWidgets ) {
277
- // Lets deserialize only if we know we have a potential case
278
- // where this message contains some data we're interested in.
279
- let mustDeserialize = false ;
280
- if ( typeof data === 'string' ) {
281
- mustDeserialize = data . includes ( WIDGET_MIMETYPE ) || data . includes ( Identifiers . DefaultCommTarget ) ;
282
- } else {
283
- // Array buffers (non-plain text data) must be deserialized.
284
- mustDeserialize = true ;
285
- }
286
- if ( ! message && mustDeserialize ) {
287
- message = this . deserialize ( data as any ) as any ;
288
- }
289
-
290
- // Check for hints that would indicate whether ipywidgest are used in outputs.
291
- if (
292
- message &&
293
- message . content &&
294
- message . content . data &&
295
- ( message . content . data [ WIDGET_MIMETYPE ] || message . content . target_name === Identifiers . DefaultCommTarget )
296
- ) {
297
- this . isUsingIPyWidgets = true ;
298
- }
299
- }
300
283
301
284
const msgUuid = uuid ( ) ;
302
285
const promise = createDeferred < void > ( ) ;
303
286
this . waitingMessageIds . set ( msgUuid , { startTime : Date . now ( ) , resultPromise : promise } ) ;
304
287
305
- // Check if we need to fully handle this message on UI and Extension side before we move to the next
306
- if ( this . isUsingIPyWidgets ) {
307
- if ( ! message ) {
308
- message = this . deserialize ( data as any ) as any ;
309
- }
310
- if ( this . messageNeedsFullHandle ( message ) ) {
311
- this . fullHandleMessage = { id : message ! . header . msg_id , promise : createDeferred < void > ( ) } ;
312
- }
313
- }
314
-
315
288
if ( typeof data === 'string' ) {
316
289
this . raisePostMessage ( IPyWidgetMessages . IPyWidgets_msg , { id : msgUuid , data } ) ;
317
290
} else {
@@ -321,21 +294,42 @@ export class IPyWidgetMessageDispatcher implements IIPyWidgetMessageDispatcher {
321
294
} ) ;
322
295
}
323
296
324
- // There are three handling states that we have for messages here
325
- // 1. If we have not detected ipywidget usage at all, we just forward messages to the kernel
326
- // 2. If we have detected ipywidget usage. We wait on our message to be received, but not
327
- // possibly processed yet by the UI kernel. This make sure our ordering is in sync
328
- // 3. For iopub comm messages we wait for them to be fully handled by the UI kernel
329
- // and the Extension kernel as they may be required to do things like
330
- // register message hooks on both sides before we process the nextExtension message
331
-
332
- // If there are no ipywidgets thusfar in the notebook, then no need to synchronize messages.
333
- if ( this . isUsingIPyWidgets ) {
334
- await promise . promise ;
335
-
336
- // Comm specific iopub messages we need to wait until they are full handled
337
- // by both the UI and extension side before we move forward
338
- if ( this . fullHandleMessage ) {
297
+ // Lets deserialize only if we know we have a potential case
298
+ // where this message contains some data we're interested in.
299
+ const mustDeserialize =
300
+ typeof data !== 'string' ||
301
+ data . includes ( WIDGET_MIMETYPE ) ||
302
+ data . includes ( Identifiers . DefaultCommTarget ) ||
303
+ data . includes ( 'comm_open' ) ||
304
+ data . includes ( 'comm_close' ) ||
305
+ data . includes ( 'comm_msg' ) ;
306
+ if ( mustDeserialize ) {
307
+ const message = this . deserialize ( data as any ) as any ;
308
+
309
+ // Check for hints that would indicate whether ipywidgest are used in outputs.
310
+ if (
311
+ message &&
312
+ message . content &&
313
+ message . content . data &&
314
+ ( message . content . data [ WIDGET_MIMETYPE ] || message . content . target_name === Identifiers . DefaultCommTarget )
315
+ ) {
316
+ this . isUsingIPyWidgets = true ;
317
+ }
318
+
319
+ const isIPYWidgetOutputModelOpen =
320
+ message . header ?. msg_type === 'comm_open' &&
321
+ message . content ?. data ?. state ?. _model_module === '@jupyter-widgets/output' &&
322
+ message . content ?. data ?. state ?. _model_name === 'OutputModel' ;
323
+ const isIPYWidgetOutputModelClose =
324
+ message . header ?. msg_type === 'comm_close' && this . outputWidgetIds . has ( message . content ?. comm_id ) ;
325
+
326
+ if ( isIPYWidgetOutputModelOpen ) {
327
+ this . outputWidgetIds . add ( message . content . comm_id ) ;
328
+ } else if ( isIPYWidgetOutputModelClose ) {
329
+ this . outputWidgetIds . delete ( message . content . comm_id ) ;
330
+ } else if ( this . messageNeedsFullHandle ( message ) ) {
331
+ this . fullHandleMessage = { id : message . header . msg_id , promise : createDeferred < void > ( ) } ;
332
+ await promise . promise ;
339
333
await this . fullHandleMessage . promise . promise ;
340
334
this . fullHandleMessage = undefined ;
341
335
}
0 commit comments