1 | var ot = require('./ot');
|
2 | var projections = require('./projections');
|
3 | var ShareDBError = require('./error');
|
4 |
|
5 | var ERROR_CODE = ShareDBError.CODES;
|
6 |
|
7 | function SubmitRequest(backend, agent, index, id, op, options) {
|
8 | this.backend = backend;
|
9 | this.agent = agent;
|
10 |
|
11 | var projection = backend.projections[index];
|
12 | this.index = index;
|
13 | this.projection = projection;
|
14 | this.collection = (projection) ? projection.target : index;
|
15 | this.id = id;
|
16 | this.op = op;
|
17 | this.options = options;
|
18 |
|
19 | this.start = Date.now();
|
20 | this._addOpMeta();
|
21 |
|
22 |
|
23 | this.action = null;
|
24 |
|
25 | this.custom = {};
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 | this.saveMilestoneSnapshot = null;
|
32 | this.suppressPublish = backend.suppressPublish;
|
33 | this.maxRetries = backend.maxSubmitRetries;
|
34 | this.retries = 0;
|
35 |
|
36 |
|
37 | this.snapshot = null;
|
38 | this.ops = [];
|
39 | this.channels = null;
|
40 | }
|
41 | module.exports = SubmitRequest;
|
42 |
|
43 | SubmitRequest.prototype.submit = function(callback) {
|
44 | var request = this;
|
45 | var backend = this.backend;
|
46 | var collection = this.collection;
|
47 | var id = this.id;
|
48 | var op = this.op;
|
49 |
|
50 |
|
51 | var fields = {$submit: true};
|
52 |
|
53 | backend.db.getSnapshot(collection, id, fields, null, function(err, snapshot) {
|
54 | if (err) return callback(err);
|
55 |
|
56 | request.snapshot = snapshot;
|
57 | request._addSnapshotMeta();
|
58 |
|
59 | if (op.v == null) {
|
60 | if (op.create && snapshot.type && op.src) {
|
61 |
|
62 |
|
63 |
|
64 |
|
65 |
|
66 |
|
67 |
|
68 | backend.db.getCommittedOpVersion(collection, id, snapshot, op, null, function(err, version) {
|
69 | if (err) return callback(err);
|
70 | if (version == null) {
|
71 | callback(request.alreadyCreatedError());
|
72 | } else {
|
73 | op.v = version;
|
74 | callback(request.alreadySubmittedError());
|
75 | }
|
76 | });
|
77 | return;
|
78 | }
|
79 |
|
80 |
|
81 |
|
82 |
|
83 |
|
84 | op.v = snapshot.v;
|
85 | }
|
86 |
|
87 | if (op.v === snapshot.v) {
|
88 |
|
89 |
|
90 | return request.apply(callback);
|
91 | }
|
92 |
|
93 | if (op.v > snapshot.v) {
|
94 |
|
95 |
|
96 | return callback(request.newerVersionError());
|
97 | }
|
98 |
|
99 |
|
100 | var from = op.v;
|
101 | backend.db.getOpsToSnapshot(collection, id, from, snapshot, null, function(err, ops) {
|
102 | if (err) return callback(err);
|
103 |
|
104 | if (ops.length !== snapshot.v - from) {
|
105 | return callback(request.missingOpsError());
|
106 | }
|
107 |
|
108 | err = request._transformOp(ops);
|
109 | if (err) return callback(err);
|
110 |
|
111 | if (op.v !== snapshot.v) {
|
112 |
|
113 |
|
114 | return callback(request.versionAfterTransformError());
|
115 | }
|
116 |
|
117 | request.apply(callback);
|
118 | });
|
119 | });
|
120 | };
|
121 |
|
122 | SubmitRequest.prototype.apply = function(callback) {
|
123 |
|
124 | var projection = this.projection;
|
125 | if (projection && !projections.isOpAllowed(this.snapshot.type, projection.fields, this.op)) {
|
126 | return callback(this.projectionError());
|
127 | }
|
128 |
|
129 |
|
130 |
|
131 | this.channels = this.backend.getChannels(this.collection, this.id);
|
132 |
|
133 | var request = this;
|
134 | this.backend.trigger('apply', this.agent, this, function(err) {
|
135 | if (err) return callback(err);
|
136 |
|
137 |
|
138 | err = ot.apply(request.snapshot, request.op);
|
139 | if (err) return callback(err);
|
140 |
|
141 | request.commit(callback);
|
142 | });
|
143 | };
|
144 |
|
145 | SubmitRequest.prototype.commit = function(callback) {
|
146 | var request = this;
|
147 | var backend = this.backend;
|
148 | backend.trigger('commit', this.agent, this, function(err) {
|
149 | if (err) return callback(err);
|
150 |
|
151 |
|
152 | backend.db.commit(
|
153 | request.collection,
|
154 | request.id,
|
155 | request.op,
|
156 | request.snapshot,
|
157 | request.options,
|
158 | function(err, succeeded) {
|
159 | if (err) return callback(err);
|
160 | if (!succeeded) {
|
161 |
|
162 |
|
163 | return request.retry(callback);
|
164 | }
|
165 | if (!request.suppressPublish) {
|
166 | var op = request.op;
|
167 | op.c = request.collection;
|
168 | op.d = request.id;
|
169 | op.m = undefined;
|
170 |
|
171 |
|
172 | if (request.collection !== request.index) op.i = request.index;
|
173 | backend.pubsub.publish(request.channels, op);
|
174 | }
|
175 | if (request._shouldSaveMilestoneSnapshot(request.snapshot)) {
|
176 | request.backend.milestoneDb.saveMilestoneSnapshot(request.collection, request.snapshot);
|
177 | }
|
178 | callback();
|
179 | });
|
180 | });
|
181 | };
|
182 |
|
183 | SubmitRequest.prototype.retry = function(callback) {
|
184 | this.retries++;
|
185 | if (this.maxRetries != null && this.retries > this.maxRetries) {
|
186 | return callback(this.maxRetriesError());
|
187 | }
|
188 | this.backend.emit('timing', 'submit.retry', Date.now() - this.start, this);
|
189 | this.submit(callback);
|
190 | };
|
191 |
|
192 | SubmitRequest.prototype._transformOp = function(ops) {
|
193 | var type = this.snapshot.type;
|
194 | for (var i = 0; i < ops.length; i++) {
|
195 | var op = ops[i];
|
196 |
|
197 | if (this.op.src && this.op.src === op.src && this.op.seq === op.seq) {
|
198 |
|
199 |
|
200 |
|
201 |
|
202 | return this.alreadySubmittedError();
|
203 | }
|
204 |
|
205 | if (this.op.v !== op.v) {
|
206 | return this.versionDuringTransformError();
|
207 | }
|
208 |
|
209 | var err = ot.transform(type, this.op, op);
|
210 | if (err) return err;
|
211 | this.ops.push(op);
|
212 | }
|
213 | };
|
214 |
|
215 | SubmitRequest.prototype._addOpMeta = function() {
|
216 | this.op.m = {
|
217 | ts: this.start
|
218 | };
|
219 | if (this.op.create) {
|
220 |
|
221 | this.op.create.type = ot.normalizeType(this.op.create.type);
|
222 | }
|
223 | };
|
224 |
|
225 | SubmitRequest.prototype._addSnapshotMeta = function() {
|
226 | var meta = this.snapshot.m || (this.snapshot.m = {});
|
227 | if (this.op.create) {
|
228 | meta.ctime = this.start;
|
229 | } else if (this.op.del) {
|
230 | this.op.m.data = this.snapshot.data;
|
231 | }
|
232 | meta.mtime = this.start;
|
233 | };
|
234 |
|
235 | SubmitRequest.prototype._shouldSaveMilestoneSnapshot = function(snapshot) {
|
236 |
|
237 | if (this.saveMilestoneSnapshot === null) {
|
238 | return snapshot && snapshot.v % this.backend.milestoneDb.interval === 0;
|
239 | }
|
240 |
|
241 | return this.saveMilestoneSnapshot;
|
242 | };
|
243 |
|
244 |
|
245 | SubmitRequest.prototype.alreadySubmittedError = function() {
|
246 | return new ShareDBError(ERROR_CODE.ERR_OP_ALREADY_SUBMITTED, 'Op already submitted');
|
247 | };
|
248 | SubmitRequest.prototype.rejectedError = function() {
|
249 | return new ShareDBError(ERROR_CODE.ERR_OP_SUBMIT_REJECTED, 'Op submit rejected');
|
250 | };
|
251 |
|
252 | SubmitRequest.prototype.alreadyCreatedError = function() {
|
253 | return new ShareDBError(ERROR_CODE.ERR_DOC_ALREADY_CREATED, 'Invalid op submitted. Document already created');
|
254 | };
|
255 | SubmitRequest.prototype.newerVersionError = function() {
|
256 | return new ShareDBError(
|
257 | ERROR_CODE.ERR_OP_VERSION_NEWER_THAN_CURRENT_SNAPSHOT,
|
258 | 'Invalid op submitted. Op version newer than current snapshot'
|
259 | );
|
260 | };
|
261 | SubmitRequest.prototype.projectionError = function() {
|
262 | return new ShareDBError(
|
263 | ERROR_CODE.ERR_OP_NOT_ALLOWED_IN_PROJECTION,
|
264 | 'Invalid op submitted. Operation invalid in projected collection'
|
265 | );
|
266 | };
|
267 |
|
268 | SubmitRequest.prototype.missingOpsError = function() {
|
269 | return new ShareDBError(
|
270 | ERROR_CODE.ERR_SUBMIT_TRANSFORM_OPS_NOT_FOUND,
|
271 | 'Op submit failed. DB missing ops needed to transform it up to the current snapshot version'
|
272 | );
|
273 | };
|
274 | SubmitRequest.prototype.versionDuringTransformError = function() {
|
275 | return new ShareDBError(
|
276 | ERROR_CODE.ERR_OP_VERSION_MISMATCH_DURING_TRANSFORM,
|
277 | 'Op submit failed. Versions mismatched during op transform'
|
278 | );
|
279 | };
|
280 | SubmitRequest.prototype.versionAfterTransformError = function() {
|
281 | return new ShareDBError(
|
282 | ERROR_CODE.ERR_OP_VERSION_MISMATCH_AFTER_TRANSFORM,
|
283 | 'Op submit failed. Op version mismatches snapshot after op transform'
|
284 | );
|
285 | };
|
286 | SubmitRequest.prototype.maxRetriesError = function() {
|
287 | return new ShareDBError(
|
288 | ERROR_CODE.ERR_MAX_SUBMIT_RETRIES_EXCEEDED,
|
289 | 'Op submit failed. Exceeded max submit retries of ' + this.maxRetries
|
290 | );
|
291 | };
|