@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 CHANGED
@@ -1,81 +1,197 @@
1
1
  # @veloxts/events Guide
2
2
 
3
- ## Drivers
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
- ### WebSocket Driver
11
+ ## Quick Start
6
12
 
7
- Real-time broadcasting via WebSocket. Supports Redis pub/sub for horizontal scaling.
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
- // Basic setup (single instance)
13
- app.use(eventsPlugin({
19
+ const app = veloxApp();
20
+
21
+ app.register(eventsPlugin({
14
22
  driver: 'ws',
15
- config: {
16
- path: '/ws',
17
- pingInterval: 30000,
18
- },
23
+ path: '/ws',
19
24
  }));
20
25
 
21
- // With Redis for horizontal scaling
22
- app.use(eventsPlugin({
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
- config: {
25
- path: '/ws',
26
- redis: process.env.REDIS_URL,
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
- // Broadcast to a channel
35
- await ctx.broadcast({
36
- channel: 'orders.123',
37
- event: 'order.shipped',
38
- data: { trackingNumber: 'TRACK123' },
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
- // Broadcast to all except sender
42
- await ctx.broadcast({
43
- channel: 'chat.room-1',
44
- event: 'message',
45
- data: { text: 'Hello!' },
46
- except: ctx.socketId, // Exclude sender
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
- channel: 'announcements',
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.subscribe('announcements');
173
+ socket.send(JSON.stringify({
174
+ type: 'subscribe',
175
+ channel: 'announcements',
176
+ }));
66
177
  ```
67
178
 
68
179
  ### Private Channels
69
180
 
70
- Require authorization. Prefix with `private-`.
181
+ Require authentication. Prefix with `private-`.
71
182
 
72
183
  ```typescript
73
- // Server - requires authorization middleware
74
- await ctx.broadcast({
75
- channel: 'private-user.123',
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
- channel: 'presence-chat.room-1',
89
- event: 'typing',
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
- // Client receives member_added/member_removed events automatically
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
- ### Browser Client
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
- // Subscribe to channel
275
+ console.log('Connected');
276
+
277
+ // Subscribe to public channel
105
278
  socket.send(JSON.stringify({
106
279
  type: 'subscribe',
107
- channel: 'orders.123',
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
- if (message.type === 'event') {
115
- console.log(`${message.event}:`, message.data);
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.123',
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
- ### Presence Channels (Client)
314
+ ### Private Channel Authentication (Browser)
127
315
 
128
316
  ```typescript
129
- // Join with user info
130
- socket.send(JSON.stringify({
131
- type: 'subscribe',
132
- channel: 'presence-chat.room-1',
133
- data: { id: '123', name: 'John' },
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
- // Handle presence events
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 subscribers for a channel
153
- const subscribers = await ctx.events.getSubscribers('orders.123');
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.getPresenceMembers('presence-chat.room-1');
372
+ const members = await ctx.events.presenceMembers('presence-chat.room-1');
373
+ ```
157
374
 
158
- // Get connection count
159
- const count = await ctx.events.getConnectionCount('orders.123');
375
+ ## Scaling with Redis
160
376
 
161
- // Get all active channels
162
- const channels = await ctx.events.getChannels();
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
- ## HTTP Upgrade (Manual Setup)
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
- If not using the plugin, handle WebSocket upgrade manually:
400
+ **Configuration:**
168
401
 
169
402
  ```typescript
170
- import { createWsDriver } from '@veloxts/events';
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
- const events = await createWsDriver({ path: '/ws' });
410
+ ## Standalone Usage
173
411
 
174
- // In your HTTP server
175
- server.on('upgrade', (request, socket, head) => {
176
- if (request.url === '/ws') {
177
- events.handleUpgrade(request, socket, head);
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
- ## Scaling with Redis
430
+ ## Testing
183
431
 
184
- For multi-instance deployments, events are broadcast via Redis pub/sub:
432
+ For integration tests with Redis pub/sub, use testcontainers:
185
433
 
186
434
  ```typescript
187
- app.use(eventsPlugin({
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
- config: {
190
- path: '/ws',
191
- redis: process.env.REDIS_URL,
192
- },
193
- }));
194
- ```
442
+ path: '/ws',
443
+ redis: redis.url,
444
+ });
195
445
 
196
- All instances subscribe to a shared Redis channel. When you broadcast:
197
- 1. Event is sent to local WebSocket clients
198
- 2. Event is published to Redis
199
- 3. Other instances receive from Redis and broadcast to their clients
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[EVENTS_KEY] = events;
154
- // Decorate request with events accessor
155
- fastify.decorateRequest('events', {
156
- getter() {
157
- return events;
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
- const events = fastify[EVENTS_KEY];
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
- * Reset the standalone events instance.
235
- * Primarily used for testing.
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 _resetStandaloneEvents() {
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.86",
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.86"
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": "22.15.29",
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.8.3",
50
+ "typescript": "5.9.3",
51
51
  "vitest": "4.0.16",
52
- "@veloxts/testing": "0.6.86"
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",