Skip to content

Commit 188156f

Browse files
committed
Rewrote ot.coffee -> javascript
1 parent 06bf8b7 commit 188156f

File tree

3 files changed

+139
-127
lines changed

3 files changed

+139
-127
lines changed

lib/ot.coffee

Lines changed: 0 additions & 127 deletions
This file was deleted.

lib/ot.js

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
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+

test/ot.coffee

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ describe 'ot', ->
2828
assert.ok ot.checkOpData {del:true, v:5, seq:123}
2929
assert.ok ot.checkOpData {del:true, v:5, src:'hi', seq:'there'}
3030

31+
it 'fails if a create operation is missing its type', ->
32+
assert.ok ot.checkOpData {create:{}}
33+
assert.ok ot.checkOpData {create:123}
34+
3135
it 'accepts valid create operations', ->
3236
assert.equal null, ot.checkOpData {create:{type:simple.uri}}
3337
assert.equal null, ot.checkOpData {create:{type:simple.uri, data:'hi there'}}

0 commit comments

Comments
 (0)