Search code examples
node.jswebsocketgraphqlapolloapollo-server

Tracking online user with GraphQL Apollo


I need to handle events "user is now online" and "user is now offline" on GraphQL Apollo Node.js server. What's the best way to do it?

My investigation: I pretty sure that I don't need to implement any heartbeat logic, because subscriptions are working on WebSockets. But I didn't find any info in their docs how to handle WebSockets events like "connecting" and "disconnecting" from the subscription... Actually I can handle those events from the outside of actual subscription:

SubscriptionServer.create({
    execute,
    subscribe,
    schema,
    onConnect = (...args) => {
        console.log('User connected')
    },
    onDisconnect = (...args) => {
        console.log('User disconnected')
    }
}, {
    server: ws,
    path: '/subscriptions'
})

But can't determine which user is connected via this socket.

My implementation: for now I made it work like that:

  1. We have express middleware for all the calls, it is pushing user object from jsonwebtoken to req object. Here I can trigger "user is now online" logic.

  2. I've created separate subscription, client subscribes on it on login and unsubscribes on logout. Since there is no unsubscribe handler, I manage to determine that filter function gets called on user disconnect without payload, so I did this approach:

    userOnlineSubscription: {
        subscribe: withFilter(
            () => pubSub.asyncIterator('userOnlineSubscription'),
                async (payload, variables) => {
                    if (!payload) {
                        // set user offline
                }
                return false
            }
        )
    }
    

As for me, the solution above is ugly. Can someone recommend the better approach?


Solution

  • I used this approach

    onConnect (connectionParams, webSocket) {
      const userPromise = new Promise((resolve, reject) => {
        if (connectionParams.jwt) {
          jsonwebtoken.verify(
            connectionParams.jwt,
            JWT_SECRET,
            (err, decoded) => {
              if (err) {
                reject(new Error('Invalid Token'))
              }
    
              resolve(
                User.findOne({
                  where: { id: decoded.id }
                })
              )
            }
          )
        } else {
          reject(new Error('No Token'))
        }
      })
    
      return userPromise.then(user => {
        if (user) {
          return { user: Promise.resolve(user) }
        }
    
        return Promise.reject(new Error('No User'))
      })
    }