@gwakko/shared-websocket 0.6.1 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,6 +2,43 @@
2
2
 
3
3
  Share ONE WebSocket connection across multiple browser tabs. Zero dependencies. React and Vue adapters included.
4
4
 
5
+ ## Table of Contents
6
+
7
+ - [Problem](#problem)
8
+ - [Solution](#solution)
9
+ - [Installation](#installation)
10
+ - [Usage — Vanilla TypeScript](#usage--vanilla-typescript)
11
+ - [Scoped Lifecycle — withSocket()](#scoped-lifecycle--withsocket)
12
+ - [Usage — React](#usage--react)
13
+ - [Usage — Vue 3](#usage--vue-3)
14
+ - [API Reference](#api-reference)
15
+ - [Options](#options)
16
+ - [Authentication](#authentication)
17
+ - [React Hooks](#react-hooks-react-19-useeffectevent-for-stable-refs)
18
+ - [Vue Composables](#vue-composables)
19
+ - [How It Works](#how-it-works)
20
+ - [When to Use `useWorker: true`](#when-to-use-useworker-true)
21
+ - [Typed Events](#typed-events)
22
+ - [Type narrowing](#type-narrowing-for-untyped-events)
23
+ - [Runtime validation with Zod](#runtime-validation-with-zod)
24
+ - [Middleware](#middleware)
25
+ - [Debug Mode & Custom Logger](#debug-mode--custom-logger)
26
+ - [Custom Event Protocol](#custom-event-protocol)
27
+ - [Advanced Examples](#advanced-examples)
28
+ - [Stream](#stream--consume-events-as-async-iterator)
29
+ - [Request](#request--requestresponse-through-server)
30
+ - [Protocols](#protocols--websocket-subprotocols)
31
+ - [Worker URL](#worker-url--custom-worker-file)
32
+ - [Lifecycle Hooks](#lifecycle-hooks)
33
+ - [Private Channels](#private-channels--chat-rooms-tenant-notifications)
34
+ - [Server-side channel handling](#server-side-channel-handling)
35
+ - [Topics](#topics--server-side-filtered-subscriptions)
36
+ - [Push Notifications](#push-notifications)
37
+ - [Server-Side Implementation Guide](#server-side-implementation-guide)
38
+ - [Exported Types](#exported-types)
39
+ - [Browser Support](#browser-support)
40
+ - [License](#license)
41
+
5
42
  ## Problem
6
43
 
7
44
  5 tabs open = 5 WebSocket connections = 5x server resources for the same user.
@@ -1040,6 +1077,394 @@ wss.on('connection', (ws) => {
1040
1077
  });
1041
1078
  ```
1042
1079
 
1080
+ ## Topics — Server-Side Filtered Subscriptions
1081
+
1082
+ Subscribe to specific topics so the server only sends relevant events:
1083
+
1084
+ ```typescript
1085
+ // Vanilla
1086
+ ws.subscribe('notifications:orders');
1087
+ ws.subscribe('notifications:payments');
1088
+ ws.subscribe(`user:${userId}:mentions`);
1089
+
1090
+ // Later — unsubscribe
1091
+ ws.unsubscribe('notifications:orders');
1092
+ ```
1093
+
1094
+ ```tsx
1095
+ // React — auto-subscribe on mount, unsubscribe on unmount
1096
+ function OrdersDashboard() {
1097
+ useTopics(['notifications:orders', 'notifications:payments']);
1098
+
1099
+ const order = useSocketEvent('notifications:orders:new');
1100
+ return order ? <div>New order #{order.id}</div> : null;
1101
+ }
1102
+
1103
+ // Dynamic topics
1104
+ function UserMentions({ userId }: { userId: string }) {
1105
+ useTopics([`user:${userId}:mentions`]);
1106
+ useSocketCallback(`user:${userId}:mentions:mention`, showMentionToast);
1107
+ return null;
1108
+ }
1109
+ ```
1110
+
1111
+ ```vue
1112
+ <!-- Vue — same pattern -->
1113
+ <script setup>
1114
+ const props = defineProps<{ userId: string }>();
1115
+ useTopics([`user:${props.userId}:mentions`]);
1116
+ useSocketEvent(`user:${props.userId}:mentions:mention`, showToast);
1117
+ </script>
1118
+ ```
1119
+
1120
+ Server receives `$topic:subscribe` / `$topic:unsubscribe` events (configurable via `events.topicSubscribe`).
1121
+
1122
+ ## Push Notifications
1123
+
1124
+ Two modes: **custom render** (sonner, react-hot-toast, your UI) and/or **browser Notification API**. Both respect `leaderOnly` + `onlyWhenHidden` to prevent duplicates across tabs.
1125
+
1126
+ ### Custom Render — you control the display
1127
+
1128
+ ```typescript
1129
+ // Vanilla — sonner toast
1130
+ import { toast } from 'sonner';
1131
+
1132
+ ws.push('notification', {
1133
+ render: (n) => toast(n.title, { description: n.body }),
1134
+ });
1135
+
1136
+ ws.push('order.created', {
1137
+ render: (order) => toast.success(`New Order #${order.id}`, {
1138
+ description: `$${order.total} from ${order.customer}`,
1139
+ action: { label: 'View', onClick: () => navigate(`/orders/${order.id}`) },
1140
+ }),
1141
+ });
1142
+ ```
1143
+
1144
+ ```tsx
1145
+ // React — sonner
1146
+ import { toast } from 'sonner';
1147
+
1148
+ function NotificationSetup() {
1149
+ usePush('notification', {
1150
+ render: (n) => toast(n.title, { description: n.body }),
1151
+ });
1152
+
1153
+ usePush('order.created', {
1154
+ render: (order) => toast.success(`Order #${order.id} — $${order.total}`),
1155
+ });
1156
+
1157
+ return null;
1158
+ }
1159
+
1160
+ // React — react-hot-toast
1161
+ import hotToast from 'react-hot-toast';
1162
+
1163
+ function NotificationSetup() {
1164
+ usePush('notification', {
1165
+ render: (n) => hotToast(n.title),
1166
+ });
1167
+ return null;
1168
+ }
1169
+ ```
1170
+
1171
+ ```vue
1172
+ <!-- Vue — sonner-vue -->
1173
+ <script setup>
1174
+ import { toast } from 'sonner-vue';
1175
+
1176
+ usePush('notification', {
1177
+ render: (n) => toast(n.title, { description: n.body }),
1178
+ });
1179
+
1180
+ usePush('order.created', {
1181
+ render: (order) => toast.success(`Order #${order.id} — $${order.total}`),
1182
+ });
1183
+ </script>
1184
+ ```
1185
+
1186
+ ### Browser Notification API — native OS notifications
1187
+
1188
+ ```typescript
1189
+ // Vanilla — browser native (no render needed)
1190
+ ws.push('notification', {
1191
+ title: (n) => n.title,
1192
+ body: (n) => n.body,
1193
+ icon: '/icons/bell.png',
1194
+ tag: (n) => `notif-${n.id}`, // deduplication
1195
+ onClick: (n) => window.open(n.url),
1196
+ leaderOnly: true, // default: true
1197
+ onlyWhenHidden: true, // default: true
1198
+ });
1199
+ ```
1200
+
1201
+ ```tsx
1202
+ // React
1203
+ usePush('order.created', {
1204
+ title: (order) => `New Order #${order.id}`,
1205
+ body: (order) => `$${order.total}`,
1206
+ icon: '/icons/order.png',
1207
+ onClick: (order) => navigate(`/orders/${order.id}`),
1208
+ });
1209
+ ```
1210
+
1211
+ ```vue
1212
+ <!-- Vue -->
1213
+ <script setup>
1214
+ usePush('order.created', {
1215
+ title: (order) => `New Order #${order.id}`,
1216
+ body: (order) => `$${order.total}`,
1217
+ });
1218
+ </script>
1219
+ ```
1220
+
1221
+ ### Both — toast in UI + browser notification
1222
+
1223
+ ```typescript
1224
+ // Show sonner toast AND browser notification
1225
+ ws.push('order.created', {
1226
+ render: (order) => toast.success(`Order #${order.id}`), // in-app toast
1227
+ title: (order) => `New Order #${order.id}`, // + native notification
1228
+ body: (order) => `$${order.total}`,
1229
+ });
1230
+ ```
1231
+
1232
+ ## Server-Side Implementation Guide
1233
+
1234
+ Complete server reference — what events to listen for and how to respond.
1235
+
1236
+ ### Message Format
1237
+
1238
+ All messages are JSON with two fields (configurable via `events` option):
1239
+
1240
+ ```
1241
+ Client → Server: { "event": "event.name", "data": { ... } }
1242
+ Server → Client: { "event": "event.name", "data": { ... } }
1243
+ ```
1244
+
1245
+ ### System Events (sent by client automatically)
1246
+
1247
+ | Event | When | Payload | Your Server Should |
1248
+ |-------|------|---------|-------------------|
1249
+ | `ping` | Every 30s (heartbeat) | `{ "type": "ping" }` | Respond with `{ "type": "pong" }` or ignore |
1250
+ | `$channel:join` | `ws.channel('name')` | `{ "channel": "chat:room_1" }` | Track which channels this connection belongs to |
1251
+ | `$channel:leave` | `channel.leave()` | `{ "channel": "chat:room_1" }` | Remove connection from channel |
1252
+ | `$topic:subscribe` | `ws.subscribe('topic')` | `{ "topic": "notifications:orders" }` | Start sending events for this topic to this connection |
1253
+ | `$topic:unsubscribe` | `ws.unsubscribe('topic')` | `{ "topic": "notifications:orders" }` | Stop sending events for this topic |
1254
+
1255
+ ### Node.js (ws) — Complete Server Example
1256
+
1257
+ ```typescript
1258
+ import { WebSocketServer, WebSocket } from 'ws';
1259
+
1260
+ const wss = new WebSocketServer({ port: 8080 });
1261
+
1262
+ // Track per-connection state
1263
+ interface ClientState {
1264
+ userId?: string;
1265
+ channels: Set<string>;
1266
+ topics: Set<string>;
1267
+ }
1268
+
1269
+ const clients = new Map<WebSocket, ClientState>();
1270
+
1271
+ wss.on('connection', (ws, req) => {
1272
+ // Extract auth token from URL
1273
+ const url = new URL(req.url!, `http://${req.headers.host}`);
1274
+ const token = url.searchParams.get('token');
1275
+ const userId = verifyToken(token); // your auth logic
1276
+
1277
+ const state: ClientState = {
1278
+ userId,
1279
+ channels: new Set(),
1280
+ topics: new Set(),
1281
+ };
1282
+ clients.set(ws, state);
1283
+
1284
+ // Send welcome
1285
+ send(ws, 'welcome', { userId, timestamp: Date.now() });
1286
+
1287
+ ws.on('message', (raw) => {
1288
+ const msg = JSON.parse(raw.toString());
1289
+ const { event, data } = msg;
1290
+
1291
+ switch (event) {
1292
+ // ─── System Events ───────────────────────
1293
+
1294
+ case 'ping':
1295
+ // Respond to heartbeat (optional — some servers ignore pings)
1296
+ ws.send(JSON.stringify({ type: 'pong' }));
1297
+ break;
1298
+
1299
+ // ─── Channel Events ──────────────────────
1300
+
1301
+ case '$channel:join':
1302
+ state.channels.add(data.channel);
1303
+ console.log(`${userId} joined ${data.channel}`);
1304
+ // Send channel history, presence, etc.
1305
+ break;
1306
+
1307
+ case '$channel:leave':
1308
+ state.channels.delete(data.channel);
1309
+ console.log(`${userId} left ${data.channel}`);
1310
+ break;
1311
+
1312
+ // ─── Topic Events ────────────────────────
1313
+
1314
+ case '$topic:subscribe':
1315
+ state.topics.add(data.topic);
1316
+ console.log(`${userId} subscribed to ${data.topic}`);
1317
+ break;
1318
+
1319
+ case '$topic:unsubscribe':
1320
+ state.topics.delete(data.topic);
1321
+ break;
1322
+
1323
+ // ─── App Events ──────────────────────────
1324
+
1325
+ case 'chat.send':
1326
+ // Broadcast to all clients in the same channel
1327
+ const channel = data.roomId ? `chat:${data.roomId}` : null;
1328
+ broadcastToChannel(channel, 'chat.message', {
1329
+ id: crypto.randomUUID(),
1330
+ userId: state.userId,
1331
+ text: data.text,
1332
+ timestamp: Date.now(),
1333
+ });
1334
+ break;
1335
+
1336
+ case 'chat.typing':
1337
+ broadcastToChannel(`chat:${data.roomId}`, 'chat.typing', {
1338
+ userId: state.userId,
1339
+ }, ws); // exclude sender
1340
+ break;
1341
+
1342
+ default:
1343
+ console.log('Unknown event:', event, data);
1344
+ }
1345
+ });
1346
+
1347
+ ws.on('close', () => {
1348
+ clients.delete(ws);
1349
+ });
1350
+ });
1351
+
1352
+ // ─── Helpers ─────────────────────────────────────
1353
+
1354
+ function send(ws: WebSocket, event: string, data: unknown) {
1355
+ if (ws.readyState === WebSocket.OPEN) {
1356
+ ws.send(JSON.stringify({ event, data }));
1357
+ }
1358
+ }
1359
+
1360
+ function broadcastToChannel(
1361
+ channel: string | null,
1362
+ event: string,
1363
+ data: unknown,
1364
+ exclude?: WebSocket,
1365
+ ) {
1366
+ for (const [ws, state] of clients) {
1367
+ if (ws === exclude) continue;
1368
+ if (channel && !state.channels.has(channel)) continue;
1369
+ send(ws, event, data);
1370
+ }
1371
+ }
1372
+
1373
+ function broadcastToTopic(topic: string, event: string, data: unknown) {
1374
+ for (const [ws, state] of clients) {
1375
+ if (!state.topics.has(topic)) continue;
1376
+ send(ws, `${topic}:${event}`, data);
1377
+ }
1378
+ }
1379
+
1380
+ // ─── Example: Send notifications by topic ────────
1381
+
1382
+ function notifyNewOrder(order: Order) {
1383
+ broadcastToTopic('notifications:orders', 'new', {
1384
+ id: order.id,
1385
+ total: order.total,
1386
+ customer: order.customerName,
1387
+ });
1388
+ // Only clients who called ws.subscribe('notifications:orders') receive this
1389
+ }
1390
+ ```
1391
+
1392
+ ### Go — Server Example
1393
+
1394
+ ```go
1395
+ // Message format
1396
+ type Message struct {
1397
+ Event string `json:"event"`
1398
+ Data json.RawMessage `json:"data"`
1399
+ }
1400
+
1401
+ // Handle incoming messages
1402
+ func handleMessage(conn *websocket.Conn, state *ClientState, msg Message) {
1403
+ switch msg.Event {
1404
+ case "$channel:join":
1405
+ var payload struct{ Channel string `json:"channel"` }
1406
+ json.Unmarshal(msg.Data, &payload)
1407
+ state.Channels[payload.Channel] = true
1408
+
1409
+ case "$channel:leave":
1410
+ var payload struct{ Channel string `json:"channel"` }
1411
+ json.Unmarshal(msg.Data, &payload)
1412
+ delete(state.Channels, payload.Channel)
1413
+
1414
+ case "$topic:subscribe":
1415
+ var payload struct{ Topic string `json:"topic"` }
1416
+ json.Unmarshal(msg.Data, &payload)
1417
+ state.Topics[payload.Topic] = true
1418
+
1419
+ case "$topic:unsubscribe":
1420
+ var payload struct{ Topic string `json:"topic"` }
1421
+ json.Unmarshal(msg.Data, &payload)
1422
+ delete(state.Topics, payload.Topic)
1423
+
1424
+ case "chat.send":
1425
+ // broadcast to channel...
1426
+
1427
+ case "ping":
1428
+ conn.WriteJSON(Message{Event: "pong"})
1429
+ }
1430
+ }
1431
+ ```
1432
+
1433
+ ### PHP (Laravel + Ratchet/Swoole) — Server Example
1434
+
1435
+ ```php
1436
+ // Handle incoming WebSocket message
1437
+ public function onMessage(ConnectionInterface $conn, $msg): void
1438
+ {
1439
+ $data = json_decode($msg, true);
1440
+ $event = $data['event'] ?? 'message';
1441
+ $payload = $data['data'] ?? [];
1442
+
1443
+ match ($event) {
1444
+ '$channel:join' => $this->joinChannel($conn, $payload['channel']),
1445
+ '$channel:leave' => $this->leaveChannel($conn, $payload['channel']),
1446
+ '$topic:subscribe' => $this->subscribeTopic($conn, $payload['topic']),
1447
+ '$topic:unsubscribe' => $this->unsubscribeTopic($conn, $payload['topic']),
1448
+ 'chat.send' => $this->handleChatMessage($conn, $payload),
1449
+ 'ping' => $conn->send(json_encode(['type' => 'pong'])),
1450
+ default => logger()->warning("Unknown event: {$event}"),
1451
+ };
1452
+ }
1453
+
1454
+ // Send to topic subscribers
1455
+ public function notifyTopic(string $topic, string $event, array $data): void
1456
+ {
1457
+ foreach ($this->connections as $conn) {
1458
+ if (in_array($topic, $this->topics[$conn->resourceId] ?? [])) {
1459
+ $conn->send(json_encode([
1460
+ 'event' => "{$topic}:{$event}",
1461
+ 'data' => $data,
1462
+ ]));
1463
+ }
1464
+ }
1465
+ }
1466
+ ```
1467
+
1043
1468
  ## Exported Types
1044
1469
 
1045
1470
  All types are available for import in your projects:
@@ -1082,9 +1507,9 @@ import {
1082
1507
  useSocketStatus,
1083
1508
  useSocketLifecycle,
1084
1509
  useChannel,
1510
+ useTopics,
1511
+ usePush,
1085
1512
  } from '@gwakko/shared-websocket/react';
1086
-
1087
- import type { SharedWebSocketProviderProps } from '@gwakko/shared-websocket/react';
1088
1513
  ```
1089
1514
 
1090
1515
  ```typescript
@@ -1099,7 +1524,9 @@ import {
1099
1524
  useSocketStatus,
1100
1525
  useSocketLifecycle,
1101
1526
  useChannel,
1102
- SharedWebSocketKey, // InjectionKey for custom provide/inject
1527
+ useTopics,
1528
+ usePush,
1529
+ SharedWebSocketKey,
1103
1530
  } from '@gwakko/shared-websocket/vue';
1104
1531
  ```
1105
1532
 
@@ -95,6 +95,78 @@ export declare class SharedWebSocket<TEvents extends EventMap = EventMap> implem
95
95
  * notifications.on('alert', (alert) => showToast(alert));
96
96
  */
97
97
  channel(name: string): Channel;
98
+ /**
99
+ * Subscribe to a server-side topic. Server will start sending events for this topic.
100
+ * Sends topicSubscribe event (default: "$topic:subscribe").
101
+ *
102
+ * @example
103
+ * ws.subscribe('notifications:orders');
104
+ * ws.subscribe('notifications:payments');
105
+ * ws.subscribe(`user:${userId}:mentions`);
106
+ */
107
+ subscribe(topic: string): void;
108
+ /**
109
+ * Unsubscribe from a server-side topic.
110
+ * Sends topicUnsubscribe event (default: "$topic:unsubscribe").
111
+ */
112
+ unsubscribe(topic: string): void;
113
+ /**
114
+ * Subscribe to an event and show notifications.
115
+ *
116
+ * Two modes:
117
+ * - **render** — you control how to display (sonner, react-hot-toast, custom UI)
118
+ * - **browser Notification API** — native OS notifications (title/body/icon)
119
+ *
120
+ * Both modes respect leaderOnly + onlyWhenHidden to prevent duplicates.
121
+ *
122
+ * @example
123
+ * // Custom render — sonner toast
124
+ * import { toast } from 'sonner';
125
+ * ws.push('notification', {
126
+ * render: (data) => toast(data.title, { description: data.body }),
127
+ * });
128
+ *
129
+ * @example
130
+ * // Custom render — react-hot-toast
131
+ * import toast from 'react-hot-toast';
132
+ * ws.push('order.created', {
133
+ * render: (order) => toast.success(`New Order #${order.id} — $${order.total}`),
134
+ * });
135
+ *
136
+ * @example
137
+ * // Browser Notification API (no render — uses title/body/icon)
138
+ * ws.push('notification', {
139
+ * title: (data) => data.title,
140
+ * body: (data) => data.body,
141
+ * icon: '/icons/bell.png',
142
+ * });
143
+ *
144
+ * @example
145
+ * // Both — toast in UI + browser notification
146
+ * ws.push('order.created', {
147
+ * render: (order) => toast(`Order #${order.id}`),
148
+ * title: (order) => `New Order #${order.id}`,
149
+ * body: (order) => `$${order.total}`,
150
+ * });
151
+ */
152
+ push<T = unknown>(event: string, config: {
153
+ /** Custom render function — you decide how to display. Called for every matching event. */
154
+ render?: (data: T) => void;
155
+ /** Title for browser Notification API (ignored if only render is used). */
156
+ title?: string | ((data: T) => string);
157
+ /** Body for browser Notification API. */
158
+ body?: string | ((data: T) => string);
159
+ /** Icon URL for browser Notification. */
160
+ icon?: string;
161
+ /** Tag for browser Notification deduplication. */
162
+ tag?: string | ((data: T) => string);
163
+ /** Only trigger from leader tab (default: true). Prevents N duplicates for N tabs. */
164
+ leaderOnly?: boolean;
165
+ /** Only trigger when tab is hidden/background (default: true). */
166
+ onlyWhenHidden?: boolean;
167
+ /** Called when browser Notification is clicked. */
168
+ onClick?: (data: T) => void;
169
+ }): Unsubscribe;
98
170
  disconnect(): void;
99
171
  private createSocket;
100
172
  private handleBecomeLeader;
@@ -164,3 +164,37 @@ export declare function useSocketLifecycle(handlers: SocketLifecycleHandlers): v
164
164
  * useSocketCallback(`tenant:${tenantId}:notifications:alert`, showToast);
165
165
  */
166
166
  export declare function useChannel(name: string): import("..").Channel;
167
+ /**
168
+ * Subscribe to server-side topics. Auto-unsubscribes on unmount.
169
+ *
170
+ * @example
171
+ * useTopics(['notifications:orders', 'notifications:payments']);
172
+ * useTopics([`user:${userId}:mentions`]);
173
+ */
174
+ export declare function useTopics(topics: string[]): void;
175
+ /**
176
+ * Enable browser push notifications for an event. Auto-cleanup on unmount.
177
+ *
178
+ * @example
179
+ * usePush('notification', {
180
+ * title: (n) => n.title,
181
+ * body: (n) => n.body,
182
+ * icon: '/icon.png',
183
+ * });
184
+ *
185
+ * @example
186
+ * usePush('order.created', {
187
+ * title: (order) => `New Order #${order.id}`,
188
+ * body: (order) => `$${order.total}`,
189
+ * onClick: (order) => navigate(`/orders/${order.id}`),
190
+ * });
191
+ */
192
+ export declare function usePush<T = unknown>(event: string, config: {
193
+ title: string | ((data: T) => string);
194
+ body?: string | ((data: T) => string);
195
+ icon?: string;
196
+ tag?: string | ((data: T) => string);
197
+ leaderOnly?: boolean;
198
+ onlyWhenHidden?: boolean;
199
+ onClick?: (data: T) => void;
200
+ }): void;
@@ -123,3 +123,29 @@ export declare function useSocketLifecycle(handlers: SocketLifecycleHandlers): v
123
123
  * // Send via chat.send('message', { text: 'Hello' })
124
124
  */
125
125
  export declare function useChannel(name: string): import("..").Channel;
126
+ /**
127
+ * Subscribe to server-side topics. Auto-unsubscribes on unmount.
128
+ *
129
+ * @example
130
+ * useTopics(['notifications:orders', 'notifications:payments']);
131
+ */
132
+ export declare function useTopics(topics: string[]): void;
133
+ /**
134
+ * Enable browser push notifications for an event. Auto-cleanup on unmount.
135
+ *
136
+ * @example
137
+ * usePush('notification', {
138
+ * title: (n) => n.title,
139
+ * body: (n) => n.body,
140
+ * icon: '/icon.png',
141
+ * });
142
+ */
143
+ export declare function usePush<T = unknown>(event: string, config: {
144
+ title: string | ((data: T) => string);
145
+ body?: string | ((data: T) => string);
146
+ icon?: string;
147
+ tag?: string | ((data: T) => string);
148
+ leaderOnly?: boolean;
149
+ onlyWhenHidden?: boolean;
150
+ onClick?: (data: T) => void;
151
+ }): void;