Skip to content

Commit

Permalink
Rewrote ot.coffee -> javascript
Browse files Browse the repository at this point in the history
  • Loading branch information
josephg committed Jul 30, 2013
1 parent 06bf8b7 commit 188156f
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 127 deletions.
127 changes: 0 additions & 127 deletions lib/ot.coffee

This file was deleted.

135 changes: 135 additions & 0 deletions lib/ot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// This contains the master OT functions for the database. They look like
// ot-types style operational transform functions, but they're a bit different.
// These functions understand versions and can deal with out of bound create &
// delete operations.

var otTypes = require('ottypes');

// Default validation function
var defaultValidate = function() {};

// Returns an error string on failure. Rockin' it C style.
exports.checkOpData = function(opData) {
if (typeof opData !== 'object') return 'Missing opData';
if (typeof (opData.op || opData.create) !== 'object' && opData.del !== true) return 'Missing op1';
if (opData.create && typeof opData.create.type !== 'string') return 'Missing create type';

if ((opData.src != null) && typeof opData.src !== 'string') return 'Invalid src';
if ((opData.seq != null) && typeof opData.seq !== 'number') return 'Invalid seq';
if (!!opData.seq !== !!opData.src) return 'seq but not src';
};

exports.normalize = function(opData) {
// I'd love to also normalize opData.op if it exists, but I don't know the
// type of the operation. And I can't find that out until after transforming
// the operation anyway.
if (opData.create) {
// Store the full URI of the type, not just its short name
return opData.create.type = otTypes[opData.create.type].uri;
}
};

// This is the super apply function that takes in snapshot data (including the
// type) and edits it in-place. Returns an error string or null for success.
exports.apply = function(data, opData) {
var err;

if (typeof opData !== 'object')
return 'Missing data';
if (!(typeof (opData.op || opData.create) === 'object' || opData.del === true))
return 'Missing op';

if ((data.v != null) && (opData.v != null) && data.v !== opData.v)
return 'Version mismatch';

var validate = opData.validate || defaultValidate;
var preValidate = opData.preValidate || defaultValidate;

if (opData.create) { // Create operations
if (data.type) return 'Document already exists';

// The document doesn't exist, although it might have once existed.
var create = opData.create;
var type = otTypes[create.type];
if (!type) return "Type not found";

if ((err = preValidate(opData, data))) return err;

var snapshot = type.create(create.data);
data.data = snapshot;
data.type = type.uri;
data.v++;

if ((err = validate(opData, data))) return err;

} else if (opData.del) { // Delete operations
if ((err = preValidate(opData, data))) return err;

opData.prev = {data:data.data, type:data.type};
delete data.data;
delete data.type;
data.v++;
if ((err = validate(opData, data))) return err;

} else { // Edit operations
if (!data.type) return 'Document does not exist';

var op = opData.op;
if (typeof op !== 'object') return 'Missing op';
var type = otTypes[data.type];
if (!type) return 'Type not found';

try {
// This shattering stuff is a little bit dodgy. Its important because it
// lets the OT type apply the operation incrementally, which means the
// operation can be validated piecemeal. (Even though the entire
// operation is accepted or rejected wholesale). Racer uses this, but I'm
// still not entirely sure its the right API.
var atomicOps = type.shatter ? type.shatter(op) : [op];
for (var i = 0; i < atomicOps.length; i++) {
var atom = atomicOps[i];
opData.op = atom;
if ((err = preValidate(opData, data))) return err;

// !! The money line.
data.data = type.apply(data.data, atom);

if ((err = validate(opData, data))) return err;
}
// Make sure to restore the operation before returning.
opData.op = op;

} catch (err) {
console.log(err.stack);
return err.message;
}
data.v++;
}
};

exports.transform = function(type, opData, appliedOpData) {
if (appliedOpData.del) {
if (!opData.del) return 'Document was deleted';

if (opData.v != null) {
opData.v++;
}
return;
}

if (appliedOpData.create) return 'Document created remotely';

if (!type) return 'Document does not exist';

if ((opData.v != null) && opData.v !== appliedOpData.v)
return 'Version mismatch';

if (typeof type === 'string') {
type = otTypes[type];
if (!type) return "Type not found";
}

opData.op = type.transform(opData.op, appliedOpData.op, 'left');
if (opData.v != null) opData.v++;
};

4 changes: 4 additions & 0 deletions test/ot.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ describe 'ot', ->
assert.ok ot.checkOpData {del:true, v:5, seq:123}
assert.ok ot.checkOpData {del:true, v:5, src:'hi', seq:'there'}

it 'fails if a create operation is missing its type', ->
assert.ok ot.checkOpData {create:{}}
assert.ok ot.checkOpData {create:123}

it 'accepts valid create operations', ->
assert.equal null, ot.checkOpData {create:{type:simple.uri}}
assert.equal null, ot.checkOpData {create:{type:simple.uri, data:'hi there'}}
Expand Down

0 comments on commit 188156f

Please sign in to comment.