@gwakko/shared-websocket 0.6.2 → 0.8.0
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 +432 -3
- package/dist/SharedWebSocket.d.ts +72 -0
- package/dist/adapters/react.d.ts +34 -0
- package/dist/adapters/vue.d.ts +26 -0
- package/dist/{chunk-UEOFAFLV.cjs → chunk-LGNDGW4C.cjs} +100 -2
- package/dist/chunk-LGNDGW4C.cjs.map +1 -0
- package/dist/{chunk-4D2ZDCA6.js → chunk-TYYPQ457.js} +100 -2
- package/dist/chunk-TYYPQ457.js.map +1 -0
- package/dist/index.cjs +3 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/react.cjs +19 -3
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +18 -2
- package/dist/react.js.map +1 -1
- package/dist/types.d.ts +15 -0
- package/dist/vue.cjs +17 -3
- package/dist/vue.cjs.map +1 -1
- package/dist/vue.js +16 -2
- package/dist/vue.js.map +1 -1
- package/package.json +7 -3
- package/src/SharedWebSocket.ts +146 -0
- package/src/adapters/react.ts +53 -0
- package/src/adapters/vue.ts +44 -0
- package/src/index.ts +1 -0
- package/src/types.ts +16 -0
- package/dist/chunk-4D2ZDCA6.js.map +0 -1
- package/dist/chunk-UEOFAFLV.cjs.map +0 -1
package/README.md
CHANGED
|
@@ -32,6 +32,9 @@ Share ONE WebSocket connection across multiple browser tabs. Zero dependencies.
|
|
|
32
32
|
- [Lifecycle Hooks](#lifecycle-hooks)
|
|
33
33
|
- [Private Channels](#private-channels--chat-rooms-tenant-notifications)
|
|
34
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)
|
|
35
38
|
- [Exported Types](#exported-types)
|
|
36
39
|
- [Browser Support](#browser-support)
|
|
37
40
|
- [License](#license)
|
|
@@ -1074,6 +1077,430 @@ wss.on('connection', (ws) => {
|
|
|
1074
1077
|
});
|
|
1075
1078
|
```
|
|
1076
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**.
|
|
1125
|
+
|
|
1126
|
+
`target` controls which tab(s) show the notification:
|
|
1127
|
+
|
|
1128
|
+
| Target | Behavior | Default for |
|
|
1129
|
+
|--------|----------|-------------|
|
|
1130
|
+
| `'active'` | Only the currently visible/focused tab | render (toasts) |
|
|
1131
|
+
| `'leader'` | Only the leader tab | browser Notification |
|
|
1132
|
+
| `'all'` | Every tab (critical alerts) | — |
|
|
1133
|
+
|
|
1134
|
+
### Custom Render — you control the display
|
|
1135
|
+
|
|
1136
|
+
```typescript
|
|
1137
|
+
// Vanilla — sonner toast (default: target 'active' — visible tab only)
|
|
1138
|
+
import { toast } from 'sonner';
|
|
1139
|
+
|
|
1140
|
+
ws.push('notification', {
|
|
1141
|
+
render: (n) => toast(n.title, { description: n.body }),
|
|
1142
|
+
// target: 'active' — implicit default
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
ws.push('order.created', {
|
|
1146
|
+
render: (order) => toast.success(`New Order #${order.id}`, {
|
|
1147
|
+
description: `$${order.total} from ${order.customer}`,
|
|
1148
|
+
action: { label: 'View', onClick: () => navigate(`/orders/${order.id}`) },
|
|
1149
|
+
}),
|
|
1150
|
+
});
|
|
1151
|
+
```
|
|
1152
|
+
|
|
1153
|
+
```tsx
|
|
1154
|
+
// React — sonner
|
|
1155
|
+
import { toast } from 'sonner';
|
|
1156
|
+
|
|
1157
|
+
function NotificationSetup() {
|
|
1158
|
+
usePush('notification', {
|
|
1159
|
+
render: (n) => toast(n.title, { description: n.body }),
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
usePush('order.created', {
|
|
1163
|
+
render: (order) => toast.success(`Order #${order.id} — $${order.total}`),
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
return null;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// React — react-hot-toast
|
|
1170
|
+
import hotToast from 'react-hot-toast';
|
|
1171
|
+
|
|
1172
|
+
function NotificationSetup() {
|
|
1173
|
+
usePush('notification', {
|
|
1174
|
+
render: (n) => hotToast(n.title),
|
|
1175
|
+
});
|
|
1176
|
+
return null;
|
|
1177
|
+
}
|
|
1178
|
+
```
|
|
1179
|
+
|
|
1180
|
+
```vue
|
|
1181
|
+
<!-- Vue — sonner-vue -->
|
|
1182
|
+
<script setup>
|
|
1183
|
+
import { toast } from 'sonner-vue';
|
|
1184
|
+
|
|
1185
|
+
usePush('notification', {
|
|
1186
|
+
render: (n) => toast(n.title, { description: n.body }),
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1189
|
+
usePush('order.created', {
|
|
1190
|
+
render: (order) => toast.success(`Order #${order.id} — $${order.total}`),
|
|
1191
|
+
});
|
|
1192
|
+
</script>
|
|
1193
|
+
```
|
|
1194
|
+
|
|
1195
|
+
### Browser Notification API — native OS notifications
|
|
1196
|
+
|
|
1197
|
+
```typescript
|
|
1198
|
+
// Vanilla — browser native (default: target 'leader' — one notification, not N)
|
|
1199
|
+
ws.push('notification', {
|
|
1200
|
+
title: (n) => n.title,
|
|
1201
|
+
body: (n) => n.body,
|
|
1202
|
+
icon: '/icons/bell.png',
|
|
1203
|
+
tag: (n) => `notif-${n.id}`, // deduplication
|
|
1204
|
+
onClick: (n) => window.open(n.url),
|
|
1205
|
+
// target: 'leader' — implicit default for native notifications
|
|
1206
|
+
});
|
|
1207
|
+
```
|
|
1208
|
+
|
|
1209
|
+
```tsx
|
|
1210
|
+
// React
|
|
1211
|
+
usePush('order.created', {
|
|
1212
|
+
title: (order) => `New Order #${order.id}`,
|
|
1213
|
+
body: (order) => `$${order.total}`,
|
|
1214
|
+
icon: '/icons/order.png',
|
|
1215
|
+
onClick: (order) => navigate(`/orders/${order.id}`),
|
|
1216
|
+
});
|
|
1217
|
+
```
|
|
1218
|
+
|
|
1219
|
+
```vue
|
|
1220
|
+
<!-- Vue -->
|
|
1221
|
+
<script setup>
|
|
1222
|
+
usePush('order.created', {
|
|
1223
|
+
title: (order) => `New Order #${order.id}`,
|
|
1224
|
+
body: (order) => `$${order.total}`,
|
|
1225
|
+
});
|
|
1226
|
+
</script>
|
|
1227
|
+
```
|
|
1228
|
+
|
|
1229
|
+
### Critical alerts — show in ALL tabs
|
|
1230
|
+
|
|
1231
|
+
```typescript
|
|
1232
|
+
// Vanilla — payment failed: show toast in EVERY tab
|
|
1233
|
+
ws.push('payment.failed', {
|
|
1234
|
+
render: (err) => toast.error(`Payment failed: ${err.message}`),
|
|
1235
|
+
target: 'all', // every tab sees it
|
|
1236
|
+
});
|
|
1237
|
+
```
|
|
1238
|
+
|
|
1239
|
+
```tsx
|
|
1240
|
+
// React
|
|
1241
|
+
usePush('payment.failed', {
|
|
1242
|
+
render: (err) => toast.error(`Payment failed: ${err.message}`),
|
|
1243
|
+
target: 'all',
|
|
1244
|
+
});
|
|
1245
|
+
```
|
|
1246
|
+
|
|
1247
|
+
```vue
|
|
1248
|
+
<!-- Vue -->
|
|
1249
|
+
<script setup>
|
|
1250
|
+
usePush('payment.failed', {
|
|
1251
|
+
render: (err) => toast.error(`Payment failed: ${err.message}`),
|
|
1252
|
+
target: 'all',
|
|
1253
|
+
});
|
|
1254
|
+
</script>
|
|
1255
|
+
```
|
|
1256
|
+
|
|
1257
|
+
### Both — toast in UI + browser notification
|
|
1258
|
+
|
|
1259
|
+
```typescript
|
|
1260
|
+
// Active tab gets sonner toast, leader sends native notification
|
|
1261
|
+
ws.push('order.created', {
|
|
1262
|
+
render: (order) => toast.success(`Order #${order.id}`), // in-app toast
|
|
1263
|
+
title: (order) => `New Order #${order.id}`, // + native notification
|
|
1264
|
+
body: (order) => `$${order.total}`,
|
|
1265
|
+
});
|
|
1266
|
+
```
|
|
1267
|
+
|
|
1268
|
+
## Server-Side Implementation Guide
|
|
1269
|
+
|
|
1270
|
+
Complete server reference — what events to listen for and how to respond.
|
|
1271
|
+
|
|
1272
|
+
### Message Format
|
|
1273
|
+
|
|
1274
|
+
All messages are JSON with two fields (configurable via `events` option):
|
|
1275
|
+
|
|
1276
|
+
```
|
|
1277
|
+
Client → Server: { "event": "event.name", "data": { ... } }
|
|
1278
|
+
Server → Client: { "event": "event.name", "data": { ... } }
|
|
1279
|
+
```
|
|
1280
|
+
|
|
1281
|
+
### System Events (sent by client automatically)
|
|
1282
|
+
|
|
1283
|
+
| Event | When | Payload | Your Server Should |
|
|
1284
|
+
|-------|------|---------|-------------------|
|
|
1285
|
+
| `ping` | Every 30s (heartbeat) | `{ "type": "ping" }` | Respond with `{ "type": "pong" }` or ignore |
|
|
1286
|
+
| `$channel:join` | `ws.channel('name')` | `{ "channel": "chat:room_1" }` | Track which channels this connection belongs to |
|
|
1287
|
+
| `$channel:leave` | `channel.leave()` | `{ "channel": "chat:room_1" }` | Remove connection from channel |
|
|
1288
|
+
| `$topic:subscribe` | `ws.subscribe('topic')` | `{ "topic": "notifications:orders" }` | Start sending events for this topic to this connection |
|
|
1289
|
+
| `$topic:unsubscribe` | `ws.unsubscribe('topic')` | `{ "topic": "notifications:orders" }` | Stop sending events for this topic |
|
|
1290
|
+
|
|
1291
|
+
### Node.js (ws) — Complete Server Example
|
|
1292
|
+
|
|
1293
|
+
```typescript
|
|
1294
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
1295
|
+
|
|
1296
|
+
const wss = new WebSocketServer({ port: 8080 });
|
|
1297
|
+
|
|
1298
|
+
// Track per-connection state
|
|
1299
|
+
interface ClientState {
|
|
1300
|
+
userId?: string;
|
|
1301
|
+
channels: Set<string>;
|
|
1302
|
+
topics: Set<string>;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
const clients = new Map<WebSocket, ClientState>();
|
|
1306
|
+
|
|
1307
|
+
wss.on('connection', (ws, req) => {
|
|
1308
|
+
// Extract auth token from URL
|
|
1309
|
+
const url = new URL(req.url!, `http://${req.headers.host}`);
|
|
1310
|
+
const token = url.searchParams.get('token');
|
|
1311
|
+
const userId = verifyToken(token); // your auth logic
|
|
1312
|
+
|
|
1313
|
+
const state: ClientState = {
|
|
1314
|
+
userId,
|
|
1315
|
+
channels: new Set(),
|
|
1316
|
+
topics: new Set(),
|
|
1317
|
+
};
|
|
1318
|
+
clients.set(ws, state);
|
|
1319
|
+
|
|
1320
|
+
// Send welcome
|
|
1321
|
+
send(ws, 'welcome', { userId, timestamp: Date.now() });
|
|
1322
|
+
|
|
1323
|
+
ws.on('message', (raw) => {
|
|
1324
|
+
const msg = JSON.parse(raw.toString());
|
|
1325
|
+
const { event, data } = msg;
|
|
1326
|
+
|
|
1327
|
+
switch (event) {
|
|
1328
|
+
// ─── System Events ───────────────────────
|
|
1329
|
+
|
|
1330
|
+
case 'ping':
|
|
1331
|
+
// Respond to heartbeat (optional — some servers ignore pings)
|
|
1332
|
+
ws.send(JSON.stringify({ type: 'pong' }));
|
|
1333
|
+
break;
|
|
1334
|
+
|
|
1335
|
+
// ─── Channel Events ──────────────────────
|
|
1336
|
+
|
|
1337
|
+
case '$channel:join':
|
|
1338
|
+
state.channels.add(data.channel);
|
|
1339
|
+
console.log(`${userId} joined ${data.channel}`);
|
|
1340
|
+
// Send channel history, presence, etc.
|
|
1341
|
+
break;
|
|
1342
|
+
|
|
1343
|
+
case '$channel:leave':
|
|
1344
|
+
state.channels.delete(data.channel);
|
|
1345
|
+
console.log(`${userId} left ${data.channel}`);
|
|
1346
|
+
break;
|
|
1347
|
+
|
|
1348
|
+
// ─── Topic Events ────────────────────────
|
|
1349
|
+
|
|
1350
|
+
case '$topic:subscribe':
|
|
1351
|
+
state.topics.add(data.topic);
|
|
1352
|
+
console.log(`${userId} subscribed to ${data.topic}`);
|
|
1353
|
+
break;
|
|
1354
|
+
|
|
1355
|
+
case '$topic:unsubscribe':
|
|
1356
|
+
state.topics.delete(data.topic);
|
|
1357
|
+
break;
|
|
1358
|
+
|
|
1359
|
+
// ─── App Events ──────────────────────────
|
|
1360
|
+
|
|
1361
|
+
case 'chat.send':
|
|
1362
|
+
// Broadcast to all clients in the same channel
|
|
1363
|
+
const channel = data.roomId ? `chat:${data.roomId}` : null;
|
|
1364
|
+
broadcastToChannel(channel, 'chat.message', {
|
|
1365
|
+
id: crypto.randomUUID(),
|
|
1366
|
+
userId: state.userId,
|
|
1367
|
+
text: data.text,
|
|
1368
|
+
timestamp: Date.now(),
|
|
1369
|
+
});
|
|
1370
|
+
break;
|
|
1371
|
+
|
|
1372
|
+
case 'chat.typing':
|
|
1373
|
+
broadcastToChannel(`chat:${data.roomId}`, 'chat.typing', {
|
|
1374
|
+
userId: state.userId,
|
|
1375
|
+
}, ws); // exclude sender
|
|
1376
|
+
break;
|
|
1377
|
+
|
|
1378
|
+
default:
|
|
1379
|
+
console.log('Unknown event:', event, data);
|
|
1380
|
+
}
|
|
1381
|
+
});
|
|
1382
|
+
|
|
1383
|
+
ws.on('close', () => {
|
|
1384
|
+
clients.delete(ws);
|
|
1385
|
+
});
|
|
1386
|
+
});
|
|
1387
|
+
|
|
1388
|
+
// ─── Helpers ─────────────────────────────────────
|
|
1389
|
+
|
|
1390
|
+
function send(ws: WebSocket, event: string, data: unknown) {
|
|
1391
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
1392
|
+
ws.send(JSON.stringify({ event, data }));
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
function broadcastToChannel(
|
|
1397
|
+
channel: string | null,
|
|
1398
|
+
event: string,
|
|
1399
|
+
data: unknown,
|
|
1400
|
+
exclude?: WebSocket,
|
|
1401
|
+
) {
|
|
1402
|
+
for (const [ws, state] of clients) {
|
|
1403
|
+
if (ws === exclude) continue;
|
|
1404
|
+
if (channel && !state.channels.has(channel)) continue;
|
|
1405
|
+
send(ws, event, data);
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
function broadcastToTopic(topic: string, event: string, data: unknown) {
|
|
1410
|
+
for (const [ws, state] of clients) {
|
|
1411
|
+
if (!state.topics.has(topic)) continue;
|
|
1412
|
+
send(ws, `${topic}:${event}`, data);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// ─── Example: Send notifications by topic ────────
|
|
1417
|
+
|
|
1418
|
+
function notifyNewOrder(order: Order) {
|
|
1419
|
+
broadcastToTopic('notifications:orders', 'new', {
|
|
1420
|
+
id: order.id,
|
|
1421
|
+
total: order.total,
|
|
1422
|
+
customer: order.customerName,
|
|
1423
|
+
});
|
|
1424
|
+
// Only clients who called ws.subscribe('notifications:orders') receive this
|
|
1425
|
+
}
|
|
1426
|
+
```
|
|
1427
|
+
|
|
1428
|
+
### Go — Server Example
|
|
1429
|
+
|
|
1430
|
+
```go
|
|
1431
|
+
// Message format
|
|
1432
|
+
type Message struct {
|
|
1433
|
+
Event string `json:"event"`
|
|
1434
|
+
Data json.RawMessage `json:"data"`
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
// Handle incoming messages
|
|
1438
|
+
func handleMessage(conn *websocket.Conn, state *ClientState, msg Message) {
|
|
1439
|
+
switch msg.Event {
|
|
1440
|
+
case "$channel:join":
|
|
1441
|
+
var payload struct{ Channel string `json:"channel"` }
|
|
1442
|
+
json.Unmarshal(msg.Data, &payload)
|
|
1443
|
+
state.Channels[payload.Channel] = true
|
|
1444
|
+
|
|
1445
|
+
case "$channel:leave":
|
|
1446
|
+
var payload struct{ Channel string `json:"channel"` }
|
|
1447
|
+
json.Unmarshal(msg.Data, &payload)
|
|
1448
|
+
delete(state.Channels, payload.Channel)
|
|
1449
|
+
|
|
1450
|
+
case "$topic:subscribe":
|
|
1451
|
+
var payload struct{ Topic string `json:"topic"` }
|
|
1452
|
+
json.Unmarshal(msg.Data, &payload)
|
|
1453
|
+
state.Topics[payload.Topic] = true
|
|
1454
|
+
|
|
1455
|
+
case "$topic:unsubscribe":
|
|
1456
|
+
var payload struct{ Topic string `json:"topic"` }
|
|
1457
|
+
json.Unmarshal(msg.Data, &payload)
|
|
1458
|
+
delete(state.Topics, payload.Topic)
|
|
1459
|
+
|
|
1460
|
+
case "chat.send":
|
|
1461
|
+
// broadcast to channel...
|
|
1462
|
+
|
|
1463
|
+
case "ping":
|
|
1464
|
+
conn.WriteJSON(Message{Event: "pong"})
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
```
|
|
1468
|
+
|
|
1469
|
+
### PHP (Laravel + Ratchet/Swoole) — Server Example
|
|
1470
|
+
|
|
1471
|
+
```php
|
|
1472
|
+
// Handle incoming WebSocket message
|
|
1473
|
+
public function onMessage(ConnectionInterface $conn, $msg): void
|
|
1474
|
+
{
|
|
1475
|
+
$data = json_decode($msg, true);
|
|
1476
|
+
$event = $data['event'] ?? 'message';
|
|
1477
|
+
$payload = $data['data'] ?? [];
|
|
1478
|
+
|
|
1479
|
+
match ($event) {
|
|
1480
|
+
'$channel:join' => $this->joinChannel($conn, $payload['channel']),
|
|
1481
|
+
'$channel:leave' => $this->leaveChannel($conn, $payload['channel']),
|
|
1482
|
+
'$topic:subscribe' => $this->subscribeTopic($conn, $payload['topic']),
|
|
1483
|
+
'$topic:unsubscribe' => $this->unsubscribeTopic($conn, $payload['topic']),
|
|
1484
|
+
'chat.send' => $this->handleChatMessage($conn, $payload),
|
|
1485
|
+
'ping' => $conn->send(json_encode(['type' => 'pong'])),
|
|
1486
|
+
default => logger()->warning("Unknown event: {$event}"),
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
// Send to topic subscribers
|
|
1491
|
+
public function notifyTopic(string $topic, string $event, array $data): void
|
|
1492
|
+
{
|
|
1493
|
+
foreach ($this->connections as $conn) {
|
|
1494
|
+
if (in_array($topic, $this->topics[$conn->resourceId] ?? [])) {
|
|
1495
|
+
$conn->send(json_encode([
|
|
1496
|
+
'event' => "{$topic}:{$event}",
|
|
1497
|
+
'data' => $data,
|
|
1498
|
+
]));
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
```
|
|
1503
|
+
|
|
1077
1504
|
## Exported Types
|
|
1078
1505
|
|
|
1079
1506
|
All types are available for import in your projects:
|
|
@@ -1116,9 +1543,9 @@ import {
|
|
|
1116
1543
|
useSocketStatus,
|
|
1117
1544
|
useSocketLifecycle,
|
|
1118
1545
|
useChannel,
|
|
1546
|
+
useTopics,
|
|
1547
|
+
usePush,
|
|
1119
1548
|
} from '@gwakko/shared-websocket/react';
|
|
1120
|
-
|
|
1121
|
-
import type { SharedWebSocketProviderProps } from '@gwakko/shared-websocket/react';
|
|
1122
1549
|
```
|
|
1123
1550
|
|
|
1124
1551
|
```typescript
|
|
@@ -1133,7 +1560,9 @@ import {
|
|
|
1133
1560
|
useSocketStatus,
|
|
1134
1561
|
useSocketLifecycle,
|
|
1135
1562
|
useChannel,
|
|
1136
|
-
|
|
1563
|
+
useTopics,
|
|
1564
|
+
usePush,
|
|
1565
|
+
SharedWebSocketKey,
|
|
1137
1566
|
} from '@gwakko/shared-websocket/vue';
|
|
1138
1567
|
```
|
|
1139
1568
|
|
|
@@ -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
|
+
* **target** controls which tab(s) display the notification:
|
|
117
|
+
* - `'active'` — only the currently visible tab (default for render)
|
|
118
|
+
* - `'leader'` — only the leader tab (default for browser Notification)
|
|
119
|
+
* - `'all'` — every tab (for critical alerts)
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* // Custom render — sonner toast on active tab only
|
|
123
|
+
* ws.push('notification', {
|
|
124
|
+
* render: (n) => toast(n.title),
|
|
125
|
+
* target: 'active', // default for render
|
|
126
|
+
* });
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* // Critical alert — show in ALL tabs
|
|
130
|
+
* ws.push('payment.failed', {
|
|
131
|
+
* render: (n) => toast.error('Payment failed!'),
|
|
132
|
+
* target: 'all',
|
|
133
|
+
* });
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* // Browser Notification — only from leader
|
|
137
|
+
* ws.push('order.created', {
|
|
138
|
+
* title: (order) => `New Order #${order.id}`,
|
|
139
|
+
* target: 'leader', // default for browser Notification
|
|
140
|
+
* });
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* // Both render + native with different targets
|
|
144
|
+
* ws.push('order.created', {
|
|
145
|
+
* render: (order) => toast(`Order #${order.id}`), // active tab
|
|
146
|
+
* title: (order) => `New Order #${order.id}`, // leader → native
|
|
147
|
+
* });
|
|
148
|
+
*/
|
|
149
|
+
push<T = unknown>(event: string, config: {
|
|
150
|
+
/** Custom render function — you decide how to display. */
|
|
151
|
+
render?: (data: T) => void;
|
|
152
|
+
/** Title for browser Notification API. */
|
|
153
|
+
title?: string | ((data: T) => string);
|
|
154
|
+
/** Body for browser Notification API. */
|
|
155
|
+
body?: string | ((data: T) => string);
|
|
156
|
+
/** Icon URL for browser Notification. */
|
|
157
|
+
icon?: string;
|
|
158
|
+
/** Tag for browser Notification deduplication. */
|
|
159
|
+
tag?: string | ((data: T) => string);
|
|
160
|
+
/**
|
|
161
|
+
* Which tab(s) show the notification:
|
|
162
|
+
* - `'active'` — only the visible/focused tab (default for render)
|
|
163
|
+
* - `'leader'` — only the leader tab (default for browser Notification)
|
|
164
|
+
* - `'all'` — every tab (critical alerts)
|
|
165
|
+
*/
|
|
166
|
+
target?: 'active' | 'leader' | 'all';
|
|
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;
|
package/dist/adapters/react.d.ts
CHANGED
|
@@ -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;
|
package/dist/adapters/vue.d.ts
CHANGED
|
@@ -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;
|