sharedb 3.2.3 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/agent.js CHANGED
@@ -718,9 +718,10 @@ Agent.prototype._unsubscribeBulk = function(collection, ids, callback) {
718
718
 
719
719
  Agent.prototype._submit = function(collection, id, op, callback) {
720
720
  var agent = this;
721
- this.backend.submit(this, collection, id, op, null, function(err, ops) {
721
+ this.backend.submit(this, collection, id, op, null, function(err, ops, request) {
722
722
  // Message to acknowledge the op was successfully submitted
723
723
  var ack = {src: op.src, seq: op.seq, v: op.v};
724
+ if (request._fixupOps.length) ack[ACTIONS.fixup] = request._fixupOps;
724
725
  if (err) {
725
726
  // Occasional 'Op already submitted' errors are expected to happen as
726
727
  // part of normal operation, since inflight ops need to be resent after
package/lib/backend.js CHANGED
@@ -229,7 +229,7 @@ Backend.prototype.submit = function(agent, index, id, op, options, originalCallb
229
229
 
230
230
  var callback = function(error, ops) {
231
231
  backend.emit('submitRequestEnd', error, request);
232
- originalCallback(error, ops);
232
+ originalCallback(error, ops, request);
233
233
  };
234
234
 
235
235
  var err = ot.checkOp(op);
@@ -661,10 +661,32 @@ Backend.prototype.querySubscribe = function(agent, index, query, options, callba
661
661
  'DB does not support subscribe'
662
662
  ));
663
663
  }
664
- backend.pubsub.subscribe(request.channel, function(err, stream) {
665
- if (err) return callback(err);
664
+
665
+ var channels = request.channels;
666
+
667
+ if (request.channel) {
668
+ logger.warn(
669
+ '[DEPRECATED] "query" middleware\'s context.channel is deprecated, use context.channels instead. ' +
670
+ 'Read more: https://share.github.io/sharedb/middleware/actions#query'
671
+ );
672
+ channels = [request.channel];
673
+ }
674
+
675
+ if (!channels || !channels.length) {
676
+ return callback(new ShareDBError(ERROR_CODE.ERR_QUERY_CHANNEL_MISSING, 'Required minimum one query channel.'));
677
+ }
678
+
679
+ var streams = [];
680
+
681
+ function destroyStreams() {
682
+ streams.forEach(function(stream) {
683
+ stream.destroy();
684
+ });
685
+ }
686
+
687
+ function createQueryEmitter() {
666
688
  if (options.ids) {
667
- var queryEmitter = new QueryEmitter(request, stream, options.ids);
689
+ var queryEmitter = new QueryEmitter(request, streams, options.ids);
668
690
  backend.emit('timing', 'querySubscribe.reconnect', Date.now() - start, request);
669
691
  callback(null, queryEmitter);
670
692
  return;
@@ -672,14 +694,29 @@ Backend.prototype.querySubscribe = function(agent, index, query, options, callba
672
694
  // Issue query on db to get our initial results
673
695
  backend._query(agent, request, function(err, snapshots, extra) {
674
696
  if (err) {
675
- stream.destroy();
697
+ destroyStreams();
676
698
  return callback(err);
677
699
  }
678
700
  var ids = pluckIds(snapshots);
679
- var queryEmitter = new QueryEmitter(request, stream, ids, extra);
701
+ var queryEmitter = new QueryEmitter(request, streams, ids, extra);
680
702
  backend.emit('timing', 'querySubscribe.initial', Date.now() - start, request);
681
703
  callback(null, queryEmitter, snapshots, extra);
682
704
  });
705
+ }
706
+
707
+ channels.forEach(function(channel) {
708
+ backend.pubsub.subscribe(channel, function(err, stream) {
709
+ if (err) {
710
+ destroyStreams();
711
+ return callback(err);
712
+ }
713
+ streams.push(stream);
714
+
715
+ var subscribedToAllChannels = streams.length === channels.length;
716
+ if (subscribedToAllChannels) {
717
+ createQueryEmitter();
718
+ }
719
+ });
683
720
  });
684
721
  });
685
722
  };
@@ -693,7 +730,7 @@ Backend.prototype._triggerQuery = function(agent, index, query, options, callbac
693
730
  collection: collection,
694
731
  projection: projection,
695
732
  fields: fields,
696
- channel: this.getCollectionChannel(collection),
733
+ channels: [this.getCollectionChannel(collection)],
697
734
  query: query,
698
735
  options: options,
699
736
  db: null,
package/lib/client/doc.js CHANGED
@@ -5,6 +5,7 @@ var types = require('../types');
5
5
  var util = require('../util');
6
6
  var clone = util.clone;
7
7
  var deepEqual = require('fast-deep-equal');
8
+ var ACTIONS = require('../message-actions').ACTIONS;
8
9
 
9
10
  var ERROR_CODE = ShareDBError.CODES;
10
11
 
@@ -960,6 +961,23 @@ Doc.prototype._opAcknowledged = function(message) {
960
961
  return this.fetch();
961
962
  }
962
963
 
964
+ if (message[ACTIONS.fixup]) {
965
+ for (var i = 0; i < message[ACTIONS.fixup].length; i++) {
966
+ var fixupOp = message[ACTIONS.fixup][i];
967
+
968
+ for (var j = 0; j < this.pendingOps.length; j++) {
969
+ var transformErr = transformX(this.pendingOps[i], fixupOp);
970
+ if (transformErr) return this._hardRollback(transformErr);
971
+ }
972
+
973
+ try {
974
+ this._otApply(fixupOp, false);
975
+ } catch (error) {
976
+ return this._hardRollback(error);
977
+ }
978
+ }
979
+ }
980
+
963
981
  // The op was committed successfully. Increment the version number
964
982
  this.version++;
965
983
 
package/lib/error.js CHANGED
@@ -15,6 +15,8 @@ ShareDBError.prototype.name = 'ShareDBError';
15
15
  ShareDBError.CODES = {
16
16
  ERR_APPLY_OP_VERSION_DOES_NOT_MATCH_SNAPSHOT: 'ERR_APPLY_OP_VERSION_DOES_NOT_MATCH_SNAPSHOT',
17
17
  ERR_APPLY_SNAPSHOT_NOT_PROVIDED: 'ERR_APPLY_SNAPSHOT_NOT_PROVIDED',
18
+ ERR_FIXUP_IS_ONLY_VALID_ON_APPLY: 'ERR_FIXUP_IS_ONLY_VALID_ON_APPLY',
19
+ ERR_CANNOT_FIXUP_DELETION: 'ERR_CANNOT_FIXUP_DELETION',
18
20
  ERR_CLIENT_ID_BADLY_FORMED: 'ERR_CLIENT_ID_BADLY_FORMED',
19
21
  ERR_CANNOT_PING_OFFLINE: 'ERR_CANNOT_PING_OFFLINE',
20
22
  ERR_CONNECTION_SEQ_INTEGER_OVERFLOW: 'ERR_CONNECTION_SEQ_INTEGER_OVERFLOW',
@@ -45,6 +47,7 @@ ShareDBError.CODES = {
45
47
  ERR_OT_OP_NOT_PROVIDED: 'ERR_OT_OP_NOT_PROVIDED',
46
48
  ERR_PRESENCE_TRANSFORM_FAILED: 'ERR_PRESENCE_TRANSFORM_FAILED',
47
49
  ERR_PROTOCOL_VERSION_NOT_SUPPORTED: 'ERR_PROTOCOL_VERSION_NOT_SUPPORTED',
50
+ ERR_QUERY_CHANNEL_MISSING: 'ERR_QUERY_CHANNEL_MISSING',
48
51
  ERR_QUERY_EMITTER_LISTENER_NOT_ASSIGNED: 'ERR_QUERY_EMITTER_LISTENER_NOT_ASSIGNED',
49
52
  /**
50
53
  * A special error that a "readSnapshots" middleware implementation can use to indicate that it
@@ -67,6 +70,7 @@ ShareDBError.CODES = {
67
70
  ERR_SNAPSHOT_READS_REJECTED: 'ERR_SNAPSHOT_READS_REJECTED',
68
71
  ERR_SUBMIT_TRANSFORM_OPS_NOT_FOUND: 'ERR_SUBMIT_TRANSFORM_OPS_NOT_FOUND',
69
72
  ERR_TYPE_CANNOT_BE_PROJECTED: 'ERR_TYPE_CANNOT_BE_PROJECTED',
73
+ ERR_TYPE_DOES_NOT_SUPPORT_COMPOSE: 'ERR_TYPE_DOES_NOT_SUPPORT_COMPOSE',
70
74
  ERR_TYPE_DOES_NOT_SUPPORT_PRESENCE: 'ERR_TYPE_DOES_NOT_SUPPORT_PRESENCE',
71
75
  ERR_UNKNOWN_ERROR: 'ERR_UNKNOWN_ERROR'
72
76
  };
@@ -9,6 +9,7 @@ exports.ACTIONS = {
9
9
  bulkSubscribe: 'bs',
10
10
  bulkUnsubscribe: 'bu',
11
11
  fetch: 'f',
12
+ fixup: 'fixup',
12
13
  subscribe: 's',
13
14
  unsubscribe: 'u',
14
15
  op: 'op',
@@ -5,7 +5,7 @@ var util = require('./util');
5
5
 
6
6
  var ERROR_CODE = ShareDBError.CODES;
7
7
 
8
- function QueryEmitter(request, stream, ids, extra) {
8
+ function QueryEmitter(request, streams, ids, extra) {
9
9
  this.backend = request.backend;
10
10
  this.agent = request.agent;
11
11
  this.db = request.db;
@@ -15,7 +15,7 @@ function QueryEmitter(request, stream, ids, extra) {
15
15
  this.fields = request.fields;
16
16
  this.options = request.options;
17
17
  this.snapshotProjection = request.snapshotProjection;
18
- this.stream = stream;
18
+ this.streams = streams;
19
19
  this.ids = ids;
20
20
  this.extra = extra;
21
21
 
@@ -23,10 +23,12 @@ function QueryEmitter(request, stream, ids, extra) {
23
23
  this.canPollDoc = this.db.canPollDoc(this.collection, this.query);
24
24
  this.pollDebounce =
25
25
  (typeof this.options.pollDebounce === 'number') ? this.options.pollDebounce :
26
- (typeof this.db.pollDebounce === 'number') ? this.db.pollDebounce : 0;
26
+ (typeof this.db.pollDebounce === 'number') ? this.db.pollDebounce :
27
+ streams.length > 1 ? 1000 : 0;
27
28
  this.pollInterval =
28
29
  (typeof this.options.pollInterval === 'number') ? this.options.pollInterval :
29
- (typeof this.db.pollInterval === 'number') ? this.db.pollInterval : 0;
30
+ (typeof this.db.pollInterval === 'number') ? this.db.pollInterval :
31
+ streams.length > 1 ? 1000 : 0;
30
32
 
31
33
  this._polling = false;
32
34
  this._pendingPoll = null;
@@ -41,15 +43,19 @@ QueryEmitter.prototype._open = function() {
41
43
  this._defaultCallback = function(err) {
42
44
  if (err) emitter.onError(err);
43
45
  };
44
- emitter.stream.on('data', function(data) {
45
- if (data.error) {
46
- return emitter.onError(data.error);
47
- }
48
- emitter._update(data);
49
- });
50
- emitter.stream.on('end', function() {
51
- emitter.destroy();
46
+
47
+ emitter.streams.forEach(function(stream) {
48
+ stream.on('data', function(data) {
49
+ if (data.error) {
50
+ return emitter.onError(data.error);
51
+ }
52
+ emitter._update(data);
53
+ });
54
+ stream.on('end', function() {
55
+ emitter.destroy();
56
+ });
52
57
  });
58
+
53
59
  // Make sure we start polling if pollInterval is being used
54
60
  this._flushPoll();
55
61
  };
@@ -57,7 +63,12 @@ QueryEmitter.prototype._open = function() {
57
63
  QueryEmitter.prototype.destroy = function() {
58
64
  clearTimeout(this._pollDebounceId);
59
65
  clearTimeout(this._pollIntervalId);
60
- this.stream.destroy();
66
+
67
+ var stream;
68
+
69
+ while (stream = this.streams.pop()) {
70
+ stream.destroy();
71
+ }
61
72
  };
62
73
 
63
74
  QueryEmitter.prototype._emitTiming = function(action, start) {
@@ -140,8 +151,8 @@ QueryEmitter.prototype._flushPoll = function() {
140
151
  if (this._pendingPoll) {
141
152
  this.queryPoll();
142
153
 
143
- // If a pollInterval is specified, poll if the query doesn't get polled in
144
- // the time of the interval
154
+ // If a pollInterval is specified, poll if the query doesn't get polled in
155
+ // the time of the interval
145
156
  } else if (this.pollInterval) {
146
157
  var emitter = this;
147
158
  this._pollIntervalId = setTimeout(function() {
@@ -301,14 +312,14 @@ QueryEmitter.prototype.queryPollDoc = function(id, callback) {
301
312
  // all messages are received and applied in order, so it is critical that none
302
313
  // are dropped.
303
314
  QueryEmitter.prototype.onError =
304
- QueryEmitter.prototype.onDiff =
305
- QueryEmitter.prototype.onExtra =
306
- QueryEmitter.prototype.onOp = function() {
307
- throw new ShareDBError(
308
- ERROR_CODE.ERR_QUERY_EMITTER_LISTENER_NOT_ASSIGNED,
309
- 'Required QueryEmitter listener not assigned'
310
- );
311
- };
315
+ QueryEmitter.prototype.onDiff =
316
+ QueryEmitter.prototype.onExtra =
317
+ QueryEmitter.prototype.onOp = function() {
318
+ throw new ShareDBError(
319
+ ERROR_CODE.ERR_QUERY_EMITTER_LISTENER_NOT_ASSIGNED,
320
+ 'Required QueryEmitter listener not assigned'
321
+ );
322
+ };
312
323
 
313
324
  function getInserted(diff) {
314
325
  var inserted = [];
@@ -1,6 +1,7 @@
1
1
  var ot = require('./ot');
2
2
  var projections = require('./projections');
3
3
  var ShareDBError = require('./error');
4
+ var types = require('./types');
4
5
 
5
6
  var ERROR_CODE = ShareDBError.CODES;
6
7
 
@@ -40,9 +41,47 @@ function SubmitRequest(backend, agent, index, id, op, options) {
40
41
  this.snapshot = null;
41
42
  this.ops = [];
42
43
  this.channels = null;
44
+ this._fixupOps = [];
43
45
  }
44
46
  module.exports = SubmitRequest;
45
47
 
48
+ SubmitRequest.prototype.$fixup = function(op) {
49
+ if (this.action !== this.backend.MIDDLEWARE_ACTIONS.apply) {
50
+ throw new ShareDBError(
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) {
57
+ throw new ShareDBError(
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;
64
+ var type = types.map[typeId];
65
+ if (typeof type.compose !== 'function') {
66
+ throw new ShareDBError(
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);
73
+ else this.op.op = type.compose(this.op.op, op);
74
+
75
+ var fixupOp = {
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);
83
+ };
84
+
46
85
  SubmitRequest.prototype.submit = function(callback) {
47
86
  var request = this;
48
87
  var backend = this.backend;
@@ -103,7 +142,7 @@ SubmitRequest.prototype.submit = function(callback) {
103
142
 
104
143
  // Transform the op up to the current snapshot version, then apply
105
144
  var from = op.v;
106
- backend.db.getOpsToSnapshot(collection, id, from, snapshot, null, function(err, ops) {
145
+ backend.db.getOpsToSnapshot(collection, id, from, snapshot, {metadata: true}, function(err, ops) {
107
146
  if (err) return callback(err);
108
147
 
109
148
  if (ops.length !== snapshot.v - from) {
@@ -134,6 +173,8 @@ SubmitRequest.prototype.apply = function(callback) {
134
173
  // Always set the channels before each attempt to apply. If the channels are
135
174
  // modified in a middleware and we retry, we want to reset to a new array
136
175
  this.channels = this.backend.getChannels(this.collection, this.id);
176
+ this._fixupOps = [];
177
+ delete this.op.m.fixup;
137
178
 
138
179
  var request = this;
139
180
  this.backend.trigger(this.backend.MIDDLEWARE_ACTIONS.apply, this.agent, this, function(err) {
@@ -152,6 +193,7 @@ SubmitRequest.prototype.commit = function(callback) {
152
193
  var backend = this.backend;
153
194
  backend.trigger(backend.MIDDLEWARE_ACTIONS.commit, this.agent, this, function(err) {
154
195
  if (err) return callback(err);
196
+ if (request._fixupOps.length) request.op.m.fixup = request._fixupOps;
155
197
 
156
198
  // Try committing the operation and snapshot to the database atomically
157
199
  backend.db.commit(
@@ -204,6 +246,7 @@ SubmitRequest.prototype._transformOp = function(ops) {
204
246
  // can happen in normal operation, such as a client resending an
205
247
  // unacknowledged operation at reconnect. It's important we don't apply
206
248
  // the same op twice
249
+ if (op.m.fixup) this._fixupOps = op.m.fixup;
207
250
  return this.alreadySubmittedError();
208
251
  }
209
252
 
@@ -213,6 +256,7 @@ SubmitRequest.prototype._transformOp = function(ops) {
213
256
 
214
257
  var err = ot.transform(type, this.op, op);
215
258
  if (err) return err;
259
+ delete op.m;
216
260
  this.ops.push(op);
217
261
  }
218
262
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sharedb",
3
- "version": "3.2.3",
3
+ "version": "3.3.0",
4
4
  "description": "JSON OT database backend",
5
5
  "main": "lib/index.js",
6
6
  "dependencies": {
@@ -22,7 +22,7 @@
22
22
  "ot-json1": "^0.3.0",
23
23
  "rich-text": "^4.1.0",
24
24
  "sharedb-legacy": "npm:sharedb@=1.1.0",
25
- "sinon": "^7.5.0"
25
+ "sinon": "^9.2.4"
26
26
  },
27
27
  "files": [
28
28
  "lib/",
@@ -38,7 +38,6 @@ describe('Presence', function() {
38
38
  });
39
39
 
40
40
  afterEach(function(done) {
41
- sinon.restore();
42
41
  connection1.close();
43
42
  connection2.close();
44
43
  backend.close(done);
@@ -147,30 +146,6 @@ describe('Presence', function() {
147
146
  ], errorHandler(done));
148
147
  });
149
148
 
150
- it('destroys old local presence but keeps new local presence when getting during destroy', function(done) {
151
- presence2.create('presence-2');
152
- var presence2a;
153
-
154
- async.series([
155
- presence2.subscribe.bind(presence2),
156
- function(next) {
157
- presence2.destroy(function() {
158
- expect(presence2).to.equal(presence2a);
159
- expect(Object.keys(presence2.localPresences)).to.eql(['presence-2a']);
160
- done();
161
- });
162
- next();
163
- },
164
- function(next) {
165
- presence2a = connection2.getPresence('test-channel');
166
- presence2a.create('presence-2a');
167
- presence2a.subscribe(function(error) {
168
- next(error);
169
- });
170
- }
171
- ], errorHandler(done));
172
- });
173
-
174
149
  it('throws if trying to create local presence when wanting destroy', function(done) {
175
150
  presence2.destroy(errorHandler(done));
176
151
  expect(function() {