question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Can't use `realtime-listener` on 2 attributes

See original GitHub issue

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.

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Comments:13 (8 by maintainers)

github_iconTop GitHub Comments

2reactions
bnettercommented, 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.

2reactions
iofusioncommented, 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);
    }
}
Read more comments on GitHub >

github_iconTop Results From Across the Web

Firestore - React JS realtime listener of a collection
This is the expected behavior. Since data is loaded from Firestore asynchronously, all code that needs the data needs to be inside the ......
Read more >
Firebase Firestore Tutorial #8 - Real-time Data - YouTube
Hey gang, in this Firestore tutorial I'll show you how to use the Firstore's real-time capabilities ... Your browser can't play this video....
Read more >
CTGDJB - IBM
The tdisrvctl command was unable to connect to the Tombstone manager in the remote server. ... Use keyword 'all' or specify a set...
Read more >
Read and Write Data on Android | Firebase Realtime Database
This document covers the basics of reading and writing Firebase data. Firebase data is written to a FirebaseDatabase reference and retrieved by attaching...
Read more >
How to set Solace properties with Boomi?
I want to set properties in a Boomi process like ... but setting Solace properties using the JMS custom property feature doesn't work...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found