Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Can't use realtime-listener on 2 attributes #576

Closed
bnetter opened this issue May 5, 2019 · 13 comments
Closed

Can't use realtime-listener on 2 attributes #576

bnetter opened this issue May 5, 2019 · 13 comments

Comments

@bnetter
Copy link
Contributor

bnetter commented May 5, 2019

So I've been trying to use the realtime-listener despite the complete lack of instructions. It works okay.

In my component, in the didReceiveAttrs method, I'm doing the following:

this.get('realtime').subscribe(this, this.get('user'));

Then in the willDestroyElement I do:

this.get('realtime').unsubscribe(this);

Now the user would be dynamically updated. But if I add an additional subscription in my didReceiveAttrs method, like this:

this.get('realtime').subscribe(this, this.get('user'));
this.get('realtime').subscribe(this, this.get('post'));

Then the user is not synchronized anymore.

@bnetter
Copy link
Contributor Author

bnetter commented May 12, 2019

Alternatively, I understand that relationships are not in realtime. Is there a trick to have them synced as well?

@bnetter
Copy link
Contributor Author

bnetter commented May 13, 2019

@jamesdaniels now that Google IO is over, maybe you can help me with this one. It's a real pain right now to use emberfire.

@jamesdaniels
Copy link
Contributor

Yeah, I'll be back on this now that I/O is wrapped.

@bnetter
Copy link
Contributor Author

bnetter commented May 28, 2019

Any news @jamesdaniels ?

@bhernez
Copy link

bhernez commented Jun 3, 2019

Looking at this code it seems that you can only use one listener per route (object) so at this line

this.get('realtime').subscribe(this, this.get('post'));

the code is replacing the previous listener, b/c it's the same "route object" (it's the same this) and the result to do route.toString() is the same.

I'm thinking that maybe a hackish way could be to wrap this within an ObjectProxy, so it would be a different object, but with access to the same component. One "problem" with this solution it's to keep track of the ObjectProxies you create, so you can unsubscribe listeners later; another one is to override toString method for each ObjectProxy.

@arun-prasad-r
Copy link

@jamesdaniels Please help us with emberfire. We need this feature.

@bnetter
Copy link
Contributor Author

bnetter commented Jul 16, 2019

@jamesdaniels 🙏 it's been so long!

@iofusion
Copy link

iofusion commented Jul 25, 2019

I worked through this problem, by updating the realtime-listener service and the realtime-route mixin. The update adds a subscriptionId attribute for tracking subscriptions, rather than relying on aRouteInstance.toString() as the key. The subscriptionId attribute is generated in a similar fashion; but rather than using a route instance to generate a key, the actual RecordArray or Model produced by a query is used to generate they key. This approach provides a more specific way to identify subscriptions and only changes the way subscriptions are identified.

RealtimeRoute Mixin Update

This update to the mixin creates a subscriptionId for and from each model. It supports using RSVP.hash to manage multiple queries in a route's model. Each model in the RSVP.hash is subscribed to, and unsubscribed from, realtime updates during the route's lifecycle. The mixin also serves as an example for managing subscriptions from a component if preferred. The subscriptionId is generated in the mixin since the model may be destoryed before an unsubscribe is complete. This Mixin requires the update to the realtime-listner shown in the next section.

app/mixins/realtime-route.js

import Mixin from '@ember/object/mixin';
import DS from 'ember-data';
import { subscribe, unsubscribe } from 'emberfire/services/realtime-listener';

export default Mixin.create({

  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() {
    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();
  }
});

RealtimeListener Service Update

setRouterSubscription has been changed to setSubscription, and a subscriptionId has been added to support multiple subscriptions from route/component/service.

node_modules/emberfire/addon/services/realtime-listener.js

import Service from '@ember/service';
import { getOwner } from '@ember/application';
import { get } from '@ember/object';
import { run } from '@ember/runloop';
// 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 getThisService = (object) => getOwner(object).lookup('service:realtime-listener');
const isFastboot = (object) => {
    const fastboot = getOwner(object).lookup('service:fastboot');
    return fastboot && fastboot.isFastBoot;
};
export const subscribe = (subscriber, model, subscriptionId) => !isFastboot(subscriber) && getThisService(subscriber).subscribe(subscriber, model, subscriptionId);
export const unsubscribe = (subscriber, subscriptionId) => !isFastboot(subscriber) && getThisService(subscriber).unsubscribe(subscriber, subscriptionId);
const setSubscription = (thisService, subscriptionId, unsubscribe) => {
    const subscriptions = get(thisService, `subscriptions`);
    const existingSubscription = get(subscriptions, subscriptionId);
    if (existingSubscription) {
        existingSubscription();
    }
    if (unsubscribe) {
        subscriptions[subscriptionId] = unsubscribe;
    }
    else {
        delete subscriptions[subscriptionId];
    }
};
function isFirestoreQuery(arg) {
    return arg.onSnapshot !== undefined;
}
function isFirestoreDocumentRefernce(arg) {
    return arg.onSnapshot !== undefined;
}
export default class RealtimeListenerService extends Service.extend({
    subscriptions: {}
}) {
    subscribe(subscriber, model, subscriptionId) {
        const store = model.store;
        const modelName = (model.modelName || model.get('_internalModel.modelName'));
        const modelClass = store.modelFor(modelName);
        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) {
                            case 'added': {
                                const current = model.content.objectAt(change.newIndex);
                                if (current == null || current.id !== change.doc.id) {
                                    const doc = store.push(normalizedData);
                                    model.content.insertAt(change.newIndex, doc._internalModel);
                                }
                                break;
                            }
                            case 'modified': {
                                const current = model.content.objectAt(change.oldIndex);
                                if (current == null || current.id == change.doc.id) {
                                    if (change.newIndex !== change.oldIndex) {
                                        model.content.removeAt(change.oldIndex);
                                        model.content.insertAt(change.newIndex, current);
                                    }
                                }
                                store.push(normalizedData);
                                break;
                            }
                            case 'removed': {
                                const current = model.content.objectAt(change.oldIndex);
                                if (current && current.id == change.doc.id) {
                                    model.content.removeAt(change.oldIndex);
                                }
                                break;
                            }
                        }
                    }));
                });
                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);
                            const existing = model.content.find((record) => record.id === doc.id);
                            if (existing) {
                                model.content.removeObject(existing);
                            }
                            let insertIndex = 0;
                            if (priorKey) {
                                const record = model.content.find((record) => record.id === priorKey);
                                insertIndex = model.content.indexOf(record) + 1;
                            }
                            const current = model.content.objectAt(insertIndex);
                            if (current == null || current.id !== doc.id) {
                                model.content.insertAt(insertIndex, doc._internalModel);
                            }
                        }
                    });
                });
                const onChildRemoved = query.on('child_removed', snapshot => {
                    run(() => {
                        if (snapshot) {
                            const record = model.content.find((record) => record.id === snapshot.key);
                            if (record) {
                                model.content.removeObject(record);
                            }
                        }
                    });
                });
                const onChildChanged = query.on('child_changed', snapshot => {
                    run(() => {
                        if (snapshot) {
                            const normalizedData = databaseNormalize(store, modelClass, snapshot);
                            store.push(normalizedData);
                        }
                    });
                });
                const onChildMoved = query.on('child_moved', (snapshot, priorKey) => {
                    run(() => {
                        if (snapshot) {
                            const normalizedData = databaseNormalize(store, modelClass, snapshot);
                            const doc = store.push(normalizedData);
                            const existing = model.content.find((record) => record.id === doc.id);
                            if (existing) {
                                model.content.removeObject(existing);
                            }
                            if (priorKey) {
                                const record = model.content.find((record) => record.id === priorKey);
                                const index = model.content.indexOf(record);
                                model.content.insertAt(index + 1, doc._internalModel);
                            }
                            else {
                                model.content.insertAt(0, doc._internalModel);
                            }
                        }
                    });
                });
                const unsubscribe = () => {
                    query.off('child_added', onChildAdded);
                    query.off('child_removed', onChildRemoved);
                    query.off('child_changed', onChildChanged);
                    query.off('child_moved', onChildMoved);
                };
                setSubscription(this, subscriptionId, unsubscribe);
            }
        }
        else if (ref) {
            if (isFirestoreDocumentRefernce(ref)) {
                const unsubscribe = ref.onSnapshot(doc => {
                    run(() => {
                        const normalizedData = firestoreNormalize(store, modelClass, doc);
                        store.push(normalizedData);
                    });
                });
                setSubscription(this, subscriptionId, unsubscribe);
            }
            else {
                const listener = ref.on('value', snapshot => {
                    run(() => {
                        if (snapshot) {
                            if (snapshot.exists()) {
                                const normalizedData = databaseNormalize(store, modelClass, snapshot);
                                store.push(normalizedData);
                            }
                            else {
                                const record = store.findRecord(modelName, snapshot.key);
                                if (record) {
                                    store.deleteRecord(record);
                                }
                            }
                        }
                    });
                });
                const unsubscribe = () => ref.off('value', listener);
                setSubscription(this, subscriptionId, unsubscribe);
            }
        }
    }
    unsubscribe(subscriber, subscriptionId) {
       setSubscription(this, subscriptionId, null);
    }
}

@arun-prasad-r
Copy link

@iofusion Can you get this merged?

@iofusion
Copy link

@arun-prasad-r I am not able to merge on this repo.

@jamesdaniels What are your thoughts on this issue? Does the approach seem inline with your vision for emberfire? Would you like me to make a Pull Request?

@bnetter
Copy link
Contributor Author

bnetter commented Sep 9, 2019

Hey @jamesdaniels, hope you had a good 5-months holidays. When can we expect some news on this? It's been 18 months since this package is broken and no one can actually use this properly.

Make Firebase great again.

arun-prasad-r added a commit to arun-prasad-r/emberfire that referenced this issue Sep 13, 2019
…multiple queries. Thanks to @iofusion

This solution is adapted from @iofusion's comments on this issue FirebaseExtended#576 (comment)
@jamesdaniels
Copy link
Contributor

Addressed in 40a88dc

@jamesdaniels
Copy link
Contributor

rc.4 released.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants