Skip to content

Commit 71528b9

Browse files
authored
Merge pull request #19 from zeevl/issue-81-onsnapshot
onSnapshot support
2 parents ba40182 + 746b287 commit 71528b9

File tree

8 files changed

+407
-115
lines changed

8 files changed

+407
-115
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
node_modules
44
coverage
55
browser
6+
.vscode

package-lock.json

Lines changed: 66 additions & 66 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/firestore-document-snapshot.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,21 @@
22

33
var _ = require('./lodash');
44

5-
function MockFirestoreDocumentSnapshot (id, ref, data) {
5+
function MockFirestoreDocumentSnapshot(id, ref, data) {
66
this.id = id;
77
this.ref = ref;
88
this._snapshotdata = _.cloneDeep(data) || null;
99
this.data = function() {
1010
return _.cloneDeep(this._snapshotdata);
1111
};
1212
this.exists = this._snapshotdata !== null;
13+
this.metadata = {
14+
fromCache: true,
15+
hasPendingWrites: false
16+
};
1317
}
1418

15-
MockFirestoreDocumentSnapshot.prototype.get = function (path) {
19+
MockFirestoreDocumentSnapshot.prototype.get = function(path) {
1620
if (!path || !this.exists) return undefined;
1721

1822
var parts = path.split('.');

src/firestore-document.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,45 @@ MockFirestoreDocument.prototype.delete = function (callback) {
200200
});
201201
};
202202

203+
MockFirestoreDocument.prototype.onSnapshot = function (optionsOrObserverOrOnNext, observerOrOnNextOrOnError, onErrorArg) {
204+
var err = this._nextErr('onSnapshot');
205+
var self = this;
206+
var onNext = optionsOrObserverOrOnNext;
207+
var onError = observerOrOnNextOrOnError;
208+
var includeMetadataChanges = optionsOrObserverOrOnNext.includeMetadataChanges;
209+
210+
if (includeMetadataChanges) {
211+
// Note this doesn't truly mimic the firestore metadata changes behavior, however
212+
// since everything is syncronous, there isn't any difference in behavior.
213+
onNext = observerOrOnNextOrOnError;
214+
onError = onErrorArg;
215+
}
216+
var context = {
217+
data: self._getData(),
218+
};
219+
var onSnapshot = function (forceTrigger) {
220+
// compare the current state to the one from when this function was created
221+
// and send the data to the callback if different.
222+
if (err === null) {
223+
if (!_.isEqual(self.data, context.data) || includeMetadataChanges || forceTrigger) {
224+
onNext(new DocumentSnapshot(self.id, self.ref, self._getData()));
225+
context.data = self._getData();
226+
}
227+
} else {
228+
onError(err);
229+
}
230+
};
231+
232+
// onSnapshot should always return when initially called, then
233+
// every time data changes.
234+
onSnapshot(true);
235+
var unsubscribe = this.queue.onPostFlush(onSnapshot);
236+
237+
// return the unsubscribe function
238+
return unsubscribe;
239+
};
240+
241+
203242
/**
204243
* Fetches the subcollections that are direct children of the document.
205244
* @see https://cloud.google.com/nodejs/docs/reference/firestore/0.15.x/DocumentReference#getCollections

src/firestore-query.js

Lines changed: 103 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
'use strict';
22

33
var _ = require('./lodash');
4-
var assert = require('assert');
54
var Stream = require('stream');
65
var Promise = require('rsvp').Promise;
7-
var autoId = require('firebase-auto-ids');
8-
var DocumentSnapshot = require('./firestore-document-snapshot');
96
var QuerySnapshot = require('./firestore-query-snapshot');
7+
var DocumentSnapshot = require('./firestore-document-snapshot');
108
var Queue = require('./queue').Queue;
119
var utils = require('./utils');
12-
var validate = require('./validators');
1310

1411
function MockFirestoreQuery(path, data, parent, name) {
1512
this.errs = {};
@@ -69,51 +66,9 @@ MockFirestoreQuery.prototype.get = function () {
6966
var self = this;
7067
return new Promise(function (resolve, reject) {
7168
self._defer('get', _.toArray(arguments), function () {
72-
var results = {};
73-
var limit = 0;
74-
var atStart = false;
75-
var atEnd = false;
76-
var startFinder = this.buildStartFinder();
77-
78-
var inRange = function(data, key) {
79-
if (atEnd) {
80-
return false;
81-
} else if (atStart) {
82-
return true;
83-
} else {
84-
atStart = startFinder(data, key);
85-
return atStart;
86-
}
87-
};
88-
69+
var results = self._results();
8970
if (err === null) {
9071
if (_.size(self.data) !== 0) {
91-
if (self.orderedProperties.length === 0) {
92-
_.forEach(self.data, function(data, key) {
93-
if (inRange(data, key) && (self.limited <= 0 || limit < self.limited)) {
94-
results[key] = _.cloneDeep(data);
95-
limit++;
96-
}
97-
});
98-
} else {
99-
var queryable = [];
100-
_.forEach(self.data, function(data, key) {
101-
queryable.push({
102-
data: data,
103-
key: key
104-
});
105-
});
106-
107-
queryable = _.orderBy(queryable, _.map(self.orderedProperties, function(p) { return 'data.' + p; }), self.orderedDirections);
108-
109-
queryable.forEach(function(q) {
110-
if (inRange(q.data, q.key) && (self.limited <= 0 || limit < self.limited)) {
111-
results[q.key] = _.cloneDeep(q.data);
112-
limit++;
113-
}
114-
});
115-
}
116-
11772
resolve(new QuerySnapshot(self.parent === null ? self : self.parent.collection(self.id), results));
11873
} else {
11974
resolve(new QuerySnapshot(self.parent === null ? self : self.parent.collection(self.id)));
@@ -233,6 +188,107 @@ MockFirestoreQuery.prototype.clone = function () {
233188
return query;
234189
};
235190

191+
MockFirestoreQuery.prototype.onSnapshot = function (optionsOrObserverOrOnNext, observerOrOnNextOrOnError, onErrorArg) {
192+
var err = this._nextErr('onSnapshot');
193+
var self = this;
194+
var onNext = optionsOrObserverOrOnNext;
195+
var onError = observerOrOnNextOrOnError;
196+
var includeMetadataChanges = optionsOrObserverOrOnNext.includeMetadataChanges;
197+
198+
if (includeMetadataChanges) {
199+
// Note this doesn't truly mimic the firestore metadata changes behavior, however
200+
// since everything is syncronous, there isn't any difference in behavior.
201+
onNext = observerOrOnNextOrOnError;
202+
onError = onErrorArg;
203+
}
204+
var context = {
205+
data: self._results(),
206+
};
207+
var onSnapshot = function (forceTrigger) {
208+
// compare the current state to the one from when this function was created
209+
// and send the data to the callback if different.
210+
if (err === null) {
211+
if (forceTrigger) {
212+
const results = self._results();
213+
if (_.size(self.data) !== 0) {
214+
onNext(new QuerySnapshot(self.parent === null ? self : self.parent.collection(self.id), results));
215+
} else {
216+
onNext(new QuerySnapshot(self.parent === null ? self : self.parent.collection(self.id)));
217+
}
218+
} else {
219+
self.get().then(function (querySnapshot) {
220+
var results = self._results();
221+
if (!_.isEqual(results, context.data) || includeMetadataChanges) {
222+
onNext(new QuerySnapshot(self.parent === null ? self : self.parent.collection(self.id), results));
223+
context.data = results;
224+
}
225+
});
226+
}
227+
} else {
228+
onError(err);
229+
}
230+
};
231+
232+
// onSnapshot should always return when initially called, then
233+
// every time data changes.
234+
onSnapshot(true);
235+
var unsubscribe = this.queue.onPostFlush(onSnapshot);
236+
237+
// return the unsubscribe function
238+
return unsubscribe;
239+
};
240+
241+
MockFirestoreQuery.prototype._results = function () {
242+
var results = {};
243+
var limit = 0;
244+
var atStart = false;
245+
var atEnd = false;
246+
var startFinder = this.buildStartFinder();
247+
248+
var inRange = function(data, key) {
249+
if (atEnd) {
250+
return false;
251+
} else if (atStart) {
252+
return true;
253+
} else {
254+
atStart = startFinder(data, key);
255+
return atStart;
256+
}
257+
};
258+
if (_.size(this.data) === 0) {
259+
return results;
260+
}
261+
262+
var self = this;
263+
if (this.orderedProperties.length === 0) {
264+
_.forEach(this.data, function(data, key) {
265+
if (inRange(data, key) && (self.limited <= 0 || limit < self.limited)) {
266+
results[key] = _.cloneDeep(data);
267+
limit++;
268+
}
269+
});
270+
} else {
271+
var queryable = [];
272+
_.forEach(self.data, function(data, key) {
273+
queryable.push({
274+
data: data,
275+
key: key
276+
});
277+
});
278+
279+
queryable = _.orderBy(queryable, _.map(self.orderedProperties, function(p) { return 'data.' + p; }), self.orderedDirections);
280+
281+
queryable.forEach(function(q) {
282+
if (inRange(q.data, q.key) && (self.limited <= 0 || limit < self.limited)) {
283+
results[q.key] = _.cloneDeep(q.data);
284+
limit++;
285+
}
286+
});
287+
}
288+
289+
return results;
290+
};
291+
236292
MockFirestoreQuery.prototype._defer = function (sourceMethod, sourceArgs, callback) {
237293
this.queue.push({
238294
fn: callback,

src/queue.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ var EventEmitter = require('events').EventEmitter;
66

77
function FlushQueue () {
88
this.events = [];
9+
this.postFlushListeners = [];
910
}
1011

1112
FlushQueue.prototype.push = function () {
@@ -23,6 +24,14 @@ FlushQueue.prototype.push = function () {
2324
}));
2425
};
2526

27+
FlushQueue.prototype.onPostFlush = function(subscriber) {
28+
this.postFlushListeners.push(subscriber);
29+
var self = this;
30+
return function() {
31+
self.postFlushListeners.pop(subscriber);
32+
};
33+
};
34+
2635
FlushQueue.prototype.flushing = false;
2736

2837
FlushQueue.prototype.flush = function (delay) {
@@ -33,6 +42,9 @@ FlushQueue.prototype.flush = function (delay) {
3342
}
3443
function process () {
3544
self.flushing = true;
45+
_.forEach(self.postFlushListeners, function (subscriber) {
46+
self.push(subscriber);
47+
});
3648
while (self.events.length) {
3749
self.events[0].run();
3850
}

test/unit/firestore-collection.js

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,4 +419,99 @@ describe('MockFirestoreCollection', function () {
419419
]);
420420
});
421421
});
422+
423+
describe('#onSnapshot', function () {
424+
it('returns value after collection is updated', function (done) {
425+
var callCount = 0;
426+
collection.onSnapshot(function(snap) {
427+
callCount += 1;
428+
var names = [];
429+
snap.docs.forEach(function(doc) {
430+
names.push(doc.data().name);
431+
});
432+
433+
if (callCount === 2) {
434+
expect(names).to.contain('A');
435+
expect(names).not.to.contain('a');
436+
done();
437+
}
438+
});
439+
collection.doc('a').update({name: 'A'}, {setMerge: true});
440+
collection.flush();
441+
});
442+
443+
it('calls callback after multiple updates', function (done) {
444+
var callCount = 0;
445+
collection.onSnapshot(function(snap) {
446+
callCount += 1;
447+
var names = [];
448+
snap.docs.forEach(function(doc) {
449+
names.push(doc.data().name);
450+
});
451+
452+
if (callCount === 2) {
453+
expect(names).to.contain('A');
454+
expect(names).not.to.contain('a');
455+
}
456+
457+
if (callCount === 3) {
458+
expect(names).to.contain('AA');
459+
expect(names).not.to.contain('A');
460+
done();
461+
}
462+
});
463+
464+
collection.doc('a').update({name: 'A'}, {setMerge: true});
465+
collection.flush();
466+
collection.doc('a').update({name: 'AA'}, {setMerge: true});
467+
collection.flush();
468+
});
469+
470+
it('should unsubscribe', function (done) {
471+
var callCount = 0;
472+
var unsubscribe = collection.onSnapshot(function(snap) {
473+
callCount += 1;
474+
});
475+
476+
collection.doc('a').update({name: 'A'}, {setMerge: true});
477+
collection.flush();
478+
479+
process.nextTick(function() {
480+
expect(callCount).to.equal(2);
481+
482+
collection.doc('a').update({name: 'AA'}, {setMerge: true});
483+
unsubscribe();
484+
485+
collection.flush();
486+
487+
process.nextTick(function() {
488+
expect(callCount).to.equal(2);
489+
done();
490+
});
491+
});
492+
493+
494+
});
495+
496+
it('Calls onError if error', function (done) {
497+
var error = new Error("An error occured.");
498+
collection.errs.onSnapshot = error;
499+
var callCount = 0;
500+
collection.onSnapshot(function(snap) {
501+
throw new Error("This should not be called.");
502+
}, function(err) {
503+
// onSnapshot always returns when first called and then
504+
// after data changes so we get 2 calls here.
505+
if (callCount == 0) {
506+
callCount++;
507+
return;
508+
}
509+
expect(err).to.equal(error);
510+
done();
511+
});
512+
collection.doc('a').update({name: 'A'}, {setMerge: true});
513+
collection.flush();
514+
});
515+
516+
});
422517
});

0 commit comments

Comments
 (0)