Skip to content

Commit a2ec50e

Browse files
committed
Widgetsnbextension: Use control comm channel
1 parent 22e3ce7 commit a2ec50e

File tree

1 file changed

+184
-62
lines changed

1 file changed

+184
-62
lines changed

python/widgetsnbextension/src/manager.js

Lines changed: 184 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ var embedWidgets = require('./embed_widgets');
1111

1212
var MIME_TYPE = 'application/vnd.jupyter.widget-view+json';
1313

14+
var CONTROL_COMM_TARGET = 'jupyter.widget.control';
15+
var CONTROL_COMM_VERSION = '1.0.0';
16+
1417
function polyfill_new_comm_buffers(
1518
manager,
1619
target_name,
@@ -99,9 +102,189 @@ export class WidgetManager extends ManagerBase {
99102
this.handle_comm_open.bind(this)
100103
);
101104

105+
var that = this;
106+
107+
this._loadFromKernel()
108+
.then(function () {
109+
// Now that we have mirrored any widgets from the kernel...
110+
// Restore any widgets from saved state that are not live (3)
111+
if (
112+
widget_md &&
113+
widget_md['application/vnd.jupyter.widget-state+json']
114+
) {
115+
var state =
116+
notebook.metadata.widgets[
117+
'application/vnd.jupyter.widget-state+json'
118+
];
119+
state = that.filterExistingModelState(state);
120+
return that.set_state(state);
121+
}
122+
})
123+
.then(function () {
124+
// Rerender cells that have widget data
125+
that.notebook.get_cells().forEach(function (cell) {
126+
var rerender =
127+
cell.output_area &&
128+
cell.output_area.outputs.find(function (output) {
129+
return output.data && output.data[MIME_TYPE];
130+
});
131+
if (rerender) {
132+
that.notebook.render_cell_output(cell);
133+
}
134+
});
135+
});
136+
137+
// Create the actions and menu
138+
this._init_actions();
139+
this._init_menu();
140+
}
141+
142+
loadClass(className, moduleName, moduleVersion) {
143+
const failure = () => {
144+
throw new Error(
145+
'Class ' + className + ' not found in module ' + moduleName
146+
);
147+
};
148+
if (moduleName === '@jupyter-widgets/controls') {
149+
return widgets[className]
150+
? Promise.resolve(widgets[className])
151+
: failure();
152+
} else if (moduleName === '@jupyter-widgets/base') {
153+
return base[className] ? Promise.resolve(base[className]) : failure();
154+
} else if (moduleName == '@jupyter-widgets/output') {
155+
return outputWidgets[className]
156+
? Promise.resolve(outputWidgets[className])
157+
: failure();
158+
} else {
159+
return new Promise(function (resolve, reject) {
160+
window.require([moduleName], resolve, reject);
161+
}).then(function (mod) {
162+
if (mod[className]) {
163+
return mod[className];
164+
} else {
165+
return failure();
166+
}
167+
});
168+
}
169+
}
170+
171+
/**
172+
* Fetch all widgets states using the control comm channel, or fallback to `_loadFromKernelSlow`
173+
* if the backend does not implement the control comm.
174+
*/
175+
_loadFromKernel() {
176+
var that = this;
177+
178+
const commId = base.uuid();
179+
return this._create_comm(
180+
CONTROL_COMM_TARGET,
181+
commId,
182+
{ widgets: null },
183+
{ version: CONTROL_COMM_VERSION }
184+
).then(function (initComm) {
185+
// Try fetching all widget states through the control comm
186+
let data;
187+
let buffers;
188+
try {
189+
var controlCommPromise = new Promise(function (resolve, reject) {
190+
initComm.on_msg(function (msg) {
191+
data = msg['content']['data'];
192+
193+
if (data.method !== 'update_states') {
194+
console.warn(`
195+
Unknown ${data.method} message on the Control channel
196+
`);
197+
return;
198+
}
199+
200+
buffers = (msg.buffers || []).map(function (b) {
201+
if (b instanceof DataView) {
202+
return b;
203+
} else {
204+
return new DataView(b instanceof ArrayBuffer ? b : b.buffer);
205+
}
206+
});
207+
208+
resolve(null);
209+
});
210+
211+
initComm.on_close(reject);
212+
213+
// Send a states request msg
214+
initComm.send({ method: 'request_states' }, {});
215+
});
216+
217+
return controlCommPromise.then(function () {
218+
initComm.close();
219+
220+
const states = data.states;
221+
222+
// Extract buffer paths
223+
const bufferPaths = {};
224+
for (const bufferPath of data.buffer_paths) {
225+
if (!bufferPaths[bufferPath[0]]) {
226+
bufferPaths[bufferPath[0]] = [];
227+
}
228+
bufferPaths[bufferPath[0]].push(bufferPath.slice(1));
229+
}
230+
231+
// Start creating all widgets
232+
return Promise.all(
233+
Object.keys(states).map(function (widget_id) {
234+
const state = states[widget_id];
235+
236+
try {
237+
return that
238+
._create_comm('jupyter.widget', widget_id)
239+
.then(function (comm) {
240+
// Put binary buffers
241+
if (widget_id in bufferPaths) {
242+
const nBuffers = bufferPaths[widget_id].length;
243+
base.put_buffers(
244+
state,
245+
bufferPaths[widget_id],
246+
buffers.splice(0, nBuffers)
247+
);
248+
}
249+
250+
return that.new_model(
251+
{
252+
model_name: state.model_name,
253+
model_module: state.model_module,
254+
model_module_version: state.model_module_version,
255+
model_id: widget_id,
256+
comm: comm,
257+
},
258+
state.state
259+
);
260+
});
261+
} catch (error) {
262+
// Failed to create a widget model, we continue creating other models so that
263+
// other widgets can render
264+
console.error(error);
265+
}
266+
})
267+
);
268+
});
269+
} catch (error) {
270+
console.warn(
271+
'Failed to open "jupyter.widget.control" comm channel, fallback to slow fetching of widgets.',
272+
error
273+
);
274+
// Fallback to the old implementation for old ipywidgets backend versions (<=7.6)
275+
return this._loadFromKernelSlow();
276+
}
277+
});
278+
}
279+
280+
/**
281+
* Old implementation of fetching widgets one by one using
282+
* the request_state message on each comm.
283+
*/
284+
_loadFromKernelSlow() {
102285
// Attempt to reconstruct any live comms by requesting them from the back-end (2).
103286
var that = this;
104-
this._get_comm_info().then(function (comm_ids) {
287+
return this._get_comm_info().then(function (comm_ids) {
105288
// Create comm class instances from comm ids (2).
106289
var comm_promises = Object.keys(comm_ids).map(function (comm_id) {
107290
return that._create_comm(that.comm_target_name, comm_id);
@@ -156,69 +339,8 @@ export class WidgetManager extends ManagerBase {
156339
);
157340
})
158341
);
159-
})
160-
.then(function () {
161-
// Now that we have mirrored any widgets from the kernel...
162-
// Restore any widgets from saved state that are not live (3)
163-
if (
164-
widget_md &&
165-
widget_md['application/vnd.jupyter.widget-state+json']
166-
) {
167-
var state =
168-
notebook.metadata.widgets[
169-
'application/vnd.jupyter.widget-state+json'
170-
];
171-
state = that.filterExistingModelState(state);
172-
return that.set_state(state);
173-
}
174-
})
175-
.then(function () {
176-
// Rerender cells that have widget data
177-
that.notebook.get_cells().forEach(function (cell) {
178-
var rerender =
179-
cell.output_area &&
180-
cell.output_area.outputs.find(function (output) {
181-
return output.data && output.data[MIME_TYPE];
182-
});
183-
if (rerender) {
184-
that.notebook.render_cell_output(cell);
185-
}
186-
});
187342
});
188343
});
189-
190-
// Create the actions and menu
191-
this._init_actions();
192-
this._init_menu();
193-
}
194-
195-
loadClass(className, moduleName, moduleVersion) {
196-
const failure = () => {
197-
throw new Error(
198-
'Class ' + className + ' not found in module ' + moduleName
199-
);
200-
};
201-
if (moduleName === '@jupyter-widgets/controls') {
202-
return widgets[className]
203-
? Promise.resolve(widgets[className])
204-
: failure();
205-
} else if (moduleName === '@jupyter-widgets/base') {
206-
return base[className] ? Promise.resolve(base[className]) : failure();
207-
} else if (moduleName == '@jupyter-widgets/output') {
208-
return outputWidgets[className]
209-
? Promise.resolve(outputWidgets[className])
210-
: failure();
211-
} else {
212-
return new Promise(function (resolve, reject) {
213-
window.require([moduleName], resolve, reject);
214-
}).then(function (mod) {
215-
if (mod[className]) {
216-
return mod[className];
217-
} else {
218-
return failure();
219-
}
220-
});
221-
}
222344
}
223345

224346
/**

0 commit comments

Comments
 (0)