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

Subscription implementation questions #378

Open
gfmartinez opened this issue Dec 2, 2020 · 2 comments
Open

Subscription implementation questions #378

gfmartinez opened this issue Dec 2, 2020 · 2 comments

Comments

@gfmartinez
Copy link

Some questions about implementing subscriptions.

The docs show the suggested/example setup:

import Route from '@ember/routing/route';
import { queryManager } from 'ember-apollo-client';
import query from 'app/gql/subscriptions/new-human';
import { addListener, removeListener } from '@ember/object/events';

const handleEvent = event => alert(`${event.name} was just born!`);

export default Route.extend({
  apollo: queryManager(),

  model() {
    return this.get('apollo').subscribe({ query }, 'human');
  },

  setupController(controller, model) {
    addListener(model, 'event', handleEvent);
  },

  resetController(controller, isExiting, transition) {
    if (isExiting) {
      removeListener(controller.model, 'event', handleEvent);
    }
  }
});

I ran into several issues with this setup.
Most problematic is that the apollo.subscribe() function returns an object of type EmberApolloSubscription with a structure like

EmberApolloSubscription {
  lastEvent: { human.attrs },
  _apolloClientSubscription: Subscription info
}

In the cases of apollo.query and apollo.watchQuery we get a POJO returned, which fits well with the use of model in the route and controller. However, here we are getting the EmberApolloSubscription which requires the listener setup to extract the real model that we're after. We therefore can't rely on using controller.model in the route templates or for passing down to child components.

Instead of the setup suggested, and using Octane, we had to use something like:

import Route from '@ember/routing/route';
import { queryManager } from 'ember-apollo-client';
import query from 'app/gql/subscriptions/new-human';
import { addListener, removeListener } from '@ember/object/events';

export default class SubscribedRoute extends Route {
  @queryManager apollo;

  model(params) {
    const variables = { id: params.id };
    return this.apollo.subscribe({
      query,
      variables
    }, 'humanWasModified.human');
  }

  setupController(_controller, model) {
    super.setupController(...arguments);
    addListener(model, 'event', this, 'updateSubscribedModel');
  }

  resetController(controller, isExiting) {
    if (isExiting) removeListener(controller.model, 'event', this, 'updateSubscribedModel');
  }

  updateSubscribedModel(humanModel) {
    const controller = this.controllerFor(this.routeName);
    controller.subscribedModel = humanModel;
  }
}

And the controller then has @tracked subscribedModel that will propagate changes when changed.

This works fine, but a bit counter-intuitive and not well documented as to how to deal with this.

Am I missing something? Or is there a better way to do this?

I had thought about hijacking the model by using the event listener to update the controller.model instead of a differently named and tracked property, but that seemed confusing and anti-pattern and made debugging difficult.

Our setup uses ActionCable to establish the web socket link, but that is aside from this issue. The most notable thing there was that we had to make sure that we returned the desired model information on first subscription, but that's no big deal to accomplish.

Thanks in advance for any thoughts and help here.

@grantcupps
Copy link

@gfmartinez, we've had luck simply using lastEvent, which provides a POJO much like apollo.watchQuery.

In your example, it would be something like this:

import Route from '@ember/routing/route';
import { queryManager } from 'ember-apollo-client';
import query from 'app/gql/subscriptions/new-human';
import { addListener, removeListener } from '@ember/object/events';

export default class SubscribedRoute extends Route {
  @queryManager apollo;

  model(params) {
    const variables = { id: params.id };
    return this.apollo.subscribe({
      query,
      variables
    }, 'humanWasModified.human');
  }
}
import Controller from '@ember/controller';
import { alias } from '@ember/object/computed';

export default class SubscribedController extends Controller {
  @alias('model.lastEvent') subscribedModel;
}

@gfmartinez
Copy link
Author

@grantcupps Thank you so much. I did some work after this issue and came up with almost the same solution, which is much cleaner and easier for our needs. The only difference between what we did and what you've got is that we used readOnly since we are not mutating the lastEvent.

Would be great to see this as part of the docs, since it is an easier implementation.
Further, with this method, I don't see any need for the invocation of listeners as shown in current docs.

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

2 participants