A Nuxt module aimed to simplify real-time communication with built-in automatic topic subscriptions, reactive shared state management, and type safety.
useWS
: A WebSocket implementation with built-in shared state management, topic subscriptions, and type safety.defineWSHandler
: wraps Nitro'sdefineWebSocketHandler
to provide additional configuration, hooks and automatic topic subscription.
Install the module to your Nuxt application with one command:
npx nuxi module add nuxt-ws
Ideally you should set your default path, based on where you will place your WebSocket api handlers:
export default defineNuxtConfig({
modules: ['nuxt-ws'],
ws: {
path: '/_ws', // default undefined
}
})
A WebSocket implementation with built-in shared state management, topic subscriptions, and type safety.
- π Automatic reconnection
- π¦ Built-in shared state management per-topic
- π€ Build-in validation with Standard Schema
- π Type-safe messages and topics
- π’ Hooking system to trigger messages globaly
- βοΈ Integrates easily with other Nuxt modules
Take in example the following setup:
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['nuxt-ws'],
ws: {
route: '/_ws', // WebSocket endpoint to auto-connect
topics: {
defaults: ['session'], // Auto-subscribed topics
}
}
})
Client-Side (pages/chat.client.vue
):
<template>
<div>
<div v-if="status === 'OPEN'">
Connected, Users: {{ states['session'].users }}
</div>
<div v-else>
Disconnected
</div>
<div v-for="({ user, text }, key) in states['chat']" :key>
{{ user }}: {{ text }}
</div>
<input v-model="textRef" @keyup.enter="sendMessage()">
</div>
</template>
<script setup lang="ts">
const textRef = ref('')
const { states, status, send } = useWS<{
chat: {
user?: string
text: string
}[]
session: {
users: number
}
}>(['chat'])
function sendMessage() {
send('publish', 'chat', {
text: textRef.value,
})
textRef.value = ''
}
</script>
Server-Side (server/routes/_ws.ts
):
import * as v from 'valibot'
export default defineWSHandler({
async open(peer) {
// Update peer with 'chat' data from storage
const chat = await useStorage('ws').getItem('chat')
if (chat)
peer.send(JSON.stringify({
topic: 'chat',
payload: chat,
}), { compress: true })
// Update everyone's session metadata
const payload = JSON.stringify({ topic: 'session', payload: { users: peer.peers.size } })
peer.send(payload, { compress: true })
peer.publish('session', payload, { compress: true })
},
async message(peer, message) {
// Validate the incoming chat message
const parsedMessage = await wsValidateMessage( // built-in validation util
v.object({
type: v.literal('publish'),
topic: v.literal('chat'),
payload: v.object({ text: v.string() }),
}),
message,
)
// Update chat data in storage
const mem = useStorage('ws')
const { topic, payload } = parsedMessage
const _chat = await mem.getItem<Array<{ user: string, text: string }>>('chat') || []
const newChat = [..._chat, { ...payload, user: peer.id }]
await mem.setItem(topic, newChat)
// Broadcast the updated chat to everyone
peer.send(JSON.stringify({ topic, payload: newChat }), { compress: true })
peer.publish(topic, JSON.stringify({ topic, payload: newChat }), { compress: true })
},
close(peer) {
// Update everyone's session metadata
peer.publish(
'session',
JSON.stringify({
topic: 'session',
payload: {
users: peer.peers.size,
},
}),
{ compress: true },
)
},
})
Local development
# Install dependencies
pnpm install
# Generate type stubs
pnpm run dev:prepare
# Develop with the playground
pnpm run dev
# Build the playground
pnpm run dev:build
# Run ESLint
pnpm run lint
# Run Vitest
pnpm run test
pnpm run test:watch
Published under the MIT license.