DEV Community

Matt Chaffe
Matt Chaffe

Posted on • Originally published at blog.feathersjs.com on

2

FeathersJS Channel Subscriptions

Photo by chuttersnap on Unsplash

FeathersJS Channels are a great way to handle real-time data. However, sometimes you might only want certain data on a certain page. For example, you may have a chatroom app which has multiple rooms and you only want to provide data for a room that a user is viewing.

On the server

To be able to handle channels on the fly, I am going to configure a custom service which will handle joining and leaving channels.

class RoomMembershipService {
async create (data, params) {
if (params.connection) {
this.app.channel(`room/${data.roomId}`).join(params.connection)
}
return data;
}
async remove (id, params) {
if (params.connection) {
this.app.channel(`room/${id}`).leave(params.connection)
}
return { id };
}
setup (app) {
this.app = app;
}
}

In the above snippet custom service class we use the create method to join the channel and the remove method to leave the channel.

Service

Now to configure the service to emit data to the channels, within the *-service.js file, we can configure a custom publish. This will emit data only to the channels which exist and will filter out the current user. So the user that created the message within the room, won’t get an .on('created')event when they create a new message, but all other subscribed users will get this.

const service = app.service('messages')
service.hooks(hooks)
// Publish events
service.publish((data, context) => {
// Filter the channels to only authenticated
return app.channel(`room/${data.roomId}`)
.filter(connection => connection.user._id.toString() !== data.userId.toString())
})
view raw service.js hosted with ❤ by GitHub

Awesome! Now we are only emitting data to those that want it. Now let’s take a look at setting up the client.

The client

For our discussion here, I am just going to show a brief example of where you would emit the subscribe/unsubscribe events.

Component.extend({
tag: 'messages',
view: `<div>
<h1>Messages</h1>
<div class="wrapper" >
{{# for(message of messages) }}
<section>
<div>
TO: {{ users.usersById[message.to].email }}
</div>
<div>
MSG: {{ message.name }}
</div>
<div>
FROM: {{ users.usersById[message.from].email }}
</div>
{{/ for}}
</section>
</div>
<div>
<form on:submit="sendMessage(scope.event)">
<input name="to" placeholder="To" value:bind="to">
<input name="msg" placeholder="Message.." value:bind="msg">
<button type="submit">Create</button>
</form>
</div>
</div>`,
ViewModel: {
msg: 'string',
to: 'string',
messages: 'any',
users: 'any',
roomId: 'string',
get messagesAndUsers () {
return Promise.all([
this.messagesPromise,
this.usersPromise
])
},
user: {
get: () => Session.current.user
},
messagesPromise: {
default: () => Messages.getList({ $or: [{ from: Session.current.user._id }, { to: Session.current.user._id }] })
},
usersPromise: {
default: () => User.getList()
},
sendMessage (event) {
event.preventDefault()
const toUser = this.users.usersByEmail[this.to]
if (toUser) {
new Messages({ name: this.msg, to: toUser._id, roomId: this.roomId })
.save()
.then(() => {
this.msg = ''
this.to = ''
})
}
},
connectedCallback () {
// Listen to the session prop so we can load messages
// once we have an user
this.listenTo('user', (e, newVal) => {
// Load both users and messages and assign to VM
this.messagesAndUsers.then(([messages, users]) => {
this.messages = messages
this.users = users
})
})
feathersClient.service('room-membership').create({ roomId: this.roomId })
// Teardown handler
return () => {
feathersClient.service('room-membership').remove(this.roomId)
// Remove all listeners
this.stopListening()
}
}
}
})

The above example is of a CanJS component. We use connectedCallback which is a lifecycle hook called after the component’s element is inserted into the document. This can also return a function which will be used when the component is torn down / removed from the document.

Within the connectedCallback we can emit the subscribe event, and in the teardown, we can emit the unsubscribe. When a user navigates to this component it will automatically subscribe the current user to the room, so all messages that are created with this roomId will be sent to this room channel.

To see how this affects your app, if you open up the network tab in devtools, and select the ws sub-tab you will see in the frames section all the data transferred to and from your server.

If the server was configured to publish to the authenticated channel which would be all logged in users. Every user would see created events for messages and rooms which they are not viewing. This means that the server is sending data to those that aren’t using it or require it, so we can prevent this by only sending data to those that are subscribed to specific channel.

Thanks for reading.

If you noticed anything incorrect or that could be improved please comment below. I appreciate all constructive feedback.


SurveyJS custom survey software

Simplify data collection in your JS app with a fully integrated form management platform. Includes support for custom question types, skip logic, integrated CCS editor, PDF export, real-time analytics & more. Integrates with any backend system, giving you full control over your data and no user limits.

Learn more

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs