jaz
jaz

Reputation: 121

How to manage and selectively stop a published subscription from Meteor server side code

I'm developing a competitive turn based game. When an anonymous visitor visits a page, he is automatically subscribing to a non-full instance of the game he can then either just observe or join the action. There are limited number of spots in each game instance. When joining the game (taking up a spot), the old subscription is stopped and new one is created that also includes private information based on his chosen spot. So far so good.

Now I want to make the server free up a spot whenever a player doesn't complete his turn in reasonable amount of time.

The question is how can I make sure the player kicked out of his spot no longer receives updates that are now intended to go to someone else occupying his spot? Obviously this has to happen on server side as clients cannot be trusted. Ideally the kicked out user would seamlessly become the observer again.

I know there is a method stop() that can be called inside publish(), but how to use it to stop a published subscription for one particular client when a callback set by Meteor.setTimeout() gets called on the server?

Some heavily modified code of what I'm trying to do (not meant to work, only to give you an idea)

  if Meteor.isClient
      publicGameHandle = Meteor.subscribe 'GameInstances'

      join = (gameInstanceId, spot) ->
          Meteor.call "join", gameInstanceId, spot, (err, guestId) ->
              Session.set("guestId", guestId)
              privateGameHandle = Meteor.subscribe 'GameInstances',
              gameInstanceId, spot, guestId, ->
                  publicGameHandle.stop()

   if Meteor.isServer
       privateSubscriptions = {}

       Meteor.publish 'GameInstances', (gameInstanceId, spot, guestId) ->
           if gameInstanceId
               GameInstances.find {_id: gameInstanceId}
               privateSubscriptions[guestId] = @
           else
               secretFields = {spots.guestId:false, spots.privateGameInfo:false}
               GameInstances.find {openSpots: {$gt: 0}}, {fields: secretFields}

       Meteor.methods({
           join: (gameInstanceId, spot) ->
               guestId = Random.id()
                   gameInstances[gameInstanceId].addPlayer(spot, guestId)
               guestId

           completePlayerTurn: (gameInstanceId, spot, guestId) ->
               gameInstance = gameInstances[gameInstanceId]
               Meteor.clearTimeout(gameInstance.timer)
               nextPlayer = gameInstance.getNextPlayer()

               kick = () ->
                   privateSubcriptions[nextPlayer.guestId].stop()
                   gameInstance.removePlayer(nextPlayer.guestId)
               gameinstance.timer = Meteor.setTimeout(kick, 60000)

Upvotes: 1

Views: 1047

Answers (1)

colllin
colllin

Reputation: 9779

Create two separate subscriptions that both return game data. One subscription should return public game data, the other should return game data that is viewable by the currentUser. They will be merged into one collection on the client side. Games the user has joined will contain private info AND public info, games they haven't joined will only contain public info. Then on the client side, a liveQuery like Game.findOne(thisGame.id) will automatically receive the private information when the player joins the game and cause your template to be re-rendered, etc.

Pseudo-code for illustrative purposes:

if Meteor.isClient
  Meteor.subscribe 'GameInstances'
  Meteor.subscribe 'MyGameInstances', guestId

  join = (gameInstanceId, spot) ->
      Meteor.call "join", gameInstanceId, spot, (err, guestId) ->
          Session.set("guestId", guestId)

if Meteor.isServer

   Meteor.publish 'GameInstances', () ->
       secretFields = {spots.guestId:false, spots.privateGameInfo:false}
       GameInstances.find {openSpots: {$gt: 0}}, {fields: secretFields}

   Meteor.publish 'MyGameInstances', (guestId) ->
       GameInstances.find {'spots.guestId': guestId}, {fields: secretFields}

   Meteor.methods({
       join: (gameInstanceId, spot) ->
           guestId = Random.id()
           gameInstances[gameInstanceId].addPlayer(spot, guestId)
           guestId

       completePlayerTurn: (gameInstanceId, spot, guestId) ->
           gameInstance = gameInstances[gameInstanceId]
           Meteor.clearTimeout(gameInstance.timer)
           nextPlayer = gameInstance.getNextPlayer()

           kick = () ->
               gameInstance.removePlayer(nextPlayer.guestId)
           gameinstance.timer = Meteor.setTimeout(kick, 60000)

It sounds like you might be including many players private info on one Game Instance. So that will be harder to filter the fields that are allowed (and only publish the CURRENT guest's private info, without publishing other players' private info).

In that case, you might consider either:

  • refactor private info into a separate collection, and publish it as a separate subscription
  • custom publish function. Here's an example:

https://gist.github.com/colllin/8321227

In this code, I'm observing one collection and publishing data that will be merged into the Meteor.users collection. You can see that the subscription is publishing data like {_id: userId, wins: 25}, which makes a wins field available on user models on the client side. You will probably want to observe() the Games collection but manually filter the private data based on the current user, then publish that private data into the Games collection to be merged with the public data. You can pass in the guestId to the publish function like you did above.

Upvotes: 1

Related Questions