1
1
var hat = require ( 'hat' ) ;
2
- var util = require ( './util' ) ;
3
2
var types = require ( './types' ) ;
3
+ var util = require ( './util' ) ;
4
4
var logger = require ( './logger' ) ;
5
5
6
6
/**
@@ -105,8 +105,7 @@ Agent.prototype._subscribeToStream = function(collection, id, stream) {
105
105
logger . error ( 'Doc subscription stream error' , collection , id , data . error ) ;
106
106
return ;
107
107
}
108
- if ( agent . _isOwnOp ( collection , data ) ) return ;
109
- agent . _sendOp ( collection , id , data ) ;
108
+ agent . _onOp ( collection , id , data ) ;
110
109
} ) ;
111
110
stream . on ( 'end' , function ( ) {
112
111
// The op stream is done sending, so release its reference
@@ -149,13 +148,45 @@ Agent.prototype._subscribeToQuery = function(emitter, queryId, collection, query
149
148
150
149
emitter . onOp = function ( op ) {
151
150
var id = op . d ;
152
- if ( agent . _isOwnOp ( collection , op ) ) return ;
153
- agent . _sendOp ( collection , id , op ) ;
151
+ agent . _onOp ( collection , id , op ) ;
154
152
} ;
155
153
156
154
emitter . _open ( ) ;
157
155
} ;
158
156
157
+ Agent . prototype . _onOp = function ( collection , id , op ) {
158
+ if ( this . _isOwnOp ( collection , op ) ) return ;
159
+
160
+ // Ops emitted here are coming directly from pubsub, which emits the same op
161
+ // object to listeners without making a copy. The pattern in middleware is to
162
+ // manipulate the passed in object, and projections are implemented the same
163
+ // way currently.
164
+ //
165
+ // Deep copying the op would be safest, but deep copies are very expensive,
166
+ // especially over arbitrary objects. This function makes a shallow copy of an
167
+ // op, and it requires that projections and any user middleware copy deep
168
+ // properties as needed when they modify the op.
169
+ //
170
+ // Polling of query subscriptions is determined by the same op objects. As a
171
+ // precaution against op middleware breaking query subscriptions, we delay
172
+ // before calling into projection and middleware code
173
+ var agent = this ;
174
+ process . nextTick ( function ( ) {
175
+ var copy = shallowCopyOp ( op ) ;
176
+ if ( ! copy ) {
177
+ logger . error ( 'Op emitted from subscription failed to copy' , collection , id , op ) ;
178
+ return ;
179
+ }
180
+ agent . backend . sanitizeOp ( agent , collection , id , copy , function ( err ) {
181
+ if ( err ) {
182
+ logger . error ( 'Error sanitizing op emitted from subscription' , collection , id , copy , err ) ;
183
+ return ;
184
+ }
185
+ agent . _sendOp ( collection , id , copy ) ;
186
+ } ) ;
187
+ } ) ;
188
+ } ;
189
+
159
190
Agent . prototype . _isOwnOp = function ( collection , op ) {
160
191
// Detect ops from this client on the same projection. Since the client sent
161
192
// these in, the submit reply will be sufficient and we can silently ignore
@@ -186,12 +217,17 @@ Agent.prototype._sendOp = function(collection, id, op) {
186
217
187
218
this . send ( message ) ;
188
219
} ;
189
-
190
220
Agent . prototype . _sendOps = function ( collection , id , ops ) {
191
221
for ( var i = 0 ; i < ops . length ; i ++ ) {
192
222
this . _sendOp ( collection , id , ops [ i ] ) ;
193
223
}
194
224
} ;
225
+ Agent . prototype . _sendOpsBulk = function ( collection , opsMap ) {
226
+ for ( var id in opsMap ) {
227
+ var ops = opsMap [ id ] ;
228
+ this . _sendOps ( collection , id , ops ) ;
229
+ }
230
+ } ;
195
231
196
232
function getReplyErrorObject ( err ) {
197
233
if ( typeof err === 'string' ) {
@@ -316,7 +352,8 @@ Agent.prototype._handleMessage = function(request, callback) {
316
352
case 'u' :
317
353
return this . _unsubscribe ( request . c , request . d , callback ) ;
318
354
case 'op' :
319
- var op = this . _createOp ( request ) ;
355
+ // Normalize the properties submitted
356
+ var op = createClientOp ( request , this . clientId ) ;
320
357
if ( ! op ) return callback ( { code : 4000 , message : 'Invalid op message' } ) ;
321
358
return this . _submit ( request . c , request . d , op , callback ) ;
322
359
case 'nf' :
@@ -493,10 +530,7 @@ Agent.prototype._fetchBulkOps = function(collection, versions, callback) {
493
530
var agent = this ;
494
531
this . backend . getOpsBulk ( this , collection , versions , null , function ( err , opsMap ) {
495
532
if ( err ) return callback ( err ) ;
496
- for ( var id in opsMap ) {
497
- var ops = opsMap [ id ] ;
498
- agent . _sendOps ( collection , id , ops ) ;
499
- }
533
+ agent . _sendOpsBulk ( collection , opsMap ) ;
500
534
callback ( ) ;
501
535
} ) ;
502
536
} ;
@@ -505,8 +539,18 @@ Agent.prototype._subscribe = function(collection, id, version, callback) {
505
539
// If the version is specified, catch the client up by sending all ops
506
540
// since the specified version
507
541
var agent = this ;
508
- this . backend . subscribe ( this , collection , id , version , function ( err , stream , snapshot ) {
542
+ this . backend . subscribe ( this , collection , id , version , function ( err , stream , snapshot , ops ) {
509
543
if ( err ) return callback ( err ) ;
544
+ // If we're subscribing from a known version, send any ops committed since
545
+ // the requested version to bring the client's doc up to date
546
+ if ( ops ) {
547
+ agent . _sendOps ( collection , id , ops ) ;
548
+ }
549
+ // In addition, ops may already be queued on the stream by pubsub.
550
+ // Subscribe is called before the ops or snapshot are fetched, so it is
551
+ // possible that some ops may be duplicates. Clients should ignore any
552
+ // duplicate ops they may receive. This will flush ops already queued and
553
+ // subscribe to ongoing ops from the stream
510
554
agent . _subscribeToStream ( collection , id , stream ) ;
511
555
// Snapshot is returned only when subscribing from a null version.
512
556
// Otherwise, ops will have been pushed into the stream
@@ -519,9 +563,13 @@ Agent.prototype._subscribe = function(collection, id, version, callback) {
519
563
} ;
520
564
521
565
Agent . prototype . _subscribeBulk = function ( collection , versions , callback ) {
566
+ // See _subscribe() above. This function's logic should match but in bulk
522
567
var agent = this ;
523
- this . backend . subscribeBulk ( this , collection , versions , function ( err , streams , snapshotMap ) {
568
+ this . backend . subscribeBulk ( this , collection , versions , function ( err , streams , snapshotMap , opsMap ) {
524
569
if ( err ) return callback ( err ) ;
570
+ if ( opsMap ) {
571
+ agent . _sendOpsBulk ( collection , opsMap ) ;
572
+ }
525
573
for ( var id in streams ) {
526
574
agent . _subscribeToStream ( collection , id , streams [ id ] ) ;
527
575
}
@@ -572,45 +620,57 @@ Agent.prototype._submit = function(collection, id, op, callback) {
572
620
} ) ;
573
621
} ;
574
622
575
- function CreateOp ( src , seq , v , create ) {
623
+ Agent . prototype . _fetchSnapshot = function ( collection , id , version , callback ) {
624
+ this . backend . fetchSnapshot ( this , collection , id , version , callback ) ;
625
+ } ;
626
+
627
+ Agent . prototype . _fetchSnapshotByTimestamp = function ( collection , id , timestamp , callback ) {
628
+ this . backend . fetchSnapshotByTimestamp ( this , collection , id , timestamp , callback ) ;
629
+ } ;
630
+
631
+
632
+ function createClientOp ( request , clientId ) {
633
+ // src can be provided if it is not the same as the current agent,
634
+ // such as a resubmission after a reconnect, but it usually isn't needed
635
+ var src = request . src || clientId ;
636
+ // c, d, and m arguments are intentionally undefined. These are set later
637
+ return ( request . op ) ? new EditOp ( src , request . seq , request . v , request . op ) :
638
+ ( request . create ) ? new CreateOp ( src , request . seq , request . v , request . create ) :
639
+ ( request . del ) ? new DeleteOp ( src , request . seq , request . v , request . del ) :
640
+ undefined ;
641
+ }
642
+
643
+ function shallowCopyOp ( op ) {
644
+ return ( op . op ) ? new EditOp ( op . src , op . seq , op . v , op . op , op . c , op . d , op . m ) :
645
+ ( op . create ) ? new CreateOp ( op . src , op . seq , op . v , op . create , op . c , op . d , op . m ) :
646
+ ( op . del ) ? new DeleteOp ( op . src , op . seq , op . v , op . del , op . c , op . d , op . m ) :
647
+ undefined ;
648
+ }
649
+
650
+ function CreateOp ( src , seq , v , create , c , d , m ) {
576
651
this . src = src ;
577
652
this . seq = seq ;
578
653
this . v = v ;
579
654
this . create = create ;
580
- this . m = null ;
655
+ this . c = c ;
656
+ this . d = d ;
657
+ this . m = m ;
581
658
}
582
- function EditOp ( src , seq , v , op ) {
659
+ function EditOp ( src , seq , v , op , c , d , m ) {
583
660
this . src = src ;
584
661
this . seq = seq ;
585
662
this . v = v ;
586
663
this . op = op ;
587
- this . m = null ;
664
+ this . c = c ;
665
+ this . d = d ;
666
+ this . m = m ;
588
667
}
589
- function DeleteOp ( src , seq , v , del ) {
668
+ function DeleteOp ( src , seq , v , del , c , d , m ) {
590
669
this . src = src ;
591
670
this . seq = seq ;
592
671
this . v = v ;
593
672
this . del = del ;
594
- this . m = null ;
673
+ this . c = c ;
674
+ this . d = d ;
675
+ this . m = m ;
595
676
}
596
- // Normalize the properties submitted
597
- Agent . prototype . _createOp = function ( request ) {
598
- // src can be provided if it is not the same as the current agent,
599
- // such as a resubmission after a reconnect, but it usually isn't needed
600
- var src = request . src || this . clientId ;
601
- if ( request . op ) {
602
- return new EditOp ( src , request . seq , request . v , request . op ) ;
603
- } else if ( request . create ) {
604
- return new CreateOp ( src , request . seq , request . v , request . create ) ;
605
- } else if ( request . del ) {
606
- return new DeleteOp ( src , request . seq , request . v , request . del ) ;
607
- }
608
- } ;
609
-
610
- Agent . prototype . _fetchSnapshot = function ( collection , id , version , callback ) {
611
- this . backend . fetchSnapshot ( this , collection , id , version , callback ) ;
612
- } ;
613
-
614
- Agent . prototype . _fetchSnapshotByTimestamp = function ( collection , id , timestamp , callback ) {
615
- this . backend . fetchSnapshotByTimestamp ( this , collection , id , timestamp , callback ) ;
616
- } ;
0 commit comments