Skip to content

Commit d8f2f97

Browse files
committed
Merge pull request #63 from GoogleWebComponents/fix-firebase-collection-regressions
Fix firebase collection regressions
2 parents cdaafc2 + b4dd34c commit d8f2f97

File tree

6 files changed

+390
-228
lines changed

6 files changed

+390
-228
lines changed

.travis.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
language: node_js
2+
sudo: false
3+
matrix:
4+
include:
5+
- node_js: stable
6+
script: xvfb-run -a wct --simpleOutput -l firefox -l chrome
7+
addons:
8+
firefox: latest
9+
apt:
10+
sources:
11+
- google-chrome
12+
packages:
13+
- google-chrome-stable
14+
- node_js: node
15+
script:
16+
- |
17+
if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then
18+
wct --simpleOutput -s 'Windows 10/microsoftedge' -s 'Windows 8.1/internet explorer@11' -s 'Windows 7/internet explorer@10' -s 'OS X 10.10/safari@8' -s 'OS X 10.9/safari@7'
19+
fi
20+
before_script:
21+
- npm install bower
22+
- npm install web-component-tester
23+
- export PATH=$PWD/node_modules/.bin:$PATH
24+
- bower install
25+
env:
26+
global:
27+
- secure: dlr2l5btcMEnaJI/VzBDIKhcJTUgnrQ6AN/S+qbPFqsckokV5SCt3k0f0Z22CBhBXJi2qVrLDX05+wZ4FkVkLLdGOf6zR0sSNvXubzvUu8oKaZczo2B8EAs+OjsuvaZWPYTabAOGvyAwuBzfZaYDlXbk6Dlb/51hjzSl2D5/WbY=
28+
- secure: dFreaGtRTAwcFtSZ57ktyiDejTkJ7vI9TVbJZ0Yd9Adp/4mINOOgHDFiVDl5kDCy9xx21vCRel+IrGkOdRllk+OWxO8Ga1OQF4EzchvUsxzngDwi3I0P1+uNuzjn8MyEvE4HYPwDZ0mDYzMLJS9b1GlcpMTvJvE2Sg3ly8h4wYc=

firebase-collection.html

Lines changed: 106 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -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];

firebase-document.html

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
'firebase-value': '_onFirebaseValue'
6565
},
6666

67-
_applyLocalDataChanges: function(change) {
67+
_localDataChanged: function(change) {
6868
var pathFragments = change.path.split('.');
6969

7070
if (pathFragments.length === 1) {
@@ -98,8 +98,10 @@
9898
},
9999

100100
_setRemoteDocumentChild: function(key, value) {
101-
this._log('Setting child "' + key + '" to', value);
102-
this.query.child(key).set(value);
101+
this.debounce('set-' + key, function() {
102+
this._log('Setting child "' + key + '" to', value);
103+
this.query.child(key).set(value);
104+
});
103105
},
104106

105107
_removeRemoteDocumentChild: function(key) {

firebase-query-behavior.html

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -40,18 +40,18 @@
4040
type: Boolean,
4141
value: false,
4242
reflectToAttribute: true
43-
},
44-
45-
_receivingRemoteChanges: {
46-
type: Boolean,
47-
value: false
4843
}
4944
},
5045

5146
observers: [
5247
'_dataChanged(data.*)'
5348
],
5449

50+
created: function() {
51+
this._receivingLocalChanges = false;
52+
this._receivingRemoteChanges = false;
53+
},
54+
5555
get dataAsObject() {
5656
if (Array.isArray(this.data)) {
5757
return this.data.reduce(function(object, value, index) {
@@ -69,27 +69,29 @@
6969
this.location = '';
7070
},
7171

72-
_applyLocalDataChanges: function(changes) {
72+
_localDataChanged: function(changes) {
7373
// Virtual..
7474
},
7575

76+
_applyLocalDataChange: function(applyChange) {
77+
this._receivingLocalChanges = true;
78+
applyChange.call(this);
79+
this._receivingLocalChanges = false;
80+
},
81+
7682
_applyRemoteDataChange: function(applyChange) {
77-
if (this._applyingLocalDataChanges) {
78-
return;
79-
}
8083
this._receivingRemoteChanges = true;
8184
applyChange.call(this);
8285
this._receivingRemoteChanges = false;
8386
},
8487

8588
_dataChanged: function(changes) {
86-
if (this._receivingRemoteChanges) {
89+
if (this._receivingRemoteChanges ||
90+
this._receivingLocalChanges) {
8791
return;
8892
}
8993

90-
this._applyingLocalDataChanges = true;
91-
this._applyLocalDataChanges(changes);
92-
this._applyingLocalDataChanges = false;
94+
this._localDataChanged(changes);
9395
},
9496

9597
_queryChanged: function(query, oldQuery) {
@@ -181,9 +183,15 @@
181183
}
182184
},
183185

186+
_warn: function() {
187+
if (this.log) {
188+
Polymer.Base._warn(this._logf.apply(this, arguments));
189+
}
190+
},
191+
184192
_error: function() {
185193
if (this.log) {
186-
console.error.apply(console, arguments);
194+
Polymer.Base._error(this._logf.apply(this, arguments));
187195
}
188196
}
189197
};

0 commit comments

Comments
 (0)