@@ -233,6 +233,11 @@ <h4>[[dinosaur.__firebaseKey__]]</h4>
233233 'firebase-child-moved' : '_onFirebaseChildMoved' ,
234234 } ,
235235
236+ created : function ( ) {
237+ this . _pendingSplices = [ ] ;
238+ this . _lastLocallyAddedIndex = null ;
239+ } ,
240+
236241 /**
237242 * Add an item to the document referenced at `location`. A key associated
238243 * with the item will be created by Firebase, and can be accessed via the
@@ -261,7 +266,10 @@ <h4>[[dinosaur.__firebaseKey__]]</h4>
261266 */
262267 remove : function ( data ) {
263268 if ( data == null || data . __firebaseKey__ == null ) {
264- this . _error ( 'Failed to remove unknown value:' , data ) ;
269+ // NOTE(cdata): This might be better as an error message in the
270+ // console, but `Polymer.Base._error` throws, which we don't want
271+ // to happen in this case.
272+ this . _warn ( 'Refusing to remove unknown value:' , data ) ;
265273 return ;
266274 }
267275
@@ -287,49 +295,82 @@ <h4>[[dinosaur.__firebaseKey__]]</h4>
287295 * located at `location`.
288296 */
289297 removeByKey : function ( key ) {
298+ if ( ! this . query ) {
299+ this . _error ( 'Cannot remove items before query has been initialized.' ) ;
300+ return ;
301+ }
302+
290303 this . query . ref ( ) . child ( key ) . remove ( ) ;
291304 } ,
292305
293- _applyLocalDataChanges : function ( change ) {
306+ _localDataChanged : function ( change ) {
294307 var pathParts = change . path . split ( '.' ) ;
295- var firebaseKey ;
296- var key ;
297- var value ;
298308
309+ // We don't care about self-changes, and we don't respond directly to
310+ // length changes:
299311 if ( pathParts . length < 2 || pathParts [ 1 ] === 'length' ) {
300312 return ;
301313 }
302314
303- if ( pathParts [ 1 ] === 'splices' ) {
304- this . _applySplicesToRemoteData ( change . value . indexSplices ) ;
315+ // Handle splices via the adoption process. `indexSplices` is known to
316+ // sometimes be null, so guard against that.
317+ if ( pathParts [ 1 ] === 'splices' && change . value . indexSplices != null ) {
318+ this . _adoptSplices ( change . value . indexSplices ) ;
305319 return ;
306320 }
307321
308- key = pathParts [ 1 ] ;
309- value = Polymer . Collection . get ( change . base ) . getItem ( key ) ;
322+ // Otherwise, the change is happening to a sub-path of the array.
323+ this . _applySubPathChange ( change ) ;
324+ } ,
325+
326+ _applySubPathChange : function ( change ) {
327+ var key = change . path . split ( '.' ) [ 1 ] ;
328+ var value = Polymer . Collection . get ( change . base ) . getItem ( key ) ;
329+ var firebaseKey = value . __firebaseKey__ ;
310330
311- // Temporarily remove the client-only `__firebaseKey__` property:
312- firebaseKey = value . __firebaseKey__ ;
331+ // We don't want to accidentally reflect `__firebaseKey__` in the
332+ // remote data, so we remove it temporarily. `null` values will be
333+ // discarded by Firebase, so `delete` is not necessary:
313334 value . __firebaseKey__ = null ;
314-
315335 this . query . ref ( ) . child ( firebaseKey ) . set ( value ) ;
316-
317336 value . __firebaseKey__ = firebaseKey ;
318337 } ,
319338
320- _applySplicesToRemoteData : function ( splices ) {
321- this . _log ( 'splices' , splices ) ;
322- splices . forEach ( function ( splice ) {
323- var added = splice . object . slice ( splice . index , splice . index + splice . addedCount ) ;
339+ _adoptSplices : function ( splices ) {
340+ this . _pendingSplices = this . _pendingSplices . concat ( splices ) ;
324341
325- splice . removed . forEach ( function ( removed ) {
326- this . remove ( removed ) ;
342+ // We can afford apply removals synchronously, so we do that first
343+ // and save the `added` operations for the `debounce` below.
344+ this . _applyLocalDataChange ( function ( ) {
345+ splices . forEach ( function ( splice ) {
346+ splice . removed . forEach ( function ( removed ) {
347+ this . remove ( removed ) ;
348+ } , this ) ;
327349 } , this ) ;
350+ } ) ;
328351
329- added . forEach ( function ( added ) {
330- this . add ( added ) ;
331- } , this ) ;
332- } , this ) ;
352+ // We async until the next turn. The reason we want to do this is
353+ // that splicing within a splice handler will break the data binding
354+ // system in some places. This is referred to as the "re-entrancy"
355+ // problem. See polymer/polymer#2491.
356+ this . debounce ( '_adoptSplices' , function ( ) {
357+ this . _applyLocalDataChange ( function ( ) {
358+ var splices = this . _pendingSplices ;
359+
360+ this . _pendingSplices = [ ] ;
361+
362+ splices . forEach ( function ( splice ) {
363+ var added = splice . object . slice ( splice . index , splice . index + splice . addedCount ) ;
364+
365+ added . forEach ( function ( added , index ) {
366+ this . _lastLocallyAddedIndex = splice . index + index ;
367+ this . add ( added ) ;
368+ } , this ) ;
369+ } , this ) ;
370+
371+ this . _lastLocallyAddedIndex = null ;
372+ } ) ;
373+ } ) ;
333374 } ,
334375
335376 _computeQuery : function ( location , limitToFirst , limitToLast , orderByMethodName , startAt , endAt , equalTo ) {
@@ -339,6 +380,8 @@ <h4>[[dinosaur.__firebaseKey__]]</h4>
339380 return ;
340381 }
341382
383+ this . _log ( 'Recomputing query.' , arguments ) ;
384+
342385 query = new Firebase ( location ) ;
343386
344387 if ( orderByMethodName ) {
@@ -460,17 +503,56 @@ <h4>[[dinosaur.__firebaseKey__]]</h4>
460503 _onFirebaseChildAdded : function ( event ) {
461504 this . _applyRemoteDataChange ( function ( ) {
462505 var value = this . _valueFromSnapshot ( event . detail . childSnapshot ) ;
506+ var key = value . __firebaseKey__ ;
463507 var previousValueKey = event . detail . previousChildName ;
464508 var index = previousValueKey != null ?
465509 this . data . indexOf ( this . _valueMap [ previousValueKey ] ) + 1 : 0 ;
510+ var lastLocallyAddedValue ;
466511
467512 this . _valueMap [ value . __firebaseKey__ ] = value ;
468513
469- this . splice ( 'data' , index , 0 , value ) ;
514+ // NOTE(cdata): The rationale for this conditional dance around the
515+ // last locally added index (since you will inevitably be wondering
516+ // why we do it):
517+ // There may be a "locally" added value which was spliced in. If
518+ // there is, it may be in the "wrong" place in the array. This is
519+ // due to the fact that Firebase may be applying a sort to the
520+ // data, so we won't know the correct index for the locally added
521+ // value until the `child_added` event is fired.
522+ // Once we get the `child_added` event, we can check to see if the
523+ // locally added value is in the right place. If it is, we just
524+ // `set` it to the Firebase-provided value. If it is not, then
525+ // we grab the original value, splice in the Firebase-provided
526+ // value in the right place, and then (importantly: at the end)
527+ // find the locally-added value again (since its index may have
528+ // changed by splicing-in Firebase's value) and splice it out of the
529+ // array.
530+ if ( this . _lastLocallyAddedIndex === index ) {
531+ this . set ( [ 'data' , index ] , value ) ;
532+ } else {
533+ if ( this . _lastLocallyAddedIndex != null ) {
534+ lastLocallyAddedValue = this . data [ this . _lastLocallyAddedIndex ] ;
535+ }
536+
537+ this . splice ( 'data' , index , 0 , value ) ;
538+
539+ if ( this . _lastLocallyAddedIndex != null ) {
540+ this . splice ( 'data' , this . data . indexOf ( lastLocallyAddedValue ) , 1 ) ;
541+ }
542+ }
470543 } ) ;
471544 } ,
472545
473546 _onFirebaseChildRemoved : function ( event ) {
547+ if ( this . _receivingLocalChanges ) {
548+ this . _valueMap [ event . detail . oldChildSnapshot . key ( ) ] = null ;
549+ // NOTE(cdata): If we are receiving local changes, that means that
550+ // the splices have already been performed and items are already
551+ // removed from the local data representation. No need to remove
552+ // them again.
553+ return ;
554+ }
555+
474556 this . _applyRemoteDataChange ( function ( ) {
475557 var key = event . detail . oldChildSnapshot . key ( ) ;
476558 var value = this . _valueMap [ key ] ;
0 commit comments