Skip to content
This repository was archived by the owner on May 19, 2025. It is now read-only.

Commit 6cbe2ec

Browse files
committed
Refactor RealtimeListenerService a bit, support findAll
1 parent a4ebe62 commit 6cbe2ec

4 files changed

Lines changed: 144 additions & 98 deletions

File tree

‎addon/adapters/firestore.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ const getDoc = (adapter: FirestoreAdapter, type: DS.Model, id: string) => docRef
236236
const collectionNameForType = (type: any) => pluralize(camelize(typeof(type) === 'string' ? type : type.modelName));
237237
const docReference = (adapter: FirestoreAdapter, type: any, id: string) => rootCollection(adapter, type).then(collection => collection.doc(id));
238238
const getDocs = (query: CollectionReferenceOrQuery) => query.get();
239-
const rootCollection = (adapter: FirestoreAdapter, type: any) => getFirestore(adapter).then(firestore => {
239+
export const rootCollection = (adapter: FirestoreAdapter, type: any) => getFirestore(adapter).then(firestore => {
240240
const namespace = get(adapter, 'namespace');
241241
const root = namespace ? firestore.doc(namespace) : firestore;
242242
return root.collection(collectionNameForType(type));

‎addon/adapters/realtime-database.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ const noop = (ref: database.Reference) => ref;
226226
const queryDocs = (referenceOrQuery: ReferenceOrQuery, query?: ReferenceOrQueryFn) => getDocs((query || noop)(referenceOrQuery));
227227
// TODO allow override
228228
const collectionNameForType = (type: any) => pluralize(camelize(typeof(type) === 'string' ? type : type.modelName));
229-
const rootCollection = (adapter: RealtimeDatabaseAdapter, type: any) => databaseInstance(adapter).then(database => database.ref([get(adapter, 'namespace'), collectionNameForType(type)].join('/')));
229+
export const rootCollection = (adapter: RealtimeDatabaseAdapter, type: any) => databaseInstance(adapter).then(database => database.ref([get(adapter, 'namespace'), collectionNameForType(type)].join('/')));
230230
const getDocs = (query: ReferenceOrQuery) => query.once('value').then(value => ((value as any).query = query) && value);
231231
const docReference = (adapter: RealtimeDatabaseAdapter, type: any, id: string) => rootCollection(adapter, type).then(ref => ref.child(id));
232232

‎addon/services/realtime-listener.ts‎

Lines changed: 139 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import { run } from '@ember/runloop';
66
import { firestore, database } from 'firebase/app';
77

88
// TODO don't hardcode these, but having trouble otherwise
9-
import { normalize as firestoreNormalize } from '../serializers/firestore';
10-
import { normalize as databaseNormalize } from '../serializers/realtime-database';
9+
import { rootCollection as firestoreRootCollection } from '../adapters/firestore';
10+
import { rootCollection as realtimeDatabaseRootCollection } from '../adapters/realtime-database';
1111

1212
const getService = (object:Object) => getOwner(object).lookup('service:realtime-listener') as RealtimeListenerService;
1313
const isFastboot = (object:Object) => {
@@ -56,6 +56,24 @@ function isFirestoreDocumentRefernce(arg: any): arg is firestore.DocumentReferen
5656
return arg.onSnapshot !== undefined;
5757
}
5858

59+
type SubscribeArgs = {
60+
model: any;
61+
store: DS.Store;
62+
modelName: never;
63+
modelClass: never;
64+
uniqueIdentifier: any;
65+
serializer: any;
66+
adapter: never;
67+
}
68+
69+
type FirestoreQueryArgs = {
70+
query: firestore.Query | firestore.CollectionReference
71+
} & SubscribeArgs
72+
73+
type RealtimeDatabaseQueryArgs = {
74+
ref: database.Reference
75+
} & SubscribeArgs
76+
5977
export default class RealtimeListenerService extends Service.extend({
6078

6179
routeSubscriptions: {} as {[key:string]: {[key:string]: () => void}}
@@ -69,108 +87,22 @@ export default class RealtimeListenerService extends Service.extend({
6987
const query = model.get('meta.query') as firestore.Query|database.Reference|undefined;
7088
const ref = model.get('_internalModel._recordData._data._ref') as firestore.DocumentReference|database.Reference|undefined;
7189
const uniqueIdentifier = model.toString();
90+
const serializer = store.serializerFor(modelName) as any; // TODO type
91+
const adapter = store.adapterFor(modelName);
92+
const args = { model, store, modelName, modelClass, uniqueIdentifier, serializer, adapter };
7293
if (query) {
7394
if (isFirestoreQuery(query)) {
74-
const unsubscribe = query.onSnapshot(snapshot => {
75-
snapshot.docChanges().forEach(change => run(() => {
76-
const normalizedData = firestoreNormalize(store, modelClass, change.doc);
77-
switch(change.type) {
78-
case 'added': {
79-
const current = model.content.objectAt(change.newIndex);
80-
if (current == null || current.id !== change.doc.id ) {
81-
const doc = store.push(normalizedData) as any;
82-
model.content.insertAt(change.newIndex, doc._internalModel);
83-
}
84-
break;
85-
}
86-
case 'modified': {
87-
const current = model.content.objectAt(change.oldIndex);
88-
if (current == null || current.id == change.doc.id) {
89-
if (change.newIndex !== change.oldIndex) {
90-
model.content.removeAt(change.oldIndex);
91-
model.content.insertAt(change.newIndex, current)
92-
}
93-
}
94-
store.push(normalizedData);
95-
break;
96-
}
97-
case 'removed': {
98-
const current = model.content.objectAt(change.oldIndex);
99-
if (current && current.id == change.doc.id) {
100-
model.content.removeAt(change.oldIndex);
101-
}
102-
break;
103-
}
104-
}
105-
}))
106-
});
95+
const unsubscribe = runFirestoreCollectionListener({query, ...args});
10796
setRouteSubscription(this, route, uniqueIdentifier, unsubscribe);
10897
} else {
109-
const onChildAdded = query.on('child_added', (snapshot, priorKey) => {
110-
run(() => {
111-
if (snapshot) {
112-
const normalizedData = databaseNormalize(store, modelClass, snapshot);
113-
const doc = store.push(normalizedData) as any;
114-
const existing = model.content.find((record:any) => record.id === doc.id);
115-
if (existing) { model.content.removeObject(existing); }
116-
let insertIndex = 0;
117-
if (priorKey) {
118-
const record = model.content.find((record:any) => record.id === priorKey);
119-
insertIndex = model.content.indexOf(record) + 1;
120-
}
121-
const current = model.content.objectAt(insertIndex);
122-
if (current == null || current.id !== doc.id ) {
123-
model.content.insertAt(insertIndex, doc._internalModel);
124-
}
125-
}
126-
});
127-
});
128-
const onChildRemoved = query.on('child_removed', snapshot => {
129-
run(() => {
130-
if (snapshot) {
131-
const record = model.content.find((record:any) => record.id === snapshot.key)
132-
if (record) { model.content.removeObject(record); }
133-
}
134-
});
135-
});
136-
const onChildChanged = query.on('child_changed', snapshot => {
137-
run(() => {
138-
if (snapshot) {
139-
const normalizedData = databaseNormalize(store, modelClass, snapshot);
140-
store.push(normalizedData);
141-
}
142-
});
143-
});
144-
const onChildMoved = query.on('child_moved', (snapshot, priorKey) => {
145-
run(() => {
146-
if (snapshot) {
147-
const normalizedData = databaseNormalize(store, modelClass, snapshot);
148-
const doc = store.push(normalizedData) as any;
149-
const existing = model.content.find((record:any) => record.id === doc.id);
150-
if (existing) { model.content.removeObject(existing); }
151-
if (priorKey) {
152-
const record = model.content.find((record:any) => record.id === priorKey);
153-
const index = model.content.indexOf(record);
154-
model.content.insertAt(index+1, doc._internalModel);
155-
} else {
156-
model.content.insertAt(0, doc._internalModel);
157-
}
158-
}
159-
});
160-
});
161-
const unsubscribe = () => {
162-
query.off('child_added', onChildAdded);
163-
query.off('child_removed', onChildRemoved);
164-
query.off('child_changed', onChildChanged);
165-
query.off('child_moved', onChildMoved);
166-
}
98+
const unsubscribe = runRealtimeDatabaseListListener({ref: query, ...args});
16799
setRouteSubscription(this, route, uniqueIdentifier, unsubscribe);
168100
}
169101
} else if (ref) {
170102
if (isFirestoreDocumentRefernce(ref)) {
171103
const unsubscribe = ref.onSnapshot(doc => {
172104
run(() => {
173-
const normalizedData = firestoreNormalize(store, modelClass, doc);
105+
const normalizedData = serializer.normalizeSingleResponse(store, modelClass, doc);
174106
store.push(normalizedData);
175107
});
176108
});
@@ -180,7 +112,7 @@ export default class RealtimeListenerService extends Service.extend({
180112
run(() => {
181113
if (snapshot) {
182114
if (snapshot.exists()) {
183-
const normalizedData = databaseNormalize(store, modelClass, snapshot);
115+
const normalizedData = serializer.normalizeSingleResponse(store, modelClass, snapshot);
184116
store.push(normalizedData);
185117
} else {
186118
const record = store.findRecord(modelName, snapshot.key!)
@@ -192,6 +124,19 @@ export default class RealtimeListenerService extends Service.extend({
192124
const unsubscribe = () => ref.off('value', listener);
193125
setRouteSubscription(this, route, uniqueIdentifier, unsubscribe);
194126
}
127+
} else {
128+
// this might be a findAll, findAll strips metadata :(
129+
if (serializer.constructor.name == 'FirestoreSerializer') {
130+
firestoreRootCollection(adapter, modelName).then(query => {
131+
const unsubscribe = runFirestoreCollectionListener({query, ...args});
132+
setRouteSubscription(this, route, uniqueIdentifier, unsubscribe);
133+
});
134+
} else if (serializer.constructor.name == 'RealtimeDatabaseSerializer') {
135+
realtimeDatabaseRootCollection(adapter, modelName).then(ref => {
136+
const unsubscribe = runRealtimeDatabaseListListener({ref, ...args});
137+
setRouteSubscription(this, route, uniqueIdentifier, unsubscribe);
138+
});
139+
}
195140
}
196141
}
197142

@@ -201,6 +146,105 @@ export default class RealtimeListenerService extends Service.extend({
201146

202147
}
203148

149+
const runFirestoreCollectionListener = ({query, model, store, serializer, modelClass}: FirestoreQueryArgs) => {
150+
const unsubscribe = query.onSnapshot(snapshot => {
151+
snapshot.docChanges().forEach(change => run(() => {
152+
const normalizedData = serializer.normalizeSingleResponse(store, modelClass, change.doc);
153+
switch(change.type) {
154+
case 'added': {
155+
const current = model.content.objectAt(change.newIndex);
156+
if (current == null || current.id !== change.doc.id ) {
157+
const doc = store.push(normalizedData) as any;
158+
model.content.insertAt(change.newIndex, doc._internalModel);
159+
}
160+
break;
161+
}
162+
case 'modified': {
163+
const current = model.content.objectAt(change.oldIndex);
164+
if (current == null || current.id == change.doc.id) {
165+
if (change.newIndex !== change.oldIndex) {
166+
model.content.removeAt(change.oldIndex);
167+
model.content.insertAt(change.newIndex, current)
168+
}
169+
}
170+
store.push(normalizedData);
171+
break;
172+
}
173+
case 'removed': {
174+
const current = model.content.objectAt(change.oldIndex);
175+
if (current && current.id == change.doc.id) {
176+
model.content.removeAt(change.oldIndex);
177+
}
178+
break;
179+
}
180+
}
181+
}))
182+
});
183+
return unsubscribe;
184+
}
185+
186+
const runRealtimeDatabaseListListener = ({model, ref, serializer, store, modelClass}: RealtimeDatabaseQueryArgs) => {
187+
const onChildAdded = ref.on('child_added', (snapshot, priorKey) => {
188+
run(() => {
189+
if (snapshot) {
190+
const normalizedData = serializer.normalizeSingleResponse(store, modelClass, snapshot);
191+
const doc = store.push(normalizedData) as any;
192+
const existing = model.content.find((record:any) => record.id === doc.id);
193+
if (existing) { model.content.removeObject(existing); }
194+
let insertIndex = 0;
195+
if (priorKey) {
196+
const record = model.content.find((record:any) => record.id === priorKey);
197+
insertIndex = model.content.indexOf(record) + 1;
198+
}
199+
const current = model.content.objectAt(insertIndex);
200+
if (current == null || current.id !== doc.id ) {
201+
model.content.insertAt(insertIndex, doc._internalModel);
202+
}
203+
}
204+
});
205+
});
206+
const onChildRemoved = ref.on('child_removed', snapshot => {
207+
run(() => {
208+
if (snapshot) {
209+
const record = model.content.find((record:any) => record.id === snapshot.key)
210+
if (record) { model.content.removeObject(record); }
211+
}
212+
});
213+
});
214+
const onChildChanged = ref.on('child_changed', snapshot => {
215+
run(() => {
216+
if (snapshot) {
217+
const normalizedData = serializer.normalizeSingleResponse(store, modelClass, snapshot);
218+
store.push(normalizedData);
219+
}
220+
});
221+
});
222+
const onChildMoved = ref.on('child_moved', (snapshot, priorKey) => {
223+
run(() => {
224+
if (snapshot) {
225+
const normalizedData = serializer.normalizeSingleResponse(store, modelClass, snapshot);
226+
const doc = store.push(normalizedData) as any;
227+
const existing = model.content.find((record:any) => record.id === doc.id);
228+
if (existing) { model.content.removeObject(existing); }
229+
if (priorKey) {
230+
const record = model.content.find((record:any) => record.id === priorKey);
231+
const index = model.content.indexOf(record);
232+
model.content.insertAt(index+1, doc._internalModel);
233+
} else {
234+
model.content.insertAt(0, doc._internalModel);
235+
}
236+
}
237+
});
238+
});
239+
const unsubscribe = () => {
240+
ref.off('child_added', onChildAdded);
241+
ref.off('child_removed', onChildRemoved);
242+
ref.off('child_changed', onChildChanged);
243+
ref.off('child_moved', onChildMoved);
244+
}
245+
return unsubscribe;
246+
}
247+
204248
declare module '@ember/service' {
205249
interface Registry {
206250
"realtime-listener": RealtimeListenerService;

‎tests/dummy/app/routes/comments.js‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ export default Route.extend(RealtimeRouteMixin, {
77
firebaseApp: service(),
88
model() {
99
return this.firebaseApp.auth().then(({currentUser}) =>
10-
currentUser && this.store.query('comment', { filter: { user: currentUser.uid } }) || reject()
10+
currentUser &&
11+
this.store.query('comment', { filter: { user: currentUser.uid } }) ||
12+
this.store.findAll('comment')
1113
);
1214
}
1315
})

0 commit comments

Comments
 (0)