diff --git a/packages/apputils-extension/src/index.ts b/packages/apputils-extension/src/index.ts index c5446cdd4441..bf08b6d78ddc 100644 --- a/packages/apputils-extension/src/index.ts +++ b/packages/apputils-extension/src/index.ts @@ -12,7 +12,8 @@ import { } from '@jupyterlab/apputils'; import { - DataConnector, ISettingRegistry, IStateDB, SettingRegistry, StateDB, URLExt + DataConnector, ISettingRegistry, IStateDB, IWindowResolver, SettingRegistry, + StateDB, URLExt, WindowResolver } from '@jupyterlab/coreutils'; import { @@ -225,6 +226,23 @@ const themes: JupyterLabPlugin = { }; +/** + * The default window name resolver provider. + */ +const resolver: JupyterLabPlugin = { + id: '@jupyterlab/apputils-extension:resolver', + autoStart: true, + provides: IWindowResolver, + activate: app => { + const resolver = new WindowResolver({ + candidate: Promise.resolve('hello') + }); + + return resolver; + } +}; + + /** * The default splash screen provider. */ @@ -252,11 +270,15 @@ const state: JupyterLabPlugin = { id: '@jupyterlab/apputils-extension:state', autoStart: true, provides: IStateDB, - requires: [IRouter], - activate: (app: JupyterLab, router: IRouter) => { + requires: [IRouter, IWindowResolver], + activate: (app: JupyterLab, router: IRouter, resolver: IWindowResolver) => { let debouncer: number; let resolved = false; + resolver.resolve().then(name => { + console.log(`The window resolver returns the name ${name}`); + }); + const { commands, info, serviceManager } = app; const { workspaces } = serviceManager; const transform = new PromiseDelegate(); @@ -433,7 +455,7 @@ const state: JupyterLabPlugin = { * Export the plugins as default. */ const plugins: JupyterLabPlugin[] = [ - palette, settings, state, splash, themes + palette, resolver, settings, state, splash, themes ]; export default plugins; diff --git a/packages/coreutils/src/statedb.ts b/packages/coreutils/src/statedb.ts index aab16472f1a8..0dfc8c344b2e 100644 --- a/packages/coreutils/src/statedb.ts +++ b/packages/coreutils/src/statedb.ts @@ -98,12 +98,12 @@ class StateDB implements IStateDB { * @param options - The instantiation options for a state database. */ constructor(options: StateDB.IOptions) { - const { namespace, transform } = options; + const { namespace, transform, windowName } = options; this.namespace = namespace; // Retrieve the window name, which is used as a namespace prefix. - this._ready = Private.windowName().then(name => { + this._ready = (windowName || Promise.resolve('')).then(name => { this._window = name; if (!transform) { @@ -453,6 +453,17 @@ namespace StateDB { * client requests. */ transform?: Promise; + + /** + * An optional promise that resolves to a name for the application window. + * + * #### Notes + * In environments where multiple windows can instantiate a state database, + * a window name is necessary to prefix all keys that are stored within the + * local storage that is shared by all windows. In JupyterLab, this window + * name is generated by the `IWindowResolver` extension. + */ + windowName?: Promise; } } @@ -466,138 +477,4 @@ namespace Private { */ export type Envelope = { readonly v: ReadonlyJSONValue }; - - /** - * The timeout (in ms) to wait for beacon responders. - */ - const TIMEOUT = 100; - - /** - * The internal prefix for private local storage keys. - */ - const PREFIX = '@jupyterlab/coreutils:StateDB'; - - /** - * The local storage beacon key. - */ - const BEACON = `${PREFIX}:beacon`; - - /** - * The local storage window prefix. - */ - const WINDOW = `${PREFIX}:window-`; - - /** - * The window name. - */ - let name: string; - - /** - * The window name promise. - */ - let promise: Promise; - - /** - * Wait until a window name is available and resolve. - */ - function awaitName(resolve: (value: string) => void): void { - window.setTimeout(() => { - if (name) { - return resolve(name); - } - - createName().then(value => { - name = value; - resolve(name); - }); - }, TIMEOUT); - } - - /** - * Create a name for this window. - */ - function createName(): Promise { - console.log('I should generate a name'); - return Promise.resolve(''); - } - - /** - * Fetch the known window names. - */ - function fetchWindowNames(): { [name: string]: number } { - const names: { [name: string]: number } = { }; - const { localStorage } = window; - let i = localStorage.length; - - while (i) { - let key = localStorage.key(--i); - - if (key && key.indexOf(WINDOW) === 0) { - let name = key.replace(WINDOW, ''); - - names[name] = parseInt(localStorage.getItem(key), 10); - } - } - - return names; - } - - /** - * Fire off the signal beacon to solicit pings from other JupyterLab windows. - */ - function beacon(): void { - window.localStorage.setItem(BEACON, `${(new Date()).getTime()}`); - } - - /** - * Respond to a signal beacon. - */ - function ping(): void { - if (name) { - window.localStorage.setItem(name, `${(new Date()).getTime()}`); - } - } - - /** - * The window storage event handler. - */ - function storageHandler(event: StorageEvent) { - const { key } = event; - - console.log('event key', key); - - if (key === BEACON) { - console.log('The beacon has been fired'); - return ping(); - } - - if (key.indexOf(WINDOW) !== 0) { - return; - } - - const windows = fetchWindowNames(); - const name = key.replace(WINDOW, ''); - - if (name in windows) { - console.log(`Window ${name} is a known window.`); - } else { - console.log(`Window ${name} is an unknown window.`); - } - } - - /** - * Returns a promise that resolves with the window name used for restoration. - */ - export - function windowName(): Promise { - return promise || (promise = new Promise((resolve) => { - beacon(); - awaitName(resolve); - })); - } - - /** - * Start the storage event handler immediately. - */ - window.addEventListener('storage', storageHandler); } diff --git a/packages/coreutils/src/windowresolver.ts b/packages/coreutils/src/windowresolver.ts index c109db4dcebe..99083e441fd8 100644 --- a/packages/coreutils/src/windowresolver.ts +++ b/packages/coreutils/src/windowresolver.ts @@ -34,7 +34,7 @@ interface IWindowResolver { */ export class WindowResolver implements IWindowResolver { - constructor(options: WindowResolver.IOptions = { candidate: '' }) { + constructor(options: WindowResolver.IOptions = { candidate: Promise.resolve('') }) { this._candidate = options.candidate; } @@ -44,11 +44,14 @@ class WindowResolver implements IWindowResolver { * @returns A promise that resolves to a window name. */ resolve(): Promise { - console.log('Start with candidate:', this._candidate); - return Private.windowName(); + return this._candidate.then(name => { + console.log('Start with candidate:', name); + return Private.windowName(); + }); + } - private _candidate: string; + private _candidate: Promise; } @@ -65,7 +68,7 @@ namespace WindowResolver { /** * A potential preferred default window name. */ - candidate: string; + candidate: Promise; } }