manager.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import { ManagerBase, shims, put_buffers, serialize_state } from '@jupyter-widgets/base';
  4. import { PromiseDelegate } from '@lumino/coreutils';
  5. import { Widget } from '@lumino/widgets';
  6. import { Signal } from '@lumino/signaling';
  7. import { valid } from 'semver';
  8. import { SemVerCache } from './semvercache';
  9. /**
  10. * The mime type for a widget view.
  11. */
  12. export const WIDGET_VIEW_MIMETYPE = 'application/vnd.jupyter.widget-view+json';
  13. /**
  14. * The mime type for widget state data.
  15. */
  16. export const WIDGET_STATE_MIMETYPE = 'application/vnd.jupyter.widget-state+json';
  17. /**
  18. * The class name added to an BackboneViewWrapper widget.
  19. */
  20. const BACKBONEVIEWWRAPPER_CLASS = 'jp-BackboneViewWrapper';
  21. export class BackboneViewWrapper extends Widget {
  22. /**
  23. * Construct a new `Backbone` wrapper widget.
  24. *
  25. * @param view - The `Backbone.View` instance being wrapped.
  26. */
  27. constructor(view) {
  28. super();
  29. this._view = null;
  30. this._view = view;
  31. view.on('remove', () => {
  32. this.dispose();
  33. });
  34. this.addClass(BACKBONEVIEWWRAPPER_CLASS);
  35. this.node.appendChild(view.el);
  36. }
  37. onAfterAttach(msg) {
  38. this._view.trigger('displayed');
  39. }
  40. dispose() {
  41. this._view = null;
  42. super.dispose();
  43. }
  44. }
  45. /**
  46. * A widget manager that returns phosphor widgets.
  47. */
  48. export class WidgetManager extends ManagerBase {
  49. constructor(context, rendermime, settings) {
  50. var _a, _b;
  51. super();
  52. this._registry = new SemVerCache();
  53. this._restored = new Signal(this);
  54. this._restoredStatus = false;
  55. this._initialRestoredStatus = false;
  56. this._modelsSync = new Map();
  57. this._onUnhandledIOPubMessage = new Signal(this);
  58. this._context = context;
  59. this._rendermime = rendermime;
  60. // Set _handleCommOpen so `this` is captured.
  61. this._handleCommOpen = async (comm, msg) => {
  62. let oldComm = new shims.services.Comm(comm);
  63. await this.handle_comm_open(oldComm, msg);
  64. };
  65. context.sessionContext.kernelChanged.connect((sender, args) => {
  66. this._handleKernelChanged(args);
  67. });
  68. context.sessionContext.statusChanged.connect((sender, args) => {
  69. this._handleKernelStatusChange(args);
  70. });
  71. context.sessionContext.connectionStatusChanged.connect((sender, args) => {
  72. this._handleKernelConnectionStatusChange(args);
  73. });
  74. if ((_a = context.sessionContext.session) === null || _a === void 0 ? void 0 : _a.kernel) {
  75. this._handleKernelChanged({ name: 'kernel', oldValue: null, newValue: (_b = context.sessionContext.session) === null || _b === void 0 ? void 0 : _b.kernel });
  76. }
  77. this.restoreWidgets(this._context.model);
  78. this._settings = settings;
  79. context.saveState.connect((sender, saveState) => {
  80. if (saveState === 'started' && settings.saveState) {
  81. this._saveState();
  82. }
  83. });
  84. }
  85. /**
  86. * Save the widget state to the context model.
  87. */
  88. _saveState() {
  89. const state = this.get_state_sync({ drop_defaults: true });
  90. this._context.model.metadata.set('widgets', {
  91. 'application/vnd.jupyter.widget-state+json': state
  92. });
  93. }
  94. /**
  95. * Default callback handler to emit unhandled kernel messages.
  96. */
  97. callbacks(view) {
  98. return {
  99. iopub: {
  100. output: (msg) => {
  101. this._onUnhandledIOPubMessage.emit(msg);
  102. }
  103. }
  104. };
  105. }
  106. /**
  107. * Register a new kernel
  108. */
  109. _handleKernelChanged({ oldValue, newValue }) {
  110. if (oldValue) {
  111. oldValue.removeCommTarget(this.comm_target_name, this._handleCommOpen);
  112. }
  113. if (newValue) {
  114. newValue.registerCommTarget(this.comm_target_name, this._handleCommOpen);
  115. }
  116. }
  117. _handleKernelConnectionStatusChange(status) {
  118. if (status === 'connected') {
  119. // Only restore if our initial restore at construction is finished
  120. if (this._initialRestoredStatus) {
  121. // We only want to restore widgets from the kernel, not ones saved in the notebook.
  122. this.restoreWidgets(this._context.model, { loadKernel: true, loadNotebook: false });
  123. }
  124. }
  125. }
  126. _handleKernelStatusChange(status) {
  127. if (status === 'restarting') {
  128. this.disconnect();
  129. }
  130. }
  131. /**
  132. * Restore widgets from kernel and saved state.
  133. */
  134. async restoreWidgets(notebook, { loadKernel, loadNotebook } = { loadKernel: true, loadNotebook: true }) {
  135. if (loadKernel) {
  136. await this._loadFromKernel();
  137. }
  138. if (loadNotebook) {
  139. await this._loadFromNotebook(notebook);
  140. }
  141. this._restoredStatus = true;
  142. this._initialRestoredStatus = true;
  143. this._restored.emit();
  144. }
  145. /**
  146. * Disconnect the widget manager from the kernel, setting each model's comm
  147. * as dead.
  148. */
  149. disconnect() {
  150. super.disconnect();
  151. this._restoredStatus = false;
  152. }
  153. async _loadFromKernel() {
  154. var _a;
  155. if (!this.context.sessionContext) {
  156. return;
  157. }
  158. await this.context.sessionContext.ready;
  159. // TODO: when we upgrade to @jupyterlab/services 4.1 or later, we can
  160. // remove this 'any' cast.
  161. if (((_a = this.context.sessionContext.session) === null || _a === void 0 ? void 0 : _a.kernel.handleComms) === false) {
  162. return;
  163. }
  164. const comm_ids = await this._get_comm_info();
  165. // For each comm id that we do not know about, create the comm, and request the state.
  166. const widgets_info = await Promise.all(Object.keys(comm_ids).map(async (comm_id) => {
  167. try {
  168. await this.get_model(comm_id);
  169. // If we successfully get the model, do no more.
  170. return;
  171. }
  172. catch (e) {
  173. // If we have the widget model not found error, then we can create the
  174. // widget. Otherwise, rethrow the error. We have to check the error
  175. // message text explicitly because the get_model function in this
  176. // class throws a generic error with this specific text.
  177. if (e.message !== 'widget model not found') {
  178. throw e;
  179. }
  180. const comm = await this._create_comm(this.comm_target_name, comm_id);
  181. let msg_id;
  182. const info = new PromiseDelegate();
  183. comm.on_msg((msg) => {
  184. if (msg.parent_header.msg_id === msg_id
  185. && msg.header.msg_type === 'comm_msg'
  186. && msg.content.data.method === 'update') {
  187. let data = msg.content.data;
  188. let buffer_paths = data.buffer_paths || [];
  189. // Make sure the buffers are DataViews
  190. let buffers = (msg.buffers || []).map(b => {
  191. if (b instanceof DataView) {
  192. return b;
  193. }
  194. else {
  195. return new DataView(b instanceof ArrayBuffer ? b : b.buffer);
  196. }
  197. });
  198. put_buffers(data.state, buffer_paths, buffers);
  199. info.resolve({ comm, msg });
  200. }
  201. });
  202. msg_id = comm.send({
  203. method: 'request_state'
  204. }, this.callbacks(undefined));
  205. return info.promise;
  206. }
  207. }));
  208. // We put in a synchronization barrier here so that we don't have to
  209. // topologically sort the restored widgets. `new_model` synchronously
  210. // registers the widget ids before reconstructing their state
  211. // asynchronously, so promises to every widget reference should be available
  212. // by the time they are used.
  213. await Promise.all(widgets_info.map(async (widget_info) => {
  214. if (!widget_info) {
  215. return;
  216. }
  217. const content = widget_info.msg.content;
  218. await this.new_model({
  219. model_name: content.data.state._model_name,
  220. model_module: content.data.state._model_module,
  221. model_module_version: content.data.state._model_module_version,
  222. comm: widget_info.comm,
  223. }, content.data.state);
  224. }));
  225. }
  226. /**
  227. * Load widget state from notebook metadata
  228. */
  229. async _loadFromNotebook(notebook) {
  230. const widget_md = notebook.metadata.get('widgets');
  231. // Restore any widgets from saved state that are not live
  232. if (widget_md && widget_md[WIDGET_STATE_MIMETYPE]) {
  233. let state = widget_md[WIDGET_STATE_MIMETYPE];
  234. state = this.filterExistingModelState(state);
  235. await this.set_state(state);
  236. }
  237. }
  238. /**
  239. * Return a phosphor widget representing the view
  240. */
  241. async display_view(msg, view, options) {
  242. return view.pWidget || new BackboneViewWrapper(view);
  243. }
  244. /**
  245. * Create a comm.
  246. */
  247. async _create_comm(target_name, model_id, data, metadata, buffers) {
  248. var _a;
  249. let kernel = (_a = this._context.sessionContext.session) === null || _a === void 0 ? void 0 : _a.kernel;
  250. if (!kernel) {
  251. throw new Error('No current kernel');
  252. }
  253. let comm = kernel.createComm(target_name, model_id);
  254. if (data || metadata) {
  255. comm.open(data, metadata, buffers);
  256. }
  257. return new shims.services.Comm(comm);
  258. }
  259. /**
  260. * Get the currently-registered comms.
  261. */
  262. async _get_comm_info() {
  263. var _a;
  264. let kernel = (_a = this._context.sessionContext.session) === null || _a === void 0 ? void 0 : _a.kernel;
  265. if (!kernel) {
  266. throw new Error('No current kernel');
  267. }
  268. const reply = await kernel.requestCommInfo({ target_name: this.comm_target_name });
  269. if (reply.content.status === 'ok') {
  270. return reply.content.comms;
  271. }
  272. else {
  273. return {};
  274. }
  275. }
  276. /**
  277. * Get whether the manager is disposed.
  278. *
  279. * #### Notes
  280. * This is a read-only property.
  281. */
  282. get isDisposed() {
  283. return this._context === null;
  284. }
  285. /**
  286. * Dispose the resources held by the manager.
  287. */
  288. dispose() {
  289. if (this.isDisposed) {
  290. return;
  291. }
  292. if (this._commRegistration) {
  293. this._commRegistration.dispose();
  294. }
  295. this._context = null;
  296. }
  297. /**
  298. * Resolve a URL relative to the current notebook location.
  299. */
  300. async resolveUrl(url) {
  301. const partial = await this.context.urlResolver.resolveUrl(url);
  302. return this.context.urlResolver.getDownloadUrl(partial);
  303. }
  304. /**
  305. * Load a class and return a promise to the loaded object.
  306. */
  307. async loadClass(className, moduleName, moduleVersion) {
  308. // Special-case the Jupyter base and controls packages. If we have just a
  309. // plain version, with no indication of the compatible range, prepend a ^ to
  310. // get all compatible versions. We may eventually apply this logic to all
  311. // widget modules. See issues #2006 and #2017 for more discussion.
  312. if ((moduleName === '@jupyter-widgets/base'
  313. || moduleName === '@jupyter-widgets/controls')
  314. && valid(moduleVersion)) {
  315. moduleVersion = `^${moduleVersion}`;
  316. }
  317. const mod = this._registry.get(moduleName, moduleVersion);
  318. if (!mod) {
  319. throw new Error(`Module ${moduleName}, semver range ${moduleVersion} is not registered as a widget module`);
  320. }
  321. let module;
  322. if (typeof mod === 'function') {
  323. module = await mod();
  324. }
  325. else {
  326. module = await mod;
  327. }
  328. const cls = module[className];
  329. if (!cls) {
  330. throw new Error(`Class ${className} not found in module ${moduleName}`);
  331. }
  332. return cls;
  333. }
  334. get context() {
  335. return this._context;
  336. }
  337. get rendermime() {
  338. return this._rendermime;
  339. }
  340. /**
  341. * A signal emitted when state is restored to the widget manager.
  342. *
  343. * #### Notes
  344. * This indicates that previously-unavailable widget models might be available now.
  345. */
  346. get restored() {
  347. return this._restored;
  348. }
  349. /**
  350. * Whether the state has been restored yet or not.
  351. */
  352. get restoredStatus() {
  353. return this._restoredStatus;
  354. }
  355. /**
  356. * A signal emitted for unhandled iopub kernel messages.
  357. *
  358. */
  359. get onUnhandledIOPubMessage() {
  360. return this._onUnhandledIOPubMessage;
  361. }
  362. register(data) {
  363. this._registry.set(data.name, data.version, data.exports);
  364. }
  365. /**
  366. * Get a model
  367. *
  368. * #### Notes
  369. * Unlike super.get_model(), this implementation always returns a promise and
  370. * never returns undefined. The promise will reject if the model is not found.
  371. */
  372. async get_model(model_id) {
  373. const modelPromise = super.get_model(model_id);
  374. if (modelPromise === undefined) {
  375. throw new Error('widget model not found');
  376. }
  377. return modelPromise;
  378. }
  379. /**
  380. * Register a widget model.
  381. */
  382. register_model(model_id, modelPromise) {
  383. super.register_model(model_id, modelPromise);
  384. // Update the synchronous model map
  385. modelPromise.then(model => {
  386. this._modelsSync.set(model_id, model);
  387. model.once('comm:close', () => {
  388. this._modelsSync.delete(model_id);
  389. });
  390. });
  391. this.setDirty();
  392. }
  393. /**
  394. * Close all widgets and empty the widget state.
  395. * @return Promise that resolves when the widget state is cleared.
  396. */
  397. async clear_state() {
  398. await super.clear_state();
  399. this._modelsSync = new Map();
  400. this.setDirty();
  401. }
  402. /**
  403. * Synchronously get the state of the live widgets in the widget manager.
  404. *
  405. * This includes all of the live widget models, and follows the format given in
  406. * the @jupyter-widgets/schema package.
  407. *
  408. * @param options - The options for what state to return.
  409. * @returns Promise for a state dictionary
  410. */
  411. get_state_sync(options = {}) {
  412. const models = [];
  413. for (let model of this._modelsSync.values()) {
  414. if (model.comm_live) {
  415. models.push(model);
  416. }
  417. }
  418. return serialize_state(models, options);
  419. }
  420. /**
  421. * Set the dirty state of the notebook model if applicable.
  422. *
  423. * TODO: perhaps should also set dirty when any model changes any data
  424. */
  425. setDirty() {
  426. if (this._settings.saveState) {
  427. this._context.model.dirty = true;
  428. }
  429. }
  430. }