Bläddra i källkod

Merge pull request #2039 from ian-r-rose/modelDB

[WIP] Partial merge of ModelDB proposal.
Steven Silvester 8 år sedan
förälder
incheckning
afc241f8aa

+ 85 - 46
packages/cells/src/model.ts

@@ -14,11 +14,11 @@ import {
 } from '@jupyterlab/codeeditor';
 
 import {
-  IChangedArgs, nbformat
+  IChangedArgs, nbformat, uuid
 } from '@jupyterlab/coreutils';
 
 import {
-  IObservableJSON, ObservableJSON
+  IObservableJSON, IModelDB, IObservableValue, ObservableValue
 } from '@jupyterlab/coreutils';
 
 import {
@@ -36,6 +36,11 @@ interface ICellModel extends CodeEditor.IModel {
    */
   readonly type: nbformat.CellType;
 
+  /**
+   * A unique identifier for the cell.
+   */
+  readonly id: string;
+
   /**
    * A signal emitted when the content of the model changes.
    */
@@ -121,13 +126,27 @@ class CellModel extends CodeEditor.Model implements ICellModel {
    * Construct a cell model from optional cell content.
    */
   constructor(options: CellModel.IOptions) {
-    super();
+    super({modelDB: options.modelDB});
+
+    this.id = options.id || uuid();
+
     this.value.changed.connect(this.onGenericChange, this);
+
+    let cellType = this.modelDB.createValue('type');
+    cellType.set(this.type);
+
+    let observableMetadata = this.modelDB.createMap('metadata');
+    observableMetadata.changed.connect(this.onGenericChange, this);
+
     let cell = options.cell;
+    let trusted = this.modelDB.createValue('trusted');
+    trusted.changed.connect(this.onTrustedChanged, this);
+
     if (!cell) {
+      trusted.set(false);
       return;
     }
-    this._trusted = !!cell.metadata['trusted'];
+    trusted.set(!!cell.metadata['trusted']);
     delete cell.metadata['trusted'];
 
     if (Array.isArray(cell.source)) {
@@ -143,10 +162,10 @@ class CellModel extends CodeEditor.Model implements ICellModel {
       delete metadata['collapsed'];
       delete metadata['scrolled'];
     }
+
     for (let key in metadata) {
-      this._metadata.set(key, metadata[key]);
+      observableMetadata.set(key, metadata[key]);
     }
-    this._metadata.changed.connect(this.onGenericChange, this);
   }
 
   /**
@@ -164,39 +183,34 @@ class CellModel extends CodeEditor.Model implements ICellModel {
    */
   readonly stateChanged = new Signal<this, IChangedArgs<any>>(this);
 
+  /**
+   * The id for the cell.
+   */
+  readonly id: string;
+
   /**
    * The metadata associated with the cell.
    */
   get metadata(): IObservableJSON {
-    return this._metadata;
+    return this.modelDB.get('metadata') as IObservableJSON;
   }
 
   /**
    * Get the trusted state of the model.
    */
   get trusted(): boolean {
-    return this._trusted;
+    return this.modelDB.getValue('trusted') as boolean;
   }
 
   /**
    * Set the trusted state of the model.
    */
   set trusted(newValue: boolean) {
-    if (this._trusted === newValue) {
+    let oldValue = this.trusted;
+    if (oldValue === newValue) {
       return;
     }
-    let oldValue = this._trusted;
-    this._trusted = newValue;
-    this.onTrustedChanged(newValue);
-    this.stateChanged.emit({ name: 'trusted', oldValue, newValue });
-  }
-
-  /**
-   * Dispose of the resources held by the model.
-   */
-  dispose(): void {
-    this._metadata.dispose();
-    super.dispose();
+    this.modelDB.setValue('trusted', newValue);
   }
 
   /**
@@ -223,7 +237,7 @@ class CellModel extends CodeEditor.Model implements ICellModel {
    *
    * The default implementation is a no-op.
    */
-  onTrustedChanged(value: boolean): void { /* no-op */}
+  onTrustedChanged(trusted: IObservableValue, args: ObservableValue.IChangedArgs): void { /* no-op */ }
 
   /**
    * Handle a change to the observable value.
@@ -231,9 +245,6 @@ class CellModel extends CodeEditor.Model implements ICellModel {
   protected onGenericChange(): void {
     this.contentChanged.emit(void 0);
   }
-
-  private _metadata = new ObservableJSON();
-  private _trusted = false;
 }
 
 
@@ -250,6 +261,16 @@ namespace CellModel {
      * The source cell data.
      */
     cell?: nbformat.IBaseCell;
+
+    /**
+     * An IModelDB in which to store cell data.
+     */
+    modelDB?: IModelDB;
+
+    /**
+     * A unique identifier for this cell.
+     */
+    id?: string;
   }
 }
 
@@ -307,13 +328,21 @@ class CodeCellModel extends CellModel implements ICodeCellModel {
     let trusted = this.trusted;
     let cell = options.cell as nbformat.ICodeCell;
     let outputs: nbformat.IOutput[] = [];
-    if (cell && cell.cell_type === 'code') {
-      this.executionCount = cell.execution_count;
-      outputs = cell.outputs;
+    let executionCount = this.modelDB.createValue('executionCount');
+    if (!executionCount.get()) {
+      if (cell && cell.cell_type === 'code') {
+        executionCount.set(cell.execution_count || null);
+        outputs = cell.outputs;
+      } else {
+        executionCount.set(null);
+      }
     }
+    executionCount.changed.connect(this._onExecutionCountChanged, this);
+
     this._outputs = factory.createOutputArea({
       trusted,
-      values: outputs
+      values: outputs,
+      modelDB: this.modelDB
     });
     this._outputs.stateChanged.connect(this.onGenericChange, this);
   }
@@ -329,16 +358,14 @@ class CodeCellModel extends CellModel implements ICodeCellModel {
    * The execution count of the cell.
    */
   get executionCount(): nbformat.ExecutionCount {
-    return this._executionCount || null;
+    return this.modelDB.getValue('executionCount') as nbformat.ExecutionCount;
   }
   set executionCount(newValue: nbformat.ExecutionCount) {
-    if (newValue === this._executionCount) {
+    let oldValue = this.executionCount;
+    if (newValue === oldValue) {
       return;
     }
-    let oldValue = this.executionCount;
-    this._executionCount = newValue || null;
-    this.contentChanged.emit(void 0);
-    this.stateChanged.emit({ name: 'executionCount', oldValue, newValue });
+    this.modelDB.setValue('executionCount', newValue || null);
   }
 
   /**
@@ -372,15 +399,31 @@ class CodeCellModel extends CellModel implements ICodeCellModel {
 
   /**
    * Handle a change to the trusted state.
-   *
-   * The default implementation is a no-op.
    */
-  onTrustedChanged(value: boolean): void {
-    this._outputs.trusted = value;
+  onTrustedChanged(trusted: IObservableValue, args: ObservableValue.IChangedArgs): void {
+    if (this._outputs) {
+      this._outputs.trusted = args.newValue as boolean;
+    }
+    this.stateChanged.emit({
+      name: 'trusted',
+      oldValue: args.oldValue,
+      newValue: args.newValue
+    });
+  }
+
+  /**
+   * Handle a change to the execution count.
+   */
+  private _onExecutionCountChanged(count: IObservableValue, args: ObservableValue.IChangedArgs): void {
+    this.contentChanged.emit(void 0);
+    this.stateChanged.emit({
+      name: 'executionCount',
+      oldValue: args.oldValue,
+      newValue: args.newValue });
   }
 
+
   private _outputs: IOutputAreaModel = null;
-  private _executionCount: nbformat.ExecutionCount = null;
 }
 
 
@@ -392,12 +435,8 @@ namespace CodeCellModel {
   /**
    * The options used to initialize a `CodeCellModel`.
    */
-  export interface IOptions {
-    /**
-     * The source cell data.
-     */
-    cell?: nbformat.IBaseCell;
-
+  export
+  interface IOptions extends CellModel.IOptions {
     /**
      * The factory for output area model creation.
      */

+ 39 - 29
packages/codeeditor/src/editor.ts

@@ -14,15 +14,8 @@ import {
 } from '@phosphor/signaling';
 
 import {
-  IChangedArgs
-} from '@jupyterlab/coreutils';
-
-import {
-  IObservableString, ObservableString
-} from '@jupyterlab/coreutils';
-
-import {
-  IObservableMap, ObservableMap
+  IModelDB, ModelDB, IObservableValue, ObservableValue,
+  IObservableMap, IObservableString, IChangedArgs
 } from '@jupyterlab/coreutils';
 
 
@@ -40,7 +33,7 @@ namespace CodeEditor {
    * A zero-based position in the editor.
    */
   export
-  interface IPosition {
+  interface IPosition extends JSONObject {
     /**
      * The cursor line number.
      */
@@ -78,7 +71,7 @@ namespace CodeEditor {
    * A range.
    */
   export
-  interface IRange {
+  interface IRange extends JSONObject {
     /**
      * The position of the first character in the current range.
      *
@@ -102,7 +95,7 @@ namespace CodeEditor {
    * A selection style.
    */
   export
-  interface ISelectionStyle {
+  interface ISelectionStyle extends JSONObject {
     /**
      * A class name added to a selection.
      */
@@ -186,8 +179,21 @@ namespace CodeEditor {
      */
     constructor(options?: Model.IOptions) {
       options = options || {};
-      this._value = new ObservableString(options.value);
-      this._mimetype = options.mimeType || 'text/plain';
+
+      if (options.modelDB) {
+        this.modelDB = options.modelDB;
+      } else {
+        this.modelDB = new ModelDB();
+      }
+
+      let value = this.modelDB.createString('value');
+      value.text = value.text || options.value || '';
+
+      let mimeType = this.modelDB.createValue('mimeType');
+      mimeType.set(options.mimeType || 'text/plain');
+      mimeType.changed.connect(this._onMimeTypeChanged, this);
+
+      this.modelDB.createMap('selections');
     }
 
     /**
@@ -201,33 +207,28 @@ namespace CodeEditor {
      * Get the value of the model.
      */
     get value(): IObservableString {
-      return this._value;
+      return this.modelDB.get('value') as IObservableString;
     }
 
     /**
      * Get the selections for the model.
      */
     get selections(): IObservableMap<ITextSelection[]> {
-      return this._selections;
+      return this.modelDB.get('selections') as IObservableMap<ITextSelection[]>;
     }
 
     /**
      * A mime type of the model.
      */
     get mimeType(): string {
-      return this._mimetype;
+      return this.modelDB.getValue('mimeType') as string;
     }
     set mimeType(newValue: string) {
-      const oldValue = this._mimetype;
+      const oldValue = this.mimeType;
       if (oldValue === newValue) {
         return;
       }
-      this._mimetype = newValue;
-      this._mimeTypeChanged.emit({
-        name: 'mimeType',
-        oldValue,
-        newValue
-      });
+      this.modelDB.setValue('mimeType', newValue);
     }
 
     /**
@@ -246,13 +247,17 @@ namespace CodeEditor {
       }
       this._isDisposed = true;
       Signal.clearData(this);
-      this._selections.dispose();
-      this._value.dispose();
     }
 
-    private _value: ObservableString;
-    private _selections = new ObservableMap<ITextSelection[]>();
-    private _mimetype: string;
+    private _onMimeTypeChanged(mimeType: IObservableValue, args: ObservableValue.IChangedArgs): void {
+      this._mimeTypeChanged.emit({
+        name: 'mimeType',
+        oldValue: args.oldValue as string,
+        newValue: args.newValue as string
+      });
+    }
+
+    protected modelDB: IModelDB = null;
     private _isDisposed = false;
     private _mimeTypeChanged = new Signal<this, IChangedArgs<string>>(this);
   }
@@ -567,6 +572,11 @@ namespace CodeEditor {
        * The mimetype of the model.
        */
       mimeType?: string;
+
+      /**
+       * An optional modelDB for storing model state.
+       */
+      modelDB?: IModelDB;
     }
   }
 }

+ 1 - 0
packages/coreutils/src/index.ts

@@ -14,3 +14,4 @@ export * from './undoablevector';
 export * from './url';
 export * from './uuid';
 export * from './vector';
+export * from './modeldb';

+ 555 - 0
packages/coreutils/src/modeldb.ts

@@ -0,0 +1,555 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import {
+  IDisposable, DisposableSet
+} from '@phosphor/disposable';
+
+import {
+  ISignal, Signal
+} from '@phosphor/signaling';
+
+import {
+  JSONValue
+} from '@phosphor/coreutils';
+
+import {
+  PathExt
+} from './path';
+
+import {
+  ObservableMap
+} from './observablemap';
+
+import {
+  IObservableJSON, ObservableJSON
+} from './observablejson';
+
+import {
+  IObservableString, ObservableString
+} from './observablestring';
+
+import {
+  IObservableUndoableVector, ObservableUndoableVector
+} from './undoablevector';
+
+
+/**
+ * String type annotations for Observable objects that can be
+ * created and placed in the IModelDB interface.
+ */
+export
+type ObservableType = 'Map' | 'Vector' | 'String' | 'Value';
+
+/**
+ * Base interface for Observable objects.
+ */
+export
+interface IObservable extends IDisposable {
+  /**
+   * The type of this object.
+   */
+  readonly type: ObservableType;
+}
+
+/**
+ * Interface for an Observable object that represents
+ * an opaque JSON value.
+ */
+export
+interface IObservableValue extends IObservable {
+  /**
+   * The type of this object.
+   */
+  readonly type: 'Value';
+
+  /**
+   * The changed signal.
+   */
+  readonly changed: ISignal<IObservableValue, ObservableValue.IChangedArgs>;
+
+  /**
+   * Get the current value.
+   */
+  get(): JSONValue;
+
+  /**
+   * Set the value.
+   */
+  set(value: JSONValue): void;
+}
+
+/**
+ * An interface for a path based database for
+ * creating and storing values, which is agnostic
+ * to the particular type of store in the backend.
+ */
+export
+interface IModelDB extends IDisposable {
+  /**
+   * The base path for the `IModelDB`. This is prepended
+   * to all the paths that are passed in to the member
+   * functions of the object.
+   */
+  readonly basePath: string;
+
+  /**
+   * Whether the database has been disposed.
+   */
+  readonly isDisposed: boolean;
+
+  /**
+   * Whether the database has been populated
+   * with model values prior to connection.
+   */
+  readonly isPrepopulated: boolean;
+
+  /**
+   * A promise that resolves when the database
+   * has connected to its backend, if any.
+   */
+  readonly connected: Promise<void>;
+
+  /**
+   * Get a value for a path.
+   *
+   * @param path: the path for the object.
+   *
+   * @returns an `IObservable`.
+   */
+  get(path: string): IObservable;
+
+  /**
+   * Whether the `IModelDB` has an object at this path.
+   *
+   * @param path: the path for the object.
+   *
+   * @returns a boolean for whether an object is at `path`.
+   */
+  has(path: string): boolean;
+
+  /**
+   * Create a string and insert it in the database.
+   *
+   * @param path: the path for the string.
+   *
+   * @returns the string that was created.
+   */
+  createString(path: string): IObservableString;
+
+  /**
+   * Create an undoable vector and insert it in the database.
+   *
+   * @param path: the path for the vector.
+   *
+   * @returns the vector that was created.
+   *
+   * #### Notes
+   * The vector can only store objects that are simple
+   * JSON Objects and primitives.
+   */
+  createVector<T extends JSONValue>(path: string): IObservableUndoableVector<T>;
+
+  /**
+   * Create a map and insert it in the database.
+   *
+   * @param path: the path for the map.
+   *
+   * @returns the map that was created.
+   *
+   * #### Notes
+   * The map can only store objects that are simple
+   * JSON Objects and primitives.
+   */
+  createMap(path: string): IObservableJSON;
+
+  /**
+   * Create an opaque value and insert it in the database.
+   *
+   * @param path: the path for the value.
+   *
+   * @returns the value that was created.
+   */
+  createValue(path: string): IObservableValue;
+
+  /**
+   * Get a value at a path. That value must already have
+   * been created using `createValue`.
+   *
+   * @param path: the path for the value.
+   */
+  getValue(path: string): JSONValue;
+
+  /**
+   * Set a value at a path. That value must already have
+   * been created using `createValue`.
+   *
+   * @param path: the path for the value.
+   *
+   * @param value: the new value.
+   */
+  setValue(path: string, value: JSONValue): void;
+
+  /**
+   * Create a view onto a subtree of the model database.
+   *
+   * @param basePath: the path for the root of the subtree.
+   *
+   * @returns an `IModelDB` with a view onto the original
+   *   `IModelDB`, with `basePath` prepended to all paths.
+   */
+  view(basePath: string): IModelDB;
+
+  /**
+   * Dispose of the resources held by the database.
+   */
+  dispose(): void;
+}
+
+/**
+ * A concrete implementation of an `IObservableValue`.
+ */
+export
+class ObservableValue implements IObservableValue {
+  /**
+   * Constructor for the value.
+   *
+   * @param initialValue: the starting value for the `ObservableValue`.
+   */
+  constructor(initialValue?: JSONValue) {
+    this._value = initialValue;
+  }
+
+  /**
+   * The observable type.
+   */
+  get type(): 'Value' {
+    return 'Value';
+  }
+
+  /**
+   * Whether the value has been disposed.
+   */
+  get isDisposed(): boolean {
+    return this._isDisposed;
+  }
+
+  /**
+   * The changed signal.
+   */
+  get changed(): ISignal<this, ObservableValue.IChangedArgs> {
+    return this._changed;
+  }
+
+  /**
+   * Get the current value.
+   */
+  get(): JSONValue {
+    return this._value;
+  }
+
+  /**
+   * Set the current value.
+   */
+  set(value: JSONValue): void {
+    let oldValue = this._value;
+    this._value = value;
+    this._changed.emit({
+      oldValue: oldValue,
+      newValue: value
+    });
+  }
+
+  /**
+   * Dispose of the resources held by the value.
+   */
+  dispose(): void {
+    if (this._isDisposed) {
+      return;
+    }
+    this._isDisposed = true;
+    Signal.clearData(this);
+    this._value = null;
+  }
+
+  private _value: JSONValue = null;
+  private _changed = new Signal<ObservableValue, ObservableValue.IChangedArgs>(this);
+  private _isDisposed = false;
+}
+
+/**
+ * The namespace for the `ObservableValue` class statics.
+ */
+export
+namespace ObservableValue {
+  /**
+   * The changed args object emitted by the `IObservableValue`.
+   */
+  export
+  class IChangedArgs {
+    /**
+     * The old value.
+     */
+    oldValue: JSONValue;
+
+    /**
+     * The new value.
+     */
+    newValue: JSONValue;
+  }
+}
+
+
+/**
+ * A concrete implementation of an `IModelDB`.
+ */
+export
+class ModelDB implements IModelDB {
+  /**
+   * Constructor for the `ModelDB`.
+   */
+  constructor(options: ModelDB.ICreateOptions = {}) {
+    this._basePath = options.basePath || '';
+    if (options.baseDB) {
+      this._db = options.baseDB;
+    } else {
+      this._db = new ObservableMap<IObservable>();
+      this._toDispose = true;
+    }
+  }
+
+  /**
+   * The base path for the `ModelDB`. This is prepended
+   * to all the paths that are passed in to the member
+   * functions of the object.
+   */
+  get basePath(): string {
+    return this._basePath;
+  }
+
+  /**
+   * Whether the database is disposed.
+   */
+  get isDisposed(): boolean {
+    return this._db === null;
+  }
+
+  /**
+   * Whether the model has been populated with
+   * any model values.
+   */
+  get isPrepopulated(): boolean {
+    return false;
+  }
+
+  /**
+   * A promise resolved when the model is connected
+   * to its backend. For the in-memory ModelDB it
+   * is immediately resolved.
+   */
+  get connected(): Promise<void> {
+    return Promise.resolve(void 0);
+  }
+
+  /**
+   * Get a value for a path.
+   *
+   * @param path: the path for the object.
+   *
+   * @returns an `IObservable`.
+   */
+  get(path: string): IObservable {
+    return this._db.get(this._resolvePath(path));
+  }
+
+  /**
+   * Whether the `IModelDB` has an object at this path.
+   *
+   * @param path: the path for the object.
+   *
+   * @returns a boolean for whether an object is at `path`.
+   */
+  has(path: string): boolean {
+    return this._db.has(this._resolvePath(path));
+  }
+
+  /**
+   * Create a string and insert it in the database.
+   *
+   * @param path: the path for the string.
+   *
+   * @returns the string that was created.
+   */
+  createString(path: string): IObservableString {
+    let str = new ObservableString();
+    this._disposables.add(str);
+    this.set(path, str);
+    return str;
+  }
+
+  /**
+   * Create an undoable vector and insert it in the database.
+   *
+   * @param path: the path for the vector.
+   *
+   * @returns the vector that was created.
+   *
+   * #### Notes
+   * The vector can only store objects that are simple
+   * JSON Objects and primitives.
+   */
+  createVector(path: string): IObservableUndoableVector<JSONValue> {
+    let vec = new ObservableUndoableVector<JSONValue>(
+      new ObservableUndoableVector.IdentitySerializer());
+    this._disposables.add(vec);
+    this.set(path, vec);
+    return vec;
+  }
+
+  /**
+   * Create a map and insert it in the database.
+   *
+   * @param path: the path for the map.
+   *
+   * @returns the map that was created.
+   *
+   * #### Notes
+   * The map can only store objects that are simple
+   * JSON Objects and primitives.
+   */
+  createMap(path: string): IObservableJSON {
+    let map = new ObservableJSON();
+    this._disposables.add(map);
+    this.set(path, map);
+    return map;
+  }
+
+  /**
+   * Create an opaque value and insert it in the database.
+   *
+   * @param path: the path for the value.
+   *
+   * @returns the value that was created.
+   */
+  createValue(path: string): IObservableValue {
+    let val = new ObservableValue();
+    this._disposables.add(val);
+    this.set(path, val);
+    return val;
+  }
+
+  /**
+   * Get a value at a path. That value must already have
+   * been created using `createValue`.
+   *
+   * @param path: the path for the value.
+   */
+  getValue(path: string): JSONValue {
+    let val = this.get(path);
+    if (val.type !== 'Value') {
+        throw Error('Can only call getValue for an ObservableValue');
+    }
+    return (val as ObservableValue).get();
+  }
+
+
+  /**
+
+  /**
+   * Set a value at a path. That value must already have
+   * been created using `createValue`.
+   *
+   * @param path: the path for the value.
+   *
+   * @param value: the new value.
+   */
+  setValue(path: string, value: JSONValue): void {
+    let val = this.get(path);
+    if (val.type !== 'Value') {
+        throw Error('Can only call setValue on an ObservableValue');
+    }
+    (val as ObservableValue).set(value);
+  }
+
+
+  /**
+   * Create a view onto a subtree of the model database.
+   *
+   * @param basePath: the path for the root of the subtree.
+   *
+   * @returns an `IModelDB` with a view onto the original
+   *   `IModelDB`, with `basePath` prepended to all paths.
+   */
+  view(basePath: string): ModelDB {
+    let view = new ModelDB({basePath, baseDB: this});
+    this._disposables.add(view);
+    return view;
+  }
+
+  /**
+   * Set a value at a path. Not intended to
+   * be called by user code, instead use the
+   * `create*` factory methods.
+   *
+   * @param path: the path to set the value at.
+   *
+   * @param value: the value to set at the path.
+   */
+  set(path: string, value: IObservable): void {
+    this._db.set(this._resolvePath(path), value);
+  }
+
+  /**
+   * Dispose of the resources held by the database.
+   */
+  dispose(): void {
+    if (this.isDisposed) {
+      return;
+    }
+    let db = this._db;
+    this._db = null;
+
+    if (this._toDispose) {
+      db.dispose();
+    }
+    this._disposables.dispose();
+  }
+
+  /**
+   * Compute the fully resolved path for a path argument.
+   */
+  private _resolvePath(path: string): string {
+    if (this._basePath) {
+      path = this._basePath + '.' + path;
+    }
+    return PathExt.normalize(path)
+  }
+
+  private _basePath: string;
+  private _db: ModelDB | ObservableMap<IObservable> = null;
+  private _toDispose = false;
+  private _disposables = new DisposableSet();
+}
+
+/**
+ * A namespace for the `ModelDB` class statics.
+ */
+export
+namespace ModelDB {
+  /**
+   * Options for creating a `ModelDB` object.
+   */
+  export
+  interface ICreateOptions {
+    /**
+     * The base path to prepend to all the path arguments.
+     */
+    basePath?: string;
+
+    /**
+     * A ModelDB to use as the store for this
+     * ModelDB. If none is given, it uses its own store.
+     */
+    baseDB?: ModelDB;
+  }
+}

+ 18 - 1
packages/coreutils/src/observablemap.ts

@@ -9,12 +9,21 @@ import {
   ISignal, Signal
 } from '@phosphor/signaling';
 
+import {
+  IObservable
+} from './modeldb';
+
 
 /**
  * A map which can be observed for changes.
  */
 export
-interface IObservableMap<T> extends IDisposable {
+interface IObservableMap<T> extends IDisposable, IObservable {
+  /**
+   * The type of the Observable.
+   */
+  type: 'Map';
+
   /**
    * A signal emitted when the map has changed.
    */
@@ -108,6 +117,14 @@ class ObservableMap<T> implements IObservableMap<T> {
     }
   }
 
+  /**
+   * The type of the Observable.
+   */
+  get type(): 'Map' {
+    return 'Map';
+  }
+
+
   /**
    * A signal emitted when the map has changed.
    */

+ 17 - 1
packages/coreutils/src/observablestring.ts

@@ -9,12 +9,21 @@ import {
   ISignal, Signal
 } from '@phosphor/signaling';
 
+import {
+  IObservable
+} from './modeldb';
+
 
 /**
  * A string which can be observed for changes.
  */
 export
-interface IObservableString extends IDisposable {
+interface IObservableString extends IDisposable, IObservable {
+  /**
+   * The type of the Observable.
+   */
+  type: 'String';
+
   /**
    * A signal emitted when the string has changed.
    */
@@ -67,6 +76,13 @@ class ObservableString implements IObservableString {
     this._text = initialText;
   }
 
+  /**
+   * The type of the Observable.
+   */
+  get type(): 'String' {
+    return 'String';
+  }
+
   /**
    * A signal emitted when the string has changed.
    */

+ 18 - 2
packages/coreutils/src/observablevector.ts

@@ -17,12 +17,21 @@ import {
   Vector
 } from './vector';
 
+import {
+  IObservable
+} from './modeldb';
+
 
 /**
  * A vector which can be observed for changes.
  */
 export
-interface IObservableVector<T> extends IDisposable {
+interface IObservableVector<T> extends IDisposable, IObservable {
+  /**
+   * The type of the Observable.
+   */
+  type: 'Vector';
+
   /**
    * A signal emitted when the vector has changed.
    */
@@ -310,6 +319,13 @@ class ObservableVector<T> extends Vector<T> implements IObservableVector<T> {
     this._itemCmp = options.itemCmp || Private.itemCmp;
   }
 
+  /**
+   * The type of the Observable.
+   */
+  get type(): 'Vector' {
+    return 'Vector';
+  }
+
   /**
    * A signal emitted when the vector has changed.
    */
@@ -547,7 +563,7 @@ class ObservableVector<T> extends Vector<T> implements IObservableVector<T> {
    * A `fromIndex` or a `toIndex` which is non-integral.
    */
   move(fromIndex: number, toIndex: number): void {
-    if(this.length <= 1 || fromIndex === toIndex) {
+    if (this.length <= 1 || fromIndex === toIndex) {
       return;
     }
     let value = this.at(fromIndex);

+ 26 - 0
packages/coreutils/src/undoablevector.ts

@@ -296,3 +296,29 @@ class ObservableUndoableVector<T> extends ObservableVector<T> implements IObserv
   private _stack: ObservableVector.IChangedArgs<JSONValue>[][] = [];
   private _serializer: ISerializer<T> = null;
 }
+
+/**
+ * Namespace for ObservableUndoableVector utilities.
+ */
+export
+namespace ObservableUndoableVector {
+  /**
+   * A default, identity serializer.
+   */
+  export
+  class IdentitySerializer implements ISerializer<JSONValue> {
+    /**
+     * Identity serialize.
+     */
+    toJSON(value: JSONValue): JSONValue {
+      return value;
+    }
+
+    /**
+     * Identity deserialize.
+     */
+    fromJSON(value: JSONValue): JSONValue {
+      return value;
+    }
+  }
+}

+ 5 - 5
packages/docregistry/src/default.ts

@@ -22,7 +22,7 @@ import {
 } from '@jupyterlab/codeeditor';
 
 import {
-  IChangedArgs
+  IChangedArgs, IModelDB
 } from '@jupyterlab/coreutils';
 
 import {
@@ -38,8 +38,8 @@ class DocumentModel extends CodeEditor.Model implements DocumentRegistry.ICodeMo
   /**
    * Construct a new document model.
    */
-  constructor(languagePreference?: string) {
-    super();
+  constructor(languagePreference?: string, modelDB?: IModelDB) {
+    super({modelDB});
     this._defaultLang = languagePreference || '';
     this.value.changed.connect(this.triggerContentChange, this);
   }
@@ -220,8 +220,8 @@ class TextModelFactory implements DocumentRegistry.CodeModelFactory {
    *
    * @returns A new document model.
    */
-  createNew(languagePreference?: string): DocumentRegistry.ICodeModel {
-    return new DocumentModel(languagePreference);
+  createNew(languagePreference?: string, modelDB?: IModelDB): DocumentRegistry.ICodeModel {
+    return new DocumentModel(languagePreference, modelDB);
   }
 
   /**

+ 2 - 3
packages/docregistry/src/registry.ts

@@ -34,10 +34,9 @@ import {
 } from '@jupyterlab/codeeditor';
 
 import {
-  IChangedArgs as IChangedArgsGeneric, PathExt
+  IChangedArgs as IChangedArgsGeneric, PathExt, IModelDB
 } from '@jupyterlab/coreutils';
 
-
 /* tslint:disable */
 /**
  * The document registry token.
@@ -873,7 +872,7 @@ namespace DocumentRegistry {
      *
      * @returns A new document model.
      */
-    createNew(languagePreference?: string): T;
+    createNew(languagePreference?: string, modelDB?: IModelDB): T;
 
     /**
      * Get the preferred kernel language given an extension.

+ 1 - 1
packages/notebook/src/actions.ts

@@ -1109,7 +1109,7 @@ namespace Private {
       // Add a new cell if the notebook is empty. This is done
       // within the compound operation to make the deletion of
       // a notebook's last cell undoable.
-      if(!cells.length) {
+      if (!cells.length) {
         cells.pushBack(model.contentFactory.createCodeCell({}));
       }
       cells.endCompoundOperation();

+ 47 - 28
packages/notebook/src/celllist.ts

@@ -10,14 +10,18 @@ import {
 } from '@phosphor/signaling';
 
 import {
-  IObservableMap, ObservableMap, IObservableVector, ObservableVector,
-  IObservableUndoableVector, ObservableUndoableVector, uuid
+  IObservableMap, ObservableMap, ObservableVector,
+  IObservableUndoableVector, IModelDB
 } from '@jupyterlab/coreutils';
 
 import {
   ICellModel
 } from '@jupyterlab/cells';
 
+import {
+  NotebookModel
+} from './model';
+
 
 /**
  * A cell list object that supports undo/redo.
@@ -27,16 +31,17 @@ class CellList implements IObservableUndoableVector<ICellModel> {
   /**
    * Construct the cell list.
    */
-  constructor() {
-    this._cellOrder = new ObservableUndoableVector<string>({
-      toJSON: (val: string) => { return val; },
-      fromJSON: (val: string) => { return val; }
-    });
+  constructor(modelDB: IModelDB, factory: NotebookModel.IContentFactory) {
+    this._modelDB = modelDB;
+    this._factory = factory;
+    this._cellOrder = modelDB.createVector<string>('cellOrder');
     this._cellMap = new ObservableMap<ICellModel>();
 
     this._cellOrder.changed.connect(this._onOrderChanged, this);
   }
 
+  type: 'Vector';
+
   /**
    * A signal emitted when the cell list has changed.
    */
@@ -203,11 +208,9 @@ class CellList implements IObservableUndoableVector<ICellModel> {
    * not be called by other actors.
    */
   set(index: number, cell: ICellModel): void {
-    // Generate a new uuid for the cell.
-    let id = uuid();
     // Set the internal data structures.
-    this._cellMap.set(id, cell);
-    this._cellOrder.set(index, id);
+    this._cellMap.set(cell.id, cell);
+    this._cellOrder.set(index, cell.id);
   }
 
   /**
@@ -229,11 +232,9 @@ class CellList implements IObservableUndoableVector<ICellModel> {
    * not be called by other actors.
    */
   pushBack(cell: ICellModel): number {
-    // Generate a new uuid for the cell.
-    let id = uuid();
     // Set the internal data structures.
-    this._cellMap.set(id, cell);
-    let num = this._cellOrder.pushBack(id);
+    this._cellMap.set(cell.id, cell);
+    let num = this._cellOrder.pushBack(cell.id);
     return num;
   }
 
@@ -283,11 +284,9 @@ class CellList implements IObservableUndoableVector<ICellModel> {
    * not be called by other actors.
    */
   insert(index: number, cell: ICellModel): number {
-    // Generate a new uuid for the cell.
-    let id = uuid();
     // Set the internal data structures.
-    this._cellMap.set(id, cell);
-    let num = this._cellOrder.insert(index, id);
+    this._cellMap.set(cell.id, cell);
+    let num = this._cellOrder.insert(index, cell.id);
     return num;
   }
 
@@ -390,11 +389,9 @@ class CellList implements IObservableUndoableVector<ICellModel> {
   pushAll(cells: IterableOrArrayLike<ICellModel>): number {
     let newValues = toArray(cells);
     each(newValues, cell => {
-      // Generate a new uuid for the cell.
-      let id = uuid();
       // Set the internal data structures.
-      this._cellMap.set(id, cell);
-      this._cellOrder.pushBack(id);
+      this._cellMap.set(cell.id, cell);
+      this._cellOrder.pushBack(cell.id);
     });
     return this.length;
   }
@@ -428,11 +425,9 @@ class CellList implements IObservableUndoableVector<ICellModel> {
   insertAll(index: number, cells: IterableOrArrayLike<ICellModel>): number {
     let newValues = toArray(cells);
     each(newValues, cell => {
-      // Generate a new uuid for the cell.
-      let id = uuid();
-      this._cellMap.set(id, cell);
+      this._cellMap.set(cell.id, cell);
       this._cellOrder.beginCompoundOperation();
-      this._cellOrder.insert(index++, id);
+      this._cellOrder.insert(index++, cell.id);
       this._cellOrder.endCompoundOperation();
     });
     return this.length;
@@ -523,7 +518,29 @@ class CellList implements IObservableUndoableVector<ICellModel> {
     this._cellOrder.clearUndo();
   }
 
-  private _onOrderChanged(order: IObservableVector<string>, change: ObservableVector.IChangedArgs<string>): void {
+  private _onOrderChanged(order: IObservableUndoableVector<string>, change: ObservableVector.IChangedArgs<string>): void {
+    if (change.type === 'add' || change.type === 'set') {
+      each(change.newValues, (id) => {
+        if (!this._cellMap.has(id)) {
+          let cellDB = this._factory.modelDB;
+          let cellType = cellDB.createValue(id+'.type');
+          let cell: ICellModel;
+          switch (cellType.get()) {
+            case 'code':
+              cell = this._factory.createCodeCell({ id: id});
+              break;
+            case 'markdown':
+              cell = this._factory.createMarkdownCell({ id: id});
+              break;
+            case 'raw':
+            default:
+              cell = this._factory.createRawCell({ id: id});
+              break;
+          }
+          this._cellMap.set(id, cell);
+        }
+      });
+    }
     let newValues: ICellModel[] = [];
     let oldValues: ICellModel[] = [];
     each(change.newValues, (id)=>{
@@ -545,4 +562,6 @@ class CellList implements IObservableUndoableVector<ICellModel> {
   private _cellOrder: IObservableUndoableVector<string> = null;
   private _cellMap: IObservableMap<ICellModel> = null;
   private _changed = new Signal<this, ObservableVector.IChangedArgs<ICellModel>>(this);
+  private _modelDB: IModelDB = null;
+  private _factory: NotebookModel.IContentFactory = null;
 }

+ 68 - 22
packages/notebook/src/model.ts

@@ -15,8 +15,8 @@ import {
 } from '@jupyterlab/cells';
 
 import {
-  IObservableJSON, ObservableJSON, IObservableUndoableVector,
-  IObservableVector, ObservableVector, nbformat
+  IObservableJSON, IObservableUndoableVector, uuid,
+  IObservableVector, ObservableVector, nbformat, IModelDB
 } from '@jupyterlab/coreutils';
 
 import {
@@ -65,21 +65,28 @@ class NotebookModel extends DocumentModel implements INotebookModel {
    * Construct a new notebook model.
    */
   constructor(options: NotebookModel.IOptions = {}) {
-    super(options.languagePreference);
+    super(options.languagePreference, options.modelDB);
     let factory = (
       options.contentFactory || NotebookModel.defaultContentFactory
     );
+    let cellDB = this.modelDB.view('cells');
+    factory.modelDB = cellDB;
     this.contentFactory = factory;
-    this._cells = new CellList();
+    this._cells = new CellList(this.modelDB, this.contentFactory);
     // Add an initial code cell by default.
-    this._cells.pushBack(factory.createCodeCell({}));
+    if (!this._cells.length) {
+      this._cells.pushBack(factory.createCodeCell({}));
+    }
     this._cells.changed.connect(this._onCellsChanged, this);
 
     // Handle initial metadata.
-    let name = options.languagePreference || '';
-    this._metadata.set('language_info', { name });
+    let metadata = this.modelDB.createMap('metadata');
+    if (!metadata.has('language_info')) {
+      let name = options.languagePreference || '';
+      metadata.set('language_info', { name });
+    }
     this._ensureMetadata();
-    this._metadata.changed.connect(this.triggerContentChange, this);
+    metadata.changed.connect(this.triggerContentChange, this);
   }
 
   /**
@@ -91,7 +98,7 @@ class NotebookModel extends DocumentModel implements INotebookModel {
    * The metadata associated with the notebook.
    */
   get metadata(): IObservableJSON {
-    return this._metadata;
+    return this.modelDB.get('metadata') as IObservableJSON;
   }
 
   /**
@@ -119,7 +126,7 @@ class NotebookModel extends DocumentModel implements INotebookModel {
    * The default kernel name of the document.
    */
   get defaultKernelName(): string {
-    let spec = this._metadata.get('kernelspec') as nbformat.IKernelspecMetadata;
+    let spec = this.metadata.get('kernelspec') as nbformat.IKernelspecMetadata;
     return spec ? spec.name : '';
   }
 
@@ -127,7 +134,7 @@ class NotebookModel extends DocumentModel implements INotebookModel {
    * The default kernel language of the document.
    */
   get defaultKernelLanguage(): string {
-    let info = this._metadata.get('language_info') as nbformat.ILanguageInfoMetadata;
+    let info = this.metadata.get('language_info') as nbformat.ILanguageInfoMetadata;
     return info ? info.name : '';
   }
 
@@ -136,12 +143,11 @@ class NotebookModel extends DocumentModel implements INotebookModel {
    */
   dispose(): void {
     // Do nothing if already disposed.
-    if (this._cells === null) {
+    if (this.cells === null) {
       return;
     }
-    let cells = this._cells;
+    let cells = this.cells;
     this._cells = null;
-    this._metadata.dispose();
     cells.dispose();
     super.dispose();
   }
@@ -230,14 +236,14 @@ class NotebookModel extends DocumentModel implements INotebookModel {
       this.triggerStateChange({ name: 'nbformatMinor', oldValue, newValue });
     }
     // Update the metadata.
-    this._metadata.clear();
+    this.metadata.clear();
     let metadata = value.metadata;
     for (let key in metadata) {
       // orig_nbformat is not intended to be stored per spec.
       if (key === 'orig_nbformat') {
         continue;
       }
-      this._metadata.set(key, metadata[key]);
+      this.metadata.set(key, metadata[key]);
     }
     this._ensureMetadata();
     this.dirty = true;
@@ -269,12 +275,12 @@ class NotebookModel extends DocumentModel implements INotebookModel {
     }
     let factory = this.contentFactory;
     // Add code cell if there are no cells remaining.
-    if (!this._cells.length) {
+    if (!this.cells.length) {
       // Add the cell in a new context to avoid triggering another
       // cell changed event during the handling of this signal.
       requestAnimationFrame(() => {
-        if (!this.isDisposed && !this._cells.length) {
-          this._cells.pushBack(factory.createCodeCell({}));
+        if (!this.isDisposed && !this.cells.length) {
+          this.cells.pushBack(factory.createCodeCell({}));
         }
       });
     }
@@ -285,7 +291,7 @@ class NotebookModel extends DocumentModel implements INotebookModel {
    * Make sure we have the required metadata fields.
    */
   private _ensureMetadata(): void {
-    let metadata = this._metadata;
+    let metadata = this.metadata;
     if (!metadata.has('language_info')) {
       metadata.set('language_info', { name: '' });
     }
@@ -294,10 +300,9 @@ class NotebookModel extends DocumentModel implements INotebookModel {
     }
   }
 
-  private _cells: IObservableUndoableVector<ICellModel> = null;
+  private _cells: CellList;
   private _nbformat = nbformat.MAJOR_VERSION;
   private _nbformatMinor = nbformat.MINOR_VERSION;
-  private _metadata = new ObservableJSON();
 }
 
 
@@ -322,6 +327,11 @@ namespace NotebookModel {
      * The default is a shared factory instance.
      */
     contentFactory?: IContentFactory;
+
+    /**
+     * An optional modelDB for storing notebook data.
+     */
+    modelDB?: IModelDB;
   }
 
   /**
@@ -334,6 +344,8 @@ namespace NotebookModel {
      */
     readonly codeCellContentFactory: CodeCellModel.IContentFactory;
 
+    modelDB: IModelDB;
+
     /**
      * Create a new code cell.
      *
@@ -377,6 +389,7 @@ namespace NotebookModel {
       this.codeCellContentFactory = (options.codeCellContentFactory ||
         CodeCellModel.defaultContentFactory
       );
+      this._modelDB = options.modelDB || null;
     }
 
     /**
@@ -384,6 +397,14 @@ namespace NotebookModel {
      */
     readonly codeCellContentFactory: CodeCellModel.IContentFactory;
 
+    get modelDB(): IModelDB {
+      return this._modelDB;
+    }
+
+    set modelDB(db: IModelDB) {
+      this._modelDB = db;
+    }
+
     /**
      * Create a new code cell.
      *
@@ -398,6 +419,12 @@ namespace NotebookModel {
       if (options.contentFactory) {
         options.contentFactory = this.codeCellContentFactory;
       }
+      if (this._modelDB) {
+        if (!options.id) {
+          options.id = uuid();
+        }
+        options.modelDB = this._modelDB.view(options.id);
+      }
       return new CodeCellModel(options);
     }
 
@@ -410,6 +437,12 @@ namespace NotebookModel {
      *   new cell will be intialized with the data from the source.
      */
     createMarkdownCell(options: CellModel.IOptions): IMarkdownCellModel {
+      if (this._modelDB) {
+        if (!options.id) {
+          options.id = uuid();
+        }
+        options.modelDB = this._modelDB.view(options.id);
+      }
       return new MarkdownCellModel(options);
     }
 
@@ -422,8 +455,16 @@ namespace NotebookModel {
      *   new cell will be intialized with the data from the source.
      */
     createRawCell(options: CellModel.IOptions): IRawCellModel {
+      if (this._modelDB) {
+        if (!options.id) {
+          options.id = uuid();
+        }
+        options.modelDB = this._modelDB.view(options.id);
+      }
      return new RawCellModel(options);
     }
+
+    private _modelDB: IModelDB;
   }
 
   /**
@@ -435,6 +476,11 @@ namespace NotebookModel {
      * The factory for code cell model content.
      */
     codeCellContentFactory?: CodeCellModel.IContentFactory;
+
+    /**
+     * The modelDB in which to place new content.
+     */
+    modelDB?: IModelDB;
   }
 
   /**

+ 6 - 2
packages/notebook/src/modelfactory.ts

@@ -5,6 +5,10 @@ import {
   Contents
 } from '@jupyterlab/services';
 
+import {
+  IModelDB
+} from '@jupyterlab/coreutils';
+
 import {
   DocumentRegistry
 } from '@jupyterlab/docregistry';
@@ -80,9 +84,9 @@ class NotebookModelFactory implements DocumentRegistry.IModelFactory<INotebookMo
    *
    * @returns A new document model.
    */
-  createNew(languagePreference?: string): INotebookModel {
+  createNew(languagePreference?: string, modelDB?: IModelDB): INotebookModel {
     let contentFactory = this.contentFactory;
-    return new NotebookModel({ languagePreference, contentFactory });
+    return new NotebookModel({ languagePreference, contentFactory, modelDB });
   }
 
   /**

+ 35 - 1
packages/outputarea/src/model.ts

@@ -10,7 +10,8 @@ import {
 } from '@phosphor/signaling';
 
 import {
-  IObservableVector, ObservableVector, nbformat
+  IObservableVector, ObservableVector, nbformat,
+  IObservableValue, ObservableValue, IModelDB
 } from '@jupyterlab/coreutils';
 
 import {
@@ -40,6 +41,19 @@ class OutputAreaModel implements IOutputAreaModel {
       each(options.values, value => { this._add(value); });
     }
     this.list.changed.connect(this._onListChanged, this);
+
+    // If we are given a IModelDB, keep an up-to-date
+    // serialized copy of the OutputAreaModel in it.
+    if (options.modelDB) {
+      this._modelDB = options.modelDB;
+      this._serialized = this._modelDB.createValue('outputs');
+      if (this._serialized.get()) {
+        this.fromJSON(this._serialized.get() as nbformat.IOutput[]);
+      } else {
+        this._serialized.set(this.toJSON());
+      }
+      this._serialized.changed.connect(this._onSerializedChanged, this);
+    }
   }
 
   /**
@@ -233,10 +247,27 @@ class OutputAreaModel implements IOutputAreaModel {
    * Handle a change to the list.
    */
   private _onListChanged(sender: IObservableVector<IOutputModel>, args: ObservableVector.IChangedArgs<IOutputModel>) {
+    if (this._serialized && !this._changeGuard) {
+      this._changeGuard = true;
+      this._serialized.set(this.toJSON());
+      this._changeGuard = false;
+    }
     this._changed.emit(args);
     this._stateChanged.emit(void 0);
   }
 
+  /**
+   * If the serialized version of the outputs have changed due to a remote
+   * action, then update the model accordingly.
+   */
+  private _onSerializedChanged(sender: IObservableValue, args: ObservableValue.IChangedArgs) {
+    if (!this._changeGuard) {
+      this._changeGuard = true;
+      this.fromJSON(args.newValue as nbformat.IOutput[]);
+      this._changeGuard = false;
+    }
+  }
+
   /**
    * Handle a change to an item.
    */
@@ -250,6 +281,9 @@ class OutputAreaModel implements IOutputAreaModel {
   private _isDisposed = false;
   private _stateChanged = new Signal<IOutputAreaModel, void>(this);
   private _changed = new Signal<this, IOutputAreaModel.ChangedArgs>(this);
+  private _modelDB: IModelDB = null;
+  private _serialized: IObservableValue = null;
+  private _changeGuard = false;
 }
 
 

+ 6 - 1
packages/outputarea/src/widget.ts

@@ -38,7 +38,7 @@ import {
 } from '@jupyterlab/apputils';
 
 import {
-  ObservableVector, nbformat
+  ObservableVector, nbformat, IModelDB
 } from '@jupyterlab/coreutils';
 
 import {
@@ -872,6 +872,11 @@ namespace IOutputAreaModel {
      * If not given, a default factory will be used.
      */
     contentFactory?: IContentFactory;
+
+    /**
+     * An optional IModelDB to store the output area model.
+     */
+    modelDB?: IModelDB;
   }
 
   /**

+ 1 - 1
test/src/coreutils/activitymonitor.spec.ts

@@ -20,7 +20,7 @@ class TestObject {
 }
 
 
-describe('coreutils', () => {
+describe('@jupyterlab/coreutils', () => {
 
   describe('ActivityMonitor()', () => {
 

+ 373 - 0
test/src/coreutils/modeldb.spec.ts

@@ -0,0 +1,373 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import expect = require('expect.js');
+
+import {
+  JSONExt
+} from '@phosphor/coreutils';
+
+import {
+  ModelDB, ObservableString, ObservableValue,
+  ObservableUndoableVector, ObservableJSON
+} from '@jupyterlab/coreutils';
+
+
+describe('@jupyterlab/coreutils', () => {
+
+  describe('ObservableValue', () => {
+
+    describe('#constructor', () => {
+
+      it('should accept no arguments', () => {
+        let value = new ObservableValue();
+        expect(value instanceof ObservableValue).to.be(true);
+        expect(value.get()).to.be(undefined);
+      });
+
+      it('should accept an initial JSON value', () => {
+        let value = new ObservableValue('value');
+        expect(value instanceof ObservableValue).to.be(true);
+        let value2 = new ObservableValue({ one: 'one', two: 2 });
+        expect(value2 instanceof ObservableValue).to.be(true);
+      });
+
+    });
+
+    describe('#type', () => {
+
+      it('should return `Value`', () => {
+        let value = new ObservableValue();
+        expect(value.type).to.be('Value');
+      });
+    });
+
+    describe('#isDisposed', () => {
+
+      it('should test whether the value is disposed', () => {
+        let value = new ObservableValue();
+        expect(value.isDisposed).to.be(false);
+        value.dispose();
+        expect(value.isDisposed).to.be(true);
+      });
+
+    });
+
+    describe('#changed', () => {
+
+      it('should be emitted when the map changes state', () => {
+        let called = false;
+        let value = new ObservableValue();
+        value.changed.connect(() => { called = true; });
+        value.set('set');
+        expect(called).to.be(true);
+      });
+
+      it('should have value changed args', () => {
+        let called = false;
+        let value = new ObservableValue();
+        value.changed.connect((sender, args) => {
+          expect(sender).to.be(value);
+          expect(args.newValue).to.be('set');
+          expect(args.oldValue).to.be(undefined);
+          called = true;
+        });
+        value.set('set');
+        expect(called).to.be(true);
+      });
+
+    });
+
+    describe('#get', () => {
+
+      it('should get the value of the object', () => {
+        let value = new ObservableValue('value');
+        expect(value.get()).to.be('value');
+        let value2 = new ObservableValue({ one: 'one', two: 2 });
+        expect(JSONExt.deepEqual(value2.get(), { one: 'one', two: 2 })).to.be(true);
+      });
+
+    });
+
+    describe('#set', () => {
+
+      it('should set the value of the object', () => {
+        let value = new ObservableValue();
+        value.set('value');
+        expect(value.get()).to.be('value');
+      });
+
+    });
+
+  });
+
+
+  describe('ModelDB', () => {
+
+    describe('#constructor()', () => {
+
+      it('should accept no arguments', () => {
+        let db = new ModelDB();
+        expect(db instanceof ModelDB).to.be(true);
+      });
+
+      it('should accept a basePath', () => {
+        let db = new ModelDB({ basePath: 'base' });
+        expect(db instanceof ModelDB).to.be(true);
+      });
+
+      it('should accept a baseDB', () => {
+        let base = new ModelDB();
+        let db = new ModelDB({ baseDB: base });
+        expect(db instanceof ModelDB).to.be(true);
+      });
+
+    });
+
+    describe('#isDisposed', () => {
+
+      it('should test whether it is disposed', () => {
+        let db = new ModelDB();
+        expect(db.isDisposed).to.be(false);
+        db.dispose();
+        expect(db.isDisposed).to.be(true);
+      });
+
+    });
+
+    describe('#basePath', () => {
+
+      it('should return an empty string for a model without a baseDB', () => {
+        let db = new ModelDB();
+        expect(db.basePath).to.be('');
+      });
+
+      it('should return the base path', () => {
+        let db = new ModelDB({ basePath: 'base' });
+        expect(db.basePath).to.be('base');
+      });
+
+    });
+
+    describe('#isPrepopulated', () => {
+
+      it('should return false for an in-memory database', () => {
+        let db = new ModelDB();
+        expect(db.isPrepopulated).to.be(false);
+      });
+
+    });
+
+    describe('#connected', () => {
+
+      it('should resolve immediately for an in-memory database', (done) => {
+        let db = new ModelDB();
+        db.connected.then(done);
+      });
+
+    });
+
+    describe('#get', () => {
+
+      it('should get a value that exists at a path', () => {
+        let db = new ModelDB();
+        let value = db.createValue('value');
+        let value2 = db.get('value');
+        expect(value2).to.be(value);
+      });
+
+      it('should return undefined for a value that does not exist', () => {
+        let db = new ModelDB();
+        expect(db.get('value')).to.be(undefined);
+      });
+
+    });
+
+    describe('#has', () => {
+
+      it('should return true if a value exists at a path', () => {
+        let db = new ModelDB();
+        let value = db.createValue('value');
+        expect(db.has('value')).to.be(true);
+      });
+
+      it('should return false for a value that does not exist', () => {
+        let db = new ModelDB();
+        expect(db.has('value')).to.be(false);
+      });
+
+    });
+
+    describe('#createString', () => {
+
+      it('should create an ObservableString`', () => {
+        let db = new ModelDB();
+        let str = db.createString('str');
+        expect(str instanceof ObservableString).to.be(true);
+      });
+
+      it('should be able to retrieve that string using `get`', () => {
+        let db = new ModelDB();
+        let str = db.createString('str');
+        expect(db.get('str')).to.be(str);
+      });
+
+    });
+
+    describe('#createVector', () => {
+
+      it('should create an ObservableUndoableVector`', () => {
+        let db = new ModelDB();
+        let str = db.createVector('vec');
+        expect(str instanceof ObservableUndoableVector).to.be(true);
+      });
+
+      it('should be able to retrieve that vector using `get`', () => {
+        let db = new ModelDB();
+        let vec = db.createVector('vec');
+        expect(db.get('vec')).to.be(vec);
+      });
+
+    });
+
+    describe('#createMap', () => {
+
+      it('should create an ObservableMap`', () => {
+        let db = new ModelDB();
+        let map = db.createMap('map');
+        expect(map instanceof ObservableJSON).to.be(true);
+      });
+
+      it('should be able to retrieve that map using `get`', () => {
+        let db = new ModelDB();
+        let map = db.createMap('map');
+        expect(db.get('map')).to.be(map);
+      });
+
+    });
+
+    describe('#createValue', () => {
+
+      it('should create an ObservableValue`', () => {
+        let db = new ModelDB();
+        let value = db.createValue('value');
+        expect(value instanceof ObservableValue).to.be(true);
+      });
+
+      it('should be able to retrieve that value using `get`', () => {
+        let db = new ModelDB();
+        let value = db.createString('value');
+        expect(db.get('value')).to.be(value);
+      });
+
+    });
+
+    describe('#setValue', () => {
+
+      it('should set a value at a path', () => {
+        let db = new ModelDB();
+        let value = db.createValue('value');
+        db.setValue('value', 'set');
+        expect(value.get()).to.be('set');
+      });
+
+    });
+
+    describe('#getValue', () => {
+
+      it('should get a value at a path', () => {
+        let db = new ModelDB();
+        let value = db.createValue('value');
+        value.set('set');
+        expect(db.getValue('value')).to.be('set');
+      });
+
+    });
+
+    describe('#view', () => {
+
+      it('should should return a ModelDB', () => {
+        let db = new ModelDB();
+        let view = db.view('');
+        expect(view instanceof ModelDB).to.be(true);
+        expect(view === db).to.be(false);
+      });
+
+      it('should set the baseDB path on the view', () => {
+        let db = new ModelDB();
+        let view = db.view('base');
+        expect(view.basePath).to.be('base');
+      });
+
+      it('should return a view onto the base ModelDB', () => {
+        let db = new ModelDB();
+        let view = db.view('base');
+
+        db.createString('base.str1');
+        expect(db.get('base.str1')).to.be(view.get('str1'));
+
+        view.createString('str2');
+        expect(db.get('base.str2')).to.be(view.get('str2'));
+      });
+
+      it('should be stackable', () => {
+        let db = new ModelDB();
+        let view = db.view('one');
+        let viewView = view.view('two');
+
+        expect(view.basePath).to.be('one');
+        expect(viewView.basePath).to.be('two');
+
+        viewView.createString('str');
+        expect(viewView.get('str')).to.be(view.get('two.str'));
+        expect(viewView.get('str')).to.be(db.get('one.two.str'));
+      });
+
+    });
+
+    describe('#dispose', () => {
+
+      it('should dispose of the resources used by the model', () => {
+        let db = new ModelDB();
+        let str = db.createString('str');
+        let view = db.view('base');
+        let str2 = view.createString('str');
+        expect(db.isDisposed).to.be(false);
+        expect(str.isDisposed).to.be(false);
+        expect(view.isDisposed).to.be(false);
+        expect(str2.isDisposed).to.be(false);
+        db.dispose();
+        expect(db.isDisposed).to.be(true);
+        expect(str.isDisposed).to.be(true);
+        expect(view.isDisposed).to.be(true);
+        expect(str2.isDisposed).to.be(true);
+      });
+
+      it('should not dispose of resources in base databases', () => {
+        let db = new ModelDB();
+        let view = db.view('base');
+        let str = db.createString('str');
+        let str2 = view.createString('str');
+        expect(db.isDisposed).to.be(false);
+        expect(str.isDisposed).to.be(false);
+        expect(view.isDisposed).to.be(false);
+        expect(str2.isDisposed).to.be(false);
+        view.dispose();
+        expect(view.isDisposed).to.be(true);
+        expect(str2.isDisposed).to.be(true);
+        expect(db.isDisposed).to.be(false);
+        expect(str.isDisposed).to.be(false);
+      });
+
+      it('should be safe to call more than once', () => {
+        let db = new ModelDB();
+        expect(db.isDisposed).to.be(false);
+        db.dispose();
+        expect(db.isDisposed).to.be(true);
+      });
+
+    });
+
+  });
+
+});

+ 1 - 1
test/src/coreutils/observablejson.spec.ts

@@ -8,7 +8,7 @@ import {
 } from '@jupyterlab/coreutils';
 
 
-describe('coreutils', () => {
+describe('@jupyterlab/coreutils', () => {
 
   describe('ObservableJSON', () => {
 

+ 9 - 1
test/src/coreutils/observablemap.spec.ts

@@ -8,7 +8,7 @@ import {
 } from '@jupyterlab/coreutils';
 
 
-describe('common/ObservableMap', () => {
+describe('@jupyterlab/coreutils', () => {
 
   describe('ObservableMap', () => {
 
@@ -20,6 +20,14 @@ describe('common/ObservableMap', () => {
       });
     });
 
+    describe('#type', () => {
+
+      it('should return `Map`', () => {
+        let value = new ObservableMap<number>();
+        expect(value.type).to.be('Map');
+      });
+    });
+
     describe('#size', ()=>{
       it('should return the number of entries in the map', ()=>{
         let value = new ObservableMap<number>();

+ 9 - 1
test/src/coreutils/observablestring.spec.ts

@@ -8,7 +8,7 @@ import {
 } from '@jupyterlab/coreutils';
 
 
-describe('common/ObservableString', () => {
+describe('@jupyterlab/coreutils', () => {
 
   describe('ObservableString', () => {
 
@@ -31,6 +31,14 @@ describe('common/ObservableString', () => {
 
     });
 
+    describe('#type', () => {
+
+      it('should return `String`', () => {
+        let value = new ObservableString();
+        expect(value.type).to.be('String');
+      });
+    });
+
     describe('#changed', () => {
 
       it('should be emitted when the string changes', () => {

+ 10 - 1
test/src/coreutils/observablevector.spec.ts

@@ -12,7 +12,7 @@ import {
 } from '@jupyterlab/coreutils';
 
 
-describe('common/ObservableVector', () => {
+describe('@jupyterlab/coreutils', () => {
 
   describe('ObservableVector', () => {
 
@@ -31,6 +31,15 @@ describe('common/ObservableVector', () => {
 
     });
 
+    describe('#type', () => {
+
+      it('should return `Vector`', () => {
+        let value = new ObservableVector<number>();
+        expect(value.type).to.be('Vector');
+      });
+    });
+
+
     describe('#changed', () => {
 
       it('should be emitted when the vector changes state', () => {

+ 1 - 1
test/src/coreutils/undoablevector.spec.ts

@@ -43,7 +43,7 @@ const serializer = new Serializer();
 const value: JSONObject = { name: 'foo' };
 
 
-describe('notebook/common/undo', () => {
+describe('@jupyterlab/coreutils', () => {
 
   describe('ObservableUndoableVector', () => {
 

+ 1 - 0
test/src/index.ts

@@ -38,6 +38,7 @@ import './console/panel.spec';
 import './console/widget.spec';
 
 import './coreutils/activitymonitor.spec';
+import './coreutils/modeldb.spec';
 import './coreutils/nbformat.spec';
 import './coreutils/observablejson.spec';
 import './coreutils/observablemap.spec';