diff --git a/addon/mixins/realtime-route.ts b/addon/mixins/realtime-route.ts index c7551eae..ff7a8b4d 100644 --- a/addon/mixins/realtime-route.ts +++ b/addon/mixins/realtime-route.ts @@ -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(); } }); \ No newline at end of file diff --git a/addon/services/realtime-listener.ts b/addon/services/realtime-listener.ts index a92b2f9f..9d120722 100644 --- a/addon/services/realtime-listener.ts +++ b/addon/services/realtime-listener.ts @@ -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; } @@ -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); } @@ -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); @@ -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); } } @@ -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); } } }); @@ -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); } @@ -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)) { @@ -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(() => { @@ -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; } } \ No newline at end of file