N
Nuxt2mo ago
Quei

Reusable websocket component

Hi Community and @manniL / TheAlexLichter i found the super helpfull small guide on YT: https://www.youtube.com/watch?v=OfY7JcrqkPg&t=33s It works like a charm! Now I'm wondering how I can create a reusable component for sending messages. The idea is to have a central "channel" for updates across the web app. For example, I've written a mail client, and when this client receives a new email, it should send a message to the channel. Based on this message, a component refresh should be triggered. But I'm having trouble implementing this. The main idea is to have a function that I can import anywhere on the server side to send messages to this channel.
server\api\websocket.ts
export default defineWebSocketHandler({
open(peer) {
logger.warn('WebSocket opened:', peer)
},
close(peer) {
logger.warn('WebSocket closed:', peer)
},
error(peer, error) {
logger.error('WebSocket error:', error)
},
message(peer, message) {
logger.warn('WebSocket message:', peer, message)
}
})
server\api\websocket.ts
export default defineWebSocketHandler({
open(peer) {
logger.warn('WebSocket opened:', peer)
},
close(peer) {
logger.warn('WebSocket closed:', peer)
},
error(peer, error) {
logger.error('WebSocket error:', error)
},
message(peer, message) {
logger.warn('WebSocket message:', peer, message)
}
})
Alexander Lichter
YouTube
Integrating WebSockets in Nuxt and Nitro
🎉 Nitro 2.9 was released just before Vue.js Amsterdam and brings new features such as a database layer, a task API and also WebSocket support! But how can we integrate it in a Nuxt application? This video will teach you 👌 Key points: 🛠 How to set up WebSockets in Nitro and in Nuxt 💡 Working from scratch to a functional application ⚠️ Hints to m...
28 Replies
manniL
manniL2mo ago
Hey! Do you mean a Vue component or a server utility function? Your description has parts of both
Quei
QueiOP2mo ago
I meant a server utility function. sorry for not being that precise! 🙂 On the frontend, it's clear that I can consume these events using useWebsockets. But on the server side, I need a function that allows me to send events. for example:
nuxt\server\services\email\parser.ts
import fs from 'fs'
import { simpleParser } from 'mailparser'
....
import { sendWebSocetMessage } from 'websocketHelper'

// some mail parsing things

// after mail parsing send message
sendWebSocetMessage('newEmailSaved')
nuxt\server\services\email\parser.ts
import fs from 'fs'
import { simpleParser } from 'mailparser'
....
import { sendWebSocetMessage } from 'websocketHelper'

// some mail parsing things

// after mail parsing send message
sendWebSocetMessage('newEmailSaved')
So, I can't figure out how to create the sendWebSocketMessage function.
Jacek
Jacek2mo ago
How about Nitro Docs » Guide » WebSocket » Usage? https://nitro.unjs.io/guide/websocket#usage Extract the logic into services and it is lean and readable. Why would you abstract it more? For centralized logic you should rather use KV subscription or dedicated message bus. open() definition would need to include subscription to external source
let mqSubscription = null;

export default defineWebSocketHandler({
open(peer) {
mqSubscription = mq.subscribe('global-notifications', () => peer.publish(...send to client if needed...))
},
message(peer, message) {
// (optional) reacting to messages from client
},
close(peer) {
mqSubscription.destroy()
},
})
let mqSubscription = null;

export default defineWebSocketHandler({
open(peer) {
mqSubscription = mq.subscribe('global-notifications', () => peer.publish(...send to client if needed...))
},
message(peer, message) {
// (optional) reacting to messages from client
},
close(peer) {
mqSubscription.destroy()
},
})
And the sendWebSocetMessage you drafted, would post message to the MQ (or KV). From the comments I guess it should include client ID so you propagate event to the correct user.
Quei
QueiOP2mo ago
This is exactly where I’m struggling... I’m not sure how to properly extract this so that it works in the end. What do you mean by KV or a dedicated message bus? can you send me an example? If by "dedicated message bus" you mean a topic where the "clients" (in this case, the frontend) subscribe, then yes, that's exactly what I'm trying to do. The example above is just a quick and dirty implementation to see if it would work in my existing project—and yes, it does! Now, I’m at the stage where I want to implement the WebSockets more cleanly.
Jacek
Jacek2mo ago
Deno KV [1], [2], Redis has SUBSCRIBE command, message queues usually have some kind of listener method built into their SDK, and some NoSQL databases may also support it. Some options may be more suitable for your use case than others.
Quei
QueiOP2mo ago
The plan was to use WebSockets for this since a chat feature will also be added later. Maybe the better question is: how do I send a message?
I can import the message from server\api\websocket.ts, but how do I send it to all peers?
Jacek
Jacek2mo ago
With peer.publish(...), but I guess you will rather post to the subset of users that joined a named room. This is why you should rather utilize peer.send(...) or figure out some other way. See example public chat I think it would be cleaner to rename peer to ctx or something.. but do what you want 😉
Quei
QueiOP2mo ago
I think we're not talking about the same thing! 🙂 I adapted the example from here:
https://stackblitz.com/edit/nitro-web-sockets-fmeygr?file=server%2Fservice%2Fmail%2FparseEmail.ts I hope this example makes things clearer! I know how to send and react to messages inside _ws.ts, but I’m unsure how to reuse this to send a message from another component to the same "room." I created service/mail/parseEmail.ts to clarify what I want to do. As a test, I simply created a function that logs a message every 10 seconds, and // message('newMail') ??? is a placeholder where it should send something to all peers. Hopefully, this makes the problem clearer! It would be awesome if you could replace the placeholder with something that works 😄
NowyQuei
StackBlitz
Nuxt 3 + Nitro WebSockets (forked) - StackBlitz
Starter of Nuxt 3 with daisyUI.
Jacek
Jacek2mo ago
I was trying to outline this in pseudo-code above (mq.subscribe(...)) See this demo based on in-memory KV as queue.
Quei
QueiOP2mo ago
Hmm, you’re using a temporary store and all consumers will read from there. Isn’t that kind of an ugly solution? Then I’d also need to delete the entries from time to time, and the frontend would need extra logic to handle that too. I did something similar in the past with Socket.io, and I was hoping to use a built-in feature in Nuxt this time. If I can’t figure out how to do this, I’ll need to install the Socket.io package again.
Jacek
Jacek2mo ago
What's ugly about it? in-memory KV acts like a queue, you can clear it anytime (even right before when you set the value). The system is then decoupled and queue technology can be swapped (from in-memory to something cloud-based). You can wrap set/watch/clear methods with a custom service API. The only alternative would be to share/leak peer instance on open so some service could call .publish(...) anytime it thinks appropriate, while in real-world scenario you'd end up with an array of peers for all open connections and utilizing .send(...) instead to avoid broadcasting to all chat rooms. Ofc built-in solution would be preferrable, however it is always a trade - flexibility vs convenience 😉 IMO, frontend should remain dumb and avoid having any logic to "handle" broadcast messages.
Quei
QueiOP2mo ago
With the alternative using sockets, for example, I don’t need to worry about cleaning up entries, and I also don’t really need to care about who is connected and who isn’t (which I think is similar to KV). I can simply send a message to consumers, and it’s not permanent, meaning I don’t have to clean it up after sending or setting something.
Jacek
Jacek2mo ago
On the other hand socket.io is an estabilished library and should be a good fit for most cases. Hopefully it won't be a large overhead on top of Nitro.
Quei
QueiOP2mo ago
ChatGPT is suggesting using WebSockets or SSE, but I’ve never used SSE before.
Jacek
Jacek2mo ago
The 2nd Deno link used SSE. Dude.. don't listen to mr-generate-random-plausible-answer-from-noise. Use it for generating tests or something, but don't ask it for tech-related stuff, because it will spit lies from training data and you will never know how much truth is there until you dig the topic yourself.
Quei
QueiOP2mo ago
Sure, but it gives me ideas, and after a bit of research, I can decide which way I want to go.
Jacek
Jacek2mo ago
So you said you worry about .clear() before .set(). I wouldn't. It means you use storage as signal bus. It does not matter if it contains an item or not. The only problem is that it is not a solution that clearly speaks "I am dedicated for chat app". If you think socket.io has dedicated methods that would make your source code more clearly state the intent (readable, maintainable), then I see no reason to seek other solutions 🙂
Quei
QueiOP2mo ago
If I take a look at this guide:
https://socket.io/how-to/use-with-nuxt It looks like it's exactly what I was searching for. I just need to trigger socket.emit("blabla").
How to use with Nuxt | Socket.IO
This guide shows how to use Socket.IO within a Nuxt application.
Jacek
Jacek2mo ago
If you can use it to replace unstorage from the last demo on Stackblitz, I would gladly fork it for later 😉 If I read the page correctly, socket lives on the client, so emit just sends message from the browser to the server. I am curious if you can figure out how to utilize it on the backend.
Quei
QueiOP2mo ago
I’m also a bit puzzled when I read this. It should actually be global according to how it’s used, but I need to test it. It’s clearly stated here that it’s global: https://nuxt-socket-io.netlify.app/usage/
nuxt socket.io
Usage
Using nuxt-socket-io in your components
Jacek
Jacek2mo ago
It refers to nuxtServerInit from Nuxt 2. Regardless, if you decide to use some "magic" global, beware it won't scale. You can only broadcast to people connected to specific backend instance.
Quei
QueiOP2mo ago
Okay, I figured it out... my issue is solved. I created a utility with this content:
let ws
const reconnectInterval = 5000 // Time in ms to wait before attempting to reconnect
let isManuallyClosed = false // Flag to prevent reconnection when connection is manually closed

// Function to handle WebSocket connection
export const wsConnect = async () => {
const isSecure = process.env.SITE_PROTOCOL === 'https'
const url = (isSecure ? 'wss://' : 'ws://') + process.env.SITE_NAME + '/api/websocket'

// Close existing connection if present
if (ws) {
console.log('ws', 'Closing previous connection before reconnecting...')
wsClose()
}

console.log('ws', 'Connecting to', url, '...')
ws = new WebSocket(url)

// Wait for the WebSocket to open
await new Promise((resolve) => ws.addEventListener('open', resolve))

console.log('ws', 'Connected!')

// Handle connection close
ws.addEventListener('close', handleWsClose)

// Handle connection errors
ws.addEventListener('error', handleWsError)
}

// Function to handle WebSocket closure
const handleWsClose = () => {
console.log('ws', 'Connection closed.')
if (!isManuallyClosed) {
console.log('ws', `Attempting to reconnect in ${reconnectInterval / 1000} seconds...`)
setTimeout(wsConnect, reconnectInterval) // Attempt to reconnect after delay
}
}

// Function to handle WebSocket errors
const handleWsError = (error: any) => {
console.error('ws', 'Connection error:', error)
ws.close() // Close and trigger the reconnection logic
}

// Function to manually close the WebSocket connection
export const wsClose = () => {
if (ws && ws.readyState === WebSocket.OPEN) {
console.log('ws', 'Manually closing the connection...')
isManuallyClosed = true
ws.close()
} else {
console.log('ws', 'Connection is already closed.')
}
}
let ws
const reconnectInterval = 5000 // Time in ms to wait before attempting to reconnect
let isManuallyClosed = false // Flag to prevent reconnection when connection is manually closed

// Function to handle WebSocket connection
export const wsConnect = async () => {
const isSecure = process.env.SITE_PROTOCOL === 'https'
const url = (isSecure ? 'wss://' : 'ws://') + process.env.SITE_NAME + '/api/websocket'

// Close existing connection if present
if (ws) {
console.log('ws', 'Closing previous connection before reconnecting...')
wsClose()
}

console.log('ws', 'Connecting to', url, '...')
ws = new WebSocket(url)

// Wait for the WebSocket to open
await new Promise((resolve) => ws.addEventListener('open', resolve))

console.log('ws', 'Connected!')

// Handle connection close
ws.addEventListener('close', handleWsClose)

// Handle connection errors
ws.addEventListener('error', handleWsError)
}

// Function to handle WebSocket closure
const handleWsClose = () => {
console.log('ws', 'Connection closed.')
if (!isManuallyClosed) {
console.log('ws', `Attempting to reconnect in ${reconnectInterval / 1000} seconds...`)
setTimeout(wsConnect, reconnectInterval) // Attempt to reconnect after delay
}
}

// Function to handle WebSocket errors
const handleWsError = (error: any) => {
console.error('ws', 'Connection error:', error)
ws.close() // Close and trigger the reconnection logic
}

// Function to manually close the WebSocket connection
export const wsClose = () => {
if (ws && ws.readyState === WebSocket.OPEN) {
console.log('ws', 'Manually closing the connection...')
isManuallyClosed = true
ws.close()
} else {
console.log('ws', 'Connection is already closed.')
}
}
// Function to send a message
export const wsSend = (message: string) => {
if (ws && ws.readyState === WebSocket.OPEN) {
console.log('sending message...', message)
ws.send(message)
} else {
console.error('WebSocket is not open. Cannot send message:', message)
}
}

// Function to send a ping message
export const ping = () => {
if (ws && ws.readyState === WebSocket.OPEN) {
console.log('ws', 'Sending ping')
ws.send('ping')
} else {
console.error('WebSocket is not open. Cannot send ping.')
}
}

// Function to connect, send a message, and disconnect
export const wsSendOnce = async (message: string) => {
if (ws && ws.readyState === WebSocket.OPEN) {
wsSend(message)
} else {
await wsConnect()
wsSend(message)
}
}
// Function to send a message
export const wsSend = (message: string) => {
if (ws && ws.readyState === WebSocket.OPEN) {
console.log('sending message...', message)
ws.send(message)
} else {
console.error('WebSocket is not open. Cannot send message:', message)
}
}

// Function to send a ping message
export const ping = () => {
if (ws && ws.readyState === WebSocket.OPEN) {
console.log('ws', 'Sending ping')
ws.send('ping')
} else {
console.error('WebSocket is not open. Cannot send ping.')
}
}

// Function to connect, send a message, and disconnect
export const wsSendOnce = async (message: string) => {
if (ws && ws.readyState === WebSocket.OPEN) {
wsSend(message)
} else {
await wsConnect()
wsSend(message)
}
}
Additionally, I use a plugin to open one connection, and after that, I can use wsSend everywhere on the server side. F***ing awesome!
Jacek
Jacek2mo ago
If I read it correctly... anytime you open a new connection you close previous one. So you actually support single user for the whole backend instance. Is it meant to run on edge and spin up worker for every user? Oh.. I may have misunderstood the whole idea. You want to send from client to backend, not the other way around.
Quei
QueiOP2mo ago
Well, this is only a partial solution.
In the plugin, I open the connection with wsConnect(), and in my services on the server side, I use the connection with wsSendOnce("bla bla bla").
Jacek
Jacek2mo ago
Is the browser involved in it? or is it service-to-service? Because you won't open ws connection straight to client browser - he does not have any URL to point to.
Quei
QueiOP2mo ago
No, no, you got me completely right... I couldn't figure out how to reuse the connection from server\api\websocket.ts, so I just created a new "client" to send the messages. In a later stage, server\api\websocket.ts will be secured so that only the server can send messages. The browser will also connect to server\api\websocket.ts, but it will only be the consumer of the messages in the end. And this will be the baseline for my reactivity—when something changes in the backend, I'll notify all consumers to update via a regular API call. And of course, the name and path of server\api\websocket.ts will also be changed! 🙂
Quei
QueiOP2mo ago
No description
Quei
QueiOP2mo ago
in ultra short this is the principle
Want results from more Discord servers?
Add your server