@veloxts/events 0.6.51

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.
@@ -0,0 +1,41 @@
1
+ /**
2
+ * WebSocket Driver
3
+ *
4
+ * Real-time event broadcasting using WebSocket connections.
5
+ * Supports optional Redis pub/sub for horizontal scaling.
6
+ */
7
+ import type { IncomingMessage } from 'node:http';
8
+ import type { Duplex } from 'node:stream';
9
+ import { WebSocketServer } from 'ws';
10
+ import type { BroadcastDriver, EventsWsOptions } from '../types.js';
11
+ /**
12
+ * Create a WebSocket broadcast driver.
13
+ *
14
+ * @param config - WebSocket driver configuration
15
+ * @returns Broadcast driver implementation
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * const driver = await createWsDriver({
20
+ * driver: 'ws',
21
+ * path: '/ws',
22
+ * redis: process.env.REDIS_URL, // Optional for scaling
23
+ * });
24
+ *
25
+ * await driver.broadcast({
26
+ * channel: 'orders.123',
27
+ * event: 'order.shipped',
28
+ * data: { trackingNumber: 'TRACK123' },
29
+ * });
30
+ * ```
31
+ */
32
+ export declare function createWsDriver(config: EventsWsOptions, _server?: {
33
+ server: unknown;
34
+ }): Promise<BroadcastDriver & {
35
+ wss: WebSocketServer;
36
+ handleUpgrade: (request: IncomingMessage, socket: Duplex, head: Buffer) => void;
37
+ }>;
38
+ /**
39
+ * WebSocket driver name.
40
+ */
41
+ export declare const DRIVER_NAME: "ws";
@@ -0,0 +1,401 @@
1
+ /**
2
+ * WebSocket Driver
3
+ *
4
+ * Real-time event broadcasting using WebSocket connections.
5
+ * Supports optional Redis pub/sub for horizontal scaling.
6
+ */
7
+ import { randomUUID } from 'node:crypto';
8
+ import { WebSocketServer } from 'ws';
9
+ /**
10
+ * Default WebSocket driver configuration.
11
+ */
12
+ const DEFAULT_CONFIG = {
13
+ path: '/ws',
14
+ pingInterval: 30000,
15
+ connectionTimeout: 60000,
16
+ maxPayloadSize: 1024 * 1024, // 1MB
17
+ };
18
+ /**
19
+ * Create a type-safe pub/sub adapter from ioredis clients.
20
+ * This wraps the Redis clients to provide a clean async interface without type assertions.
21
+ */
22
+ function createRedisPubSubAdapter(subscriberClient, publisherClient) {
23
+ const subscriber = {
24
+ async subscribe(channel) {
25
+ await subscriberClient.subscribe(channel);
26
+ },
27
+ onMessage(handler) {
28
+ subscriberClient.on('message', handler);
29
+ },
30
+ async publish() {
31
+ throw new Error('Cannot publish on subscriber connection');
32
+ },
33
+ async quit() {
34
+ await subscriberClient.quit();
35
+ },
36
+ };
37
+ const publisher = {
38
+ async subscribe() {
39
+ throw new Error('Cannot subscribe on publisher connection');
40
+ },
41
+ onMessage() {
42
+ throw new Error('Cannot receive messages on publisher connection');
43
+ },
44
+ async publish(channel, message) {
45
+ await publisherClient.publish(channel, message);
46
+ },
47
+ async quit() {
48
+ await publisherClient.quit();
49
+ },
50
+ };
51
+ return {
52
+ subscriber,
53
+ publisher,
54
+ quit: async () => {
55
+ await Promise.all([subscriber.quit(), publisher.quit()]);
56
+ },
57
+ };
58
+ }
59
+ /**
60
+ * Create a WebSocket broadcast driver.
61
+ *
62
+ * @param config - WebSocket driver configuration
63
+ * @returns Broadcast driver implementation
64
+ *
65
+ * @example
66
+ * ```typescript
67
+ * const driver = await createWsDriver({
68
+ * driver: 'ws',
69
+ * path: '/ws',
70
+ * redis: process.env.REDIS_URL, // Optional for scaling
71
+ * });
72
+ *
73
+ * await driver.broadcast({
74
+ * channel: 'orders.123',
75
+ * event: 'order.shipped',
76
+ * data: { trackingNumber: 'TRACK123' },
77
+ * });
78
+ * ```
79
+ */
80
+ export async function createWsDriver(config, _server) {
81
+ const options = { ...DEFAULT_CONFIG, ...config };
82
+ const { pingInterval, maxPayloadSize, redis } = options;
83
+ // Generate unique instance ID for filtering self-messages in Redis pub/sub
84
+ const instanceId = randomUUID();
85
+ // Create WebSocket server
86
+ const wss = new WebSocketServer({
87
+ noServer: true,
88
+ maxPayload: maxPayloadSize,
89
+ });
90
+ // Connection tracking
91
+ const connections = new Map();
92
+ // Channel subscriptions: channel -> Set of socket IDs
93
+ const channelSubscriptions = new Map();
94
+ // Presence data: channel -> Map of socket ID -> PresenceMember
95
+ const presenceData = new Map();
96
+ // Redis pub/sub for horizontal scaling
97
+ let redisPubSub = null;
98
+ if (redis) {
99
+ const { Redis: RedisClient } = await import('ioredis');
100
+ const subscriberClient = new RedisClient(redis);
101
+ const publisherClient = new RedisClient(redis);
102
+ const adapter = createRedisPubSubAdapter(subscriberClient, publisherClient);
103
+ // Subscribe to broadcast channel
104
+ await adapter.subscriber.subscribe('velox:broadcast');
105
+ adapter.subscriber.onMessage((_channel, message) => {
106
+ try {
107
+ const parsed = JSON.parse(message);
108
+ // Skip messages from this instance to prevent duplicates
109
+ if (parsed.__origin === instanceId) {
110
+ return;
111
+ }
112
+ // Extract the event without the origin field
113
+ const { __origin: _, ...event } = parsed;
114
+ // Only broadcast locally, don't re-publish
115
+ broadcastLocal(event);
116
+ }
117
+ catch {
118
+ // Ignore invalid messages
119
+ }
120
+ });
121
+ redisPubSub = adapter;
122
+ }
123
+ /**
124
+ * Generate a unique socket ID.
125
+ */
126
+ function generateSocketId() {
127
+ return `${Date.now().toString(36)}.${Math.random().toString(36).slice(2, 10)}`;
128
+ }
129
+ /**
130
+ * Maximum buffered amount before applying backpressure (64KB).
131
+ * If the WebSocket buffer exceeds this, we skip non-critical messages.
132
+ */
133
+ const MAX_BUFFERED_AMOUNT = 64 * 1024;
134
+ /**
135
+ * Send a message to a client with backpressure handling.
136
+ * Returns true if message was sent, false if backpressure was applied.
137
+ */
138
+ function send(ws, message) {
139
+ if (ws.readyState !== ws.OPEN) {
140
+ return false;
141
+ }
142
+ // Apply backpressure: skip non-critical messages if buffer is full
143
+ // This prevents memory buildup under high load
144
+ if (ws.bufferedAmount > MAX_BUFFERED_AMOUNT) {
145
+ // Allow critical messages (connection, subscription, errors) through
146
+ if (message.type === 'event' && message.event !== 'connected') {
147
+ return false; // Drop non-critical event, client can recover
148
+ }
149
+ }
150
+ ws.send(JSON.stringify(message));
151
+ return true;
152
+ }
153
+ /**
154
+ * Broadcast an event locally to connected clients.
155
+ */
156
+ function broadcastLocal(event) {
157
+ const subscribers = channelSubscriptions.get(event.channel);
158
+ if (!subscribers)
159
+ return;
160
+ const message = {
161
+ type: 'event',
162
+ channel: event.channel,
163
+ event: event.event,
164
+ data: event.data,
165
+ };
166
+ for (const socketId of subscribers) {
167
+ if (event.except && socketId === event.except)
168
+ continue;
169
+ const ws = connections.get(socketId);
170
+ if (ws) {
171
+ send(ws, message);
172
+ }
173
+ }
174
+ }
175
+ /**
176
+ * Subscribe a client to a channel.
177
+ */
178
+ function subscribe(ws, channel, member) {
179
+ // Add to connection's channels
180
+ ws.channels.add(channel);
181
+ // Add to channel subscriptions
182
+ let subscribers = channelSubscriptions.get(channel);
183
+ if (!subscribers) {
184
+ subscribers = new Set();
185
+ channelSubscriptions.set(channel, subscribers);
186
+ }
187
+ subscribers.add(ws.id);
188
+ // Handle presence channel
189
+ if (member && channel.startsWith('presence-')) {
190
+ let members = presenceData.get(channel);
191
+ if (!members) {
192
+ members = new Map();
193
+ presenceData.set(channel, members);
194
+ }
195
+ members.set(ws.id, member);
196
+ ws.presenceInfo.set(channel, member);
197
+ // Notify others of new member
198
+ broadcastLocal({
199
+ channel,
200
+ event: 'member_added',
201
+ data: member,
202
+ except: ws.id,
203
+ });
204
+ }
205
+ // Send success message
206
+ send(ws, {
207
+ type: 'subscription_succeeded',
208
+ channel,
209
+ data: member && channel.startsWith('presence-')
210
+ ? { members: Array.from(presenceData.get(channel)?.values() ?? []) }
211
+ : undefined,
212
+ });
213
+ }
214
+ /**
215
+ * Unsubscribe a client from a channel.
216
+ */
217
+ function unsubscribe(ws, channel) {
218
+ // Remove from connection's channels
219
+ ws.channels.delete(channel);
220
+ // Remove from channel subscriptions
221
+ const subscribers = channelSubscriptions.get(channel);
222
+ if (subscribers) {
223
+ subscribers.delete(ws.id);
224
+ if (subscribers.size === 0) {
225
+ channelSubscriptions.delete(channel);
226
+ }
227
+ }
228
+ // Handle presence channel
229
+ if (channel.startsWith('presence-')) {
230
+ const members = presenceData.get(channel);
231
+ if (members) {
232
+ const member = members.get(ws.id);
233
+ members.delete(ws.id);
234
+ ws.presenceInfo.delete(channel);
235
+ if (members.size === 0) {
236
+ presenceData.delete(channel);
237
+ }
238
+ // Notify others of member leaving
239
+ if (member) {
240
+ broadcastLocal({
241
+ channel,
242
+ event: 'member_removed',
243
+ data: member,
244
+ });
245
+ }
246
+ }
247
+ }
248
+ }
249
+ /**
250
+ * Handle client disconnection.
251
+ */
252
+ function handleDisconnect(ws) {
253
+ // Unsubscribe from all channels
254
+ for (const channel of ws.channels) {
255
+ unsubscribe(ws, channel);
256
+ }
257
+ // Remove from connections
258
+ connections.delete(ws.id);
259
+ }
260
+ /**
261
+ * Handle incoming client message.
262
+ */
263
+ function handleMessage(ws, data) {
264
+ try {
265
+ const message = JSON.parse(data);
266
+ ws.isAlive = true;
267
+ switch (message.type) {
268
+ case 'subscribe':
269
+ if (message.channel) {
270
+ // For now, auto-authorize. Plugin will add proper authorization.
271
+ subscribe(ws, message.channel, message.data);
272
+ }
273
+ break;
274
+ case 'unsubscribe':
275
+ if (message.channel) {
276
+ unsubscribe(ws, message.channel);
277
+ }
278
+ break;
279
+ case 'ping':
280
+ send(ws, { type: 'pong' });
281
+ break;
282
+ case 'message':
283
+ // Client-to-server events (for presence channels mainly)
284
+ if (message.channel && message.event) {
285
+ broadcastLocal({
286
+ channel: message.channel,
287
+ event: message.event,
288
+ data: message.data,
289
+ except: ws.id,
290
+ });
291
+ }
292
+ break;
293
+ }
294
+ }
295
+ catch {
296
+ send(ws, { type: 'error', error: 'Invalid message format' });
297
+ }
298
+ }
299
+ // Connection handling
300
+ wss.on('connection', (ws) => {
301
+ const extWs = ws;
302
+ extWs.id = generateSocketId();
303
+ extWs.isAlive = true;
304
+ extWs.channels = new Set();
305
+ extWs.presenceInfo = new Map();
306
+ connections.set(extWs.id, extWs);
307
+ // Send connection info
308
+ send(extWs, {
309
+ type: 'event',
310
+ event: 'connected',
311
+ data: { socketId: extWs.id },
312
+ });
313
+ extWs.on('message', (data) => {
314
+ handleMessage(extWs, data.toString());
315
+ });
316
+ extWs.on('close', () => {
317
+ handleDisconnect(extWs);
318
+ });
319
+ extWs.on('pong', () => {
320
+ extWs.isAlive = true;
321
+ });
322
+ extWs.on('error', () => {
323
+ handleDisconnect(extWs);
324
+ });
325
+ });
326
+ // Ping interval for connection health
327
+ const pingIntervalId = setInterval(() => {
328
+ for (const ws of connections.values()) {
329
+ if (!ws.isAlive) {
330
+ ws.terminate();
331
+ handleDisconnect(ws);
332
+ continue;
333
+ }
334
+ ws.isAlive = false;
335
+ ws.ping();
336
+ }
337
+ }, pingInterval);
338
+ /**
339
+ * Handle HTTP upgrade to WebSocket.
340
+ *
341
+ * @param request - The incoming HTTP request
342
+ * @param socket - The underlying TCP socket (Duplex stream)
343
+ * @param head - The first packet of the upgraded stream
344
+ */
345
+ function handleUpgrade(request, socket, head) {
346
+ wss.handleUpgrade(request, socket, head, (ws) => {
347
+ wss.emit('connection', ws, request);
348
+ });
349
+ }
350
+ const driver = {
351
+ wss,
352
+ handleUpgrade,
353
+ async broadcast(event) {
354
+ // Broadcast locally
355
+ broadcastLocal(event);
356
+ // Publish to Redis for other instances
357
+ if (redisPubSub) {
358
+ // Include instance ID to prevent self-echo
359
+ await redisPubSub.publisher.publish('velox:broadcast', JSON.stringify({ ...event, __origin: instanceId }));
360
+ }
361
+ },
362
+ async getSubscribers(channel) {
363
+ const subscribers = channelSubscriptions.get(channel);
364
+ return subscribers ? Array.from(subscribers) : [];
365
+ },
366
+ async getPresenceMembers(channel) {
367
+ const members = presenceData.get(channel);
368
+ return members ? Array.from(members.values()) : [];
369
+ },
370
+ async getConnectionCount(channel) {
371
+ const subscribers = channelSubscriptions.get(channel);
372
+ return subscribers?.size ?? 0;
373
+ },
374
+ async getChannels() {
375
+ return Array.from(channelSubscriptions.keys());
376
+ },
377
+ async close() {
378
+ clearInterval(pingIntervalId);
379
+ // Close all connections
380
+ for (const ws of connections.values()) {
381
+ ws.close(1000, 'Server shutting down');
382
+ }
383
+ connections.clear();
384
+ channelSubscriptions.clear();
385
+ presenceData.clear();
386
+ // Close Redis connections
387
+ if (redisPubSub) {
388
+ await redisPubSub.quit();
389
+ }
390
+ // Close WebSocket server
391
+ await new Promise((resolve) => {
392
+ wss.close(() => resolve());
393
+ });
394
+ },
395
+ };
396
+ return driver;
397
+ }
398
+ /**
399
+ * WebSocket driver name.
400
+ */
401
+ export const DRIVER_NAME = 'ws';
@@ -0,0 +1,45 @@
1
+ /**
2
+ * @veloxts/events
3
+ *
4
+ * Real-time event broadcasting for VeloxTS framework.
5
+ *
6
+ * Features:
7
+ * - WebSocket driver with ws library
8
+ * - SSE driver for fallback
9
+ * - Channel authorization (public, private, presence)
10
+ * - Redis pub/sub for horizontal scaling
11
+ * - Fastify plugin with BaseContext extension
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * import { eventsPlugin } from '@veloxts/events';
16
+ *
17
+ * // Register the plugin
18
+ * app.register(eventsPlugin, {
19
+ * driver: 'ws',
20
+ * path: '/ws',
21
+ * redis: process.env.REDIS_URL,
22
+ * });
23
+ *
24
+ * // In a procedure
25
+ * const createOrder = procedure
26
+ * .input(CreateOrderSchema)
27
+ * .mutation(async ({ input, ctx }) => {
28
+ * const order = await ctx.db.order.create({ data: input });
29
+ *
30
+ * // Broadcast to subscribers
31
+ * await ctx.events.broadcast(`orders.${order.userId}`, 'order.created', order);
32
+ *
33
+ * return order;
34
+ * });
35
+ * ```
36
+ *
37
+ * @packageDocumentation
38
+ */
39
+ export { type ChannelAuthSigner, createChannelAuthSigner } from './auth.js';
40
+ export { createSseDriver, DRIVER_NAME as SSE_DRIVER } from './drivers/sse.js';
41
+ export { createWsDriver, DRIVER_NAME as WS_DRIVER } from './drivers/ws.js';
42
+ export { createEventsManager, createManagerFromDriver, type EventsManager, events, } from './manager.js';
43
+ export { _resetStandaloneEvents, eventsPlugin, getEvents, getEventsFromInstance, } from './plugin.js';
44
+ export { ClientMessageSchema, formatValidationErrors, PresenceMemberSchema, SseSubscribeBodySchema, SseUnsubscribeBodySchema, type ValidationResult, validateBody, WsAuthBodySchema, } from './schemas.js';
45
+ export type { BroadcastDriver, BroadcastEvent, Channel, ChannelAuthorizer, ChannelAuthResult, ChannelType, ClientConnection, ClientMessage, EventsBaseOptions, EventsDefaultOptions, EventsManagerOptions, EventsPluginOptions, EventsSseOptions, EventsWsOptions, PresenceMember, ServerMessage, Subscription, } from './types.js';
package/dist/index.js ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * @veloxts/events
3
+ *
4
+ * Real-time event broadcasting for VeloxTS framework.
5
+ *
6
+ * Features:
7
+ * - WebSocket driver with ws library
8
+ * - SSE driver for fallback
9
+ * - Channel authorization (public, private, presence)
10
+ * - Redis pub/sub for horizontal scaling
11
+ * - Fastify plugin with BaseContext extension
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * import { eventsPlugin } from '@veloxts/events';
16
+ *
17
+ * // Register the plugin
18
+ * app.register(eventsPlugin, {
19
+ * driver: 'ws',
20
+ * path: '/ws',
21
+ * redis: process.env.REDIS_URL,
22
+ * });
23
+ *
24
+ * // In a procedure
25
+ * const createOrder = procedure
26
+ * .input(CreateOrderSchema)
27
+ * .mutation(async ({ input, ctx }) => {
28
+ * const order = await ctx.db.order.create({ data: input });
29
+ *
30
+ * // Broadcast to subscribers
31
+ * await ctx.events.broadcast(`orders.${order.userId}`, 'order.created', order);
32
+ *
33
+ * return order;
34
+ * });
35
+ * ```
36
+ *
37
+ * @packageDocumentation
38
+ */
39
+ // Auth
40
+ export { createChannelAuthSigner } from './auth.js';
41
+ // Drivers
42
+ export { createSseDriver, DRIVER_NAME as SSE_DRIVER } from './drivers/sse.js';
43
+ export { createWsDriver, DRIVER_NAME as WS_DRIVER } from './drivers/ws.js';
44
+ // Manager
45
+ export { createEventsManager, createManagerFromDriver, events, } from './manager.js';
46
+ // Plugin
47
+ export { _resetStandaloneEvents, eventsPlugin, getEvents, getEventsFromInstance, } from './plugin.js';
48
+ // Schemas (for validation)
49
+ export { ClientMessageSchema, formatValidationErrors, PresenceMemberSchema, SseSubscribeBodySchema, SseUnsubscribeBodySchema, validateBody, WsAuthBodySchema, } from './schemas.js';
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Events Manager
3
+ *
4
+ * High-level API for real-time event broadcasting.
5
+ * Wraps the underlying driver with a clean, Laravel-inspired interface.
6
+ */
7
+ import type { BroadcastDriver, EventsManager, EventsPluginOptions } from './types.js';
8
+ /**
9
+ * Create an events manager with the specified driver.
10
+ *
11
+ * @param options - Events configuration with driver selection
12
+ * @returns Events manager instance
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * // WebSocket driver (default)
17
+ * const events = await createEventsManager({
18
+ * driver: 'ws',
19
+ * path: '/ws',
20
+ * redis: process.env.REDIS_URL, // For scaling
21
+ * });
22
+ *
23
+ * // SSE driver (fallback)
24
+ * const events = await createEventsManager({
25
+ * driver: 'sse',
26
+ * path: '/events',
27
+ * });
28
+ *
29
+ * // Broadcast an event
30
+ * await events.broadcast('orders.123', 'order.shipped', {
31
+ * trackingNumber: 'TRACK123',
32
+ * });
33
+ * ```
34
+ */
35
+ export declare function createEventsManager(options?: EventsPluginOptions): Promise<EventsManager & {
36
+ driver: BroadcastDriver;
37
+ }>;
38
+ /**
39
+ * Create an events manager from an existing driver.
40
+ *
41
+ * @param driver - Broadcast driver instance
42
+ * @returns Events manager instance
43
+ */
44
+ export declare function createManagerFromDriver(driver: BroadcastDriver): EventsManager & {
45
+ driver: BroadcastDriver;
46
+ };
47
+ /**
48
+ * Alias for createEventsManager for Laravel-style API.
49
+ */
50
+ export declare const events: typeof createEventsManager;
51
+ /**
52
+ * Re-export manager type.
53
+ */
54
+ export type { EventsManager };
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Events Manager
3
+ *
4
+ * High-level API for real-time event broadcasting.
5
+ * Wraps the underlying driver with a clean, Laravel-inspired interface.
6
+ */
7
+ /**
8
+ * Create an events manager with the specified driver.
9
+ *
10
+ * @param options - Events configuration with driver selection
11
+ * @returns Events manager instance
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * // WebSocket driver (default)
16
+ * const events = await createEventsManager({
17
+ * driver: 'ws',
18
+ * path: '/ws',
19
+ * redis: process.env.REDIS_URL, // For scaling
20
+ * });
21
+ *
22
+ * // SSE driver (fallback)
23
+ * const events = await createEventsManager({
24
+ * driver: 'sse',
25
+ * path: '/events',
26
+ * });
27
+ *
28
+ * // Broadcast an event
29
+ * await events.broadcast('orders.123', 'order.shipped', {
30
+ * trackingNumber: 'TRACK123',
31
+ * });
32
+ * ```
33
+ */
34
+ export async function createEventsManager(options = {}) {
35
+ let driver;
36
+ if (options.driver === 'sse') {
37
+ const sseOptions = options;
38
+ const { createSseDriver } = await import('./drivers/sse.js');
39
+ driver = createSseDriver(sseOptions);
40
+ }
41
+ else {
42
+ // WebSocket driver (default)
43
+ const wsOptions = (options.driver === 'ws' ? options : { ...options, driver: 'ws' });
44
+ const { createWsDriver } = await import('./drivers/ws.js');
45
+ driver = await createWsDriver(wsOptions);
46
+ }
47
+ return createManagerFromDriver(driver);
48
+ }
49
+ /**
50
+ * Create an events manager from an existing driver.
51
+ *
52
+ * @param driver - Broadcast driver instance
53
+ * @returns Events manager instance
54
+ */
55
+ export function createManagerFromDriver(driver) {
56
+ const manager = {
57
+ driver,
58
+ async broadcast(channel, event, data, except) {
59
+ const broadcastEvent = {
60
+ channel,
61
+ event,
62
+ data,
63
+ except,
64
+ };
65
+ await driver.broadcast(broadcastEvent);
66
+ },
67
+ async broadcastToMany(channels, event, data) {
68
+ await Promise.all(channels.map((channel) => driver.broadcast({
69
+ channel,
70
+ event,
71
+ data,
72
+ })));
73
+ },
74
+ async toOthers(channel, event, data, except) {
75
+ await driver.broadcast({
76
+ channel,
77
+ event,
78
+ data,
79
+ except,
80
+ });
81
+ },
82
+ async subscriberCount(channel) {
83
+ return driver.getConnectionCount(channel);
84
+ },
85
+ async presenceMembers(channel) {
86
+ return driver.getPresenceMembers(channel);
87
+ },
88
+ async hasSubscribers(channel) {
89
+ const count = await driver.getConnectionCount(channel);
90
+ return count > 0;
91
+ },
92
+ async channels() {
93
+ return driver.getChannels();
94
+ },
95
+ async close() {
96
+ await driver.close();
97
+ },
98
+ };
99
+ return manager;
100
+ }
101
+ /**
102
+ * Alias for createEventsManager for Laravel-style API.
103
+ */
104
+ export const events = createEventsManager;