diff --git a/.gitignore b/.gitignore index 002cc86..ce31cfb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ /lib /node_modules - +/.idea diff --git a/package.json b/package.json index fda9aa7..c76f2af 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "kebakaran", - "version": "0.1.9", + "version": "0.1.10", "description": "high level utilities for firebase interaction", "main": "index.js", "scripts": { diff --git a/src/FirebaseList.js b/src/FirebaseList.js index d2f57f1..3a65891 100644 --- a/src/FirebaseList.js +++ b/src/FirebaseList.js @@ -2,7 +2,7 @@ import EventEmitter from 'eventemitter3'; import FirebaseStruct from './FirebaseStruct'; -const noValue = Symbol(); +const NO_VALUE = Symbol(); export default class FirebaseList extends EventEmitter { @@ -10,21 +10,28 @@ export default class FirebaseList extends EventEmitter { items = {}; values = {}; - constructor(ref, getFields, idField = 'id') { + constructor(ref, getFields, idField = 'id', instant = false) { super(); this.ref = ref; this.getFields = getFields; this.idField = idField; + this.instant = instant; + this.hasInitialData = !instant; this.subscribe(); } subscribe() { - this.onChildAdded = ::this.onChildAdded; - this.onChildRemoved = ::this.onChildRemoved; - this.ref.on('child_added', this.onChildAdded); - this.ref.on('child_removed', this.onChildRemoved); + if (this.instant) { + this.onValue = ::this.onValue; + this.ref.on('value', this.onValue); + } else { + this.onChildAdded = ::this.onChildAdded; + this.onChildRemoved = ::this.onChildRemoved; + this.ref.on('child_added', this.onChildAdded); + this.ref.on('child_removed', this.onChildRemoved); + } } on(name, listener, context) { @@ -34,39 +41,68 @@ export default class FirebaseList extends EventEmitter { } } + onValue(snapshot) { + const newKeys = []; + snapshot.forEach(itemSnapshot => { + newKeys.push(itemSnapshot.key()); + }); + for (const key of newKeys) { + if (this.keys.indexOf(key) === -1) { + this.addChild(key); + } + } + for (const key of this.keys) { + if (newKeys.indexOf(key) === -1) { + this.removeChild(key); + } + } + this.keys = newKeys; + this.hasInitialData = true; + this.flush(); + } + onChildAdded(c) { const key = c.key(); + this.addChild(key); + } + + onChildRemoved(c) { + const key = c.key(); + this.removeChild(key); + this.flush(); + } + + addChild(key) { const item = new FirebaseStruct(this.getFields, key); - this.values[key] = noValue; + this.values[key] = NO_VALUE; this.keys.push(key); this.items[key] = item; item.on('value', value => { - this.onValue(key, value); + this.onItemValue(key, value); }); } - onChildRemoved(c) { - const key = c.key(); - + removeChild(key) { this.items[key].close(); delete this.items[key]; delete this.values[key]; this.keys.splice(this.keys.indexOf(key), 1); - - this.flush(); } - onValue(key, item) { + onItemValue(key, item) { this.values[key] = item; this.flush(); } hasData() { + if (!this.hasInitialData) { + return false; + } for (const key of this.keys) { - if (this.values[key] === noValue) { + if (this.values[key] === NO_VALUE) { return false; } } @@ -89,8 +125,13 @@ export default class FirebaseList extends EventEmitter { } this.off('value'); - this.ref.off('child_added', this.onChildAdded); - this.ref.off('child_removed', this.onChildRemoved); + + if (this.instant) { + this.ref.off('value', this.onValue); + } else { + this.ref.off('child_added', this.onChildAdded); + this.ref.off('child_removed', this.onChildRemoved); + } } } diff --git a/src/FirebaseStream.js b/src/FirebaseStream.js index 4941e07..a49887a 100644 --- a/src/FirebaseStream.js +++ b/src/FirebaseStream.js @@ -8,14 +8,14 @@ function snapshotValue(snapshotOrValue) { return snapshotOrValue; } -export default class FirebaseStream { +const NO_VALUE = Symbol(); - static noValue = Symbol(); +export default class FirebaseStream { constructor(ref) { this.ref = ref; - this.sentValue = this.constructor.noValue; - this.currentValue = this.constructor.noValue; + this.sentValue = NO_VALUE; + this.currentValue = NO_VALUE; this.resolve = null; this.update = ::this.update; this.ref.on('value', this.update); diff --git a/tests/Firebase-tape.js b/tests/Firebase-tape.js index 604597b..31d470d 100644 --- a/tests/Firebase-tape.js +++ b/tests/Firebase-tape.js @@ -3,13 +3,13 @@ import Firebase from 'firebase'; import { FirebaseList } from '../src'; -test.skip('Firebase', t => { +test.skip('FirebaseList', t => { t.plan(2); const list = new FirebaseList(new Firebase('https://kebakaran-test.firebaseio.com/list'), key => ({ name: new Firebase(`https://kebakaran-test.firebaseio.com/names/${key}`), count: new Firebase(`https://kebakaran-test.firebaseio.com/counts/${key}`), - })); + }), 'id'); let step = 1; @@ -32,6 +32,8 @@ test.skip('Firebase', t => { ]); list.close(); + Firebase.goOffline(); + t.end(); } }); diff --git a/tests/FirebaseList-tape.js b/tests/FirebaseList-tape.js index ac2fd8f..09ed81d 100644 --- a/tests/FirebaseList-tape.js +++ b/tests/FirebaseList-tape.js @@ -56,3 +56,55 @@ test('FirebaseList', t => { t.end(); }); + +test('FirebaseList instant', t => { + t.plan(11); + + const listRef = new RefMock(); + const nameRefs = {}; + nameRefs.foo = new RefMock(); + nameRefs.bar = new RefMock(); + + const list = new FirebaseList(listRef, key => ({ + name: nameRefs[key], + }), 'id', true); + + let step = 1; + + list.on('value', value => { + if (step === 1) { + t.deepEqual(value, [{ name: 'foo', id: 'foo' }, { name: 'bar', id: 'bar' }]); + step = 2; + } else if (step === 2) { + t.deepEqual(value, [{ name: 'bar', id: 'bar' }]); + step = 3; + } else if (step === 3) { + t.deepEqual(value, []); + step = 4; + } + }); + + t.equal(list.listeners('value').length, 1); + t.equal(listRef.listeners('child_added').length, 0); + t.equal(listRef.listeners('child_removed').length, 0); + t.equal(listRef.listeners('value').length, 1); + t.equal(nameRefs.foo.listeners('value').length, 0); + t.equal(nameRefs.bar.listeners('value').length, 0); + + listRef.emitValue({ + foo: true, + bar: true, + }); + + t.equal(nameRefs.foo.listeners('value').length, 1); + t.equal(nameRefs.bar.listeners('value').length, 1); + + nameRefs.foo.emitValue('foo'); + nameRefs.bar.emitValue('bar'); + + listRef.emitValue({ + bar: true, + }); + + listRef.emitValue({}); +}); diff --git a/tests/RefMock.js b/tests/RefMock.js index d82f67d..2561087 100644 --- a/tests/RefMock.js +++ b/tests/RefMock.js @@ -1,28 +1,38 @@ import EventEmitter from 'eventemitter3'; -export default class RefMock extends EventEmitter { +class SnapshotMock { + + constructor(val, key) { + this._val = val; + this._key = key; + } - on(name, listener) { - super.on(name, listener); - return listener; + val() { + return this._val; } + key() { + return this._key; + } + + forEach(...args) { + Object.keys(this._val).map(val => new SnapshotMock(undefined, val)).forEach(...args); + } + +} + +export default class RefMock extends EventEmitter { + emitValue(val) { - this.emit('value', { - val: () => val, - }); + this.emit('value', new SnapshotMock(val)); } emitChildAdded(key) { - this.emit('child_added', { - key: () => key, - }); + this.emit('child_added', new SnapshotMock(undefined, key)); } emitChildRemoved(key) { - this.emit('child_removed', { - key: () => key, - }); + this.emit('child_removed', new SnapshotMock(undefined, key)); } }