|
| 1 | +// This contains the master OT functions for the database. They look like |
| 2 | +// ot-types style operational transform functions, but they're a bit different. |
| 3 | +// These functions understand versions and can deal with out of bound create & |
| 4 | +// delete operations. |
| 5 | + |
| 6 | +var otTypes = require('ottypes'); |
| 7 | + |
| 8 | +// Default validation function |
| 9 | +var defaultValidate = function() {}; |
| 10 | + |
| 11 | +// Returns an error string on failure. Rockin' it C style. |
| 12 | +exports.checkOpData = function(opData) { |
| 13 | + if (typeof opData !== 'object') return 'Missing opData'; |
| 14 | + if (typeof (opData.op || opData.create) !== 'object' && opData.del !== true) return 'Missing op1'; |
| 15 | + if (opData.create && typeof opData.create.type !== 'string') return 'Missing create type'; |
| 16 | + |
| 17 | + if ((opData.src != null) && typeof opData.src !== 'string') return 'Invalid src'; |
| 18 | + if ((opData.seq != null) && typeof opData.seq !== 'number') return 'Invalid seq'; |
| 19 | + if (!!opData.seq !== !!opData.src) return 'seq but not src'; |
| 20 | +}; |
| 21 | + |
| 22 | +exports.normalize = function(opData) { |
| 23 | + // I'd love to also normalize opData.op if it exists, but I don't know the |
| 24 | + // type of the operation. And I can't find that out until after transforming |
| 25 | + // the operation anyway. |
| 26 | + if (opData.create) { |
| 27 | + // Store the full URI of the type, not just its short name |
| 28 | + return opData.create.type = otTypes[opData.create.type].uri; |
| 29 | + } |
| 30 | +}; |
| 31 | + |
| 32 | +// This is the super apply function that takes in snapshot data (including the |
| 33 | +// type) and edits it in-place. Returns an error string or null for success. |
| 34 | +exports.apply = function(data, opData) { |
| 35 | + var err; |
| 36 | + |
| 37 | + if (typeof opData !== 'object') |
| 38 | + return 'Missing data'; |
| 39 | + if (!(typeof (opData.op || opData.create) === 'object' || opData.del === true)) |
| 40 | + return 'Missing op'; |
| 41 | + |
| 42 | + if ((data.v != null) && (opData.v != null) && data.v !== opData.v) |
| 43 | + return 'Version mismatch'; |
| 44 | + |
| 45 | + var validate = opData.validate || defaultValidate; |
| 46 | + var preValidate = opData.preValidate || defaultValidate; |
| 47 | + |
| 48 | + if (opData.create) { // Create operations |
| 49 | + if (data.type) return 'Document already exists'; |
| 50 | + |
| 51 | + // The document doesn't exist, although it might have once existed. |
| 52 | + var create = opData.create; |
| 53 | + var type = otTypes[create.type]; |
| 54 | + if (!type) return "Type not found"; |
| 55 | + |
| 56 | + if ((err = preValidate(opData, data))) return err; |
| 57 | + |
| 58 | + var snapshot = type.create(create.data); |
| 59 | + data.data = snapshot; |
| 60 | + data.type = type.uri; |
| 61 | + data.v++; |
| 62 | + |
| 63 | + if ((err = validate(opData, data))) return err; |
| 64 | + |
| 65 | + } else if (opData.del) { // Delete operations |
| 66 | + if ((err = preValidate(opData, data))) return err; |
| 67 | + |
| 68 | + opData.prev = {data:data.data, type:data.type}; |
| 69 | + delete data.data; |
| 70 | + delete data.type; |
| 71 | + data.v++; |
| 72 | + if ((err = validate(opData, data))) return err; |
| 73 | + |
| 74 | + } else { // Edit operations |
| 75 | + if (!data.type) return 'Document does not exist'; |
| 76 | + |
| 77 | + var op = opData.op; |
| 78 | + if (typeof op !== 'object') return 'Missing op'; |
| 79 | + var type = otTypes[data.type]; |
| 80 | + if (!type) return 'Type not found'; |
| 81 | + |
| 82 | + try { |
| 83 | + // This shattering stuff is a little bit dodgy. Its important because it |
| 84 | + // lets the OT type apply the operation incrementally, which means the |
| 85 | + // operation can be validated piecemeal. (Even though the entire |
| 86 | + // operation is accepted or rejected wholesale). Racer uses this, but I'm |
| 87 | + // still not entirely sure its the right API. |
| 88 | + var atomicOps = type.shatter ? type.shatter(op) : [op]; |
| 89 | + for (var i = 0; i < atomicOps.length; i++) { |
| 90 | + var atom = atomicOps[i]; |
| 91 | + opData.op = atom; |
| 92 | + if ((err = preValidate(opData, data))) return err; |
| 93 | + |
| 94 | + // !! The money line. |
| 95 | + data.data = type.apply(data.data, atom); |
| 96 | + |
| 97 | + if ((err = validate(opData, data))) return err; |
| 98 | + } |
| 99 | + // Make sure to restore the operation before returning. |
| 100 | + opData.op = op; |
| 101 | + |
| 102 | + } catch (err) { |
| 103 | + console.log(err.stack); |
| 104 | + return err.message; |
| 105 | + } |
| 106 | + data.v++; |
| 107 | + } |
| 108 | +}; |
| 109 | + |
| 110 | +exports.transform = function(type, opData, appliedOpData) { |
| 111 | + if (appliedOpData.del) { |
| 112 | + if (!opData.del) return 'Document was deleted'; |
| 113 | + |
| 114 | + if (opData.v != null) { |
| 115 | + opData.v++; |
| 116 | + } |
| 117 | + return; |
| 118 | + } |
| 119 | + |
| 120 | + if (appliedOpData.create) return 'Document created remotely'; |
| 121 | + |
| 122 | + if (!type) return 'Document does not exist'; |
| 123 | + |
| 124 | + if ((opData.v != null) && opData.v !== appliedOpData.v) |
| 125 | + return 'Version mismatch'; |
| 126 | + |
| 127 | + if (typeof type === 'string') { |
| 128 | + type = otTypes[type]; |
| 129 | + if (!type) return "Type not found"; |
| 130 | + } |
| 131 | + |
| 132 | + opData.op = type.transform(opData.op, appliedOpData.op, 'left'); |
| 133 | + if (opData.v != null) opData.v++; |
| 134 | +}; |
| 135 | + |
0 commit comments