UNPKG

7.24 kBJavaScriptView Raw
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
6var types = require('./types').map;
7var ShareDBError = require('./error');
8var util = require('./util');
9
10var ERROR_CODE = ShareDBError.CODES;
11
12// Returns an error string on failure. Rockin' it C style.
13exports.checkOp = function(op) {
14 if (op == null || typeof op !== 'object') {
15 return new ShareDBError(ERROR_CODE.ERR_OT_OP_BADLY_FORMED, 'Op must be an object');
16 }
17
18 if (op.create != null) {
19 if (typeof op.create !== 'object') {
20 return new ShareDBError(ERROR_CODE.ERR_OT_OP_BADLY_FORMED, 'Create data must be an object');
21 }
22 var typeName = op.create.type;
23 if (typeof typeName !== 'string') {
24 return new ShareDBError(ERROR_CODE.ERR_OT_OP_BADLY_FORMED, 'Missing create type');
25 }
26 var type = types[typeName];
27 if (type == null || typeof type !== 'object') {
28 return new ShareDBError(ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED, 'Unknown type');
29 }
30 } else if (op.del != null) {
31 if (op.del !== true) return new ShareDBError(ERROR_CODE.ERR_OT_OP_BADLY_FORMED, 'del value must be true');
32 } else if (op.op == null) {
33 return new ShareDBError(ERROR_CODE.ERR_OT_OP_BADLY_FORMED, 'Missing op, create, or del');
34 }
35
36 if (op.src != null && typeof op.src !== 'string') {
37 return new ShareDBError(ERROR_CODE.ERR_OT_OP_BADLY_FORMED, 'src must be a string');
38 }
39 if (op.seq != null && typeof op.seq !== 'number') {
40 return new ShareDBError(ERROR_CODE.ERR_OT_OP_BADLY_FORMED, 'seq must be a string');
41 }
42 if (
43 (op.src == null && op.seq != null) ||
44 (op.src != null && op.seq == null)
45 ) {
46 return new ShareDBError(ERROR_CODE.ERR_OT_OP_BADLY_FORMED, 'Both src and seq must be set together');
47 }
48
49 if (op.m != null && typeof op.m !== 'object') {
50 return new ShareDBError(ERROR_CODE.ERR_OT_OP_BADLY_FORMED, 'op.m must be an object or null');
51 }
52};
53
54// Takes in a string (type name or URI) and returns the normalized name (uri)
55exports.normalizeType = function(typeName) {
56 return types[typeName] && types[typeName].uri;
57};
58
59// This is the super apply function that takes in snapshot data (including the
60// type) and edits it in-place. Returns an error or null for success.
61exports.apply = function(snapshot, op) {
62 if (typeof snapshot !== 'object') {
63 return new ShareDBError(ERROR_CODE.ERR_APPLY_SNAPSHOT_NOT_PROVIDED, 'Missing snapshot');
64 }
65 if (snapshot.v != null && op.v != null && snapshot.v !== op.v) {
66 return new ShareDBError(ERROR_CODE.ERR_APPLY_OP_VERSION_DOES_NOT_MATCH_SNAPSHOT, 'Version mismatch');
67 }
68
69 // Create operation
70 if (op.create) {
71 if (snapshot.type) return new ShareDBError(ERROR_CODE.ERR_DOC_ALREADY_CREATED, 'Document already exists');
72
73 // The document doesn't exist, although it might have once existed
74 var create = op.create;
75 var type = types[create.type];
76 if (!type) return new ShareDBError(ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED, 'Unknown type');
77
78 try {
79 snapshot.data = type.create(create.data);
80 snapshot.type = type.uri;
81 snapshot.v++;
82 } catch (err) {
83 return err;
84 }
85
86 // Delete operation
87 } else if (op.del) {
88 snapshot.data = undefined;
89 snapshot.type = null;
90 snapshot.v++;
91
92 // Edit operation
93 } else if (op.op) {
94 var err = applyOpEdit(snapshot, op.op);
95 if (err) return err;
96 snapshot.v++;
97
98 // No-op, and we don't have to do anything
99 } else {
100 snapshot.v++;
101 }
102};
103
104function applyOpEdit(snapshot, edit) {
105 if (!snapshot.type) return new ShareDBError(ERROR_CODE.ERR_DOC_DOES_NOT_EXIST, 'Document does not exist');
106
107 if (edit == null) return new ShareDBError(ERROR_CODE.ERR_OT_OP_NOT_PROVIDED, 'Missing op');
108 var type = types[snapshot.type];
109 if (!type) return new ShareDBError(ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED, 'Unknown type');
110
111 try {
112 snapshot.data = type.apply(snapshot.data, edit);
113 } catch (err) {
114 return err;
115 }
116}
117
118exports.transform = function(type, op, appliedOp) {
119 // There are 16 cases this function needs to deal with - which are all the
120 // combinations of create/delete/op/noop from both op and appliedOp
121 if (op.v != null && op.v !== appliedOp.v) {
122 return new ShareDBError(ERROR_CODE.ERR_OP_VERSION_MISMATCH_DURING_TRANSFORM, 'Version mismatch');
123 }
124
125 if (appliedOp.del) {
126 if (op.create || op.op) {
127 return new ShareDBError(ERROR_CODE.ERR_DOC_WAS_DELETED, 'Document was deleted');
128 }
129 } else if (
130 (appliedOp.create && (op.op || op.create || op.del)) ||
131 (appliedOp.op && op.create)
132 ) {
133 // If appliedOp.create is not true, appliedOp contains an op - which
134 // also means the document exists remotely.
135 return new ShareDBError(ERROR_CODE.ERR_DOC_ALREADY_CREATED, 'Document was created remotely');
136 } else if (appliedOp.op && op.op) {
137 // If we reach here, they both have a .op property.
138 if (!type) return new ShareDBError(ERROR_CODE.ERR_DOC_DOES_NOT_EXIST, 'Document does not exist');
139
140 if (typeof type === 'string') {
141 type = types[type];
142 if (!type) return new ShareDBError(ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED, 'Unknown type');
143 }
144
145 try {
146 op.op = type.transform(op.op, appliedOp.op, 'left');
147 } catch (err) {
148 return err;
149 }
150 }
151
152 if (op.v != null) op.v++;
153};
154
155/**
156 * Apply an array of ops to the provided snapshot.
157 *
158 * @param snapshot - a Snapshot object which will be mutated by the provided ops
159 * @param ops - an array of ops to apply to the snapshot
160 * @return an error object if applicable
161 */
162exports.applyOps = function(snapshot, ops) {
163 var type = null;
164
165 if (snapshot.type) {
166 type = types[snapshot.type];
167 if (!type) return new ShareDBError(ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED, 'Unknown type');
168 }
169
170 for (var index = 0; index < ops.length; index++) {
171 var op = ops[index];
172
173 snapshot.v = op.v + 1;
174
175 if (op.create) {
176 type = types[op.create.type];
177 if (!type) return new ShareDBError(ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED, 'Unknown type');
178 snapshot.data = type.create(op.create.data);
179 snapshot.type = type.uri;
180 } else if (op.del) {
181 snapshot.data = undefined;
182 type = null;
183 snapshot.type = null;
184 } else {
185 snapshot.data = type.apply(snapshot.data, op.op);
186 }
187 }
188};
189
190exports.transformPresence = function(presence, op, isOwnOp) {
191 var opError = this.checkOp(op);
192 if (opError) return opError;
193
194 var type = presence.t;
195 if (typeof type === 'string') {
196 type = types[type];
197 }
198 if (!type) return {code: ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED, message: 'Unknown type'};
199 if (!util.supportsPresence(type)) {
200 return {code: ERROR_CODE.ERR_TYPE_DOES_NOT_SUPPORT_PRESENCE, message: 'Type does not support presence'};
201 }
202
203 if (op.create || op.del) {
204 presence.p = null;
205 presence.v++;
206 return;
207 }
208
209 try {
210 presence.p = presence.p === null ?
211 null :
212 type.transformPresence(presence.p, op.op, isOwnOp);
213 } catch (error) {
214 return {code: ERROR_CODE.ERR_PRESENCE_TRANSFORM_FAILED, message: error.message || error};
215 }
216
217 presence.v++;
218};