Followed docs for publication joins, but not updating. What am I doing wrong?

I was following the documentation to simulate “joins” in publications in my application

Meteor.publish('Player.profile', function (playerId, gameId) {
  check(playerId, String);
  check(gameId, String);

  // get player and the profile for a single game.
  var player = Players.findOne(playerId);
  var gameProfile = PlayerGames.find({
    playerId: playerId,
    gameId: gameId
  });

  if(player && gameProfile) {
    return [
      Players.find({ _id: playerId }),
      Games.find({ _id: gameId }),
      Guilds.find({ _id: gameProfile.guildId }),
      Battlegroups.find({ _id: gameProfile.bgId })
    ]
  } else {
    this.error(404, "Player or player profile not found");
    return;
  }
});

Then in my route:

Router.map(function () {
  this.route('Player.profile', {
    path: '/player/:_id',
    action: function () {
      // some other stuff happens here
      this.render('Player.profile', {
        data: {
          player: Players.findOne(),
          games: Games.find(),
          guild: Guilds.findOne(),
          battlegroup: Battlegroups.findOne()
        }
      })
    },
    waitOn: function () {
      return Meteor.subscribe('player', this.params._id, {
        onError: function (error) {
          Router.go('error', error);
        }
      })
    }
  });
});

However, it seems like whenever I edit my player profile, it does reactively update the player themselves, however, it does not seem to update the Games or the Group reactively on the page.

I go to an edit page, change the “games” the player is a part of, however, it does not seem to update correctly. If I look directly into the database, it seems to have changed, but not after being redirected to the page

Am I doing something wrong here?

Meteor publish functions are non reactive (except to userId). See https://www.discovermeteor.com/blog/reactive-joins-in-meteor/ for solutions.

Can you use before and waitOn both to do subscriptions? I always get the same error with that.

You called wait() after calling ready() inside the same computation tree.

I guess you should return your view context with

Router.map(function () {
  this.route('Player.profile', {
    path: '/player/:_id',
    action: function () {
      // some other stuff happens here
      this.render('Player.profile');
    })
    data: function () {
       return {
          player: Players.findOne(),
          games: Games.find(),
          guild: Guilds.findOne(),
          battlegroup: Battlegroups.findOne()
        }
      })
    },

You have several options

  • use observeChanges (you may end up writing a lot of code)
  • check for the game profile in the client, and every time that it change call a publication on the server
  • using some package for Reactive-joins

(an example using publish-relations)

Meteor.publishRelations('Player.profile', function (playerId, gameId) {
  check(playerId, String);
  check(gameId, String);

  // get player and the profile for a single game.
  var player = Players.find(playerId);

  if (player.fetch()[0]) {
    this.cursor(PlayerGames.find({
      playerId: playerId,
      gameId: gameId
    }), function (gameProfileId, gameProfile) {
      if (gameProfile.guildId)
        this.cursor(Guilds.find({ _id: gameProfile.guildId }));
      if (gameProfile.bgId)
        this.cursor(Battlegroups.find({ _id: gameProfile.bgId }));
    });

    return [
      player,
      Games.find({ _id: gameId })
    ];
  } else {
    this.error(404, "Player not found");
    return;
  }
});
2 Likes

Good suggestion, @cottz
tbh, I would probably not use the library just because it’s adding a layer of complexity. I am a wee bit confused by one part, however.

Meteor.publish('Player.profile', function(gameId, playerId) {
  var sub = this;

  playerGameHandle = PlayerGames.find({
    playerId: playerId,
    gameId: gameId
  });

  playerGameHandle.observeChanges({
    added: function (id, table) {
      sub.added('player_games', id, table);
    },
    changed: function (id, fields) {
      if (fields.battlegroup) {
        console.log("Player's battlegroup changed");
        // I need to change my "Battlegroup" subscription here?
      }
      sub.changed('player_games', id, fields);
    }
  });

  bgHandler = Battlegroups.find({
    _id: {
      $in: playerGameHandle.map(function (doc) { return doc.bgId })
    }
  }).observeChanges({
    added: function (id, bg) {
      sub.added('battlegroup', id, bg);
    },
    changed: function (id, fields) {
      sub.changed('battlegroup', id, fields);
    }
  })

  sub.ready();
  sub.onStop(function () {
    playerGameHandle.stop();
    bgHandler.stop();
  });
});

Wouldn’t I have to change the subscription… from another subscription? Seems weird.

1 Like

to change the battlegroup subscription you gotta do what you did to playerGameHandle. Meteor by default when you return an array of cursors uses a basic observeChanges in each one, that’s exactly what you’re doing and what publish-relations do.

you can use Meteor.Collection._publishCursor inside the changed event or use observeChanges again (both are the same)

ah, @cottz, your suggestions did wonders and can do almost everything, it just never seems to “unpublish” old info

var handles = {
  playerGame: [],
  battlegroup: []
}
handles.playerGame = playerGame.observeChanges({
  added: function (id, playerGame) {
    var bgCursor = Battlegroups.find({ _id: playerGame.bgId });
    handles.battlegroup[id] = Mongo.Collection._publishCursor(bgCursor, sub, 'battlegroups');
    sub.added('player_games', id, playerGame);
  }
});
// ...
sub.ready();
sub.onStop(function () {
  _.each(_.values(handles), function(handle) {
    handle.stop();
  })
});

And the same for changed and all of that. It seems to work, just keeps other things published at the same time. I’ve compensated for it on the client

Mongo.Collection._publishCursor method does not return the stop method, so to stop it have to stop all publication.
you can create a method similar to Mongo.Collection._publishCursor and when you need a new cursor stop it

Mongo.Collection._publishCursor = function (cursor, sub, collection) {
  var observeHandle = cursor.observeChanges({
    added: function (id, fields) {
      sub.added(collection, id, fields);
    },
    changed: function (id, fields) {
      sub.changed(collection, id, fields);
    },
    removed: function (id) {
      sub.removed(collection, id);
    }
  });

  sub.onStop(function () {observeHandle.stop();});
}

You can change the last line

// sub.onStop(function () {observeHandle.stop();});
return observeHandle;