UNPKG

@jupyterlab/application

Version:
554 lines 19.9 kB
/* ----------------------------------------------------------------------------- | Copyright (c) Jupyter Development Team. | Distributed under the terms of the Modified BSD License. |----------------------------------------------------------------------------*/ import { JSONExt, PromiseDelegate, Token } from '@lumino/coreutils'; import { AttachedProperty } from '@lumino/properties'; /** * The layout restorer token. */ export const ILayoutRestorer = new Token('@jupyterlab/application:ILayoutRestorer', 'A service providing application layout restoration functionality. Use this to have your activities restored across page loads.'); /** * The data connector key for restorer data. */ const KEY = 'layout-restorer:data'; /** * The default implementation of a layout restorer. * * #### Notes * The lifecycle for state restoration is subtle. The sequence of events is: * * 1. The layout restorer plugin is instantiated and makes a `fetch` call to * the data connector that stores the layout restoration data. The `fetch` * call returns a promise that resolves in step 6, below. * * 2. Other plugins that care about state restoration require the layout * restorer as a dependency. * * 3. As each load-time plugin initializes (which happens before the front-end * application has `started`), it instructs the layout restorer whether * the restorer ought to `restore` its widgets by passing in its widget * tracker. * Alternatively, a plugin that does not require its own widget tracker * (because perhaps it only creates a single widget, like a command palette), * can simply `add` its widget along with a persistent unique name to the * layout restorer so that its layout state can be restored when the lab * application restores. * * 4. After all the load-time plugins have finished initializing, the front-end * application `started` promise will resolve. This is the `first` * promise that the layout restorer waits for. By this point, all of the * plugins that care about restoration will have instructed the layout * restorer to `restore` their widget trackers. * * 5. The layout restorer will then instruct each plugin's widget tracker * to restore its state and reinstantiate whichever widgets it wants. The * tracker returns a promise to the layout restorer that resolves when it * has completed restoring the tracked widgets it cares about. * * 6. As each widget tracker finishes restoring the widget instances it cares * about, it resolves the promise that was returned to the layout restorer * (in step 5). After all of the promises that the restorer is awaiting have * settled, the restorer then resolves the outstanding `fetch` promise * (from step 1) and hands off a layout state object to the application * shell's `restoreLayout` method for restoration. * * 7. Once the application shell has finished restoring the layout, the * JupyterLab application's `restored` promise is resolved. * * Of particular note are steps 5 and 6: since data restoration of plugins * is accomplished by executing commands, the command that is used to restore * the data of each plugin must return a promise that only resolves when the * widget has been created and added to the plugin's widget tracker. */ export class LayoutRestorer { /** * Create a layout restorer. */ constructor(options) { this._deferred = new Array(); this._deferredMainArea = null; this._firstDone = false; this._promisesDone = false; this._promises = []; this._restored = new PromiseDelegate(); this._trackers = new Set(); this._widgets = new Map(); this._mode = 'multiple-document'; this._connector = options.connector; this._first = options.first; this._registry = options.registry; if (options.mode) { this._mode = options.mode; } void this._first .then(() => { this._firstDone = true; }) .then(() => Promise.all(this._promises)) .then(() => { this._promisesDone = true; // Release the tracker set. this._trackers.clear(); }) .then(() => { this._restored.resolve(void 0); }); } /** * Whether full layout restoration is deferred and is currently incomplete. * * #### Notes * This flag is useful for tracking when the application has started in * 'single-document' mode and the main area has not yet been restored. */ get isDeferred() { return this._deferred.length > 0; } /** * A promise resolved when the layout restorer is ready to receive signals. */ get restored() { return this._restored.promise; } /** * Add a widget to be tracked by the layout restorer. */ add(widget, name) { Private.nameProperty.set(widget, name); this._widgets.set(name, widget); widget.disposed.connect(this._onWidgetDisposed, this); } /** * Fetch the layout state for the application. * * #### Notes * Fetching the layout relies on all widget restoration to be complete, so * calls to `fetch` are guaranteed to return after restoration is complete. */ async fetch() { var _a; const blank = { fresh: true, mainArea: null, downArea: null, leftArea: null, rightArea: null, topArea: null, relativeSizes: null }; const layout = this._connector.fetch(KEY); try { const [data] = await Promise.all([layout, this.restored]); if (!data) { return blank; } const { main, down, left, right, relativeSizes, top } = data; // If any data exists, then this is not a fresh session. const fresh = false; // Rehydrate main area. let mainArea = null; if (this._mode === 'multiple-document') { mainArea = this._rehydrateMainArea(main); } else { this._deferredMainArea = main; } // Rehydrate down area. const downArea = this._rehydrateDownArea(down); // Rehydrate left area. const leftArea = this._rehydrateSideArea(left); // Rehydrate right area. const rightArea = this._rehydrateSideArea(right); return { fresh, mainArea, downArea, leftArea, rightArea, relativeSizes: relativeSizes || null, topArea: (_a = top) !== null && _a !== void 0 ? _a : null }; } catch (error) { return blank; } } /** * Restore the widgets of a particular widget tracker. * * @param tracker - The widget tracker whose widgets will be restored. * * @param options - The restoration options. */ async restore(tracker, options) { if (this._firstDone) { throw new Error('restore() must be called before `first` has resolved.'); } const { namespace } = tracker; if (this._trackers.has(namespace)) { throw new Error(`The tracker "${namespace}" is already restored.`); } const { args, command, name, when } = options; // Add the tracker to the private trackers collection. this._trackers.add(namespace); // Whenever a new widget is added to the tracker, record its name. tracker.widgetAdded.connect((_, widget) => { const widgetName = name(widget); if (widgetName) { this.add(widget, `${namespace}:${widgetName}`); } }, this); // Whenever a widget is updated, get its new name. tracker.widgetUpdated.connect((_, widget) => { const widgetName = name(widget); if (widgetName) { const name = `${namespace}:${widgetName}`; Private.nameProperty.set(widget, name); this._widgets.set(name, widget); } }); const first = this._first; if (this._mode == 'multiple-document') { const promise = tracker .restore({ args: args || (() => JSONExt.emptyObject), command, connector: this._connector, name, registry: this._registry, when: when ? [first].concat(when) : first }) .catch(error => { console.error(error); }); this._promises.push(promise); return promise; } tracker.defer({ args: args || (() => JSONExt.emptyObject), command, connector: this._connector, name, registry: this._registry, when: when ? [first].concat(when) : first }); this._deferred.push(tracker); } /** * Restore the application layout if its restoration has been deferred. * * @returns - the rehydrated main area. */ async restoreDeferred() { if (!this.isDeferred) { return null; } // Empty the deferred list and wait for all trackers to restore. const wait = Promise.resolve(); const promises = this._deferred.map(t => wait.then(() => t.restore())); this._deferred.length = 0; await Promise.all(promises); // Rehydrate the main area layout. return this._rehydrateMainArea(this._deferredMainArea); } /** * Save the layout state for the application. */ save(layout) { // If there are promises that are unresolved, bail. if (!this._promisesDone) { const warning = 'save() was called prematurely.'; console.warn(warning); return Promise.reject(warning); } const dehydrated = {}; // Save the cached main area layout if restoration is deferred. dehydrated.main = this.isDeferred ? this._deferredMainArea : this._dehydrateMainArea(layout.mainArea); dehydrated.down = this._dehydrateDownArea(layout.downArea); dehydrated.left = this._dehydrateSideArea(layout.leftArea); dehydrated.right = this._dehydrateSideArea(layout.rightArea); dehydrated.relativeSizes = layout.relativeSizes; dehydrated.top = { ...layout.topArea }; return this._connector.save(KEY, dehydrated); } /** * Dehydrate a main area description into a serializable object. */ _dehydrateMainArea(area) { if (!area) { return null; } return Private.serializeMain(area); } /** * Rehydrate a serialized main area description object. * * #### Notes * This function consumes data that can become corrupted, so it uses type * coercion to guarantee the dehydrated object is safely processed. */ _rehydrateMainArea(area) { if (!area) { return null; } return Private.deserializeMain(area, this._widgets); } /** * Dehydrate a down area description into a serializable object. */ _dehydrateDownArea(area) { if (!area) { return null; } const dehydrated = { size: area.size }; if (area.currentWidget) { const current = Private.nameProperty.get(area.currentWidget); if (current) { dehydrated.current = current; } } if (area.widgets) { dehydrated.widgets = area.widgets .map(widget => Private.nameProperty.get(widget)) .filter(name => !!name); } return dehydrated; } /** * Rehydrate a serialized side area description object. * * #### Notes * This function consumes data that can become corrupted, so it uses type * coercion to guarantee the dehydrated object is safely processed. */ _rehydrateDownArea(area) { var _a; if (!area) { return { currentWidget: null, size: 0.0, widgets: null }; } const internal = this._widgets; const currentWidget = area.current && internal.has(`${area.current}`) ? internal.get(`${area.current}`) : null; const widgets = !Array.isArray(area.widgets) ? null : area.widgets .map(name => internal.has(`${name}`) ? internal.get(`${name}`) : null) .filter(widget => !!widget); return { currentWidget: currentWidget, size: (_a = area.size) !== null && _a !== void 0 ? _a : 0.0, widgets: widgets }; } /** * Dehydrate a side area description into a serializable object. */ _dehydrateSideArea(area) { if (!area) { return null; } const dehydrated = { collapsed: area.collapsed, visible: area.visible }; if (area.currentWidget) { const current = Private.nameProperty.get(area.currentWidget); if (current) { dehydrated.current = current; } } if (area.widgets) { dehydrated.widgets = area.widgets .map(widget => Private.nameProperty.get(widget)) .filter(name => !!name); } if (area.widgetStates) { dehydrated.widgetStates = area.widgetStates; } return dehydrated; } /** * Rehydrate a serialized side area description object. * * #### Notes * This function consumes data that can become corrupted, so it uses type * coercion to guarantee the dehydrated object is safely processed. */ _rehydrateSideArea(area) { var _a, _b; if (!area) { return { collapsed: true, currentWidget: null, visible: true, widgets: null, widgetStates: { ['null']: { sizes: null, expansionStates: null } } }; } const internal = this._widgets; const collapsed = (_a = area.collapsed) !== null && _a !== void 0 ? _a : false; const currentWidget = area.current && internal.has(`${area.current}`) ? internal.get(`${area.current}`) : null; const widgets = !Array.isArray(area.widgets) ? null : area.widgets .map(name => internal.has(`${name}`) ? internal.get(`${name}`) : null) .filter(widget => !!widget); const widgetStates = area.widgetStates; return { collapsed, currentWidget: currentWidget, widgets: widgets, visible: (_b = area.visible) !== null && _b !== void 0 ? _b : true, widgetStates: widgetStates }; } /** * Handle a widget disposal. */ _onWidgetDisposed(widget) { const name = Private.nameProperty.get(widget); this._widgets.delete(name); } } /* * A namespace for private data. */ var Private; (function (Private) { /** * An attached property for a widget's ID in the serialized restore data. */ Private.nameProperty = new AttachedProperty({ name: 'name', create: owner => '' }); /** * Serialize individual areas within the main area. */ function serializeArea(area) { if (!area || !area.type) { return null; } if (area.type === 'tab-area') { return { type: 'tab-area', currentIndex: area.currentIndex, widgets: area.widgets .map(widget => Private.nameProperty.get(widget)) .filter(name => !!name) }; } return { type: 'split-area', orientation: area.orientation, sizes: area.sizes, children: area.children .map(serializeArea) .filter(area => !!area) }; } /** * Return a dehydrated, serializable version of the main dock panel. */ function serializeMain(area) { const dehydrated = { dock: (area && area.dock && serializeArea(area.dock.main)) || null }; if (area) { if (area.currentWidget) { const current = Private.nameProperty.get(area.currentWidget); if (current) { dehydrated.current = current; } } } return dehydrated; } Private.serializeMain = serializeMain; /** * Deserialize individual areas within the main area. * * #### Notes * Because this data comes from a potentially unreliable foreign source, it is * typed as a `JSONObject`; but the actual expected type is: * `ITabArea | ISplitArea`. * * For fault tolerance, types are manually checked in deserialization. */ function deserializeArea(area, names) { if (!area) { return null; } // Because this data is saved to a foreign data source, its type safety is // not guaranteed when it is retrieved, so exhaustive checks are necessary. const type = area.type || 'unknown'; if (type === 'unknown' || (type !== 'tab-area' && type !== 'split-area')) { console.warn(`Attempted to deserialize unknown type: ${type}`); return null; } if (type === 'tab-area') { const { currentIndex, widgets } = area; const hydrated = { type: 'tab-area', currentIndex: currentIndex || 0, widgets: (widgets && widgets .map(widget => names.get(widget)) .filter(widget => !!widget)) || [] }; // Make sure the current index is within bounds. if (hydrated.currentIndex > hydrated.widgets.length - 1) { hydrated.currentIndex = 0; } return hydrated; } const { orientation, sizes, children } = area; const hydrated = { type: 'split-area', orientation: orientation, sizes: sizes || [], children: (children && children .map(child => deserializeArea(child, names)) .filter(widget => !!widget)) || [] }; return hydrated; } /** * Return the hydrated version of the main dock panel, ready to restore. * * #### Notes * Because this data comes from a potentially unreliable foreign source, it is * typed as a `JSONObject`; but the actual expected type is: `IMainArea`. * * For fault tolerance, types are manually checked in deserialization. */ function deserializeMain(area, names) { if (!area) { return null; } const name = area.current || null; const dock = area.dock || null; return { currentWidget: (name && names.has(name) && names.get(name)) || null, dock: dock ? { main: deserializeArea(dock, names) } : null }; } Private.deserializeMain = deserializeMain; })(Private || (Private = {})); //# sourceMappingURL=layoutrestorer.js.map