Bläddra i källkod

Merge pull request #3372 from blink1073/clear-state

Clear the application state automatically if needed
Afshin Darian 7 år sedan
förälder
incheckning
dec8f91aa5

+ 4 - 1
packages/apputils-extension/src/index.ts

@@ -166,7 +166,10 @@ const state: JupyterLabPlugin<IStateDB> = {
   autoStart: true,
   provides: IStateDB,
   activate: (app: JupyterLab) => {
-    const state = new StateDB({ namespace: app.info.namespace });
+    const state = new StateDB({
+      namespace: app.info.namespace,
+      when: app.restored.then(() => { /* no-op */ })
+    });
     const version = app.info.version;
     const key = 'statedb:version';
     const fetch = state.fetch(key);

+ 53 - 14
packages/coreutils/src/statedb.ts

@@ -88,6 +88,9 @@ class StateDB implements IStateDB {
    */
   constructor(options: StateDB.IOptions) {
     this.namespace = options.namespace;
+    if (options.when) {
+      this._handleSentinel(options.when);
+    }
   }
 
   /**
@@ -109,16 +112,7 @@ class StateDB implements IStateDB {
    * Clear the entire database.
    */
   clear(): Promise<void> {
-    const prefix = `${this.namespace}:`;
-    let i = window.localStorage.length;
-
-    while (i) {
-      let key = window.localStorage.key(--i);
-
-      if (key && key.indexOf(prefix) === 0) {
-        window.localStorage.removeItem(key);
-      }
-    }
+    this._clear();
 
     return Promise.resolve(undefined);
   }
@@ -174,16 +168,17 @@ class StateDB implements IStateDB {
    * This promise will always succeed.
    */
   fetchNamespace(namespace: string): Promise<IStateItem[]> {
+    const { localStorage } = window;
     const prefix = `${this.namespace}:${namespace}:`;
     const regex = new RegExp(`^${this.namespace}\:`);
     let items: IStateItem[] = [];
-    let i = window.localStorage.length;
+    let i = localStorage.length;
 
     while (i) {
-      let key = window.localStorage.key(--i);
+      let key = localStorage.key(--i);
 
       if (key && key.indexOf(prefix) === 0) {
-        let value = window.localStorage.getItem(key);
+        let value = localStorage.getItem(key);
 
         try {
           let envelope = JSON.parse(value) as Private.Envelope;
@@ -194,7 +189,7 @@ class StateDB implements IStateDB {
           });
         } catch (error) {
           console.warn(error);
-          window.localStorage.removeItem(key);
+          localStorage.removeItem(key);
         }
       }
     }
@@ -250,6 +245,44 @@ class StateDB implements IStateDB {
       return Promise.reject(error);
     }
   }
+
+  /**
+   * Clear the entire database.
+   *
+   * #### Notes
+   * Unlike the public `clear` method, this method is synchronous.
+   */
+  private _clear(): void {
+    const { localStorage } = window;
+    const prefix = `${this.namespace}:`;
+    let i = localStorage.length;
+
+    while (i) {
+      let key = localStorage.key(--i);
+
+      if (key && key.indexOf(prefix) === 0) {
+        localStorage.removeItem(key);
+      }
+    }
+  }
+
+  /**
+   * Handle the startup sentinel.
+   */
+  private _handleSentinel(when: Promise<void>): void {
+    const { localStorage } = window;
+    let key = `${this.namespace}:statedb:sentinel`;
+    let sentinel = localStorage.getItem(key);
+
+    // Clear state if the sentinel was not properly cleared on last page load.
+    if (sentinel) {
+      this._clear();
+    }
+
+    // Set the sentinel value and clear it when the statedb is initialized.
+    localStorage.setItem(key, 'sentinel');
+    when.then(() => { localStorage.removeItem(key); });
+  }
 }
 
 /**
@@ -266,6 +299,12 @@ namespace StateDB {
      * The namespace prefix for all state database entries.
      */
     namespace: string;
+
+    /**
+     * An optional Promise for when the state database should be considered
+     * initialized.
+     */
+    when?: Promise<void>;
   }
 }
 

+ 34 - 7
test/src/apputils/statedb.spec.ts → test/src/coreutils/statedb.spec.ts

@@ -7,9 +7,17 @@ import {
   StateDB
 } from '@jupyterlab/coreutils';
 
+import {
+  PromiseDelegate
+} from '@phosphor/coreutils';
+
 
 describe('StateDB', () => {
 
+  beforeEach(() => {
+    window.localStorage.clear();
+  });
+
   describe('#constructor()', () => {
 
     it('should create a state database', () => {
@@ -17,6 +25,32 @@ describe('StateDB', () => {
       expect(db).to.be.a(StateDB);
     });
 
+    it('should take an optional when promise', () => {
+      let { localStorage } = window;
+      let promise = new PromiseDelegate<void>();
+      let db = new StateDB({ namespace: 'test', when: promise.promise });
+      let key = 'foo:bar';
+      let value = { baz: 'qux' };
+      promise.resolve(void 0);
+      return promise.promise.then(() => {
+        expect(localStorage.length).to.be(0);
+        return db.save(key, value);
+      }).then(() => db.fetch(key))
+      .then(fetched => { expect(fetched).to.eql(value); });
+    });
+
+    it('should clear the namespace if the sentinel is set', () => {
+      let { localStorage } = window;
+      let key = 'test:statedb:sentinel';
+      localStorage.setItem(key, 'sentinel');
+      localStorage.setItem('test:foo', 'bar');
+      let promise = new PromiseDelegate<void>();
+      let db = new StateDB({ namespace: 'test', when: promise.promise });
+      expect(db).to.be.a(StateDB);
+      expect(localStorage.length).to.be(1);
+      expect(localStorage.getItem('test:foo')).to.be(null);
+    });
+
   });
 
   describe('#maxLength', () => {
@@ -46,7 +80,6 @@ describe('StateDB', () => {
 
     it('should empty the items in a state database', done => {
       let { localStorage } = window;
-      localStorage.clear();
 
       let db = new StateDB({ namespace: 'test-namespace' });
       let key = 'foo:bar';
@@ -63,7 +96,6 @@ describe('StateDB', () => {
 
     it('should only clear its own namespace', done => {
       let { localStorage } = window;
-      localStorage.clear();
 
       let db1 = new StateDB({ namespace: 'test-namespace-1' });
       let db2 = new StateDB({ namespace: 'test-namespace-2' });
@@ -87,7 +119,6 @@ describe('StateDB', () => {
 
     it('should fetch a stored key', done => {
       let { localStorage } = window;
-      localStorage.clear();
 
       let db = new StateDB({ namespace: 'test-namespace' });
       let key = 'foo:bar';
@@ -105,7 +136,6 @@ describe('StateDB', () => {
 
     it('should resolve a nonexistent key fetch with undefined', done => {
       let { localStorage } = window;
-      localStorage.clear();
 
       let db = new StateDB({ namespace: 'test-namespace' });
       let key = 'foo:bar';
@@ -123,7 +153,6 @@ describe('StateDB', () => {
 
     it('should fetch a stored namespace', done => {
       let { localStorage } = window;
-      localStorage.clear();
 
       let db = new StateDB({ namespace: 'test-namespace' });
       let keys = [
@@ -170,7 +199,6 @@ describe('StateDB', () => {
 
     it('should remove a stored key', done => {
       let { localStorage } = window;
-      localStorage.clear();
 
       let db = new StateDB({ namespace: 'test-namespace' });
       let key = 'foo:bar';
@@ -191,7 +219,6 @@ describe('StateDB', () => {
 
     it('should save a key and a value', done => {
       let { localStorage } = window;
-      localStorage.clear();
 
       let db = new StateDB({ namespace: 'test-namespace' });
       let key = 'foo:bar';

+ 2 - 2
test/src/index.ts

@@ -11,7 +11,6 @@ import './apputils/iframe.spec';
 import './apputils/instancetracker.spec';
 import './apputils/mainareawidget.spec';
 import './apputils/sanitizer.spec';
-import './apputils/statedb.spec';
 import './apputils/styling.spec';
 import './apputils/toolbar.spec';
 import './apputils/vdom.spec';
@@ -37,14 +36,15 @@ import './console/panel.spec';
 import './console/widget.spec';
 
 import './coreutils/activitymonitor.spec';
+import './coreutils/markdowncodeblocks.spec';
 import './coreutils/nbformat.spec';
 import './coreutils/pageconfig.spec';
 import './coreutils/path.spec';
 import './coreutils/settingregistry.spec';
+import './coreutils/statedb.spec';
 import './coreutils/time.spec';
 import './coreutils/url.spec';
 import './coreutils/uuid.spec';
-import './coreutils/markdowncodeblocks.spec';
 
 import './csvviewer/toolbar.spec';
 import './csvviewer/widget.spec';