@veloxts/events 0.6.86 → 0.6.88
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/GUIDE.md +337 -86
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/plugin.d.ts +33 -0
- package/dist/plugin.js +37 -11
- package/dist/schemas.d.ts +14 -14
- package/package.json +6 -5
package/GUIDE.md
CHANGED
|
@@ -1,81 +1,197 @@
|
|
|
1
1
|
# @veloxts/events Guide
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Real-time event broadcasting for VeloxTS applications using WebSocket or Server-Sent Events (SSE).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @veloxts/events
|
|
9
|
+
```
|
|
4
10
|
|
|
5
|
-
|
|
11
|
+
## Quick Start
|
|
6
12
|
|
|
7
|
-
|
|
13
|
+
### Single Instance (Development)
|
|
8
14
|
|
|
9
15
|
```typescript
|
|
16
|
+
import { veloxApp } from '@veloxts/core';
|
|
10
17
|
import { eventsPlugin } from '@veloxts/events';
|
|
11
18
|
|
|
12
|
-
|
|
13
|
-
|
|
19
|
+
const app = veloxApp();
|
|
20
|
+
|
|
21
|
+
app.register(eventsPlugin({
|
|
14
22
|
driver: 'ws',
|
|
15
|
-
|
|
16
|
-
path: '/ws',
|
|
17
|
-
pingInterval: 30000,
|
|
18
|
-
},
|
|
23
|
+
path: '/ws',
|
|
19
24
|
}));
|
|
20
25
|
|
|
21
|
-
|
|
22
|
-
|
|
26
|
+
await app.start();
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Multiple Instances (Production with Redis)
|
|
30
|
+
|
|
31
|
+
For horizontal scaling across multiple server instances, enable Redis pub/sub:
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { veloxApp } from '@veloxts/core';
|
|
35
|
+
import { eventsPlugin } from '@veloxts/events';
|
|
36
|
+
|
|
37
|
+
const app = veloxApp();
|
|
38
|
+
|
|
39
|
+
app.register(eventsPlugin({
|
|
23
40
|
driver: 'ws',
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
41
|
+
path: '/ws',
|
|
42
|
+
redis: process.env.REDIS_URL,
|
|
43
|
+
authSecret: process.env.EVENTS_SECRET,
|
|
44
|
+
authorizer: async (channel, request) => {
|
|
45
|
+
if (channel.type === 'public') {
|
|
46
|
+
return { authorized: true };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const user = request.user;
|
|
50
|
+
if (!user) {
|
|
51
|
+
return { authorized: false, error: 'Authentication required' };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (channel.type === 'presence') {
|
|
55
|
+
return {
|
|
56
|
+
authorized: true,
|
|
57
|
+
member: { id: user.id, info: { name: user.name } },
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { authorized: true };
|
|
27
62
|
},
|
|
28
63
|
}));
|
|
64
|
+
|
|
65
|
+
await app.start();
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Environment Variables:**
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# .env
|
|
72
|
+
REDIS_URL=redis://localhost:6379
|
|
73
|
+
EVENTS_SECRET=your-32-char-secret-for-signing-tokens
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Drivers
|
|
77
|
+
|
|
78
|
+
### WebSocket Driver (Recommended)
|
|
79
|
+
|
|
80
|
+
Real-time bidirectional communication. Supports Redis pub/sub for horizontal scaling.
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
app.register(eventsPlugin({
|
|
84
|
+
driver: 'ws',
|
|
85
|
+
path: '/ws',
|
|
86
|
+
redis: process.env.REDIS_URL, // Optional: for horizontal scaling
|
|
87
|
+
authSecret: process.env.EVENTS_SECRET, // Required for private/presence channels
|
|
88
|
+
pingInterval: 30000, // Keep-alive interval (default: 30s)
|
|
89
|
+
maxPayloadSize: 1048576, // Max message size (default: 1MB)
|
|
90
|
+
}));
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### SSE Driver (Fallback)
|
|
94
|
+
|
|
95
|
+
Unidirectional server-to-client streaming. Use when WebSocket isn't available.
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
app.register(eventsPlugin({
|
|
99
|
+
driver: 'sse',
|
|
100
|
+
path: '/events',
|
|
101
|
+
heartbeatInterval: 15000, // Keep-alive interval (default: 15s)
|
|
102
|
+
retryInterval: 3000, // Client reconnect delay (default: 3s)
|
|
103
|
+
}));
|
|
29
104
|
```
|
|
30
105
|
|
|
31
106
|
## Broadcasting Events
|
|
32
107
|
|
|
108
|
+
### In Procedures
|
|
109
|
+
|
|
33
110
|
```typescript
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
111
|
+
import { procedure, procedures } from '@veloxts/router';
|
|
112
|
+
import { z } from 'zod';
|
|
113
|
+
|
|
114
|
+
export const orderProcedures = procedures('orders', {
|
|
115
|
+
createOrder: procedure()
|
|
116
|
+
.input(z.object({ productId: z.string(), quantity: z.number() }))
|
|
117
|
+
.mutation(async ({ input, ctx }) => {
|
|
118
|
+
const order = await ctx.db.order.create({ data: input });
|
|
119
|
+
|
|
120
|
+
// Broadcast to public channel
|
|
121
|
+
await ctx.events.broadcast('orders', 'order.created', {
|
|
122
|
+
orderId: order.id,
|
|
123
|
+
total: order.total,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Broadcast to user's private channel
|
|
127
|
+
await ctx.events.broadcast(
|
|
128
|
+
`private-user.${ctx.user.id}`,
|
|
129
|
+
'order.confirmed',
|
|
130
|
+
order
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
return order;
|
|
134
|
+
}),
|
|
39
135
|
});
|
|
136
|
+
```
|
|
40
137
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
138
|
+
### Broadcasting Methods
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
// Basic broadcast
|
|
142
|
+
await ctx.events.broadcast('channel', 'event-name', { data: 'value' });
|
|
143
|
+
|
|
144
|
+
// Broadcast to multiple channels
|
|
145
|
+
await ctx.events.broadcastToMany(
|
|
146
|
+
['user.1', 'user.2', 'user.3'],
|
|
147
|
+
'notification',
|
|
148
|
+
{ message: 'System maintenance scheduled' }
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
// Broadcast to all except sender (e.g., chat messages)
|
|
152
|
+
await ctx.events.broadcastExcept(
|
|
153
|
+
'chat.room-1',
|
|
154
|
+
'message.sent',
|
|
155
|
+
{ text: 'Hello!' },
|
|
156
|
+
senderSocketId
|
|
157
|
+
);
|
|
48
158
|
```
|
|
49
159
|
|
|
50
160
|
## Channel Types
|
|
51
161
|
|
|
52
162
|
### Public Channels
|
|
53
163
|
|
|
54
|
-
Anyone can subscribe.
|
|
164
|
+
Anyone can subscribe. No prefix required.
|
|
55
165
|
|
|
56
166
|
```typescript
|
|
57
167
|
// Server
|
|
58
|
-
await ctx.broadcast({
|
|
59
|
-
|
|
60
|
-
event: 'new-feature',
|
|
61
|
-
data: { title: 'New Feature!' },
|
|
168
|
+
await ctx.events.broadcast('announcements', 'new-feature', {
|
|
169
|
+
title: 'Dark Mode Released!',
|
|
62
170
|
});
|
|
63
171
|
|
|
64
172
|
// Client
|
|
65
|
-
socket.
|
|
173
|
+
socket.send(JSON.stringify({
|
|
174
|
+
type: 'subscribe',
|
|
175
|
+
channel: 'announcements',
|
|
176
|
+
}));
|
|
66
177
|
```
|
|
67
178
|
|
|
68
179
|
### Private Channels
|
|
69
180
|
|
|
70
|
-
Require
|
|
181
|
+
Require authentication. Prefix with `private-`.
|
|
71
182
|
|
|
72
183
|
```typescript
|
|
73
|
-
// Server -
|
|
74
|
-
await ctx.broadcast({
|
|
75
|
-
|
|
76
|
-
event: 'notification',
|
|
77
|
-
data: { message: 'You have a new message' },
|
|
184
|
+
// Server - only authorized users receive
|
|
185
|
+
await ctx.events.broadcast('private-user.123', 'notification', {
|
|
186
|
+
message: 'You have a new message',
|
|
78
187
|
});
|
|
188
|
+
|
|
189
|
+
// Client - subscription requires auth token
|
|
190
|
+
socket.send(JSON.stringify({
|
|
191
|
+
type: 'subscribe',
|
|
192
|
+
channel: 'private-user.123',
|
|
193
|
+
auth: authToken, // Obtained from /ws/auth endpoint
|
|
194
|
+
}));
|
|
79
195
|
```
|
|
80
196
|
|
|
81
197
|
### Presence Channels
|
|
@@ -84,64 +200,158 @@ Track who's online. Prefix with `presence-`.
|
|
|
84
200
|
|
|
85
201
|
```typescript
|
|
86
202
|
// Server
|
|
87
|
-
await ctx.broadcast({
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
data: { userId: '123' },
|
|
203
|
+
await ctx.events.broadcast('presence-chat.room-1', 'typing', {
|
|
204
|
+
userId: '123',
|
|
205
|
+
userName: 'Alice',
|
|
91
206
|
});
|
|
92
207
|
|
|
93
|
-
//
|
|
208
|
+
// Get who's online
|
|
209
|
+
const members = await ctx.events.presenceMembers('presence-chat.room-1');
|
|
210
|
+
// [{ id: '123', info: { name: 'Alice' } }, { id: '456', info: { name: 'Bob' } }]
|
|
211
|
+
|
|
212
|
+
// Client - receives member_added/member_removed automatically
|
|
213
|
+
socket.send(JSON.stringify({
|
|
214
|
+
type: 'subscribe',
|
|
215
|
+
channel: 'presence-chat.room-1',
|
|
216
|
+
data: { id: 'user-123', name: 'Alice' },
|
|
217
|
+
auth: authToken,
|
|
218
|
+
}));
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Channel Authorization
|
|
222
|
+
|
|
223
|
+
Configure the `authorizer` callback to control channel access:
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
app.register(eventsPlugin({
|
|
227
|
+
driver: 'ws',
|
|
228
|
+
path: '/ws',
|
|
229
|
+
authSecret: process.env.EVENTS_SECRET,
|
|
230
|
+
authorizer: async (channel, request) => {
|
|
231
|
+
// Public channels - allow all
|
|
232
|
+
if (channel.type === 'public') {
|
|
233
|
+
return { authorized: true };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Get user from request (set by @veloxts/auth)
|
|
237
|
+
const user = request.user;
|
|
238
|
+
if (!user) {
|
|
239
|
+
return { authorized: false, error: 'Not authenticated' };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Private user channels - only owner can subscribe
|
|
243
|
+
if (channel.name.startsWith('private-user.')) {
|
|
244
|
+
const channelUserId = channel.name.replace('private-user.', '');
|
|
245
|
+
if (channelUserId !== user.id) {
|
|
246
|
+
return { authorized: false, error: 'Access denied' };
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Presence channels - include member info
|
|
251
|
+
if (channel.type === 'presence') {
|
|
252
|
+
return {
|
|
253
|
+
authorized: true,
|
|
254
|
+
member: {
|
|
255
|
+
id: user.id,
|
|
256
|
+
info: { name: user.name, avatar: user.avatar },
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return { authorized: true };
|
|
262
|
+
},
|
|
263
|
+
}));
|
|
94
264
|
```
|
|
95
265
|
|
|
96
266
|
## Client Integration
|
|
97
267
|
|
|
98
|
-
###
|
|
268
|
+
### WebSocket Client (Browser)
|
|
99
269
|
|
|
100
270
|
```typescript
|
|
101
271
|
const socket = new WebSocket('ws://localhost:3030/ws');
|
|
102
272
|
|
|
273
|
+
// Connection established
|
|
103
274
|
socket.onopen = () => {
|
|
104
|
-
|
|
275
|
+
console.log('Connected');
|
|
276
|
+
|
|
277
|
+
// Subscribe to public channel
|
|
105
278
|
socket.send(JSON.stringify({
|
|
106
279
|
type: 'subscribe',
|
|
107
|
-
channel: 'orders
|
|
280
|
+
channel: 'orders',
|
|
108
281
|
}));
|
|
109
282
|
};
|
|
110
283
|
|
|
284
|
+
// Receive messages
|
|
111
285
|
socket.onmessage = (event) => {
|
|
112
286
|
const message = JSON.parse(event.data);
|
|
113
287
|
|
|
114
|
-
|
|
115
|
-
|
|
288
|
+
switch (message.type) {
|
|
289
|
+
case 'event':
|
|
290
|
+
console.log(`${message.event}:`, message.data);
|
|
291
|
+
break;
|
|
292
|
+
case 'subscription_succeeded':
|
|
293
|
+
console.log(`Subscribed to ${message.channel}`);
|
|
294
|
+
break;
|
|
295
|
+
case 'subscription_error':
|
|
296
|
+
console.error(`Failed to subscribe: ${message.error}`);
|
|
297
|
+
break;
|
|
116
298
|
}
|
|
117
299
|
};
|
|
118
300
|
|
|
119
301
|
// Unsubscribe
|
|
120
302
|
socket.send(JSON.stringify({
|
|
121
303
|
type: 'unsubscribe',
|
|
122
|
-
channel: 'orders
|
|
304
|
+
channel: 'orders',
|
|
123
305
|
}));
|
|
306
|
+
|
|
307
|
+
// Handle reconnection
|
|
308
|
+
socket.onclose = () => {
|
|
309
|
+
console.log('Disconnected, reconnecting...');
|
|
310
|
+
setTimeout(() => reconnect(), 3000);
|
|
311
|
+
};
|
|
124
312
|
```
|
|
125
313
|
|
|
126
|
-
###
|
|
314
|
+
### Private Channel Authentication (Browser)
|
|
127
315
|
|
|
128
316
|
```typescript
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
317
|
+
async function subscribeToPrivateChannel(socket, channel) {
|
|
318
|
+
// Get auth token from server
|
|
319
|
+
const response = await fetch('/ws/auth', {
|
|
320
|
+
method: 'POST',
|
|
321
|
+
headers: { 'Content-Type': 'application/json' },
|
|
322
|
+
credentials: 'include', // Send session cookie
|
|
323
|
+
body: JSON.stringify({
|
|
324
|
+
socketId: socket.socketId,
|
|
325
|
+
channel,
|
|
326
|
+
}),
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
const { auth, channel_data } = await response.json();
|
|
330
|
+
|
|
331
|
+
// Subscribe with auth
|
|
332
|
+
socket.send(JSON.stringify({
|
|
333
|
+
type: 'subscribe',
|
|
334
|
+
channel,
|
|
335
|
+
auth,
|
|
336
|
+
channel_data,
|
|
337
|
+
}));
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### Presence Channel Events
|
|
135
342
|
|
|
136
|
-
|
|
343
|
+
```typescript
|
|
137
344
|
socket.onmessage = (event) => {
|
|
138
345
|
const message = JSON.parse(event.data);
|
|
139
346
|
|
|
140
347
|
if (message.event === 'member_added') {
|
|
141
348
|
console.log('User joined:', message.data);
|
|
349
|
+
// { id: '123', info: { name: 'Alice' } }
|
|
142
350
|
}
|
|
351
|
+
|
|
143
352
|
if (message.event === 'member_removed') {
|
|
144
353
|
console.log('User left:', message.data);
|
|
354
|
+
// { id: '123' }
|
|
145
355
|
}
|
|
146
356
|
};
|
|
147
357
|
```
|
|
@@ -149,51 +359,92 @@ socket.onmessage = (event) => {
|
|
|
149
359
|
## Server API
|
|
150
360
|
|
|
151
361
|
```typescript
|
|
152
|
-
// Get
|
|
153
|
-
const
|
|
362
|
+
// Get subscriber count
|
|
363
|
+
const count = await ctx.events.subscriberCount('orders');
|
|
364
|
+
|
|
365
|
+
// Check if channel has subscribers
|
|
366
|
+
const hasSubscribers = await ctx.events.hasSubscribers('orders');
|
|
367
|
+
|
|
368
|
+
// Get all active channels
|
|
369
|
+
const channels = await ctx.events.channels();
|
|
154
370
|
|
|
155
371
|
// Get presence members
|
|
156
|
-
const members = await ctx.events.
|
|
372
|
+
const members = await ctx.events.presenceMembers('presence-chat.room-1');
|
|
373
|
+
```
|
|
157
374
|
|
|
158
|
-
|
|
159
|
-
const count = await ctx.events.getConnectionCount('orders.123');
|
|
375
|
+
## Scaling with Redis
|
|
160
376
|
|
|
161
|
-
|
|
162
|
-
|
|
377
|
+
For multi-instance deployments behind a load balancer:
|
|
378
|
+
|
|
379
|
+
```
|
|
380
|
+
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
|
381
|
+
│ Instance 1 │ │ Instance 2 │ │ Instance 3 │
|
|
382
|
+
│ WebSocket │ │ WebSocket │ │ WebSocket │
|
|
383
|
+
│ Driver │ │ Driver │ │ Driver │
|
|
384
|
+
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
|
|
385
|
+
│ │ │
|
|
386
|
+
└───────────────────────┼───────────────────────┘
|
|
387
|
+
│
|
|
388
|
+
┌────────────▼────────────┐
|
|
389
|
+
│ Redis │
|
|
390
|
+
│ (pub/sub) │
|
|
391
|
+
└─────────────────────────┘
|
|
163
392
|
```
|
|
164
393
|
|
|
165
|
-
|
|
394
|
+
When you call `ctx.events.broadcast()`:
|
|
395
|
+
1. Event is sent to local WebSocket clients
|
|
396
|
+
2. Event is published to Redis
|
|
397
|
+
3. Other instances receive from Redis
|
|
398
|
+
4. Each instance delivers to its local clients
|
|
166
399
|
|
|
167
|
-
|
|
400
|
+
**Configuration:**
|
|
168
401
|
|
|
169
402
|
```typescript
|
|
170
|
-
|
|
403
|
+
app.register(eventsPlugin({
|
|
404
|
+
driver: 'ws',
|
|
405
|
+
path: '/ws',
|
|
406
|
+
redis: process.env.REDIS_URL, // e.g., "redis://localhost:6379"
|
|
407
|
+
}));
|
|
408
|
+
```
|
|
171
409
|
|
|
172
|
-
|
|
410
|
+
## Standalone Usage
|
|
173
411
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
412
|
+
Use events outside of Fastify request context (background jobs, CLI, scripts):
|
|
413
|
+
|
|
414
|
+
```typescript
|
|
415
|
+
import { getEvents, closeEvents } from '@veloxts/events';
|
|
416
|
+
|
|
417
|
+
// Get standalone instance
|
|
418
|
+
const events = await getEvents({
|
|
419
|
+
driver: 'ws',
|
|
420
|
+
redis: process.env.REDIS_URL,
|
|
179
421
|
});
|
|
422
|
+
|
|
423
|
+
// Broadcast from background job
|
|
424
|
+
await events.broadcast('jobs', 'job.completed', { jobId: '123' });
|
|
425
|
+
|
|
426
|
+
// Clean up on shutdown
|
|
427
|
+
await closeEvents();
|
|
180
428
|
```
|
|
181
429
|
|
|
182
|
-
##
|
|
430
|
+
## Testing
|
|
183
431
|
|
|
184
|
-
For
|
|
432
|
+
For integration tests with Redis pub/sub, use testcontainers:
|
|
185
433
|
|
|
186
434
|
```typescript
|
|
187
|
-
|
|
435
|
+
import { startRedisContainer } from '@veloxts/testing';
|
|
436
|
+
import { createWsDriver } from '@veloxts/events';
|
|
437
|
+
|
|
438
|
+
const redis = await startRedisContainer();
|
|
439
|
+
|
|
440
|
+
const driver = await createWsDriver({
|
|
188
441
|
driver: 'ws',
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
},
|
|
193
|
-
}));
|
|
194
|
-
```
|
|
442
|
+
path: '/ws',
|
|
443
|
+
redis: redis.url,
|
|
444
|
+
});
|
|
195
445
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
446
|
+
// Run tests...
|
|
447
|
+
|
|
448
|
+
await driver.close();
|
|
449
|
+
await redis.stop();
|
|
450
|
+
```
|
package/dist/index.d.ts
CHANGED
|
@@ -40,7 +40,7 @@ export { type ChannelAuthSigner, createChannelAuthSigner } from './auth.js';
|
|
|
40
40
|
export { createSseDriver, DRIVER_NAME as SSE_DRIVER } from './drivers/sse.js';
|
|
41
41
|
export { createWsDriver, DRIVER_NAME as WS_DRIVER } from './drivers/ws.js';
|
|
42
42
|
export { createEventsManager, createManagerFromDriver, type EventsManager, events, } from './manager.js';
|
|
43
|
-
export { _resetStandaloneEvents, eventsPlugin, getEvents, getEventsFromInstance, } from './plugin.js';
|
|
43
|
+
export { _resetStandaloneEvents, closeEvents, eventsPlugin, getEvents, getEventsFromInstance, } from './plugin.js';
|
|
44
44
|
export { ClientMessageSchema, formatValidationErrors, PresenceMemberSchema, SseSubscribeBodySchema, SseUnsubscribeBodySchema, type ValidationResult, validateBody, WsAuthBodySchema, } from './schemas.js';
|
|
45
45
|
export type { BroadcastDriver, BroadcastEvent, Channel, ChannelAuthorizer, ChannelAuthResult, ChannelType, ClientConnection, ClientMessage, EventsBaseOptions, EventsDefaultOptions, EventsManagerOptions, EventsPluginOptions, EventsSseOptions, EventsWsOptions, PresenceMember, ServerMessage, Subscription, } from './types.js';
|
|
46
46
|
/**
|
package/dist/index.js
CHANGED
|
@@ -44,7 +44,7 @@ export { createWsDriver, DRIVER_NAME as WS_DRIVER } from './drivers/ws.js';
|
|
|
44
44
|
// Manager
|
|
45
45
|
export { createEventsManager, createManagerFromDriver, events, } from './manager.js';
|
|
46
46
|
// Plugin
|
|
47
|
-
export { _resetStandaloneEvents, eventsPlugin, getEvents, getEventsFromInstance, } from './plugin.js';
|
|
47
|
+
export { _resetStandaloneEvents, closeEvents, eventsPlugin, getEvents, getEventsFromInstance, } from './plugin.js';
|
|
48
48
|
// Schemas (for validation)
|
|
49
49
|
export { ClientMessageSchema, formatValidationErrors, PresenceMemberSchema, SseSubscribeBodySchema, SseUnsubscribeBodySchema, validateBody, WsAuthBodySchema, } from './schemas.js';
|
|
50
50
|
// ============================================================================
|
package/dist/plugin.d.ts
CHANGED
|
@@ -7,6 +7,22 @@
|
|
|
7
7
|
import type { FastifyInstance } from 'fastify';
|
|
8
8
|
import '@veloxts/core';
|
|
9
9
|
import type { EventsManager, EventsPluginOptions } from './types.js';
|
|
10
|
+
/**
|
|
11
|
+
* Symbol for accessing events manager from Fastify instance.
|
|
12
|
+
* Using a symbol prevents naming conflicts with other plugins.
|
|
13
|
+
*/
|
|
14
|
+
declare const EVENTS_KEY: unique symbol;
|
|
15
|
+
/**
|
|
16
|
+
* Extend Fastify types with events manager.
|
|
17
|
+
*/
|
|
18
|
+
declare module 'fastify' {
|
|
19
|
+
interface FastifyInstance {
|
|
20
|
+
[EVENTS_KEY]?: EventsManager;
|
|
21
|
+
}
|
|
22
|
+
interface FastifyRequest {
|
|
23
|
+
events?: EventsManager;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
10
26
|
/**
|
|
11
27
|
* Extend BaseContext with events manager.
|
|
12
28
|
*
|
|
@@ -98,8 +114,25 @@ export declare function getEventsFromInstance(fastify: FastifyInstance): EventsM
|
|
|
98
114
|
* ```
|
|
99
115
|
*/
|
|
100
116
|
export declare function getEvents(options?: EventsPluginOptions): Promise<EventsManager>;
|
|
117
|
+
/**
|
|
118
|
+
* Close the standalone events instance.
|
|
119
|
+
*
|
|
120
|
+
* Call this when shutting down your application to close all connections.
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ```typescript
|
|
124
|
+
* import { closeEvents } from '@veloxts/events';
|
|
125
|
+
*
|
|
126
|
+
* // On shutdown
|
|
127
|
+
* await closeEvents();
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
export declare function closeEvents(): Promise<void>;
|
|
101
131
|
/**
|
|
102
132
|
* Reset the standalone events instance.
|
|
103
133
|
* Primarily used for testing.
|
|
134
|
+
*
|
|
135
|
+
* @deprecated Use `closeEvents()` instead. Will be removed in v2.0.
|
|
104
136
|
*/
|
|
105
137
|
export declare function _resetStandaloneEvents(): Promise<void>;
|
|
138
|
+
export {};
|
package/dist/plugin.js
CHANGED
|
@@ -14,6 +14,7 @@ import { createManagerFromDriver } from './manager.js';
|
|
|
14
14
|
import { formatValidationErrors, SseSubscribeBodySchema, SseUnsubscribeBodySchema, validateBody, WsAuthBodySchema, } from './schemas.js';
|
|
15
15
|
/**
|
|
16
16
|
* Symbol for accessing events manager from Fastify instance.
|
|
17
|
+
* Using a symbol prevents naming conflicts with other plugins.
|
|
17
18
|
*/
|
|
18
19
|
const EVENTS_KEY = Symbol.for('velox.events');
|
|
19
20
|
/**
|
|
@@ -149,13 +150,18 @@ export function eventsPlugin(options = {}) {
|
|
|
149
150
|
}
|
|
150
151
|
// Create events manager
|
|
151
152
|
const events = createManagerFromDriver(driver);
|
|
152
|
-
// Store on fastify instance
|
|
153
|
-
fastify
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
153
|
+
// Store on fastify instance using Object.defineProperty for type safety
|
|
154
|
+
Object.defineProperty(fastify, EVENTS_KEY, {
|
|
155
|
+
value: events,
|
|
156
|
+
writable: false,
|
|
157
|
+
enumerable: false,
|
|
158
|
+
configurable: false,
|
|
159
|
+
});
|
|
160
|
+
// Decorate request with events accessor (matching cache/mail/queue/storage pattern)
|
|
161
|
+
fastify.decorateRequest('events', undefined);
|
|
162
|
+
// Add events to request context
|
|
163
|
+
fastify.addHook('onRequest', async (request) => {
|
|
164
|
+
request.events = events;
|
|
159
165
|
});
|
|
160
166
|
// Register cleanup hook
|
|
161
167
|
fastify.addHook('onClose', async () => {
|
|
@@ -192,7 +198,9 @@ function parseChannel(name) {
|
|
|
192
198
|
* ```
|
|
193
199
|
*/
|
|
194
200
|
export function getEventsFromInstance(fastify) {
|
|
195
|
-
|
|
201
|
+
// Type-safe property access using Object.getOwnPropertyDescriptor
|
|
202
|
+
const descriptor = Object.getOwnPropertyDescriptor(fastify, EVENTS_KEY);
|
|
203
|
+
const events = descriptor?.value;
|
|
196
204
|
if (!events) {
|
|
197
205
|
throw new Error('Events plugin not registered. Register it with: app.register(eventsPlugin, { ... })');
|
|
198
206
|
}
|
|
@@ -231,12 +239,30 @@ export async function getEvents(options) {
|
|
|
231
239
|
return standaloneEvents;
|
|
232
240
|
}
|
|
233
241
|
/**
|
|
234
|
-
*
|
|
235
|
-
*
|
|
242
|
+
* Close the standalone events instance.
|
|
243
|
+
*
|
|
244
|
+
* Call this when shutting down your application to close all connections.
|
|
245
|
+
*
|
|
246
|
+
* @example
|
|
247
|
+
* ```typescript
|
|
248
|
+
* import { closeEvents } from '@veloxts/events';
|
|
249
|
+
*
|
|
250
|
+
* // On shutdown
|
|
251
|
+
* await closeEvents();
|
|
252
|
+
* ```
|
|
236
253
|
*/
|
|
237
|
-
export async function
|
|
254
|
+
export async function closeEvents() {
|
|
238
255
|
if (standaloneEvents) {
|
|
239
256
|
await standaloneEvents.close();
|
|
240
257
|
standaloneEvents = null;
|
|
241
258
|
}
|
|
242
259
|
}
|
|
260
|
+
/**
|
|
261
|
+
* Reset the standalone events instance.
|
|
262
|
+
* Primarily used for testing.
|
|
263
|
+
*
|
|
264
|
+
* @deprecated Use `closeEvents()` instead. Will be removed in v2.0.
|
|
265
|
+
*/
|
|
266
|
+
export async function _resetStandaloneEvents() {
|
|
267
|
+
await closeEvents();
|
|
268
|
+
}
|
package/dist/schemas.d.ts
CHANGED
|
@@ -35,15 +35,15 @@ export declare const SseSubscribeBodySchema: z.ZodObject<{
|
|
|
35
35
|
info?: Record<string, unknown> | undefined;
|
|
36
36
|
}>>;
|
|
37
37
|
}, "strip", z.ZodTypeAny, {
|
|
38
|
-
channel: string;
|
|
39
38
|
connectionId: string;
|
|
39
|
+
channel: string;
|
|
40
40
|
member?: {
|
|
41
41
|
id: string;
|
|
42
42
|
info?: Record<string, unknown> | undefined;
|
|
43
43
|
} | undefined;
|
|
44
44
|
}, {
|
|
45
|
-
channel: string;
|
|
46
45
|
connectionId: string;
|
|
46
|
+
channel: string;
|
|
47
47
|
member?: {
|
|
48
48
|
id: string;
|
|
49
49
|
info?: Record<string, unknown> | undefined;
|
|
@@ -58,11 +58,11 @@ export declare const SseUnsubscribeBodySchema: z.ZodObject<{
|
|
|
58
58
|
connectionId: z.ZodString;
|
|
59
59
|
channel: z.ZodString;
|
|
60
60
|
}, "strip", z.ZodTypeAny, {
|
|
61
|
-
channel: string;
|
|
62
61
|
connectionId: string;
|
|
63
|
-
}, {
|
|
64
62
|
channel: string;
|
|
63
|
+
}, {
|
|
65
64
|
connectionId: string;
|
|
65
|
+
channel: string;
|
|
66
66
|
}>;
|
|
67
67
|
export type SseUnsubscribeBody = z.infer<typeof SseUnsubscribeBodySchema>;
|
|
68
68
|
/**
|
|
@@ -100,32 +100,32 @@ export declare const ClientMessageSchema: z.ZodDiscriminatedUnion<"type", [z.Zod
|
|
|
100
100
|
info?: Record<string, unknown> | undefined;
|
|
101
101
|
}>>;
|
|
102
102
|
}, "strip", z.ZodTypeAny, {
|
|
103
|
-
type: "subscribe";
|
|
104
103
|
channel: string;
|
|
104
|
+
type: "subscribe";
|
|
105
|
+
auth?: string | undefined;
|
|
105
106
|
data?: {
|
|
106
107
|
id: string;
|
|
107
108
|
info?: Record<string, unknown> | undefined;
|
|
108
109
|
} | undefined;
|
|
109
|
-
auth?: string | undefined;
|
|
110
110
|
channelData?: string | undefined;
|
|
111
111
|
}, {
|
|
112
|
-
type: "subscribe";
|
|
113
112
|
channel: string;
|
|
113
|
+
type: "subscribe";
|
|
114
|
+
auth?: string | undefined;
|
|
114
115
|
data?: {
|
|
115
116
|
id: string;
|
|
116
117
|
info?: Record<string, unknown> | undefined;
|
|
117
118
|
} | undefined;
|
|
118
|
-
auth?: string | undefined;
|
|
119
119
|
channelData?: string | undefined;
|
|
120
120
|
}>, z.ZodObject<{
|
|
121
121
|
type: z.ZodLiteral<"unsubscribe">;
|
|
122
122
|
channel: z.ZodString;
|
|
123
123
|
}, "strip", z.ZodTypeAny, {
|
|
124
|
-
type: "unsubscribe";
|
|
125
124
|
channel: string;
|
|
126
|
-
}, {
|
|
127
125
|
type: "unsubscribe";
|
|
126
|
+
}, {
|
|
128
127
|
channel: string;
|
|
128
|
+
type: "unsubscribe";
|
|
129
129
|
}>, z.ZodObject<{
|
|
130
130
|
type: z.ZodLiteral<"ping">;
|
|
131
131
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -138,14 +138,14 @@ export declare const ClientMessageSchema: z.ZodDiscriminatedUnion<"type", [z.Zod
|
|
|
138
138
|
event: z.ZodString;
|
|
139
139
|
data: z.ZodOptional<z.ZodUnknown>;
|
|
140
140
|
}, "strip", z.ZodTypeAny, {
|
|
141
|
-
event: string;
|
|
142
|
-
type: "message";
|
|
143
141
|
channel: string;
|
|
142
|
+
type: "message";
|
|
143
|
+
event: string;
|
|
144
144
|
data?: unknown;
|
|
145
145
|
}, {
|
|
146
|
-
event: string;
|
|
147
|
-
type: "message";
|
|
148
146
|
channel: string;
|
|
147
|
+
type: "message";
|
|
148
|
+
event: string;
|
|
149
149
|
data?: unknown;
|
|
150
150
|
}>]>;
|
|
151
151
|
export type ValidatedClientMessage = z.infer<typeof ClientMessageSchema>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@veloxts/events",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.88",
|
|
4
4
|
"description": "Real-time event broadcasting for VeloxTS framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"superjson": "2.2.2",
|
|
31
31
|
"ws": "8.18.2",
|
|
32
32
|
"zod": "3.24.4",
|
|
33
|
-
"@veloxts/core": "0.6.
|
|
33
|
+
"@veloxts/core": "0.6.88"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"fastify": "^5.0.0",
|
|
@@ -42,18 +42,19 @@
|
|
|
42
42
|
}
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@types/node": "
|
|
45
|
+
"@types/node": "25.0.3",
|
|
46
46
|
"@types/ws": "8.18.1",
|
|
47
47
|
"@vitest/coverage-v8": "4.0.16",
|
|
48
48
|
"fastify": "5.6.2",
|
|
49
49
|
"ioredis": "5.6.1",
|
|
50
|
-
"typescript": "5.
|
|
50
|
+
"typescript": "5.9.3",
|
|
51
51
|
"vitest": "4.0.16",
|
|
52
|
-
"@veloxts/testing": "0.6.
|
|
52
|
+
"@veloxts/testing": "0.6.88"
|
|
53
53
|
},
|
|
54
54
|
"publishConfig": {
|
|
55
55
|
"access": "public"
|
|
56
56
|
},
|
|
57
|
+
"homepage": "https://veloxts.dev/",
|
|
57
58
|
"repository": {
|
|
58
59
|
"type": "git",
|
|
59
60
|
"url": "https://github.com/veloxts/velox-ts-framework.git",
|