diff --git a/lib/ot.coffee b/lib/ot.coffee deleted file mode 100644 index da888f62..00000000 --- a/lib/ot.coffee +++ /dev/null @@ -1,127 +0,0 @@ -# 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. - -otTypes = require 'ottypes' - -# Returns an error string on failure. -exports.checkOpData = (opData) -> - return 'Missing opData' unless typeof opData is 'object' - return 'Missing op1' if typeof (opData.op or opData.create) isnt 'object' and opData.del isnt true - return 'Missing create type' if opData.create and typeof opData.create.type isnt 'string' - - return 'Invalid src' if opData.src? and typeof opData.src isnt 'string' - return 'Invalid seq' if opData.seq? and typeof opData.seq isnt 'number' - return 'seq but not src' if !!opData.seq isnt !!opData.src - -exports.normalize = (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 - # We should store the full URI of the type, not just its short name - opData.create.type = otTypes[opData.create.type].uri - -defaultValidate = -> - -# 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 = (data, opData) -> - #console.log 'apply', data, opData - return 'Missing data' unless typeof opData is 'object' - return 'Missing op' unless typeof (opData.op or opData.create) is 'object' or opData.del is true - - return 'Version mismatch' if data.v? && opData.v? and data.v != opData.v - - validate = opData.validate or defaultValidate - preValidate = opData.preValidate or defaultValidate - - if opData.create - return 'Document already exists' if data.type - - # The document doesn't exist, although it might have once existed. Here we will only allow creates. - create = opData.create - - type = otTypes[create.type] - return "Type not found" unless type - - err = preValidate opData, data - return err if err - - snapshot = type.create create.data - - data.data = snapshot - data.type = type.uri - data.v++ - - err = validate opData, data - return err if err - - else if opData.del - err = preValidate opData, data - return err if err - - opData.prev = {data:data.data, type:data.type} - delete data.data - delete data.type - data.v++ - - err = validate opData, data - return err if err - - else - return 'Document does not exist' unless data.type - # Apply the op data - op = opData.op - return 'Missing op' unless typeof op is 'object' - type = otTypes[data.type] - return 'Type not found' unless type - - try - atomicOps = if type.shatter then type.shatter op else [op] - - for atom in atomicOps - # kinda dodgy. - opData.op = atom - err = preValidate opData, data - return err if err - - data.data = type.apply data.data, atom - - err = validate opData, data - return err if err - - # Make sure to restore the original operation before we return. - opData.op = op - - catch e - console.log e.stack - return e.message - data.v++ - - return - -exports.transform = (type, opData, appliedOpData) -> - if appliedOpData.del - if opData.del - opData.v++ if opData.v? - return - else - return 'Document was deleted' - - return 'Document created remotely' if appliedOpData.create # type will be null / undefined in this case. - - return 'Document does not exist' unless type - - return 'Version mismatch' if opData.v? and opData.v != appliedOpData.v - - if typeof type is 'string' - type = otTypes[type] - return "Type not found" unless type - - opData.op = type.transform opData.op, appliedOpData.op, 'left' - opData.v++ if opData.v? - - return - - diff --git a/lib/ot.js b/lib/ot.js new file mode 100644 index 00000000..d7273ec4 --- /dev/null +++ b/lib/ot.js @@ -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++; +}; + diff --git a/test/ot.coffee b/test/ot.coffee index d96fa119..f312f5cf 100644 --- a/test/ot.coffee +++ b/test/ot.coffee @@ -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'}}