Skip to content

Commit

Permalink
Update realtime-listener service and realtime-route mixin to support …
Browse files Browse the repository at this point in the history
…multiple queries. Thanks to @iofusion

This solution is adapted from @iofusion's comments on this issue FirebaseExtended#576 (comment)
  • Loading branch information
arun-prasad-r committed Sep 13, 2019
1 parent e89fbb9 commit 66b83d2
Show file tree
Hide file tree
Showing 2 changed files with 79 additions and 50 deletions.
38 changes: 35 additions & 3 deletions addon/mixins/realtime-route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,44 @@ import DS from 'ember-data';
// TODO make sure realtime works on findAll
// handle includes
export default Mixin.create({
afterModel(model:DS.Model) {
subscribe(this, model);

subscribeModel(model) {
let subscriptionId = model.toString();
subscribe(this, model, subscriptionId);
},
unsubscribeModel(model) {
let subscriptionId = model.toString()
unsubscribe(this, subscriptionId);
},

afterModel(model) {
if (model instanceof (DS.Model) || model instanceof (DS.RecordArray)) {
this.subscribeModel(model);
} else {
let keys = Object.keys(model);
keys.forEach((key) => {
let individualModel = model[key];
if (individualModel instanceof (DS.Model) || individualModel instanceof (DS.RecordArray)) {
this.subscribeModel(model[key]);
}
});
}
return this._super(model);
},

deactivate() {
unsubscribe(this);
let model = this.currentModel;
if (model instanceof (DS.Model) || model instanceof (DS.RecordArray)) {
this.unsubscribeModel(model);
} else {
let keys = Object.keys(model);
keys.forEach((key) => {
let individualModel = model[key];
if (individualModel instanceof (DS.Model) || individualModel instanceof (DS.RecordArray)) {
this.unsubscribeModel(individualModel);
}
});
}
return this._super();
}
});
91 changes: 44 additions & 47 deletions addon/services/realtime-listener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,33 @@ import { getOwner } from '@ember/application';
import DS from 'ember-data';
import { get } from '@ember/object';
import { run } from '@ember/runloop';
import { firestore, database } from 'firebase/app';
import { firestore, /*database*/ } from 'firebase/app';

// TODO don't hardcode these, but having trouble otherwise
import { normalize as firestoreNormalize } from '../serializers/firestore';
import { normalize as databaseNormalize } from '../serializers/realtime-database';

const getService = (object:Object) => getOwner(object).lookup('service:realtime-listener') as RealtimeListenerService;
const isFastboot = (object:Object) => {
const getThisService = (object: Object) => getOwner(object).lookup('service:realtime-listener') as RealtimeListenerService;
const isFastboot = (object: Object) => {
const fastboot = getOwner(object).lookup('service:fastboot');
return fastboot && fastboot.isFastBoot;
}
};

export const subscribe = (route: Object, model: DS.Model) => !isFastboot(route) && getService(route).subscribe(route, model);
export const unsubscribe = (route: Object) => !isFastboot(route) && getService(route).unsubscribe(route);
export const subscribe = (subscriber: any, model: DS.Model, subscriptionId: String) => !isFastboot(subscriber) && getThisService(subscriber).subscribe(subscriber, model, subscriptionId);
export const unsubscribe = (subscriber: any, subscriptionId: String) => !isFastboot(subscriber) && getThisService(subscriber).unsubscribe(subscriber, subscriptionId);

const setRouteSubscription = (service: RealtimeListenerService, route: Object, unsubscribe: (() => void)|null) => {
const routeSubscriptions = get(service, `routeSubscriptions`);
const existingSubscription = get(routeSubscriptions, route.toString());
if (existingSubscription) { existingSubscription() }
const setSubscription = (thisService: RealtimeListenerService, subscriptionId: String, unsubscribe: (() => void) | null) => {
const subscriptions = get(thisService, `subscriptions`);
const existingSubscription = get(subscriptions, subscriptionId);
if (existingSubscription) {
existingSubscription();
}
if (unsubscribe) {
routeSubscriptions[route.toString()] = unsubscribe;
subscriptions[subscriptionId] = unsubscribe;
} else {
delete routeSubscriptions[route.toString()];
delete subscriptions[subscriptionId];
}
}

};
function isFirestoreQuery(arg: any): arg is firestore.Query {
return arg.onSnapshot !== undefined;
}
Expand All @@ -38,26 +39,23 @@ function isFirestoreDocumentRefernce(arg: any): arg is firestore.DocumentReferen
}

export default class RealtimeListenerService extends Service.extend({

routeSubscriptions: {} as {[key:string]: () => void}

subscriptions: {}
}) {

subscribe(route: Object, model: any) {
const store = model.store as DS.Store;
const modelName = (model.modelName || model.get('_internalModel.modelName')) as never
subscribe(subscriber: any, model: any, subscriptionId: String) {
const store = model.store;
const modelName = (model.modelName || model.get('_internalModel.modelName'));
const modelClass = store.modelFor(modelName);
const query = model.get('meta.query') as firestore.Query|database.Reference|undefined;
const ref = model.get('_internalModel._recordData._data._ref') as firestore.DocumentReference|database.Reference|undefined;
const query = model.get('meta.query');
const ref = model.get('_internalModel._recordData._data._ref');
if (query) {
if (isFirestoreQuery(query)) {
const unsubscribe = query.onSnapshot(snapshot => {
snapshot.docChanges().forEach(change => run(() => {
const normalizedData = firestoreNormalize(store, modelClass, change.doc);
switch(change.type) {
switch (change.type) {
case 'added': {
const current = model.content.objectAt(change.newIndex);
if (current == null || current.id !== change.doc.id ) {
if (current == null || current.id !== change.doc.id) {
const doc = store.push(normalizedData) as any;
model.content.insertAt(change.newIndex, doc._internalModel);
}
Expand All @@ -68,7 +66,7 @@ export default class RealtimeListenerService extends Service.extend({
if (current == null || current.id == change.doc.id) {
if (change.newIndex !== change.oldIndex) {
model.content.removeAt(change.oldIndex);
model.content.insertAt(change.newIndex, current)
model.content.insertAt(change.newIndex, current);
}
}
store.push(normalizedData);
Expand All @@ -82,24 +80,24 @@ export default class RealtimeListenerService extends Service.extend({
break;
}
}
}))
}));
});
setRouteSubscription(this, route, unsubscribe);
setSubscription(this, subscriptionId, unsubscribe);
} else {
const onChildAdded = query.on('child_added', (snapshot, priorKey) => {
run(() => {
if (snapshot) {
const normalizedData = databaseNormalize(store, modelClass, snapshot);
const doc = store.push(normalizedData) as any;
const existing = model.content.find((record:any) => record.id === doc.id);
const existing = model.content.find((record: any) => record.id === doc.id);
if (existing) { model.content.removeObject(existing); }
let insertIndex = 0;
if (priorKey) {
const record = model.content.find((record:any) => record.id === priorKey);
const record = model.content.find((record: any) => record.id === priorKey);
insertIndex = model.content.indexOf(record) + 1;
}
const current = model.content.objectAt(insertIndex);
if (current == null || current.id !== doc.id ) {
if (current == null || current.id !== doc.id) {
model.content.insertAt(insertIndex, doc._internalModel);
}
}
Expand All @@ -108,7 +106,7 @@ export default class RealtimeListenerService extends Service.extend({
const onChildRemoved = query.on('child_removed', snapshot => {
run(() => {
if (snapshot) {
const record = model.content.find((record:any) => record.id === snapshot.key)
const record = model.content.find((record: any) => record.id === snapshot.key);
if (record) { model.content.removeObject(record); }
}
});
Expand All @@ -126,12 +124,12 @@ export default class RealtimeListenerService extends Service.extend({
if (snapshot) {
const normalizedData = databaseNormalize(store, modelClass, snapshot);
const doc = store.push(normalizedData) as any;
const existing = model.content.find((record:any) => record.id === doc.id);
const existing = model.content.find((record: any) => record.id === doc.id);
if (existing) { model.content.removeObject(existing); }
if (priorKey) {
const record = model.content.find((record:any) => record.id === priorKey);
const record = model.content.find((record: any) => record.id === priorKey);
const index = model.content.indexOf(record);
model.content.insertAt(index+1, doc._internalModel);
model.content.insertAt(index + 1, doc._internalModel);
} else {
model.content.insertAt(0, doc._internalModel);
}
Expand All @@ -143,8 +141,8 @@ export default class RealtimeListenerService extends Service.extend({
query.off('child_removed', onChildRemoved);
query.off('child_changed', onChildChanged);
query.off('child_moved', onChildMoved);
}
setRouteSubscription(this, route, unsubscribe);
};
setSubscription(this, subscriptionId, unsubscribe);
}
} else if (ref) {
if (isFirestoreDocumentRefernce(ref)) {
Expand All @@ -154,7 +152,7 @@ export default class RealtimeListenerService extends Service.extend({
store.push(normalizedData);
});
});
setRouteSubscription(this, route, unsubscribe);
setSubscription(this, subscriptionId, unsubscribe);
} else {
const listener = ref.on('value', snapshot => {
run(() => {
Expand All @@ -163,26 +161,25 @@ export default class RealtimeListenerService extends Service.extend({
const normalizedData = databaseNormalize(store, modelClass, snapshot);
store.push(normalizedData);
} else {
const record = store.findRecord(modelName, snapshot.key!)
if (record) { store.deleteRecord(record) }
const record = store.findRecord(modelName, snapshot.key!);
if (record) {
store.deleteRecord(record);
}
}
}
});
});
const unsubscribe = () => ref.off('value', listener);
setRouteSubscription(this, route, unsubscribe);
setSubscription(this, subscriptionId, unsubscribe);
}
}
}

unsubscribe(route: Object) {
setRouteSubscription(this, route, null);
unsubscribe(subscriber: any, subscriptionId: String) {
setSubscription(this, subscriptionId, null);
}

}

declare module '@ember/service' {
interface Registry {
"realtime-listener": RealtimeListenerService;
"realtime-listener": RealtimeListenerService;
}
}

0 comments on commit 66b83d2

Please sign in to comment.