Skip to content

Commit 9f48a45

Browse files
committed
docs: document functionalities
1 parent b2965bd commit 9f48a45

File tree

8 files changed

+422
-8
lines changed

8 files changed

+422
-8
lines changed

docs/content/1.getting-started/1.index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,4 @@ There are available a number of client composables and server utilities. Used in
2626

2727
### Server-Side
2828

29-
- `defineReactiveWSHandler`: wraps Nitro's `defineWebSocketHandler` to provide additional configuration, hooks and automatic topic subscription.
29+
- `defineWSHandler`: wraps Nitro's `defineWebSocketHandler` to provide additional configuration, hooks and automatic topic subscription.

docs/content/2.usage/1.topics.md

Lines changed: 0 additions & 5 deletions
This file was deleted.

docs/content/2.usage/1.useWS.md

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
---
2+
title: useWS
3+
description: The all-in-one composable to manage reactive WebSocket connections from client-side.
4+
navigation.icon: i-lucide-smartphone-nfc
5+
---
6+
7+
The `useWS` will be the main composable to manage WebSocket connections client-side. It automatically manage the states of the subscribed topics and the connection itself.
8+
9+
Used in conjunction with [`defineWSHandler`](/usage/defineWSHandler) and [runtime configuration](/getting-started/configuration), it will provide a seamless experience for real-time communication as simple as doing `const { states, send } = useWS()`.
10+
11+
## Arguments
12+
13+
It only accepts two arguments, both of them optional.
14+
15+
### `topics`
16+
17+
An array of strings representing the topics to subscribe to. These should not include those defined in your runtime configuration.
18+
19+
```ts
20+
const { states, send } = useWS(['session', 'info'])
21+
```
22+
23+
### `options`
24+
25+
An optional object to override the default options for the WebSocket connection.
26+
27+
::field-group
28+
::field{name="route" type="string"}
29+
Defines the default route for `useWS` to connect to the WebSocket server. - Default to runtime configuration
30+
::
31+
32+
::field{name="heartbeat" type="boolean | object"}
33+
Enables or disables the heartbeat mechanism. If an object is provided, it is possible to define the interval and the message to send. - Default to `false`
34+
::
35+
36+
::field{name="autoReconnect" type="boolean | object"}
37+
Enables or disables the auto-reconnect mechanism. If an object is provided, it is possible to define the interval and the maximum number of attempts. - Default to `false`
38+
::
39+
40+
::field{name="immediate" type="boolean"}
41+
The connection will be established immediately. - Default to `true`
42+
::
43+
44+
::field{name="autoConnect" type="boolean"}
45+
Automatically connect to the websocket when `route` changes. - Default to `true`
46+
::
47+
48+
::field{name="autoClose" type="boolean"}
49+
Automatically close the websocket when the component is unmounted. - Default to `true`
50+
::
51+
52+
::field{name="protocols" type="string[]"}
53+
An array of strings representing the sub-protocols to use. - Default to `[]`
54+
::
55+
::
56+
57+
## Returns
58+
59+
It returns a number of properties and methods to manage the WebSocket connection and the subscribed topics.
60+
61+
### `states`
62+
63+
A reactive object containing the states of all the subscribed topics.
64+
65+
```ts
66+
const { states, send } = useWS<{
67+
session: {
68+
users: number
69+
}
70+
}>(['session'])
71+
72+
console.log(states.session.users)
73+
```
74+
75+
### `send`
76+
77+
A function to send messages to the WebSocket server. It can accept multiple arguments, based on use-case, but always returns a boolean indicating if the message was sent successfully.
78+
79+
- `send(payload: string | ArrayBuffer | Blob): boolean`
80+
- `send(type: 'subscribe' | 'unsubscribe', topic: string): boolean`
81+
- `send(type: 'publish', topic: string, payload: any): boolean`
82+
83+
The last two methods will always be transformed to string before being sent to the server.
84+
85+
```ts
86+
const { states, send } = useWS<{
87+
chat: {
88+
user?: string,
89+
text: string,
90+
}[]
91+
}>(['chat'])
92+
93+
const file = new Blob(['Hello, World!'], { type: 'text/plain' })
94+
send(file)
95+
96+
send('publish', 'chat', { text: 'Hello, World!' })
97+
```
98+
99+
::note
100+
Sending a `payload` directly without specifying the type and topic will send them as is and will not be automatically handled server-side.
101+
102+
By contrast, specifying the type and topic will be handled automatically by the server if [`defineWSHandler`](/usage/defineWSHandler) is used and the target is not an internal topic.
103+
::
104+
105+
### `data`
106+
107+
A ref containing any extra data received from the server and not handled by `states` automatically.
108+
109+
```ts
110+
const { data } = useWS()
111+
112+
// server sends 'Hello, World!' directly without publishing to a topic
113+
114+
console.log(data.value) // 'Hello, World!'
115+
```
116+
117+
### `status`
118+
119+
A ref containing the current status of the WebSocket connection.
120+
121+
```ts
122+
const { status } = useWS()
123+
124+
console.log(status.value) // 'OPEN' | 'CONNECTING' | 'CLOSED'
125+
```
126+
127+
### `open`/`close`
128+
129+
Methods to manually open and close the WebSocket connection.
130+
131+
### Internals
132+
133+
The following properties and methods are considered internal and should not be used directly. I decided to expose them for debugging purposes or advanced use-cases.
134+
135+
- `ws`: The WebSocket instance.
136+
- `_data`: It contains any raw data received from the server.
137+
- `_send`: It is used under the hood to send messages to the server.
138+
139+
## Type Support
140+
141+
As you might have noticed in the examples above, `useWS` accepts a type parameter. This is to provide type support for the states object.
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
---
2+
title: defineWSHandler
3+
description: The all-in-one utility to manage reactive WebSocket connections from server-side.
4+
navigation.icon: i-lucide-arrow-left-right
5+
---
6+
7+
The `defineWSHandler` will be the main utility to manage WebSocket connections server-side. It handles automatically the subscription of topics defined in the runtime configuration while also proving a hooking system to trigger messages globally from other part of the application server-side.
8+
9+
::callout{icon="i-lucide-triangle-alert" color="warning" to="https://nitro.build/guide/websocket#usage" _target="blank"}
10+
Please first read the upstream Nitro documentation on `defineWebSocketHandler` to understand the basic usage.
11+
::
12+
13+
## Example
14+
15+
```ts [/server/routes/_ws.ts]
16+
import * as v from 'valibot'
17+
18+
export default defineWSHandler({
19+
async open(peer) {
20+
// Update peer with 'chat' data from storage
21+
const chat = await useStorage('ws').getItem('chat')
22+
if (chat)
23+
peer.send(JSON.stringify({
24+
topic: 'chat',
25+
payload: chat,
26+
}), { compress: true })
27+
28+
// Update everyone's session metadata
29+
const payload = JSON.stringify({ topic: 'session', payload: { users: peer.peers.size } })
30+
peer.send(payload, { compress: true })
31+
peer.publish('session', payload, { compress: true })
32+
},
33+
34+
async message(peer, message) {
35+
// Validate the incoming chat message
36+
const parsedMessage = await wsValidateMessage( // built-in validation util
37+
v.object({
38+
type: v.literal('publish'),
39+
topic: v.literal('chat'),
40+
payload: v.object({ text: v.string() }),
41+
}),
42+
message,
43+
)
44+
45+
// Update chat data in storage
46+
const mem = useStorage('ws')
47+
const { topic, payload } = parsedMessage
48+
const _chat = await mem.getItem<Array<{ user: string, text: string }>>('chat') || []
49+
const newChat = [..._chat, { ...payload, user: peer.id }]
50+
await mem.setItem(topic, newChat)
51+
52+
// Broadcast the updated chat to everyone
53+
peer.send(JSON.stringify({ topic, payload: newChat }), { compress: true })
54+
peer.publish(topic, JSON.stringify({ topic, payload: newChat }), { compress: true })
55+
},
56+
57+
close(peer) {
58+
// Update everyone's session metadata
59+
peer.publish(
60+
'session',
61+
JSON.stringify({
62+
topic: 'session',
63+
payload: {
64+
users: peer.peers.size,
65+
},
66+
}),
67+
{ compress: true },
68+
)
69+
},
70+
})
71+
```
72+
73+
## Parsing messages
74+
75+
A small `wsParseMessage` utility is provided to parse JSON content from incoming messages. This uses [`destr`](https://github.com/unjs/destr) under the hood to parse the message and not throw an error if the message is not a valid JSON string.
76+
77+
This is also used under the hood by [validation utilities](/usage/validation).
78+
79+
## Authentication
80+
81+
You can easily validate user's sessions with modules like `nuxt-auth-utils` as described in [the documentation](https://github.com/atinux/nuxt-auth-utils?tab=readme-ov-file#websocket-support).
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
---
2+
title: Validation
3+
description: Validation utils for incoming messages from the client.
4+
navigation.icon: i-lucide-hammer
5+
---
6+
7+
Two validation utilities are available server-side to validate incoming messages from the client. They are based on [Standard Schema](https://github.com/standard-schema/standard-schema), allowing you to use your preferred validation library.
8+
9+
- `wsValidateMessage`
10+
- `wsSafeValidateMessage`
11+
12+
## `wsValidateMessage`
13+
14+
Validates a WebSocket message against a given schema. If the message is invalid, the function throws an error with the validation issues. Otherwise, it returns the validated message. If the message is a Message object, it's parsed with [`wsParseMessage`](/usage/definewshandler#parsing-message) before validation.
15+
16+
::code-group
17+
18+
```ts [valibot]
19+
import * as v from 'valibot'
20+
21+
message(peer, message) {
22+
const parsedMessage = await wsValidateMessage(
23+
v.object({
24+
type: v.picklist(['subscribe', 'unsubscribe']),
25+
topic: v.picklist(['chat', 'notifications']),
26+
}),
27+
message,
28+
)
29+
30+
// or directly the incoming message type
31+
const blob = await wsValidateMessage(v.blob(), message.blob())
32+
const helloWorld = await wsValidateMessage(v.literal('Hellow World'), message.text())
33+
}
34+
```
35+
36+
```ts [zod]
37+
import { z } from 'zod'
38+
39+
message(peer, message) {
40+
const parsedMessage = await wsValidateMessage(
41+
z.object({
42+
type: z.enum(['subscribe', 'unsubscribe']),
43+
topic: z.enum(['chat', 'notifications']),
44+
}),
45+
message,
46+
)
47+
48+
// or directly the incoming message type
49+
const blob = await wsValidateMessage(z.blob(), message.blob())
50+
const helloWorld = await wsValidateMessage(z.literal('Hellow World'), message.text())
51+
}
52+
```
53+
54+
::
55+
56+
## `wsSafeValidateMessage`
57+
58+
Same as `wsValidateMessage`, but instead of throwing an error, it returns an object with the validation issues if the message is invalid. Otherwise, it returns the validated message.
59+
60+
::code-group
61+
62+
```ts [valibot]
63+
import * as v from 'valibot'
64+
65+
message(peer, message) {
66+
const parsedMessage = await wsSafeValidateMessage(
67+
v.object({
68+
type: v.picklist(['subscribe', 'unsubscribe']),
69+
topic: v.picklist(['chat', 'notifications']),
70+
}),
71+
message,
72+
)
73+
74+
if (parsedMessage.issues) {
75+
console.error('WebSocket:', peer.id, parsedMessage.issues)
76+
peer.send(JSON.stringify({
77+
topic: '_internal', // Assuming being used
78+
message: `Invalid message: ${parsedMessage.issues}`
79+
}))
80+
}
81+
// else do something with parsedMessage.value
82+
}
83+
```
84+
85+
```ts [zod]
86+
import { z } from 'zod'
87+
88+
message(peer, message) {
89+
const parsedMessage = await wsSafeValidateMessage(
90+
z.object({
91+
type: z.enum(['subscribe', 'unsubscribe']),
92+
topic: z.enum(['chat', 'notifications']),
93+
}),
94+
message,
95+
)
96+
97+
if (parsedMessage.issues) {
98+
console.error('WebSocket:', peer.id, parsedMessage.issues)
99+
peer.send(JSON.stringify({
100+
topic: '_internal', // Assuming being used
101+
message: `Invalid message: ${parsedMessage.issues}`
102+
}))
103+
}
104+
// else do something with parsedMessage.value
105+
}
106+
```

docs/content/2.usage/4.topics.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
---
2+
title: Topics
3+
description: Automatic topic subscriptions and reactive shared state management.
4+
navigation.icon: i-lucide-messages-square
5+
---
6+
7+
Both `useWS` for the client and `defineWSHandler` for the server handle automatic topic subscriptions. This means that when a connection is established, the client will automatically subscribe to the topics defined in the runtime configuration. The server can then publish messages to these topics, and the client will receive them.
8+
9+
`useWS` also accept the topic parameter as a reactive array. This allows to automatically and dynamically subscribe and unsubscribe to topics after the connection is established.
10+
11+
## Example
12+
13+
```vue
14+
<template>
15+
<div>
16+
<div>
17+
<ul>
18+
<li v-for="topic in availableTopics" :key="topic">
19+
<label>
20+
<input type="checkbox" v-model="topics" :value="topic" />
21+
{{ topic }}
22+
</label>
23+
</li>
24+
</ul>
25+
</div>
26+
<div v-if="states['chat']">
27+
<div v-for="(message, key) in states['chat']" :key>
28+
<strong>{{ message.user }}</strong>: {{ message.text }}
29+
</div>
30+
</div>
31+
<div v-if="states['notifications']">
32+
{{ notification.message }}
33+
</div>
34+
</div>
35+
</template>
36+
37+
<script setup lang="ts">
38+
const availableTopics = ref(['chat', 'notifications'])
39+
const topics = ref(['chat'])
40+
41+
const { states, send } = useWS<{
42+
chat?: {
43+
user?: string,
44+
text: string,
45+
}[],
46+
notifications?: {
47+
message: string,
48+
},
49+
}>(topics)
50+
</script>
51+
```
52+
53+
::note
54+
Make sure to type any dynamic topic as optional, as they might not be available.
55+
::

0 commit comments

Comments
 (0)