• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

share / sharedb / 6549902929

17 Oct 2023 04:09PM CUT coverage: 97.51%. Remained the same
6549902929

push

github

alecgibson
4.1.1

1567 of 1778 branches covered (0.0%)

3368 of 3454 relevant lines covered (97.51%)

867.29 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.56
/lib/submit-request.js
1
var ot = require('./ot');
3✔
2
var projections = require('./projections');
3✔
3
var ShareDBError = require('./error');
3✔
4
var types = require('./types');
3✔
5

6
var ERROR_CODE = ShareDBError.CODES;
3✔
7

8
function SubmitRequest(backend, agent, index, id, op, options) {
9
  this.backend = backend;
3,072✔
10
  this.agent = agent;
3,072✔
11
  // If a projection, rewrite the call into a call against the collection
12
  var projection = backend.projections[index];
3,072✔
13
  this.index = index;
3,072✔
14
  this.projection = projection;
3,072✔
15
  this.collection = (projection) ? projection.target : index;
3,072✔
16
  this.id = id;
3,072✔
17
  this.op = op;
3,072✔
18
  this.options = options;
3,072✔
19

20
  this.extra = op.x;
3,072✔
21
  delete op.x;
3,072✔
22

23
  this.start = Date.now();
3,072✔
24
  this._addOpMeta();
3,072✔
25

26
  // Set as this request is sent through middleware
27
  this.action = null;
3,072✔
28
  // For custom use in middleware
29
  this.custom = {};
3,072✔
30

31
  // Whether or not to store a milestone snapshot. If left as null, the milestone
32
  // snapshots are saved according to the interval provided to the milestone db
33
  // options. If overridden to a boolean value, then that value is used instead of
34
  // the interval logic.
35
  this.saveMilestoneSnapshot = null;
3,072✔
36
  this.suppressPublish = backend.suppressPublish;
3,072✔
37
  this.maxRetries = backend.maxSubmitRetries;
3,072✔
38
  this.retries = 0;
3,072✔
39

40
  // return values
41
  this.snapshot = null;
3,072✔
42
  this.ops = [];
3,072✔
43
  this.channels = null;
3,072✔
44
  this._fixupOps = [];
3,072✔
45
}
46
module.exports = SubmitRequest;
3✔
47

48
SubmitRequest.prototype.$fixup = function(op) {
3✔
49
  if (this.action !== this.backend.MIDDLEWARE_ACTIONS.apply) {
48✔
50
    throw new ShareDBError(
3✔
51
      ERROR_CODE.ERR_FIXUP_IS_ONLY_VALID_ON_APPLY,
52
      'fixup can only be called during the apply middleware'
53
    );
54
  }
55

56
  if (this.op.del) {
45✔
57
    throw new ShareDBError(
3✔
58
      ERROR_CODE.ERR_CANNOT_FIXUP_DELETION,
59
      'fixup cannot be applied on deletion ops'
60
    );
61
  }
62

63
  var typeId = this.op.create ? this.op.create.type : this.snapshot.type;
42✔
64
  var type = types.map[typeId];
42✔
65
  if (typeof type.compose !== 'function') {
42✔
66
    throw new ShareDBError(
3✔
67
      ERROR_CODE.ERR_TYPE_DOES_NOT_SUPPORT_COMPOSE,
68
      typeId + ' does not support compose'
69
    );
70
  }
71

72
  if (this.op.create) this.op.create.data = type.apply(this.op.create.data, op);
39✔
73
  else this.op.op = type.compose(this.op.op, op);
36✔
74

75
  var fixupOp = {
39✔
76
    src: this.op.src,
77
    seq: this.op.seq,
78
    v: this.op.v,
79
    op: op
80
  };
81

82
  this._fixupOps.push(fixupOp);
39✔
83
};
84

85
SubmitRequest.prototype.submit = function(callback) {
3✔
86
  var request = this;
3,039✔
87
  var backend = this.backend;
3,039✔
88
  var collection = this.collection;
3,039✔
89
  var id = this.id;
3,039✔
90
  var op = this.op;
3,039✔
91
  // Send a special projection so that getSnapshot knows to return all fields.
92
  // With a null projection, it strips document metadata
93
  var fields = {$submit: true};
3,039✔
94

95
  var snapshotOptions = {};
3,039✔
96
  snapshotOptions.agentCustom = request.agent.custom;
3,039✔
97
  backend.db.getSnapshot(collection, id, fields, snapshotOptions, function(err, snapshot) {
3,039✔
98
    if (err) return callback(err);
3,039!
99

100
    request.snapshot = snapshot;
3,039✔
101
    request._addSnapshotMeta();
3,039✔
102

103
    if (op.v == null) {
3,039✔
104
      if (op.create && snapshot.type && op.src) {
1,872✔
105
        // If the document was already created by another op, we will return a
106
        // 'Document already exists' error in response and fail to submit this
107
        // op. However, this could also happen in the case that the op was
108
        // already committed and the create op was simply resent. In that
109
        // case, we should return a non-fatal 'Op already submitted' error. We
110
        // must get the past ops and check their src and seq values to
111
        // differentiate.
112
        backend.db.getCommittedOpVersion(collection, id, snapshot, op, null, function(err, version) {
9✔
113
          if (err) return callback(err);
9!
114
          if (version == null) {
9!
115
            callback(request.alreadyCreatedError());
9✔
116
          } else {
117
            op.v = version;
×
118
            callback(request.alreadySubmittedError());
×
119
          }
120
        });
121
        return;
9✔
122
      }
123

124
      // Submitting an op with a null version means that it should get the
125
      // version from the latest snapshot. Generally this will mean the op
126
      // won't be transformed, though transform could be called on it in the
127
      // case of a retry from a simultaneous submit
128
      op.v = snapshot.v;
1,863✔
129
    }
130

131
    if (op.v === snapshot.v) {
3,030✔
132
      // The snapshot hasn't changed since the op's base version. Apply
133
      // without transforming the op
134
      return request.apply(callback);
2,967✔
135
    }
136

137
    if (op.v > snapshot.v) {
63✔
138
      // The op version should be from a previous snapshot, so it should never
139
      // never exceed the current snapshot's version
140
      return callback(request.newerVersionError());
3✔
141
    }
142

143
    // Transform the op up to the current snapshot version, then apply
144
    var from = op.v;
60✔
145
    backend.db.getOpsToSnapshot(collection, id, from, snapshot, {metadata: true}, function(err, ops) {
60✔
146
      if (err) return callback(err);
60!
147

148
      if (ops.length !== snapshot.v - from) {
60✔
149
        return callback(request.missingOpsError());
3✔
150
      }
151

152
      err = request._transformOp(ops);
57✔
153
      if (err) return callback(err);
57✔
154

155
      if (op.v !== snapshot.v) {
45!
156
        // This shouldn't happen, but is just a final sanity check to make
157
        // sure we have transformed the op to the current snapshot version
158
        return callback(request.versionAfterTransformError());
×
159
      }
160

161
      request.apply(callback);
45✔
162
    });
163
  });
164
};
165

166
SubmitRequest.prototype.apply = function(callback) {
3✔
167
  // If we're being projected, verify that the op is allowed
168
  var projection = this.projection;
3,012✔
169
  if (projection && !projections.isOpAllowed(this.snapshot.type, projection.fields, this.op)) {
3,012✔
170
    return callback(this.projectionError());
9✔
171
  }
172

173
  // Always set the channels before each attempt to apply. If the channels are
174
  // modified in a middleware and we retry, we want to reset to a new array
175
  this.channels = this.backend.getChannels(this.collection, this.id);
3,003✔
176
  this._fixupOps = [];
3,003✔
177
  delete this.op.m.fixup;
3,003✔
178

179
  var request = this;
3,003✔
180
  this.backend.trigger(this.backend.MIDDLEWARE_ACTIONS.apply, this.agent, this, function(err) {
3,003✔
181
    if (err) return callback(err);
3,000✔
182

183
    // Apply the submitted op to the snapshot
184
    err = ot.apply(request.snapshot, request.op);
2,988✔
185
    if (err) return callback(err);
2,988✔
186

187
    request.commit(callback);
2,985✔
188
  });
189
};
190

191
SubmitRequest.prototype.commit = function(callback) {
3✔
192
  var request = this;
2,985✔
193
  var backend = this.backend;
2,985✔
194
  backend.trigger(backend.MIDDLEWARE_ACTIONS.commit, this.agent, this, function(err) {
2,985✔
195
    if (err) return callback(err);
2,970✔
196
    if (request._fixupOps.length) request.op.m.fixup = request._fixupOps;
2,967✔
197

198
    // Try committing the operation and snapshot to the database atomically
199
    backend.db.commit(
2,967✔
200
      request.collection,
201
      request.id,
202
      request.op,
203
      request.snapshot,
204
      request.options,
205
      function(err, succeeded) {
206
        if (err) return callback(err);
2,967!
207
        if (!succeeded) {
2,967✔
208
          // Between our fetch and our call to commit, another client committed an
209
          // operation. We expect this to be relatively infrequent but normal.
210
          return request.retry(callback);
21✔
211
        }
212
        if (!request.suppressPublish) {
2,946✔
213
          var op = request.op;
2,880✔
214
          op.c = request.collection;
2,880✔
215
          op.d = request.id;
2,880✔
216
          op.m = undefined;
2,880✔
217
          // Needed for agent to detect if it can ignore sending the op back to
218
          // the client that submitted it in subscriptions
219
          if (request.collection !== request.index) op.i = request.index;
2,880✔
220
          backend.pubsub.publish(request.channels, op);
2,880✔
221
        }
222
        if (request._shouldSaveMilestoneSnapshot(request.snapshot)) {
2,946✔
223
          request.backend.milestoneDb.saveMilestoneSnapshot(request.collection, request.snapshot);
60✔
224
        }
225
        callback();
2,946✔
226
      });
227
  });
228
};
229

230
SubmitRequest.prototype.retry = function(callback) {
3✔
231
  this.retries++;
21✔
232
  if (this.maxRetries != null && this.retries > this.maxRetries) {
21✔
233
    return callback(this.maxRetriesError());
3✔
234
  }
235
  this.backend.emit('timing', 'submit.retry', Date.now() - this.start, this);
18✔
236
  this.submit(callback);
18✔
237
};
238

239
SubmitRequest.prototype._transformOp = function(ops) {
3✔
240
  var type = this.snapshot.type;
57✔
241
  for (var i = 0; i < ops.length; i++) {
57✔
242
    var op = ops[i];
57✔
243

244
    if (this.op.src && this.op.src === op.src && this.op.seq === op.seq) {
57✔
245
      // The op has already been submitted. There are a variety of ways this
246
      // can happen in normal operation, such as a client resending an
247
      // unacknowledged operation at reconnect. It's important we don't apply
248
      // the same op twice
249
      if (op.m.fixup) this._fixupOps = op.m.fixup;
3!
250
      return this.alreadySubmittedError();
3✔
251
    }
252

253
    if (this.op.v !== op.v) {
54✔
254
      return this.versionDuringTransformError();
3✔
255
    }
256

257
    var err = ot.transform(type, this.op, op);
51✔
258
    if (err) return err;
51✔
259
    delete op.m;
45✔
260
    this.ops.push(op);
45✔
261
  }
262
};
263

264
SubmitRequest.prototype._addOpMeta = function() {
3✔
265
  this.op.m = {
3,072✔
266
    ts: this.start
267
  };
268
  if (this.op.create) {
3,072✔
269
    // Consistently store the full URI of the type, not just its short name
270
    this.op.create.type = ot.normalizeType(this.op.create.type);
1,914✔
271
  }
272
};
273

274
SubmitRequest.prototype._addSnapshotMeta = function() {
3✔
275
  var meta = this.snapshot.m || (this.snapshot.m = {});
3,039✔
276
  if (this.op.create) {
3,039✔
277
    meta.ctime = this.start;
1,896✔
278
  } else if (this.op.del) {
1,143✔
279
    this.op.m.data = this.snapshot.data;
120✔
280
  }
281
  meta.mtime = this.start;
3,039✔
282
};
283

284
SubmitRequest.prototype._shouldSaveMilestoneSnapshot = function(snapshot) {
3✔
285
  // If the flag is null, it's not been overridden by the consumer, so apply the interval
286
  if (this.saveMilestoneSnapshot === null) {
2,946✔
287
    return snapshot && snapshot.v % this.backend.milestoneDb.interval === 0;
2,934✔
288
  }
289

290
  return this.saveMilestoneSnapshot;
12✔
291
};
292

293
// Non-fatal client errors:
294
SubmitRequest.prototype.alreadySubmittedError = function() {
3✔
295
  return new ShareDBError(ERROR_CODE.ERR_OP_ALREADY_SUBMITTED, 'Op already submitted');
3✔
296
};
297
SubmitRequest.prototype.rejectedError = function() {
3✔
298
  return new ShareDBError(ERROR_CODE.ERR_OP_SUBMIT_REJECTED, 'Op submit rejected');
24✔
299
};
300
// Fatal client errors:
301
SubmitRequest.prototype.alreadyCreatedError = function() {
3✔
302
  return new ShareDBError(ERROR_CODE.ERR_DOC_ALREADY_CREATED, 'Invalid op submitted. Document already created');
9✔
303
};
304
SubmitRequest.prototype.newerVersionError = function() {
3✔
305
  return new ShareDBError(
3✔
306
    ERROR_CODE.ERR_OP_VERSION_NEWER_THAN_CURRENT_SNAPSHOT,
307
    'Invalid op submitted. Op version newer than current snapshot'
308
  );
309
};
310
SubmitRequest.prototype.projectionError = function() {
3✔
311
  return new ShareDBError(
9✔
312
    ERROR_CODE.ERR_OP_NOT_ALLOWED_IN_PROJECTION,
313
    'Invalid op submitted. Operation invalid in projected collection'
314
  );
315
};
316
// Fatal internal errors:
317
SubmitRequest.prototype.missingOpsError = function() {
3✔
318
  return new ShareDBError(
3✔
319
    ERROR_CODE.ERR_SUBMIT_TRANSFORM_OPS_NOT_FOUND,
320
    'Op submit failed. DB missing ops needed to transform it up to the current snapshot version'
321
  );
322
};
323
SubmitRequest.prototype.versionDuringTransformError = function() {
3✔
324
  return new ShareDBError(
3✔
325
    ERROR_CODE.ERR_OP_VERSION_MISMATCH_DURING_TRANSFORM,
326
    'Op submit failed. Versions mismatched during op transform'
327
  );
328
};
329
SubmitRequest.prototype.versionAfterTransformError = function() {
3✔
330
  return new ShareDBError(
×
331
    ERROR_CODE.ERR_OP_VERSION_MISMATCH_AFTER_TRANSFORM,
332
    'Op submit failed. Op version mismatches snapshot after op transform'
333
  );
334
};
335
SubmitRequest.prototype.maxRetriesError = function() {
3✔
336
  return new ShareDBError(
3✔
337
    ERROR_CODE.ERR_MAX_SUBMIT_RETRIES_EXCEEDED,
338
    'Op submit failed. Exceeded max submit retries of ' + this.maxRetries
339
  );
340
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc