sharedb 6.0.4 → 7.0.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 +66 -29
- package/lib/backend.js +20 -6
- package/lib/client/doc.js +14 -1
- package/lib/client/presence/remote-doc-presence.js +7 -1
- package/lib/ot.js +53 -14
- package/lib/projections.js +6 -6
- package/lib/pubsub/index.js +15 -3
- package/lib/submit-request.js +6 -0
- package/package.json +1 -1
- package/test/agent.js +240 -0
- package/test/backend.js +86 -3
- package/test/client/doc.js +215 -26
- package/test/client/presence/doc-presence.js +54 -0
- package/test/client/presence/presence.js +48 -0
- package/test/client/query-subscribe.js +56 -18
- package/test/client/snapshot-timestamp-request.js +38 -1
- package/test/middleware.js +96 -0
- package/test/ot.js +62 -0
- package/test/projections.js +18 -0
- package/test/pubsub-memory.js +45 -0
- package/test/pubsub.js +45 -0
package/lib/agent.js
CHANGED
|
@@ -25,8 +25,7 @@ function Agent(backend, stream) {
|
|
|
25
25
|
// src is a client-configurable "id" which the client will set in its handshake,
|
|
26
26
|
// and attach to its ops. This should take precedence over clientId if set.
|
|
27
27
|
// Only legacy clients, or new clients connecting for the first time will use the
|
|
28
|
-
// Agent-provided clientId.
|
|
29
|
-
// in the next breaking change.
|
|
28
|
+
// Agent-provided clientId.
|
|
30
29
|
this.src = null;
|
|
31
30
|
this.connectTime = Date.now();
|
|
32
31
|
|
|
@@ -229,6 +228,7 @@ Agent.prototype._subscribeToQuery = function(emitter, queryId, collection, query
|
|
|
229
228
|
|
|
230
229
|
Agent.prototype._onOp = function(collection, id, op) {
|
|
231
230
|
if (this._isOwnOp(collection, op)) return;
|
|
231
|
+
if (this._isOpFromCollidingChannel(collection, id, op)) return;
|
|
232
232
|
|
|
233
233
|
// Ops emitted here are coming directly from pubsub, which emits the same op
|
|
234
234
|
// object to listeners without making a copy. The pattern in middleware is to
|
|
@@ -263,6 +263,12 @@ Agent.prototype._isOwnOp = function(collection, op) {
|
|
|
263
263
|
return (this._src() === op.src) && (collection === (op.i || op.c));
|
|
264
264
|
};
|
|
265
265
|
|
|
266
|
+
Agent.prototype._isOpFromCollidingChannel = function(collection, id, op) {
|
|
267
|
+
var projection = this.backend.projections[collection];
|
|
268
|
+
var target = (projection) ? projection.target : collection;
|
|
269
|
+
return op.c !== target || op.d !== id;
|
|
270
|
+
};
|
|
271
|
+
|
|
266
272
|
Agent.prototype.send = function(message) {
|
|
267
273
|
// Quietly drop replies if the stream was closed
|
|
268
274
|
if (this.closed) return;
|
|
@@ -375,6 +381,42 @@ Agent.prototype._open = function() {
|
|
|
375
381
|
this.stream.on('close', cleanup);
|
|
376
382
|
};
|
|
377
383
|
|
|
384
|
+
function isInvalidName(name) {
|
|
385
|
+
return typeof name !== 'string' || util.isDangerousProperty(name);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function isInvalidVersion(version) {
|
|
389
|
+
return version != null && (typeof version !== 'number' || version < 0);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function isInvalidProtocol(value) {
|
|
393
|
+
return value != null && typeof value !== 'number' && typeof value !== 'string';
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function checkBulkData(data) {
|
|
397
|
+
if (!data || typeof data !== 'object') return 'Invalid bulk subscribe data';
|
|
398
|
+
if (Array.isArray(data)) {
|
|
399
|
+
if (data.some(isInvalidName)) return 'Invalid id';
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
for (var id in data) {
|
|
403
|
+
if (isInvalidName(id)) return 'Invalid id';
|
|
404
|
+
if (isInvalidVersion(data[id])) return 'Invalid version';
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function checkQueryResults(results) {
|
|
409
|
+
if (!Array.isArray(results)) return 'Invalid query results';
|
|
410
|
+
for (var i = 0; i < results.length; i++) {
|
|
411
|
+
var result = results[i];
|
|
412
|
+
if (!Array.isArray(result)) return 'Invalid query results';
|
|
413
|
+
var id = result[0];
|
|
414
|
+
var version = result[1];
|
|
415
|
+
if (isInvalidName(id)) return 'Invalid id';
|
|
416
|
+
if (isInvalidVersion(version)) return 'Invalid version';
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
378
420
|
// Check a request to see if its valid. Returns an error if there's a problem.
|
|
379
421
|
Agent.prototype._checkRequest = function(request) {
|
|
380
422
|
if (
|
|
@@ -384,6 +426,10 @@ Agent.prototype._checkRequest = function(request) {
|
|
|
384
426
|
) {
|
|
385
427
|
// Query messages need an ID property.
|
|
386
428
|
if (typeof request.id !== 'number') return 'Missing query ID';
|
|
429
|
+
if (request.r != null) {
|
|
430
|
+
var resultsError = checkQueryResults(request.r);
|
|
431
|
+
if (resultsError) return resultsError;
|
|
432
|
+
}
|
|
387
433
|
} else if (request.a === ACTIONS.op ||
|
|
388
434
|
request.a === ACTIONS.fetch ||
|
|
389
435
|
request.a === ACTIONS.subscribe ||
|
|
@@ -392,30 +438,19 @@ Agent.prototype._checkRequest = function(request) {
|
|
|
392
438
|
request.a === ACTIONS.snapshotFetchByTimestamp ||
|
|
393
439
|
request.a === ACTIONS.presence) {
|
|
394
440
|
// Doc-based request.
|
|
395
|
-
if (request.
|
|
396
|
-
if (typeof request.c !== 'string' || util.isDangerousProperty(request.c)) {
|
|
397
|
-
return 'Invalid collection';
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
if (request.d != null) {
|
|
401
|
-
if (typeof request.d !== 'string' || util.isDangerousProperty(request.d)) {
|
|
402
|
-
return 'Invalid id';
|
|
403
|
-
}
|
|
404
|
-
}
|
|
441
|
+
if (request.d != null && isInvalidName(request.d)) return 'Invalid id';
|
|
405
442
|
|
|
406
443
|
if (request.a === ACTIONS.snapshotFetch || request.a === ACTIONS.snapshotFetchByTimestamp) {
|
|
407
444
|
if (request.c == null) return 'Invalid collection';
|
|
408
445
|
if (request.d == null) return 'Invalid id';
|
|
409
446
|
}
|
|
410
447
|
|
|
411
|
-
if (request.
|
|
412
|
-
|
|
413
|
-
}
|
|
448
|
+
if (isInvalidVersion(request.v)) return 'Invalid version';
|
|
449
|
+
if (request.ts != null && typeof request.ts !== 'number') return 'Invalid timestamp';
|
|
414
450
|
|
|
415
451
|
if (request.a === ACTIONS.presence) {
|
|
416
|
-
if (
|
|
417
|
-
|
|
418
|
-
}
|
|
452
|
+
if (isInvalidName(request.id)) return 'Invalid presence ID';
|
|
453
|
+
if (isInvalidVersion(request.pv)) return 'Invalid presence version';
|
|
419
454
|
}
|
|
420
455
|
} else if (
|
|
421
456
|
request.a === ACTIONS.bulkFetch ||
|
|
@@ -423,18 +458,14 @@ Agent.prototype._checkRequest = function(request) {
|
|
|
423
458
|
request.a === ACTIONS.bulkUnsubscribe
|
|
424
459
|
) {
|
|
425
460
|
// Bulk request
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
}
|
|
431
|
-
if (typeof request.b !== 'object') return 'Invalid bulk subscribe data';
|
|
432
|
-
}
|
|
433
|
-
if (request.ch != null) {
|
|
434
|
-
if (typeof request.ch !== 'string' || util.isDangerousProperty(request.ch)) {
|
|
435
|
-
return 'Invalid presence channel';
|
|
436
|
-
}
|
|
461
|
+
var bulkError = checkBulkData(request.b);
|
|
462
|
+
if (bulkError) return bulkError;
|
|
463
|
+
} else if (request.a === ACTIONS.handshake) {
|
|
464
|
+
if (isInvalidProtocol(request.protocol) || isInvalidProtocol(request.protocolMinor)) return 'Invalid protocol';
|
|
437
465
|
}
|
|
466
|
+
if (request.c != null && isInvalidName(request.c)) return 'Invalid collection';
|
|
467
|
+
if (request.ch != null && isInvalidName(request.ch)) return 'Invalid presence channel';
|
|
468
|
+
if (request.seq != null && typeof request.seq !== 'number') return 'Invalid seq';
|
|
438
469
|
};
|
|
439
470
|
|
|
440
471
|
// Handle an incoming message from the client
|
|
@@ -533,6 +564,8 @@ function getQueryOptions(request) {
|
|
|
533
564
|
}
|
|
534
565
|
}
|
|
535
566
|
var options = request.o || {};
|
|
567
|
+
delete options.pollInterval;
|
|
568
|
+
delete options.pollDebounce;
|
|
536
569
|
options.ids = ids;
|
|
537
570
|
options.fetch = fetch;
|
|
538
571
|
options.fetchOps = fetchOps;
|
|
@@ -924,6 +957,10 @@ Agent.prototype._subscribePresence = function(channel, seq, cb) {
|
|
|
924
957
|
return callback();
|
|
925
958
|
}
|
|
926
959
|
agent.presenceSubscriptionSeq[channel] = seq;
|
|
960
|
+
if (agent.subscribedPresences[channel]) {
|
|
961
|
+
stream.destroy();
|
|
962
|
+
return callback();
|
|
963
|
+
}
|
|
927
964
|
agent.subscribedPresences[channel] = stream;
|
|
928
965
|
agent._subscribeToPresenceStream(channel, stream);
|
|
929
966
|
agent._requestPresence(channel, function(error) {
|
package/lib/backend.js
CHANGED
|
@@ -569,22 +569,25 @@ Backend.prototype.subscribeBulk = function(agent, index, versions, callback) {
|
|
|
569
569
|
var backend = this;
|
|
570
570
|
var streams = Object.create(null);
|
|
571
571
|
var doFetch = Array.isArray(versions);
|
|
572
|
-
var ids = (doFetch) ? versions : Object.keys(versions);
|
|
572
|
+
var ids = (doFetch) ? uniqueIds(versions) : Object.keys(versions);
|
|
573
573
|
var request = {
|
|
574
574
|
agent: agent,
|
|
575
575
|
index: index,
|
|
576
576
|
collection: collection,
|
|
577
577
|
versions: versions
|
|
578
578
|
};
|
|
579
|
+
var subscribeFailed = false;
|
|
579
580
|
async.each(ids, function(id, eachCb) {
|
|
580
581
|
var channel = backend.getDocChannel(collection, id);
|
|
581
582
|
backend.pubsub.subscribe(channel, function(err, stream) {
|
|
582
583
|
if (err) return eachCb(err);
|
|
584
|
+
if (subscribeFailed) return stream.destroy();
|
|
583
585
|
streams[id] = stream;
|
|
584
586
|
eachCb();
|
|
585
587
|
});
|
|
586
588
|
}, function(err) {
|
|
587
589
|
if (err) {
|
|
590
|
+
subscribeFailed = true;
|
|
588
591
|
destroyStreams(streams);
|
|
589
592
|
return callback(err);
|
|
590
593
|
}
|
|
@@ -626,6 +629,14 @@ function destroyStreams(streams) {
|
|
|
626
629
|
streams[id].destroy();
|
|
627
630
|
}
|
|
628
631
|
}
|
|
632
|
+
function uniqueIds(ids) {
|
|
633
|
+
var seen = Object.create(null);
|
|
634
|
+
return ids.filter(function(id) {
|
|
635
|
+
if (seen[id]) return false;
|
|
636
|
+
seen[id] = true;
|
|
637
|
+
return true;
|
|
638
|
+
});
|
|
639
|
+
}
|
|
629
640
|
|
|
630
641
|
Backend.prototype.queryFetch = function(agent, index, query, options, callback) {
|
|
631
642
|
var start = Date.now();
|
|
@@ -739,12 +750,16 @@ Backend.prototype._triggerQuery = function(agent, index, query, options, callbac
|
|
|
739
750
|
if (err) return callback(err);
|
|
740
751
|
// Set the DB reference for the request after the middleware trigger so
|
|
741
752
|
// that the db option can be changed in middleware
|
|
742
|
-
request.db = (options.db) ? backend
|
|
753
|
+
request.db = (options.db) ? getExtraDb(backend, options.db) : backend.db;
|
|
743
754
|
if (!request.db) return callback(new ShareDBError(ERROR_CODE.ERR_DATABASE_ADAPTER_NOT_FOUND, 'DB not found'));
|
|
744
755
|
request.snapshotProjection = backend._getSnapshotProjection(request.db, projection);
|
|
745
756
|
callback(null, request);
|
|
746
757
|
});
|
|
747
758
|
};
|
|
759
|
+
function getExtraDb(backend, name) {
|
|
760
|
+
if (typeof name !== 'string' || !util.hasOwn(backend.extraDbs, name)) return null;
|
|
761
|
+
return backend.extraDbs[name];
|
|
762
|
+
}
|
|
748
763
|
|
|
749
764
|
Backend.prototype._query = function(agent, request, callback) {
|
|
750
765
|
var backend = this;
|
|
@@ -875,8 +890,6 @@ Backend.prototype._fetchSnapshotByTimestamp = function(collection, id, timestamp
|
|
|
875
890
|
var backend = this;
|
|
876
891
|
|
|
877
892
|
var milestoneSnapshot;
|
|
878
|
-
var from = 0;
|
|
879
|
-
var to = null;
|
|
880
893
|
|
|
881
894
|
// Always fetch the current snapshot first. We request its metadata so that we
|
|
882
895
|
// can read its mtime, which lets us serve the current snapshot directly when
|
|
@@ -898,11 +911,12 @@ Backend.prototype._fetchSnapshotByTimestamp = function(collection, id, timestamp
|
|
|
898
911
|
milestoneDb.getMilestoneSnapshotAtOrBeforeTime(collection, id, timestamp, function(error, snapshot) {
|
|
899
912
|
if (error) return callback(error);
|
|
900
913
|
milestoneSnapshot = snapshot;
|
|
901
|
-
if (snapshot) from = snapshot.v;
|
|
902
914
|
|
|
903
915
|
milestoneDb.getMilestoneSnapshotAtOrAfterTime(collection, id, timestamp, function(error, snapshot) {
|
|
904
916
|
if (error) return callback(error);
|
|
905
|
-
|
|
917
|
+
var from = milestoneSnapshot ? milestoneSnapshot.v : 0;
|
|
918
|
+
var to = snapshot ? snapshot.v : currentSnapshot.v;
|
|
919
|
+
if (from >= to) return backend._buildSnapshotFromOps(id, milestoneSnapshot, [], callback);
|
|
906
920
|
|
|
907
921
|
var options = {metadata: true};
|
|
908
922
|
db.getOps(collection, id, from, to, options, function(error, ops) {
|
package/lib/client/doc.js
CHANGED
|
@@ -762,7 +762,20 @@ Doc.prototype._submit = function(op, source, callback) {
|
|
|
762
762
|
return util.callbackOrEmit(this, err, callback);
|
|
763
763
|
}
|
|
764
764
|
// Try to normalize the op. This removes trailing skip:0's and things like that.
|
|
765
|
-
if (this.type.normalize)
|
|
765
|
+
if (this.type.normalize) {
|
|
766
|
+
try {
|
|
767
|
+
op.op = this.type.normalize(op.op);
|
|
768
|
+
} catch (error) {
|
|
769
|
+
// Otherwise a type that throws on a badly formed op throws out of
|
|
770
|
+
// submitOp() rather than calling back
|
|
771
|
+
var normalizeError = error instanceof ShareDBError ? error : new ShareDBError(
|
|
772
|
+
ERROR_CODE.ERR_OT_OP_BADLY_FORMED,
|
|
773
|
+
(error && error.message) || String(error)
|
|
774
|
+
);
|
|
775
|
+
if (callback) return callback(normalizeError);
|
|
776
|
+
return this.emit('error', normalizeError);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
766
779
|
|
|
767
780
|
// This has to happen before _pushOp(), because _tryCompose() applies the op
|
|
768
781
|
// to a pending create, well before _otApply() gets a chance to check it
|
|
@@ -120,7 +120,13 @@ RemoteDocPresence.prototype._catchUpStalePresence = function() {
|
|
|
120
120
|
this._pending.p = null;
|
|
121
121
|
this._pending.v++;
|
|
122
122
|
} else {
|
|
123
|
-
ot.transformPresence(this._pending, op, isOwnOp);
|
|
123
|
+
var error = ot.transformPresence(this._pending, op, isOwnOp);
|
|
124
|
+
if (error) {
|
|
125
|
+
this._pending = null;
|
|
126
|
+
this._stopCachingOps();
|
|
127
|
+
this.presence.emit('error', error);
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
124
130
|
}
|
|
125
131
|
}
|
|
126
132
|
|
package/lib/ot.js
CHANGED
|
@@ -120,32 +120,71 @@ function applyOpEdit(snapshot, edit) {
|
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
function isJson0OpComponent(component) {
|
|
124
|
+
return !!component && typeof component === 'object' && Array.isArray(component.p);
|
|
125
|
+
}
|
|
126
|
+
|
|
123
127
|
// ot-json0 walks ops with .length and numeric indexing, so it treats array-like
|
|
124
128
|
// objects as ops too, and it coerces .length. This traversal has to match it
|
|
125
|
-
// exactly: anything ot-json0 will apply has to be checked here.
|
|
129
|
+
// exactly: anything ot-json0 will apply has to be checked here. Coercing
|
|
130
|
+
// .length or a path segment throws for some objects; ot-json0 throws on those
|
|
131
|
+
// too, so they are rejected rather than applied.
|
|
126
132
|
// See GHSA-9rqw-j2q5-gg2g
|
|
127
133
|
function checkJson0OpPaths(op) {
|
|
128
134
|
if (op == null) return;
|
|
129
|
-
|
|
130
|
-
var
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
try {
|
|
136
|
+
for (var i = 0; i < op.length; i++) {
|
|
137
|
+
var component = op[i];
|
|
138
|
+
// ot-json0's checkValidOp() rejects the whole op before applying any of it,
|
|
139
|
+
// so there is nothing beyond this component left to check
|
|
140
|
+
if (!isJson0OpComponent(component)) return;
|
|
141
|
+
for (var j = 0; j < component.p.length; j++) {
|
|
142
|
+
if (util.isDangerousProperty(component.p[j])) {
|
|
143
|
+
return new ShareDBError(ERROR_CODE.ERR_OT_OP_NOT_APPLIED, 'Invalid path segment');
|
|
144
|
+
}
|
|
137
145
|
}
|
|
138
146
|
}
|
|
147
|
+
} catch (error) {
|
|
148
|
+
return new ShareDBError(ERROR_CODE.ERR_OT_OP_NOT_APPLIED, error.message);
|
|
139
149
|
}
|
|
140
150
|
}
|
|
141
151
|
|
|
152
|
+
// Only for ops arriving at submit-request, which reach ot-json0 exactly as
|
|
153
|
+
// they came off the wire, with nothing to normalize them on the way. Ops that
|
|
154
|
+
// are already committed can only have their paths checked, since ops that
|
|
155
|
+
// ot-json0 quietly treats as no-ops were committable by older versions of
|
|
156
|
+
// ShareDB
|
|
157
|
+
function checkSubmittedJson0Op(op) {
|
|
158
|
+
if (!Array.isArray(op)) {
|
|
159
|
+
return new ShareDBError(ERROR_CODE.ERR_OT_OP_BADLY_FORMED, 'json0 op must be an array');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
for (var i = 0; i < op.length; i++) {
|
|
163
|
+
if (!isJson0OpComponent(op[i])) {
|
|
164
|
+
return new ShareDBError(ERROR_CODE.ERR_OT_OP_BADLY_FORMED, 'Missing path');
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return checkJson0OpPaths(op);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function isJson0(type) {
|
|
172
|
+
if (typeof type === 'string') type = types.map[type];
|
|
173
|
+
return !!type && type.name === 'json0';
|
|
174
|
+
}
|
|
175
|
+
|
|
142
176
|
exports.checkOpPathsForType = function(type, op) {
|
|
143
177
|
if (!('op' in op)) return;
|
|
144
|
-
if (
|
|
145
|
-
if (!type || type.name !== 'json0') return;
|
|
178
|
+
if (!isJson0(type)) return;
|
|
146
179
|
return checkJson0OpPaths(op.op);
|
|
147
180
|
};
|
|
148
181
|
|
|
182
|
+
exports.checkSubmittedOpForType = function(type, op) {
|
|
183
|
+
if (!('op' in op)) return;
|
|
184
|
+
if (!isJson0(type)) return;
|
|
185
|
+
return checkSubmittedJson0Op(op.op);
|
|
186
|
+
};
|
|
187
|
+
|
|
149
188
|
exports.transform = function(type, op, appliedOp) {
|
|
150
189
|
// There are 16 cases this function needs to deal with - which are all the
|
|
151
190
|
// combinations of create/delete/op/noop from both op and appliedOp
|
|
@@ -223,9 +262,9 @@ exports.transformPresence = function(presence, op, isOwnOp) {
|
|
|
223
262
|
if (typeof type === 'string') {
|
|
224
263
|
type = types.map[type];
|
|
225
264
|
}
|
|
226
|
-
if (!type) return
|
|
265
|
+
if (!type) return new ShareDBError(ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED, 'Unknown type');
|
|
227
266
|
if (!util.supportsPresence(type)) {
|
|
228
|
-
return
|
|
267
|
+
return new ShareDBError(ERROR_CODE.ERR_TYPE_DOES_NOT_SUPPORT_PRESENCE, 'Type does not support presence');
|
|
229
268
|
}
|
|
230
269
|
|
|
231
270
|
if (op.create || op.del) {
|
|
@@ -239,7 +278,7 @@ exports.transformPresence = function(presence, op, isOwnOp) {
|
|
|
239
278
|
null :
|
|
240
279
|
type.transformPresence(presence.p, op.op, isOwnOp);
|
|
241
280
|
} catch (error) {
|
|
242
|
-
return
|
|
281
|
+
return new ShareDBError(ERROR_CODE.ERR_PRESENCE_TRANSFORM_FAILED, error.message || error);
|
|
243
282
|
}
|
|
244
283
|
|
|
245
284
|
presence.v++;
|
package/lib/projections.js
CHANGED
|
@@ -97,13 +97,13 @@ function isSnapshotAllowed(fields, snapshot) {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
function isEditAllowed(fields, op) {
|
|
100
|
+
if (!Array.isArray(op)) return false;
|
|
100
101
|
for (var i = 0; i < op.length; i++) {
|
|
101
|
-
var
|
|
102
|
-
if (
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
102
|
+
var path = op[i] && op[i].p;
|
|
103
|
+
if (!Array.isArray(path)) return false;
|
|
104
|
+
var field = path[0];
|
|
105
|
+
if (typeof field !== 'string' && typeof field !== 'number') return false;
|
|
106
|
+
if (!isFieldAllowed(fields, field)) return false;
|
|
107
107
|
}
|
|
108
108
|
return true;
|
|
109
109
|
}
|
package/lib/pubsub/index.js
CHANGED
|
@@ -19,6 +19,8 @@ function PubSub(options) {
|
|
|
19
19
|
// isn't complete until the callback returns from Redis
|
|
20
20
|
// Maps channel -> true
|
|
21
21
|
this.subscribed = Object.create(null);
|
|
22
|
+
// Maps channel -> callbacks waiting on _subscribe
|
|
23
|
+
this._subscribing = Object.create(null);
|
|
22
24
|
|
|
23
25
|
var pubsub = this;
|
|
24
26
|
this._defaultCallback = function(err) {
|
|
@@ -76,11 +78,21 @@ PubSub.prototype.subscribe = function(channel, callback) {
|
|
|
76
78
|
});
|
|
77
79
|
return;
|
|
78
80
|
}
|
|
81
|
+
if (this._subscribing[channel]) {
|
|
82
|
+
this._subscribing[channel].push(callback);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
var callbacks = this._subscribing[channel] = [callback];
|
|
79
86
|
this._subscribe(channel, function(err) {
|
|
80
|
-
|
|
87
|
+
delete pubsub._subscribing[channel];
|
|
88
|
+
if (err) return util.callEach(callbacks, err);
|
|
81
89
|
pubsub.subscribed[channel] = true;
|
|
82
|
-
var
|
|
83
|
-
|
|
90
|
+
var streams = callbacks.map(function() {
|
|
91
|
+
return pubsub._createStream(channel);
|
|
92
|
+
});
|
|
93
|
+
callbacks.forEach(function(callback, i) {
|
|
94
|
+
callback(null, streams[i]);
|
|
95
|
+
});
|
|
84
96
|
});
|
|
85
97
|
};
|
|
86
98
|
|
package/lib/submit-request.js
CHANGED
|
@@ -109,6 +109,12 @@ SubmitRequest.prototype.submit = function(callback) {
|
|
|
109
109
|
request.snapshot = snapshot;
|
|
110
110
|
request._addSnapshotMeta();
|
|
111
111
|
|
|
112
|
+
// The type is only known once we have the snapshot, so this is the earliest
|
|
113
|
+
// we can validate the op against it. It has to happen before the op reaches
|
|
114
|
+
// any type function, including through $fixup() in the apply middleware
|
|
115
|
+
var opError = ot.checkSubmittedOpForType(snapshot.type, op);
|
|
116
|
+
if (opError) return callback(opError);
|
|
117
|
+
|
|
112
118
|
if (op.v == null) {
|
|
113
119
|
if (op.create && snapshot.type && op.src) {
|
|
114
120
|
// If the document was already created by another op, we will return a
|