// Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. import { PromiseDelegate } from '@lumino/coreutils'; import { IObservableDisposable } from '@lumino/disposable'; import { AttachedProperty } from '@lumino/properties'; import { ISignal, Signal } from '@lumino/signaling'; import { IObjectPool, IRestorable } from './interfaces'; /** * An object pool that supports restoration. * * @typeparam T - The type of object being tracked. */ export class RestorablePool< T extends IObservableDisposable = IObservableDisposable > implements IObjectPool, IRestorable { /** * Create a new restorable pool. * * @param options - The instantiation options for a restorable pool. */ constructor(options: RestorablePool.IOptions) { this.namespace = options.namespace; } /** * A namespace for all tracked objects. */ readonly namespace: string; /** * A signal emitted when an object object is added. * * #### Notes * This signal will only fire when an object is added to the pool. * It will not fire if an object injected into the pool. */ get added(): ISignal { return this._added; } /** * The current object. * * #### Notes * The restorable pool does not set `current`. It is intended for client use. * * If `current` is set to an object that does not exist in the pool, it is a * no-op. */ get current(): T | null { return this._current; } set current(obj: T | null) { if (this._current === obj) { return; } if (obj !== null && this._objects.has(obj)) { this._current = obj; this._currentChanged.emit(this._current); } } /** * A signal emitted when the current widget changes. */ get currentChanged(): ISignal { return this._currentChanged; } /** * Test whether the pool is disposed. */ get isDisposed(): boolean { return this._isDisposed; } /** * A promise resolved when the restorable pool has been restored. */ get restored(): Promise { return this._restored.promise; } /** * The number of objects held by the pool. */ get size(): number { return this._objects.size; } /** * A signal emitted when an object is updated. */ get updated(): ISignal { return this._updated; } /** * Add a new object to the pool. * * @param obj - The object object being added. * * #### Notes * The object passed into the pool is added synchronously; its existence in * the pool can be checked with the `has()` method. The promise this method * returns resolves after the object has been added and saved to an underlying * restoration connector, if one is available. */ async add(obj: T): Promise { if (obj.isDisposed) { const warning = 'A disposed object cannot be added.'; console.warn(warning, obj); throw new Error(warning); } if (this._objects.has(obj)) { const warning = 'This object already exists in the pool.'; console.warn(warning, obj); throw new Error(warning); } this._objects.add(obj); obj.disposed.connect(this._onInstanceDisposed, this); if (Private.injectedProperty.get(obj)) { return; } if (this._restore) { const { connector } = this._restore; const objName = this._restore.name(obj); if (objName) { const name = `${this.namespace}:${objName}`; const data = this._restore.args?.(obj); Private.nameProperty.set(obj, name); await connector.save(name, { data }); } } // Emit the added signal. this._added.emit(obj); } /** * Dispose of the resources held by the pool. * * #### Notes * Disposing a pool does not affect the underlying data in the data connector, * it simply disposes the client-side pool without making any connector calls. */ dispose(): void { if (this.isDisposed) { return; } this._current = null; this._isDisposed = true; this._objects.clear(); Signal.clearData(this); } /** * Find the first object in the pool that satisfies a filter function. * * @param fn The filter function to call on each object. */ find(fn: (obj: T) => boolean): T | undefined { const values = this._objects.values(); for (const value of values) { if (fn(value)) { return value; } } return undefined; } /** * Iterate through each object in the pool. * * @param fn - The function to call on each object. */ forEach(fn: (obj: T) => void): void { this._objects.forEach(fn); } /** * Filter the objects in the pool based on a predicate. * * @param fn - The function by which to filter. */ filter(fn: (obj: T) => boolean): T[] { const filtered: T[] = []; this.forEach(obj => { if (fn(obj)) { filtered.push(obj); } }); return filtered; } /** * Inject an object into the restorable pool without the pool handling its * restoration lifecycle. * * @param obj - The object to inject into the pool. */ inject(obj: T): Promise { Private.injectedProperty.set(obj, true); return this.add(obj); } /** * Check if this pool has the specified object. * * @param obj - The object whose existence is being checked. */ has(obj: T): boolean { return this._objects.has(obj); } /** * Restore the objects in this pool's namespace. * * @param options - The configuration options that describe restoration. * * @returns A promise that resolves when restoration has completed. * * #### Notes * This function should almost never be invoked by client code. Its primary * use case is to be invoked by a layout restorer plugin that handles * multiple restorable pools and, when ready, asks them each to restore their * respective objects. */ async restore(options: IRestorable.IOptions): Promise { if (this._hasRestored) { throw new Error('This pool has already been restored.'); } this._hasRestored = true; const { command, connector, registry, when } = options; const namespace = this.namespace; const promises = when ? [connector.list(namespace)].concat(when) : [connector.list(namespace)]; this._restore = options; const [saved] = await Promise.all(promises); const values = await Promise.all( saved.ids.map(async (id, index) => { const value = saved.values[index]; const args = value && (value as any).data; if (args === undefined) { return connector.remove(id); } // Execute the command and if it fails, delete the state restore data. return registry .execute(command, args) .catch(() => connector.remove(id)); }) ); this._restored.resolve(); return values; } /** * Save the restore data for a given object. * * @param obj - The object being saved. */ async save(obj: T): Promise { const injected = Private.injectedProperty.get(obj); if (!this._restore || !this.has(obj) || injected) { return; } const { connector } = this._restore; const objName = this._restore.name(obj); const oldName = Private.nameProperty.get(obj); const newName = objName ? `${this.namespace}:${objName}` : ''; if (oldName && oldName !== newName) { await connector.remove(oldName); } // Set the name property irrespective of whether the new name is null. Private.nameProperty.set(obj, newName); if (newName) { const data = this._restore.args?.(obj); await connector.save(newName, { data }); } if (oldName !== newName) { this._updated.emit(obj); } } /** * Clean up after disposed objects. */ private _onInstanceDisposed(obj: T): void { this._objects.delete(obj); if (obj === this._current) { this._current = null; this._currentChanged.emit(this._current); } if (Private.injectedProperty.get(obj)) { return; } if (!this._restore) { return; } const { connector } = this._restore; const name = Private.nameProperty.get(obj); if (name) { void connector.remove(name); } } private _added = new Signal(this); private _current: T | null = null; private _currentChanged = new Signal(this); private _hasRestored = false; private _isDisposed = false; private _objects = new Set(); private _restore: IRestorable.IOptions | null = null; private _restored = new PromiseDelegate(); private _updated = new Signal(this); } /** * A namespace for `RestorablePool` statics. */ export namespace RestorablePool { /** * The instantiation options for the restorable pool. */ export interface IOptions { /** * A namespace designating objects from this pool. */ namespace: string; } } /* * A namespace for private data. */ namespace Private { /** * An attached property to indicate whether an object has been injected. */ export const injectedProperty = new AttachedProperty< IObservableDisposable, boolean >({ name: 'injected', create: () => false }); /** * An attached property for an object's ID. */ export const nameProperty = new AttachedProperty< IObservableDisposable, string >({ name: 'name', create: () => '' }); }