Skip to content

Commit

Permalink
Create a window resolver extension.
Browse files Browse the repository at this point in the history
  • Loading branch information
afshin committed Apr 25, 2018
1 parent becb06d commit df77d6e
Show file tree
Hide file tree
Showing 3 changed files with 47 additions and 145 deletions.
30 changes: 26 additions & 4 deletions packages/apputils-extension/src/index.ts
Expand Up @@ -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 {
Expand Down Expand Up @@ -225,6 +226,23 @@ const themes: JupyterLabPlugin<IThemeManager> = {
};


/**
* The default window name resolver provider.
*/
const resolver: JupyterLabPlugin<IWindowResolver> = {
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.
*/
Expand Down Expand Up @@ -252,11 +270,15 @@ const state: JupyterLabPlugin<IStateDB> = {
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<StateDB.DataTransform>();
Expand Down Expand Up @@ -433,7 +455,7 @@ const state: JupyterLabPlugin<IStateDB> = {
* Export the plugins as default.
*/
const plugins: JupyterLabPlugin<any>[] = [
palette, settings, state, splash, themes
palette, resolver, settings, state, splash, themes
];
export default plugins;

Expand Down
149 changes: 13 additions & 136 deletions packages/coreutils/src/statedb.ts
Expand Up @@ -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) {
Expand Down Expand Up @@ -453,6 +453,17 @@ namespace StateDB {
* client requests.
*/
transform?: Promise<DataTransform>;

/**
* 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<string>;
}
}

Expand All @@ -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<string>;

/**
* 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<string> {
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<string> {
return promise || (promise = new Promise((resolve) => {
beacon();
awaitName(resolve);
}));
}

/**
* Start the storage event handler immediately.
*/
window.addEventListener('storage', storageHandler);
}
13 changes: 8 additions & 5 deletions packages/coreutils/src/windowresolver.ts
Expand Up @@ -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;
}

Expand All @@ -44,11 +44,14 @@ class WindowResolver implements IWindowResolver {
* @returns A promise that resolves to a window name.
*/
resolve(): Promise<string> {
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<string>;
}


Expand All @@ -65,7 +68,7 @@ namespace WindowResolver {
/**
* A potential preferred default window name.
*/
candidate: string;
candidate: Promise<string>;
}
}

Expand Down

0 comments on commit df77d6e

Please sign in to comment.