Skip to content

Commit

Permalink
Deal with a lot of special cases.
Browse files Browse the repository at this point in the history
Also check doc against an invariant after a new op has been applied.
  • Loading branch information
Michael Aufreiter committed Sep 19, 2012
1 parent e0140fd commit 1e67b0c
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 78 deletions.
Binary file removed assets/.DS_Store
Binary file not shown.
108 changes: 91 additions & 17 deletions document.js
Expand Up @@ -219,10 +219,70 @@ _.Events.bind = _.Events.on;
_.Events.unbind = _.Events.off;


// Invariant, that must hold after each operation
// --------

function verifyState(doc, operation, oldDoc) {

// Ensure condition
function ensure(condition, message) {
if (!condition) {

console.log('Trouble with ', operation);
console.log(operation.op[1].nodes);
console.log('before-state', oldDoc);

// Check old state
console.log('after-state', doc);

throw message;
}
}

// Correctly linked?
// -------------

if (Object.keys(doc.nodes).length > 0) {

function node(id) {
return doc.nodes[id];
}

var nodes = [];
var current = node(doc.head);

function verifyState(doc) {
// TODO: implement state checker that raises an error once the doc state gets invalid
nodes.push(current.id);

while (current.id !== doc.tail) {
ensure(current.next, "missing next pointer at node "+ current.id);
var next = node(current.next);
ensure(next, "node "+current.next+" does not exist");
current = next;
nodes.push(current.id);
}

var reverseNodes = [];
var current = node(doc.tail);
reverseNodes.push(current.id);

while (current.id !== doc.head) {
ensure(current.prev, "missing prev pointer at node "+ current.id);
var prev = node(current.prev);
ensure(prev, "node "+current.prev+" does not exist");
current = prev;
reverseNodes.push(current.id);
}

// console.log("FORWARD NODES", nodes);
// console.log("REVERSE NODES", reverseNodes);

ensure(nodes.length === Object.keys(doc.nodes).length, "Unreachable nodes, walk doesn't cover all nodes");
ensure(_.isEqual(nodes, reverseNodes.reverse()), "forward and reverse walks don't match.");
ensure(nodes.length === reverseNodes.length, "forward and reverse walks don't match.");

} else {
ensure(!doc.head && !doc.tail, "head/tail points to a node that does not exist");
}
}


Expand Down Expand Up @@ -350,8 +410,12 @@ var Document = function(options) {

// Apply a given operation on the current document state
apply: function(operation, silent) {
// TODO: this might slow things done, it's for debug purposes
var prevState = JSON.parse(JSON.stringify(this.content));
Document.methods[operation.op[0]](this.content, operation.op[1]);
verifyState(this); // This is a checker for state verification

verifyState(this.content, operation, prevState); // This is a checker for state verification

if (!silent) {
this.commit(operation);
this.trigger('operation:applied', operation);
Expand Down Expand Up @@ -476,13 +540,18 @@ Document.methods = {
doc.tail = newNode.id;
} else {
// This goes after the target node
var targetNode = doc.nodes[options.target];
newNode.prev = targetNode.id;
newNode.next = targetNode.next;
targetNode.next = newNode.id;
var t = doc.nodes[options.target]; // Target
var tn = doc.nodes[doc.nodes[options.target].next]; // Target-next

newNode.prev = t.id;
newNode.next = t.next;
t.next = newNode.id;

// Fix back reference of target-next
if (tn) tn.prev = newNode.id;

// Update tail reference if necessary
if (targetNode.id === doc.tail) doc.tail = newNode.id;
if (t.id === doc.tail) doc.tail = newNode.id;
}
},

Expand All @@ -498,20 +567,27 @@ Document.methods = {
},

move: function(doc, options) {
var f = doc.nodes[_.first(options.nodes)], // first node of selection
l = doc.nodes[_.last(options.nodes)], // last node of selection
t = doc.nodes[options.target], // target node
fp = doc.nodes[f.prev], // first-previous
ln = doc.nodes[l.next]; // last-next
var f = doc.nodes[_.first(options.nodes)], // First node of selection
l = doc.nodes[_.last(options.nodes)], // Last node of selection
t = doc.nodes[options.target], // Target
fp = doc.nodes[f.prev], // First-previous
ln = doc.nodes[l.next]; // Last-next

if (t) var tn = doc.nodes[t.next]; // target-next

// Special case last node is tail node
if (l.id === doc.tail) doc.tail = f.prev;

// Move to the front
if (options.target === "front") {
var oldHead = doc.nodes[doc.head];
oldHead.prev = l.id;
oldHead.next = l.next;
l.next = doc.head;
doc.head = f.id;
f.prev = null;
} else {

t.next = f.id;
t.prev = t.prev === l.id ? (fp ? fp.id : null)
: (t.prev ? t.prev : null);
Expand All @@ -534,9 +610,10 @@ Document.methods = {

if (tn) {
tn.prev = l.id;
} else { // Special case: target is tail node
} else if (t && t.id === doc.tail) { // Special case: target is tail node
doc.tail = l.id;
}

},

delete: function(doc, options) {
Expand Down Expand Up @@ -586,9 +663,6 @@ var AnnotatedDocument = _.inherits(Document, {
});


// Export Module
// --------

// Export Module
// --------

Expand Down
File renamed without changes.
57 changes: 57 additions & 0 deletions test/fixtures/empty_document.json
@@ -0,0 +1,57 @@
{
"document": {
"refs": {
"master": "op-3",
"patch-1": "op-4"
},
"operations": {
"op-1": {
"op": ["insert", {"id": "section:hello", "type": "section", "properties": {"content": "Hello?"}}],
"user": "michael",
"parent": null
},

"op-2": {
"op": ["insert", {"id": "text:hello", "type": "text", "target": "section:hello", "properties": {"content": "Helo wrld"}}],
"user": "michael",
"parent": "op-1"
},

"op-3": {
"op": ["insert", {"id": "text:p1", "type": "text", "target": "text:hello", "properties": {"content": "Ein erster Paragraph."}}],
"user": "michael",
"parent": "op-2"
},

"op-4": {
"op": ["insert", {"id": "text:outro", "type": "text", "target": "text:hello", "properties": {"content": "This is the end."}}],
"user": "michael",
"parent": "op-3"
}
}
},
"annotations": {
"refs": {
"master": "third-op",
"op-3": "third-op"
},
"operations": {
"first-op": {
"op": ["insert", {"id": "annotation:1", "type": "em", "properties": {"nodes": ["text:hello"], "pos": [0,4]}}],
"user": "michael",
"parent": null
},
"second-op": {
"op": ["insert", {"id": "annotation:2", "node": "text:hello", "type": "comment", "properties": {"content": "Hello", "nodes": ["text:hello"], "pos": [5,9]} }],
"user": "michael",
"parent": "first-op"
},
"third-op": {
"op": ["insert", {"id": "annotation:3", "node": "text:hello", "type": "comment", "properties": {"content": "USS New Ironsides was a wooden-hulled broadside ironclad built for the United States Navy during the American Civil War.", "nodes": ["text:p1"], "pos": [0,4]} }],
"user": "john",
"parent": "second-op"
}
}
},
"ref": "master"
}
61 changes: 0 additions & 61 deletions test/ot.js

This file was deleted.

11 changes: 11 additions & 0 deletions test/test.js
@@ -0,0 +1,11 @@
var _ = require('underscore');
var fs = require('fs');
var assert = require('assert');
var Document = require('../document').Document;
var schema = JSON.parse(fs.readFileSync(__dirname+ '/../data/substance.json', 'utf-8'));

var emptyDoc = JSON.parse(fs.readFileSync(__dirname+ '/fixtures/busy_doc.json', 'utf-8'));

// Just runs all the ops and checks the doc state against an invariant all the time.
var doc = new Document(emptyDoc, schema);

0 comments on commit 1e67b0c

Please sign in to comment.