We need a model to represent documents and notebooks that is type-safe and can be backed by a
local or remote source.
It should support change notification and modification at any level of nesting.
It would ideally support undo/redo for all document changes.
-
We have a notebook document model backed by a JSON schema
-
We have an editor model that includes mimeType and selections
-
The live notebook model is a hybrid of the two, where each cell entry is also a sub-document.
-
The rendermime objects need to be able to influence the output data
-
The output area needs to be able to influence the outputs (merging stream data)
-
The cells need to be able to modify everything in at their level and below.
-
We need changes at any of these levels to propagate up the chain to the root model.
-
We need the source of truth of the data to be abstracted - can be local or remote.
-
We need the model to be type-safe.
-
We would like to support undo/redo for the entire document.
A model hierarchy with a cascade of signals, where each model is independent until they are serialized. Model "dirtiness" is handled by propagating changes to the root model. The attributes of the models have their own ad-hoc signals for things like "mimeTypeChanged", making it harder to swap out the backend.
interface IModel<T> {
changed: ISignal<this, keyof T>;
set<K extends keyof T>(key: K, value: T[K]): void;
}
interface IMyParams {
readonly foo: string;
readonly bar: boolean;
}
interface IMyModel extends IModel<IMyParams>, IMyParams {}
We have an abstract JSON store for each model. We have get/set in the JSON store by path. The models used in the application present type-safe views into the JSON store. Use uuids for cells since they can be reordered, and an array of uuids for the order.
- Figure out the mapping of model path to JSON store - should it be in ContentsManager by url?
- Investigate rethinkdb handling of array types (DONE)
- Can only listen to changes to an array as a whole - get an oldval and newval
- Can make changes to array elements
- Present an API that can be implemented by both a simple local store and Google Drive - canUndo, undo, etc.
-
There should only be one DocumentManager and one ContentsManager.
-
There should be an app-level DirectoryHandler which is used by the default file browser, launcher, etc.
-
There can be other DirectoryHandlers that also use the DocumentManager and ContentsManager.
-
This means the "cwd" can be any url that the contents manager understands.
-
The ContentsManager fetches a model for a url - either a string or a JSONObject that is backed by a store. The contents manager enforces the final data to and from disk.
-
We can associate other values with the backend store.
Queries:
- uuid/mimeType - string
- uuid/selections - Map
- uuid/text - string
- notebookUuid/cellUuid/mimeType - string
- notebookUuid/cellUuid/selections - Map
- notebookUuid/cellUuid/text - string
- notebookUUid/cellUUid/outputUUid/data/ - Map
interface IModel {
stateChanged: ISignal<this, void>;
fromJSON(value: JSONObject): void;
toJSON(): JSONObject;
}
interface IEditorModel extends IModel {
readonly mimeType: IObservableValue<string>;
readonly text: IObservableValue<string>;
readonly selections(): IObservableMap<ITextSelection[]>;
}
class EditorModel implements IEditorModel {
constructor(backend: IJSONBackend, path: string[]) {
this.mimeType = new ObservableValue<string>(backend, path.concat('mimeType'));
this.text = new ObservableValue<string>(backend, path.concat('text'))
this.mimeType.changed.connect(this._onChange, this);
this.text.changed.connect(this._onChange, this);
let selectionsPath = path.concat('selections');
this.selections = new ObservableMap(backend, selectionsPath);
this.selections.changed.connect(this._onChange, this);
}
stateChanged: ISignal<this, void>;
readonly mimeType: IObservableValue<string>;
readonly text: IObservableValue<string>;
readonly selections(): IObservableMap<ITextSelection[]>;
fromJSON(value: JSONObject): void {
this.mimeType.setValue(value['mimeType'] || 'text/plain');
this.text.setValue(value['text'] || '');
this.selections.fromJSON(value.selections || {});
}
toJSON(): JSONObject {
return {
"mimeType": this.mimeType.getValue(),
"text": this.text.getValue(),
"selections": this.selections.toJSON()
}
}
private _onChange() {
this.changed.emit());
}
}
What would we need observablelist for?
Currently: list of cells, list of outputs
We can just make the notebook and output area widget arrange as appropriate on a bulk change.
-
We treat the root of a path as its "drive".
-
The contents manager uses models for files that are backed by the appropriate drive - akin to the Windows Explorer mapping network drives.
-
Drives must present a file-hierarchy structure, even if they are implemented in an exotic fashion.
-
Drives are registered on the contents manager - the base handler is for the '/' drive.
-
The contents model has an api for setting/getting nested values, which could notionally be based on a delimited path.
-
The contents model should also have an api for signaling changes, which could nominally be the path, oldValue, and newValue.
-
The contents model should allow for metadata associated with the file, that is not persisted to disk, such as user selections.
-
The contents model should support an undo interface - even if not implemented - canUndo, canRedo, redo, batchOperation, etc.
-
The models built on top of the content model will present type-safe properties and changed signals that call into the contents model.
Example:
interface ICodeCellModel {
changed: ISignal<this, ICellChangedArgs>;
source: string;
metadata: IObservableJSON;
outputs: IObservableMap<IOutput>;
outputOrder: string[];
}
Where ICellChangedArgs is type-safe for the given property name and change. Changes to a simple value would include newValue/oldValue, and to a map would include the key.
Some more thoughts/replies:
This also relates to our discussion here with regards to synchronized-persisted vs synchronized-ephemeral data structures. It is limiting to restrict ourselves to things that are only in the JSON schema.
Do we need this? That is, should change notifications on the model be deep or shallow?
Agreed, though this results in some difficulties at the interface with APIs like the Google Realtime API.
I would also add that, because models tend to be objects containing heterogeneous
data types, this makes static typing in the backend a challenge, since it needs to
understand how to handle synchronization of several different types which
may have different semantics.
In any event, I think that an
IObservableValue<T>
is a good idea.