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

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.

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;