@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.
- package/GUIDE.md +199 -0
- package/LICENSE +21 -0
- package/README.md +32 -0
- package/dist/auth.d.ts +51 -0
- package/dist/auth.js +71 -0
- package/dist/drivers/sse.d.ts +42 -0
- package/dist/drivers/sse.js +284 -0
- package/dist/drivers/ws.d.ts +41 -0
- package/dist/drivers/ws.js +401 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +49 -0
- package/dist/manager.d.ts +54 -0
- package/dist/manager.js +104 -0
- package/dist/plugin.d.ts +105 -0
- package/dist/plugin.js +242 -0
- package/dist/schemas.d.ts +178 -0
- package/dist/schemas.js +101 -0
- package/dist/types.d.ts +265 -0
- package/dist/types.js +7 -0
- package/package.json +83 -0
|
@@ -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';
|
package/dist/index.d.ts
ADDED
|
@@ -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 };
|
package/dist/manager.js
ADDED
|
@@ -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;
|