Permalink
Browse files

Deal with a lot of special cases.

Also check doc against an invariant after a new op has been applied.
  • Loading branch information...
1 parent e0140fd commit 1e67b0cf6aa5b78a3d40d50c663f5bff0dee0432 @michael michael committed Sep 19, 2012
View
Binary file not shown.
View
@@ -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");
+ }
}
@@ -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);
@@ -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;
}
},
@@ -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);
@@ -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) {
@@ -589,9 +666,6 @@ var AnnotatedDocument = _.inherits(Document, {
// Export Module
// --------
-// Export Module
-// --------
-
if (typeof exports !== 'undefined') {
module.exports = {
Document: Document,
File renamed without changes.
@@ -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"
+}
View
@@ -1,61 +0,0 @@
-var ot = require('operational-transformation');
-var _ = require('underscore');
-
-// The classic
-// --------
-
-var operation = new ot.Operation(0)
- .retain(11)
- .insert(" dolor");
-
-var newTxt = operation.apply("lorem ipsum");
-console.log('applied operation:', newTxt);
-
-
-// The JSON
-// --------
-
-var obj = {
- id: '1234',
- revision: 0,
- baseLength: 11,
- targetLength: 17,
- ops: [
- { retain: 11 },
- { insert: " dolor" }
- ]
-};
-
-
-var o = ot.Operation.fromJSON(obj);
-var newTxt = o.apply("lorem ipsum");
-console.log('applied operation:', newTxt);
-
-
-// The Sequence
-// --------
-
-function createOperation(ops) {
- var operation = new ot.Operation(0)
-
- function map(method) {
- if (method === "ret") return "retain";
- if (method === "del") return "delete";
- if (method === "ins") return "insert";
- }
-
- _.each(ops, function(op) {
- operation[map(op[0])](op[1]);
- });
- return operation;
-};
-
-var o = createOperation([["ret", 11], ["ins", " dolor"]]);
-var newTxt = o.apply("lorem ipsum");
-console.log('applied operation:', newTxt);
-
-// var textOp = ["update", {id: "text:1", "delta": "ret(2) ins(l) ret(4) ins(o) ret(3)"}];
-
-var o = createOperation([["ret", 2], ["ins", "l"], ["ret", 4], ["ins", "o"], ["ret", 3]]);
-var newTxt = o.apply("helo wrld");
-console.log('applied operation:', newTxt);
View
@@ -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.