Feathers: How to join a dynamic channel when the channel information is not available at login and connection handlers?

Created on 31 Oct 2019  路  9Comments  路  Source: feathersjs/feathers

This is a question.

I'm creating channels dynamically, using:

  app.service('session-manager').publish('created', data => {
    let {sessionId} = data
    return app.channel(sessionId)
  })

On the client side, how to join the channel? The client will have the sessionId only after the events connection and login, so it's not possible to join using these events.

Most helpful comment

You can get the user channel connections via channel.connections so it'd look like this:

// this is the service I've made that I use for my purpose
export default class ChannelsWrapper {
  setup(app) {
    this.app = app;
  }

  async create(data) {
    const { userId, channels, action = 'join' } = data;
    // hack to get user's connection from their private channel
    const userChannel = this.app.channel(`/users/${userId}`);
    const connections = userChannel.connections;

    if (action === 'join') {
      this.app.channel(...channels).join(...connections);
    } else if (action === 'leave') {
      this.app.channel(...channels).leave(...connections);
    }

    return { message: 'triggered' };
  }
}

All 9 comments

I understand now that the server is responsible for joining clients to channels.
This is the approach I'm trying in order to join clients to a dynamic channel and then publishing events to the channel:

When a client wants to join a channel, it sends a message to the server:

{
  "action": "join",
  "sessionId": "12345"
}

On the server side, an event handler will join the client to the channel (sessionId):

  app.service('session-manager').on('created', (msg, ctx) => {
    let {provider, connection} = ctx.params
    if (provider === 'socketio' && msg.action === 'join') {
      app.channel(msg.sessionId).join(connection)
    }
  })

Another piece of code takes care of publishing messages to the specific channel:

  app.service('session-manager').publish('created', (data, ctx) => {
    let {sessionId, action} = data
    if(action === 'msg') {
      return app.channel(sessionId)
    }
  })

Now any client can send message to that dynamic channel:

{
  "action": "msg",
  "sessionId": "12345",
  "msg": "Feathersjs rocks, I need to master it"
}

On the cliend side, there is an event handler that never gets invoked when another client sends a message to the channel:

api.service('session-manager').on('created', msg => {
  // This never get invoked
  console.info(msg)
})

What's wrong?

I have created a solution for this problem:

channels.js:

app.on('login', (authResult, {connection}) => {
    // connection can be undefined if there is no
    // real-time connection, e.g. when logging in via REST
    if (connection) {

      app.channel('anonymous').leave(connection)

      app.channel('authenticated').join(connection)

      // Function to allow joining channels at service event handlers.
      connection.joinChannel = name => app.channel(name).join(connection)

      // Function to allow leaving channels at service event handlers.
      connection.leaveChannel = name => app.channel(name).leave(connection)      
    }
  })

session-manager.js:

  app.service('session-manager').publish('created', ({sessionId}) => {
    return app.channel(sessionId)
  })

  // Join websocket clients to the requested channel.
  app.service('session-manager').on('created', (msg, ctx) => {
    let {connection} = ctx.params
    if(connection) {
      switch(msg.action) {
        case 'join':
          connection.joinChannel(msg.sessionId)
          break
        case 'leave':
          connection.leaveChannel(msg.sessionId)
          break
      }
    }
  })

Now clients can join the channel at any time by sending a message to the server and all clients connected to the corresponding channel will receive messages.

thanks a lot!

@averri you are a lifesaver, thanks for this solution!:) 馃憤 Do you have any idea why if we will try to connect to the channel in this way:
```
app.service('session-manager').on('created', (msg, context) => {
let { connection } = context.params
context.app.channel(msg.sessionId).join(connection)
})

and than:

app.service('messages').publish('created', data =>
app.channel(msg.sessionId)
);
```

it doesn't work, although I see that connection was joined to this channel in the console?

I wanted the ability to have a service that can make a given userId join or leave dynamic channels on the fly. What I've arrived at is a hacky implementation but it seems to work fine.

@daffl could you please tell us if there is a better approach. Ideally a method to return a connection for a given userId would be perfect, as that is where my hack lies.

// this is a pre-requisite, to add every connected user to their own private channel
// I anyways needed this for sending private events
const loginHandler = (_, { connection }, app) => {
  if (connection) {
    const userId = R.path(['data', '_id'], connection);
    app.channel(`/users/${userId}`).join(connection);
    app.channel('general').join(connection);
    app.service('/db/users').patch(userId, { status: 'online' });
  }
};
// this is the service I've made that I use for my purpose
export default class ChannelsWrapper {
  setup(app) {
    this.app = app;
  }

  async create(data) {
    const { userId, channels, action = 'join' } = data;
    // hack to get user's connection from their private channel
    this.app.channel(`/users/${userId}`).leave(c1 => {
      if (action === 'join') {
        this.app.channel(...channels).join(c1);
      } else if (action === 'leave') {
        this.app.channel(...channels).leave(c2 => {
          return String(c2.user._id) === userId;
        });
      }
      // don't return anything here as I don't want them to leave their private channel
      return;
    });
    return { message: 'triggered' };
  }
}

You can get the user channel connections via channel.connections so it'd look like this:

// this is the service I've made that I use for my purpose
export default class ChannelsWrapper {
  setup(app) {
    this.app = app;
  }

  async create(data) {
    const { userId, channels, action = 'join' } = data;
    // hack to get user's connection from their private channel
    const userChannel = this.app.channel(`/users/${userId}`);
    const connections = userChannel.connections;

    if (action === 'join') {
      this.app.channel(...channels).join(...connections);
    } else if (action === 'leave') {
      this.app.channel(...channels).leave(...connections);
    }

    return { message: 'triggered' };
  }
}

that is much cleaner, thank you!

it seems tightly coupled with current implementation... any plan on join/leave channels out of login/connection callback's?

this cumbersome setup is annoying 馃 "you should ) search every piece somewhere..."

imho, this is should be a part of https://docs.feathersjs.com/guides/

p.s. Great Project

Even though my use case already makes me have a single user private channel for each relevant user. What could be wonderful functionality in general is the ability to add an id to a connection when it joins, it could be the user._id by default, and then later we could do like a app.connections.get(id) to get that connection anywhere in the system and do a app.channel(name).[join/leave](connection). Not sure how easily/efficiently it can be done right now. That could be a part of the guides, would be happy to write one if we can figure out the right way.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

harrytang picture harrytang  路  3Comments

arve0 picture arve0  路  4Comments

rrubio picture rrubio  路  4Comments

arkenstan picture arkenstan  路  3Comments

NetOperatorWibby picture NetOperatorWibby  路  4Comments