├── LICENSE ├── README.md ├── apply.js ├── client.js ├── errors.js ├── messages.js ├── operations.js ├── ot.js ├── stores └── memory-store.js ├── tests ├── README ├── amd-shim.js └── tests.js └── xform.js /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Nick Fitzgerald - http://fitzgeraldnick.com/ 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | An implementation of Operational Transformation in JavaScript. 2 | 3 | Node 0.4.3 4 | RequireJS 0.24.0 5 | -------------------------------------------------------------------------------- /apply.js: -------------------------------------------------------------------------------- 1 | // This module defines a function which applies a set of operations which span a 2 | // document, to that document. The resulting document is returned. 3 | 4 | 5 | /*jslint onevar: true, undef: true, eqeqeq: true, bitwise: true, 6 | newcap: true, immed: true, nomen: false, white: false, plusplus: false, 7 | laxbreak: true */ 8 | 9 | /*global define */ 10 | 11 | define(["./operations"], function (operations) { 12 | return function (op, doc) { 13 | var i, 14 | len, 15 | index = 0, 16 | newDoc = ""; 17 | for ( i = 0, len = op.length; i < len; i += 1 ) { 18 | switch ( operations.type(op[i]) ) { 19 | case "retain": 20 | newDoc += doc.slice(0, operations.val(op[i])); 21 | doc = doc.slice(operations.val(op[i])); 22 | break; 23 | case "insert": 24 | newDoc += operations.val(op[i]); 25 | break; 26 | case "delete": 27 | if ( doc.indexOf(operations.val(op[i])) !== 0 ) { 28 | throw new TypeError("Expected '" + operations.val(op[i]) 29 | + "' to delete, found '" + doc.slice(0, 10) 30 | + "'"); 31 | } else { 32 | doc = doc.slice(operations.val(op[i]).length); 33 | } 34 | break; 35 | default: 36 | throw new TypeError("Unknown operation: " 37 | + operations.type(op[i])); 38 | } 39 | } 40 | return newDoc; 41 | }; 42 | }); -------------------------------------------------------------------------------- /client.js: -------------------------------------------------------------------------------- 1 | // This modules defines the behavior of the client's OT. It handles the 2 | // buffering of operations yet to be committed to the master document and the 3 | // responsibility of transforming those operations if the server sends new 4 | // operations before they have all been committed. 5 | // 6 | // While this code is meant to run on the client, it does not contain any DOM 7 | // manipulation, or browser specific code. All of that is abstracted and 8 | // sandboxed within the `ui` option for the `OTDocument` constructor. One could 9 | // fairly easily use some front end other than the browser with little to no 10 | // changes so long as they provide the functions required in the ui parameter. 11 | 12 | 13 | /*jslint onevar: true, undef: true, eqeqeq: true, bitwise: true, 14 | newcap: true, immed: true, nomen: false, white: false, plusplus: false, 15 | laxbreak: true */ 16 | 17 | /*global define, setTimeout */ 18 | 19 | define([ 20 | "./apply", 21 | "./xform", 22 | "./operations", 23 | "./messages" 24 | ], function (apply, xform, operations, messages) { 25 | 26 | function xformEach (outgoing, ops) { 27 | var i, len, msg; 28 | for ( i = 0, len = outgoing.length; i < len; i++ ) { 29 | msg = outgoing[i]; 30 | xform(messages.operation(msg), ops, function (aPrime, bPrime) { 31 | messages.operation(msg, aPrime); 32 | messages.document(msg, aPrime, apply(aPrime, messages.document(msg))); 33 | messages.revision(msg, messages.revision(msg)+1); 34 | ops = bPrime; 35 | }); 36 | } 37 | } 38 | 39 | function error (msg) { 40 | throw new Error(msg); 41 | } 42 | 43 | function connect (socket, docId) { 44 | socket.send({ 45 | type: "connect", 46 | data: { 47 | id: docId 48 | } 49 | }); 50 | } 51 | 52 | // Might need to start sending client id's back and forth. Don't really want 53 | // to have to do a deep equality test on every check here. 54 | function isOurOutgoing (msg, outgoing) { 55 | var top = outgoing[0], 56 | topOps = messages.operation(top), 57 | msgOps = messages.operation(msg), 58 | i = 0, 59 | len = msgOps.length; 60 | if ( messages.id(msg) !== messages.id(top) ) { 61 | return false; 62 | } 63 | if ( messages.revision(msg) !== messages.revision(top) ) { 64 | return false; 65 | } 66 | if ( len !== topOps.length ) { 67 | return false; 68 | } 69 | if ( topOps.join() !== msgOps.join() ) { 70 | return false; 71 | } 72 | return true; 73 | } 74 | 75 | function updateSelection (selection, operation) { 76 | var i = 0, 77 | len = operation.length, 78 | newSelection = { 79 | start: selection.start, 80 | end: selection.end 81 | }, 82 | size; 83 | for ( i = 0; i < len; i++ ) { 84 | if ( operations.isDelete(operation[i]) ) { 85 | size = operations.val(operation[i]).length; 86 | newSelection.start -= size; 87 | newSelection.end -= size; 88 | } else if ( operations.isInsert(operation[i]) ) { 89 | size = operations.val(operation[i]).length; 90 | newSelection.start += size; 91 | newSelection.end += size; 92 | } 93 | } 94 | return newSelection; 95 | } 96 | 97 | function init (outgoing, incoming, socket, ui, initialData) { 98 | var previousDoc = messages.document(initialData), 99 | previousRevision = messages.revision(initialData), 100 | id = messages.id(initialData); 101 | 102 | ui.update(previousDoc); 103 | 104 | function loop () { 105 | var msg, 106 | newSelection, 107 | oldOutgoingLength = outgoing.length, 108 | uiDoc = ui.getDocument(); 109 | 110 | if ( uiDoc !== previousDoc ) { 111 | msg = {}; 112 | messages.operation(msg, operations.operation(previousDoc, uiDoc)); 113 | messages.document(msg, uiDoc); 114 | messages.revision(msg, ++previousRevision); 115 | messages.id(msg, id); 116 | 117 | outgoing.push(msg); 118 | previousDoc = uiDoc; 119 | } 120 | 121 | while ( (msg = incoming.shift()) ) { 122 | if ( outgoing.length && isOurOutgoing(msg, outgoing) ) { 123 | outgoing.shift(); 124 | } else { 125 | xformEach(outgoing, messages.operation(msg)); 126 | previousRevision++; 127 | 128 | newSelection = updateSelection(ui.getSelection(), 129 | messages.operation(msg)); 130 | 131 | if ( outgoing.length ) { 132 | previousDoc = messages.document(outgoing[outgoing.length-1]); 133 | ui.update(previousDoc, newSelection); 134 | } else { 135 | previousDoc = messages.document(msg); 136 | ui.update(previousDoc, newSelection); 137 | } 138 | } 139 | } 140 | 141 | if ( outgoing.length ) { 142 | socket.send({ 143 | type: "update", 144 | data: outgoing[0] 145 | }); 146 | } 147 | 148 | setTimeout(loop, 1000); 149 | } 150 | 151 | setTimeout(loop, 10); 152 | } 153 | 154 | function noop () {} 155 | 156 | return { 157 | OTDocument: function (opts) { 158 | var outgoing = [], 159 | incoming = [], 160 | socket = opts.socket || error("socket is required"), 161 | ui = opts.ui || error("ui is required"), 162 | pubsub = opts.pubsub || { publish: noop, subscribe: noop }, 163 | docId = opts.id, 164 | initialized = false; 165 | 166 | connect(socket, docId); 167 | 168 | socket.onMessage(function (event) { 169 | switch ( event.type ) { 170 | 171 | case "connect": 172 | if ( ! initialized ) { 173 | init(outgoing, incoming, socket, ui, event.data); 174 | pubsub.publish("/ot/connect", [event.data]); 175 | initialized = true; 176 | } else { 177 | pubsub.publish("/ot/error", ["Already initialized"]); 178 | throw new Error("Already initialized"); 179 | } 180 | break; 181 | 182 | case "update": 183 | incoming.push(event.data); 184 | pubsub.publish("/ot/update", [event.data]); 185 | break; 186 | 187 | default: 188 | pubsub.publish("/ot/error", ["Unknown event type", event.type]); 189 | throw new Error("Unknown event type: " + event.type); 190 | 191 | } 192 | }); 193 | } 194 | }; 195 | 196 | }); -------------------------------------------------------------------------------- /errors.js: -------------------------------------------------------------------------------- 1 | // This module provides all of the custom errors defined by this library. 2 | 3 | 4 | /*jslint onevar: true, undef: true, eqeqeq: true, bitwise: true, 5 | newcap: true, immed: true, nomen: false, white: false, plusplus: false, 6 | laxbreak: true */ 7 | 8 | /*global define, setTimeout */ 9 | 10 | define(["util"], function (util) { 11 | 12 | var exports = {}; 13 | 14 | function defineError (name, Parent) { 15 | Parent = Parent || Error; 16 | exports[name] = function (msg) { 17 | Parent.call(this, msg); 18 | }; 19 | util.inherits(exports[name], Parent); 20 | exports[name].prototype.name = name; 21 | } 22 | 23 | defineError("BadRevision"); 24 | defineError("NoSuchDocument"); 25 | 26 | return exports; 27 | 28 | }); -------------------------------------------------------------------------------- /messages.js: -------------------------------------------------------------------------------- 1 | // This modules loosely defines the message protocol used to pass operations and 2 | // documents between the client and server. Mostly it is just a common 3 | // abstraction both the client and server can use when creating, manipulating, 4 | // and using messages. 5 | 6 | 7 | /*jslint onevar: true, undef: true, eqeqeq: true, bitwise: true, 8 | newcap: true, immed: true, nomen: false, white: false, plusplus: false, 9 | laxbreak: true */ 10 | 11 | /*global define */ 12 | 13 | define(function () { 14 | 15 | function defineGetSet (prop) { 16 | return function (obj, val) { 17 | return arguments.length === 2 18 | ? obj[prop] = val 19 | : obj[prop]; 20 | }; 21 | } 22 | 23 | return { 24 | // The 'document' attribute is only defined for server responses to a 25 | // client connect. 26 | document: defineGetSet("doc"), 27 | revision: defineGetSet("rev"), 28 | operation: defineGetSet("op"), 29 | id: defineGetSet("id") 30 | }; 31 | 32 | }); -------------------------------------------------------------------------------- /operations.js: -------------------------------------------------------------------------------- 1 | // Operations are a stream of individual edits which span the whole document 2 | // from start to finish. Edits have a type which is one of retain, insert, or 3 | // delete, and have associated data based on their type. 4 | 5 | 6 | /*jslint onevar: true, undef: true, eqeqeq: true, bitwise: true, 7 | newcap: true, immed: true, nomen: false, white: false, plusplus: false, 8 | laxbreak: true */ 9 | 10 | /*global define */ 11 | 12 | define(function () { 13 | 14 | // Simple edit constructors. 15 | 16 | function insert (chars) { 17 | return "i" + chars; 18 | } 19 | 20 | function del (chars) { 21 | return "d" + chars; 22 | } 23 | 24 | function retain (n) { 25 | return "r" + String(n); 26 | } 27 | 28 | function type (edit) { 29 | switch ( edit.charAt(0) ) { 30 | case "r": 31 | return "retain"; 32 | case "d": 33 | return "delete"; 34 | case "i": 35 | return "insert"; 36 | default: 37 | throw new TypeError("Unknown type of edit: ", edit); 38 | } 39 | } 40 | 41 | function val (edit) { 42 | return type(edit) === "r" 43 | ? Number(edit.slice(1)) 44 | : edit.slice(1); 45 | } 46 | 47 | // We don't want to copy arrays all the time, aren't mutating lists, and 48 | // only need O(1) prepend and length, we can get away with a custom singly 49 | // linked list implementation. 50 | 51 | // TODO: keep track of number of non-retain edits and use this instead of 52 | // length when choosing which path to take. 53 | 54 | var theEmptyList = { 55 | length: 0, 56 | toArray: function () { 57 | return []; 58 | } 59 | }; 60 | 61 | function toArray () { 62 | var node = this, 63 | ary = []; 64 | while ( node !== theEmptyList ) { 65 | ary.push(node.car); 66 | node = node.cdr; 67 | } 68 | return ary; 69 | } 70 | 71 | function cons (car, cdr) { 72 | return { 73 | car: car, 74 | cdr: cdr, 75 | length: 1 + cdr.length, 76 | toArray: toArray 77 | }; 78 | } 79 | 80 | // Abstract out the table in case I want to edit the implementation to 81 | // arrays of arrays or something. 82 | 83 | function put (table, x, y, edits) { 84 | return (table[String(x) + "," + String(y)] = edits); 85 | } 86 | 87 | function get (table, x, y) { 88 | var edits = table[String(x) + "," + String(y)]; 89 | if ( edits ) { 90 | return edits; 91 | } else { 92 | throw new TypeError("No operation at " + String(x) + "," + String(y)); 93 | } 94 | } 95 | 96 | function makeEditsTable (s, t) { 97 | var table = {}, 98 | n = s.length, 99 | m = t.length, 100 | i, 101 | j; 102 | put(table, 0, 0, theEmptyList); 103 | for ( i = 1; i <= m; i += 1 ) { 104 | put(table, i, 0, cons(insert(t.charAt(i-1)), 105 | get(table, i-1, 0))); 106 | } 107 | for ( j = 1; j <= n; j += 1 ) { 108 | put(table, 0, j, cons(del(s.charAt(j-1)), 109 | get(table, 0, j-1))); 110 | } 111 | return table; 112 | } 113 | 114 | function chooseCell (table, x, y, k) { 115 | var prevEdits = get(table, x, y-1), 116 | min = prevEdits.length, 117 | direction = "up"; 118 | 119 | if ( get(table, x-1, y).length < min ) { 120 | prevEdits = get(table, x-1, y); 121 | min = prevEdits.length; 122 | direction = "left"; 123 | } 124 | 125 | if ( get(table, x-1, y-1).length < min ) { 126 | prevEdits = get(table, x-1, y-1); 127 | min = prevEdits.length; 128 | direction = "diagonal"; 129 | } 130 | 131 | return k(direction, prevEdits); 132 | } 133 | 134 | return { 135 | 136 | // Constructor for operations (which are a stream of edits). Uses 137 | // variation of Levenshtein Distance. 138 | operation: function (s, t) { 139 | var n = s.length, 140 | m = t.length, 141 | i, 142 | j, 143 | edits = makeEditsTable(s, t); 144 | 145 | for ( i = 1; i <= m; i += 1 ) { 146 | for ( j = 1; j <= n; j += 1 ) { 147 | chooseCell(edits, i, j, function (direction, prevEdits) { 148 | switch ( direction ) { 149 | case "left": 150 | put(edits, i, j, cons(insert(t.charAt(i-1)), prevEdits)); 151 | break; 152 | case "up": 153 | put(edits, i, j, cons(del(s.charAt(j-1)), prevEdits)); 154 | break; 155 | case "diagonal": 156 | if ( s.charAt(j-1) === t.charAt(i-1) ) { 157 | put(edits, i, j, cons(retain(1), prevEdits)); 158 | } else { 159 | put(edits, i, j, cons(insert(t.charAt(i-1)), 160 | cons(del(s.charAt(j-1)), 161 | prevEdits))); 162 | } 163 | break; 164 | default: 165 | throw new TypeError("Unknown direction."); 166 | } 167 | }); 168 | } 169 | } 170 | 171 | return get(edits, m, n).toArray().reverse(); 172 | }, 173 | 174 | insert: insert, 175 | del: del, 176 | retain: retain, 177 | type: type, 178 | val: val, 179 | 180 | isDelete: function (edit) { 181 | return typeof edit === "object" && type(edit) === "delete"; 182 | }, 183 | 184 | isRetain: function (edit) { 185 | return typeof edit === "object" && type(edit) === "retain"; 186 | }, 187 | 188 | isInsert: function (edit) { 189 | return typeof edit === "object" && type(edit) === "insert"; 190 | } 191 | 192 | }; 193 | 194 | }); 195 | -------------------------------------------------------------------------------- /ot.js: -------------------------------------------------------------------------------- 1 | // This module is the server's equivalent of `./client.js`; that is, it provides 2 | // the high level Operational Transformation API for the server side of 3 | // things. You just need to pass it a store parameter which allows it to get, 4 | // save, and create documents in whatever backend you choose. It is an event 5 | // emitter, and it is assumed that you will listen to these events that it emits 6 | // and have some type of communication layer with the clients to let them know 7 | // of new updates and which operations have been applied to the master document. 8 | 9 | 10 | /*jslint onevar: true, undef: true, eqeqeq: true, bitwise: true, 11 | newcap: true, immed: true, nomen: false, white: false, plusplus: false, 12 | laxbreak: true */ 13 | 14 | /*global define */ 15 | 16 | define([ 17 | 'events', 18 | './messages', 19 | './apply', 20 | './errors' 21 | ], function (events, messages, apply, errors) { 22 | 23 | function nop () {} 24 | 25 | function error (msg) { 26 | throw new Error(msg); 27 | } 28 | 29 | return function (opts) { 30 | var store = opts.store || error('store is required'), 31 | manager = new events.EventEmitter(); 32 | 33 | manager.newDocument = function (callback) { 34 | callback = callback || nop; 35 | store.newDocument(function (err, doc) { 36 | if ( err ) { 37 | this.emit("error", err); 38 | return callback(err, null); 39 | } else { 40 | this.emit("new", doc); 41 | return callback(null, doc); 42 | } 43 | }.bind(this)); 44 | }; 45 | 46 | manager.applyOperation = function (message) { 47 | var id = messages.id(message), 48 | newRev = messages.revision(message), 49 | op = messages.operation(message), 50 | emit = this.emit.bind(this); 51 | 52 | store.getDocument(id, function (err, doc) { 53 | if ( err ) { 54 | emit("error", err); 55 | } else { 56 | if ( newRev === doc.rev+1 ) { 57 | try { 58 | doc.doc = apply(op, doc.doc); 59 | } catch (e) { 60 | emit("error", e); 61 | return; 62 | } 63 | 64 | doc.rev++; 65 | store.saveDocument(doc, function (err, doc) { 66 | var msg; 67 | if ( err ) { 68 | // Bad revisions aren't considered an error at this 69 | // level, just ignored. 70 | if ( ! (err instanceof errors.BadRevision) ) { 71 | emit("error", err); 72 | } 73 | } else { 74 | msg = {}; 75 | messages.revision(msg, doc.rev); 76 | messages.id(msg, doc.id); 77 | messages.operation(msg, op); 78 | messages.document(msg, doc.doc); 79 | emit("update", msg); 80 | } 81 | }); 82 | } 83 | } 84 | }); 85 | }; 86 | 87 | return manager; 88 | }; 89 | 90 | }); -------------------------------------------------------------------------------- /stores/memory-store.js: -------------------------------------------------------------------------------- 1 | /*jslint onevar: true, undef: true, eqeqeq: true, bitwise: true, 2 | newcap: true, immed: true, nomen: false, white: false, plusplus: false, 3 | laxbreak: true */ 4 | 5 | /*global define, setTimeout */ 6 | 7 | define(["../errors"], function (errors) { 8 | 9 | var documents = {}; 10 | 11 | function isPrimitive (obj) { 12 | return obj === null || typeof obj !== "object"; 13 | } 14 | 15 | function deepCopy (obj) { 16 | var copy, k; 17 | if ( isPrimitive(obj) ) { 18 | return obj; 19 | } else { 20 | copy = {}; 21 | for ( k in obj ) { 22 | if ( obj.hasOwnProperty(k) ) { 23 | copy[k] = deepCopy(obj[k]); 24 | } 25 | } 26 | return copy; 27 | } 28 | } 29 | 30 | return { 31 | 32 | newDocument: function (callback) { 33 | setTimeout(function () { 34 | var id; 35 | do { 36 | id = (new Date()).getTime() + Math.floor(Math.random() * 1000); 37 | } while ( id in documents ); 38 | documents[id] = { 39 | id: id, 40 | rev: 0, 41 | doc: "" 42 | }; 43 | callback(null, deepCopy(documents[id])); 44 | }, 10); 45 | }, 46 | 47 | getDocument: function (id, callback) { 48 | setTimeout(function () { 49 | if ( id in documents ) { 50 | callback(null, deepCopy(documents[id])); 51 | } else { 52 | callback(new errors.NoSuchDocument("No document with id = " + id), 53 | null); 54 | } 55 | }, 10); 56 | }, 57 | 58 | saveDocument: function (doc, callback) { 59 | setTimeout(function () { 60 | if ( doc.id in documents ) { 61 | if ( doc.rev === documents[doc.id].rev + 1 ) { 62 | documents[doc.id] = deepCopy(doc); 63 | callback(null, deepCopy(documents[doc.id])); 64 | } else { 65 | callback(new errors.BadRevision("Bad revision"), null); 66 | } 67 | } else { 68 | callback(new errors.NoSuchDocument("No document with id = " + doc.id), 69 | null); 70 | } 71 | }, 10); 72 | } 73 | 74 | }; 75 | 76 | }); -------------------------------------------------------------------------------- /tests/README: -------------------------------------------------------------------------------- 1 | To run the tests, do `node amd-shim.js tests.js`. -------------------------------------------------------------------------------- /tests/amd-shim.js: -------------------------------------------------------------------------------- 1 | // Written by Kris Zyp 2 | 3 | var currentModule, defaultCompile = module.constructor.prototype._compile; 4 | module.constructor.prototype._compile = function(content, filename){ 5 | currentModule = this; 6 | try{ 7 | return defaultCompile.call(this, content, filename); 8 | } 9 | finally { 10 | currentModule = null; 11 | } 12 | }; 13 | define = function (id, injects, factory) { 14 | if (currentModule == null) { 15 | throw new Error("define() may only be called during module factory instantiation"); 16 | } 17 | var module = currentModule; 18 | var req = function(relativeId){ 19 | if(relativeId.charAt(0) === '.'){ 20 | relativeId = id.substring(0, id.lastIndexOf('/') + 1) + relativeId; 21 | while(lastId !== relativeId){ 22 | var lastId = relativeId; 23 | relativeId = relativeId.replace(/\/[^\/]*\/\.\.\//,'/'); 24 | } 25 | relativeId = relativeId.replace(/\/\.\//g,'/'); 26 | } 27 | return require(relativeId); 28 | }; 29 | if (!factory) { 30 | // two or less arguments 31 | factory = injects; 32 | if (factory) { 33 | // two args 34 | if (typeof id === "string") { 35 | if (id !== module.id) { 36 | throw new Error("Can not assign module to a different id than the current file"); 37 | } 38 | // default injects 39 | injects = ["require", "exports", "module"]; 40 | } 41 | else{ 42 | // anonymous, deps included 43 | injects = id; 44 | } 45 | } 46 | else { 47 | // only one arg, just the factory 48 | factory = id; 49 | injects = ["require", "exports", "module"]; 50 | } 51 | } 52 | id = module.id; 53 | if (typeof factory !== "function"){ 54 | // we can just provide a plain object 55 | return module.exports = factory; 56 | } 57 | var returned = factory.apply(module.exports, injects.map(function (injection) { 58 | switch (injection) { 59 | // check for CommonJS injection variables 60 | case "require": return req; 61 | case "exports": return module.exports; 62 | case "module": return module; 63 | default: 64 | // a module dependency 65 | return req(injection); 66 | } 67 | })); 68 | if(returned){ 69 | // since AMD encapsulates a function/callback, it can allow the factory to return the exports. 70 | module.exports = returned; 71 | } 72 | }; 73 | 74 | require("./" + process.argv.pop()); -------------------------------------------------------------------------------- /tests/tests.js: -------------------------------------------------------------------------------- 1 | var ops = require("../operations"); 2 | var xform = require("../xform"); 3 | var apply = require("../apply"); 4 | 5 | var numTests = 0; 6 | var failed = 0; 7 | 8 | function test (original, a, b, expected) { 9 | var operationsA = ops.operation(original, a); 10 | var operationsB = ops.operation(original, b); 11 | xform(operationsA, operationsB, function (ap, bp) { 12 | numTests++; 13 | try { 14 | console.log(original + " -< (" + a + ", " + b +") >- " 15 | + expected); 16 | 17 | var docA = apply(operationsA, original); 18 | var finalA = apply(bp, docA); 19 | console.log(" " + original + " -> " + docA + " -> " 20 | + finalA); 21 | if ( finalA !== expected ) { 22 | throw new Error(finalA + " !== " + expected); 23 | } 24 | 25 | var docB = apply(operationsB, original); 26 | var finalB = apply(ap, docB); 27 | console.log(" " + original + " -> " + docB + " -> " 28 | + finalB); 29 | if ( finalB !== expected ) { 30 | throw new Error(finalB + " !== " + expected); 31 | } 32 | } catch (e) { 33 | failed++; 34 | console.log(" ERROR: " + e.message); 35 | } 36 | }); 37 | } 38 | 39 | test("at", "t", "fat", "ft"); 40 | test("nick", "Nick", "nick is cool", "Nick is cool"); 41 | test("sudo", "sumo", "suo", "sumo"); 42 | test("hello", "Hello", "Hello", "Hello"); 43 | test("care", "are", "are", "are"); 44 | test("air", "fair", "lair", "flair"); 45 | 46 | console.log(numTests - failed + " / " + numTests + " tests passed."); 47 | -------------------------------------------------------------------------------- /xform.js: -------------------------------------------------------------------------------- 1 | // This module defines the `xform` function which is at the heart of OT. 2 | 3 | 4 | /*jslint onevar: true, undef: true, eqeqeq: true, bitwise: true, 5 | newcap: true, immed: true, nomen: false, white: false, plusplus: false, 6 | laxbreak: true */ 7 | 8 | /*global define */ 9 | 10 | define(["./operations"], function (ops) { 11 | 12 | // Pattern match on two edits by looking up their transforming function in 13 | // the `xformTable`. Each function in the table should take arguments like 14 | // the following: 15 | // 16 | // xformer(editA, editB, indexA, indexB, continuation) 17 | // 18 | // and should return the results by calling the continuation 19 | // 20 | // return continuation(editAPrime || null, editBPrime || null, newIndexA, newIndexB); 21 | 22 | var xformTable = {}; 23 | 24 | function join (a, b) { 25 | return a + "," + b; 26 | } 27 | 28 | // Define a transformation function for when we are comparing two edits of 29 | // typeA and typeB. 30 | function defXformer (typeA, typeB, xformer) { 31 | xformTable[join(typeA, typeB)] = xformer; 32 | } 33 | 34 | // Assumptions currently made by all of the xformer functions: that all of 35 | // the individual edits only deal with one character at a time. 36 | 37 | defXformer("retain", "retain", function (editA, editB, indexA, indexB, k) { 38 | k(editA, editB, indexA+1, indexB+1); 39 | }); 40 | 41 | defXformer("delete", "delete", function (editA, editB, indexA, indexB, k) { 42 | if ( ops.val(editA) === ops.val(editB) ) { 43 | k(null, null, indexA+1, indexB+1); 44 | } else { 45 | throw new TypeError("Document state mismatch: delete(" 46 | + ops.val(editA) + ") !== delete(" + ops.val(editB) + ")"); 47 | } 48 | }); 49 | 50 | defXformer("insert", "insert", function (editA, editB, indexA, indexB, k) { 51 | if ( ops.val(editA) === ops.val(editB) ) { 52 | k(ops.retain(1), ops.retain(1), indexA+1, indexB+1); 53 | } else { 54 | k(editA, ops.retain(1), indexA+1, indexB); 55 | } 56 | }); 57 | 58 | defXformer("retain", "delete", function (editA, editB, indexA, indexB, k) { 59 | k(null, editB, indexA+1, indexB+1); 60 | }); 61 | 62 | defXformer("delete", "retain", function (editA, editB, indexA, indexB, k) { 63 | k(editA, null, indexA+1, indexB+1); 64 | }); 65 | 66 | defXformer("insert", "retain", function (editA, editB, indexA, indexB, k) { 67 | k(editA, editB, indexA+1, indexB); 68 | }); 69 | 70 | defXformer("retain", "insert", function (editA, editB, indexA, indexB, k) { 71 | k(editA, editB, indexA, indexB+1); 72 | }); 73 | 74 | defXformer("insert", "delete", function (editA, editB, indexA, indexB, k) { 75 | k(editA, ops.retain(1), indexA+1, indexB); 76 | }); 77 | 78 | defXformer("delete", "insert", function (editA, editB, indexA, indexB, k) { 79 | k(ops.retain(1), editB, indexA, indexB+1); 80 | }); 81 | 82 | return function (operationA, operationB, k) { 83 | var operationAPrime = [], 84 | operationBPrime = [], 85 | lenA = operationA.length, 86 | lenB = operationB.length, 87 | indexA = 0, 88 | indexB = 0, 89 | editA, 90 | editB, 91 | xformer; 92 | 93 | // Continuation for the xformer. 94 | function kk (aPrime, bPrime, newIndexA, newIndexB) { 95 | indexA = newIndexA; 96 | indexB = newIndexB; 97 | if ( aPrime ) { 98 | operationAPrime.push(aPrime); 99 | } 100 | if ( bPrime ) { 101 | operationBPrime.push(bPrime); 102 | } 103 | } 104 | 105 | while ( indexA < lenA && indexB < lenB ) { 106 | editA = operationA[indexA]; 107 | editB = operationB[indexB]; 108 | xformer = xformTable[join(ops.type(editA), ops.type(editB))]; 109 | if ( xformer ) { 110 | xformer(editA, editB, indexA, indexB, kk); 111 | } else { 112 | throw new TypeError("Unknown combination to transform: " 113 | + join(ops.type(editA), ops.type(editB))); 114 | } 115 | } 116 | 117 | // If either operation contains more edits than the other, we just 118 | // pass them on to the prime version. 119 | 120 | for ( ; indexA < lenA; indexA++ ) { 121 | operationAPrime.push(operationA[indexA]); 122 | operationBPrime.push(ops.retain(1)); 123 | } 124 | 125 | for ( ; indexB < lenB; indexB++ ) { 126 | operationBPrime.push(operationB[indexB]); 127 | operationAPrime.push(ops.retain(1)); 128 | } 129 | 130 | return k(operationAPrime, operationBPrime); 131 | }; 132 | 133 | }); --------------------------------------------------------------------------------