@onebun/core 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +233 -0
- package/package.json +1 -1
- package/src/application.test.ts +119 -0
- package/src/application.ts +112 -5
- package/src/docs-examples.test.ts +753 -0
- package/src/index.ts +96 -0
- package/src/module.ts +10 -4
- package/src/redis-client.ts +502 -0
- package/src/shared-redis.ts +231 -0
- package/src/types.ts +50 -0
- package/src/ws-base-gateway.test.ts +479 -0
- package/src/ws-base-gateway.ts +514 -0
- package/src/ws-client.test.ts +511 -0
- package/src/ws-client.ts +628 -0
- package/src/ws-client.types.ts +129 -0
- package/src/ws-decorators.test.ts +331 -0
- package/src/ws-decorators.ts +417 -0
- package/src/ws-guards.test.ts +334 -0
- package/src/ws-guards.ts +298 -0
- package/src/ws-handler.ts +658 -0
- package/src/ws-integration.test.ts +517 -0
- package/src/ws-pattern-matcher.test.ts +152 -0
- package/src/ws-pattern-matcher.ts +240 -0
- package/src/ws-service-definition.ts +223 -0
- package/src/ws-socketio-protocol.test.ts +344 -0
- package/src/ws-socketio-protocol.ts +567 -0
- package/src/ws-storage-memory.test.ts +246 -0
- package/src/ws-storage-memory.ts +222 -0
- package/src/ws-storage-redis.ts +302 -0
- package/src/ws-storage.ts +210 -0
- package/src/ws.types.ts +342 -0
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base WebSocket Gateway
|
|
3
|
+
*
|
|
4
|
+
* Abstract base class for WebSocket gateways.
|
|
5
|
+
* Provides methods for client/room management and message broadcasting.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { WsStorageAdapter, WsPubSubStorageAdapter } from './ws-storage';
|
|
9
|
+
import type {
|
|
10
|
+
WsClientData,
|
|
11
|
+
WsRoom,
|
|
12
|
+
WsMessage,
|
|
13
|
+
WsServer,
|
|
14
|
+
} from './ws.types';
|
|
15
|
+
import type { Server, ServerWebSocket } from 'bun';
|
|
16
|
+
|
|
17
|
+
import { WsStorageEvent, isPubSubAdapter } from './ws-storage';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Map of client IDs to their WebSocket connections
|
|
21
|
+
* Stored separately from storage to keep socket references in memory
|
|
22
|
+
*/
|
|
23
|
+
const clientSockets = new Map<string, ServerWebSocket<WsClientData>>();
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Clear all client sockets (for testing purposes only)
|
|
27
|
+
* @internal
|
|
28
|
+
*/
|
|
29
|
+
export function _resetClientSocketsForTesting(): void {
|
|
30
|
+
clientSockets.clear();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Base class for WebSocket gateways
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```typescript
|
|
38
|
+
* @WebSocketGateway({ path: '/ws' })
|
|
39
|
+
* export class ChatGateway extends BaseWebSocketGateway {
|
|
40
|
+
* @OnMessage('chat:message')
|
|
41
|
+
* handleMessage(@Client() client: WsClientData, @MessageData() data: any) {
|
|
42
|
+
* this.broadcast('chat:message', { userId: client.id, ...data });
|
|
43
|
+
* }
|
|
44
|
+
* }
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export abstract class BaseWebSocketGateway {
|
|
48
|
+
/** Storage adapter for persisting client/room data */
|
|
49
|
+
protected storage: WsStorageAdapter | null = null;
|
|
50
|
+
|
|
51
|
+
/** Bun server reference */
|
|
52
|
+
protected server: Server | null = null;
|
|
53
|
+
|
|
54
|
+
/** Unique instance ID (for multi-instance setups) */
|
|
55
|
+
protected instanceId: string = crypto.randomUUID();
|
|
56
|
+
|
|
57
|
+
// ============================================================================
|
|
58
|
+
// Initialization
|
|
59
|
+
// ============================================================================
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Initialize the gateway with storage and server
|
|
63
|
+
* Called internally by the framework
|
|
64
|
+
* @internal
|
|
65
|
+
*/
|
|
66
|
+
_initialize(storage: WsStorageAdapter, server: Server): void {
|
|
67
|
+
this.storage = storage;
|
|
68
|
+
this.server = server;
|
|
69
|
+
|
|
70
|
+
// Subscribe to pub/sub events if Redis storage
|
|
71
|
+
if (isPubSubAdapter(storage)) {
|
|
72
|
+
this._setupPubSub(storage);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Setup pub/sub for multi-instance support
|
|
78
|
+
* @internal
|
|
79
|
+
*/
|
|
80
|
+
private async _setupPubSub(storage: WsPubSubStorageAdapter): Promise<void> {
|
|
81
|
+
await storage.subscribe((payload) => {
|
|
82
|
+
// Ignore events from this instance
|
|
83
|
+
if (payload.sourceInstanceId === this.instanceId) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Handle remote events
|
|
88
|
+
switch (payload.type) {
|
|
89
|
+
case WsStorageEvent.BROADCAST:
|
|
90
|
+
// Broadcast to all local clients
|
|
91
|
+
if (payload.data.event && payload.data.message !== undefined) {
|
|
92
|
+
this._localBroadcast(
|
|
93
|
+
payload.data.event,
|
|
94
|
+
payload.data.message,
|
|
95
|
+
payload.data.excludeClientIds,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
break;
|
|
99
|
+
|
|
100
|
+
case WsStorageEvent.ROOM_BROADCAST:
|
|
101
|
+
// Broadcast to room's local clients
|
|
102
|
+
if (payload.data.roomName && payload.data.event && payload.data.message !== undefined) {
|
|
103
|
+
this._localEmitToRoom(
|
|
104
|
+
payload.data.roomName,
|
|
105
|
+
payload.data.event,
|
|
106
|
+
payload.data.message,
|
|
107
|
+
payload.data.excludeClientIds,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
break;
|
|
111
|
+
|
|
112
|
+
case WsStorageEvent.CLIENT_MESSAGE:
|
|
113
|
+
// Send to specific client if local
|
|
114
|
+
if (payload.data.clientId && payload.data.event && payload.data.message !== undefined) {
|
|
115
|
+
this._localEmit(payload.data.clientId, payload.data.event, payload.data.message);
|
|
116
|
+
}
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Register a client socket
|
|
124
|
+
* @internal
|
|
125
|
+
*/
|
|
126
|
+
_registerSocket(clientId: string, socket: ServerWebSocket<WsClientData>): void {
|
|
127
|
+
clientSockets.set(clientId, socket);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Unregister a client socket
|
|
132
|
+
* @internal
|
|
133
|
+
*/
|
|
134
|
+
_unregisterSocket(clientId: string): void {
|
|
135
|
+
clientSockets.delete(clientId);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get socket for a client
|
|
140
|
+
* @internal
|
|
141
|
+
*/
|
|
142
|
+
protected getSocket(clientId: string): ServerWebSocket<WsClientData> | undefined {
|
|
143
|
+
return clientSockets.get(clientId);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ============================================================================
|
|
147
|
+
// Client Getters
|
|
148
|
+
// ============================================================================
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get all connected clients
|
|
152
|
+
*/
|
|
153
|
+
get clients(): Map<string, WsClientData> {
|
|
154
|
+
// Return a map view from storage - this is async but we need sync access
|
|
155
|
+
// For sync access, maintain a local cache
|
|
156
|
+
const result = new Map<string, WsClientData>();
|
|
157
|
+
// Note: This returns only locally connected clients
|
|
158
|
+
for (const [id, socket] of clientSockets) {
|
|
159
|
+
if (socket.data) {
|
|
160
|
+
result.set(id, socket.data);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Get all rooms
|
|
169
|
+
*/
|
|
170
|
+
get rooms(): Map<string, WsRoom> {
|
|
171
|
+
// Note: For async storage, use getRoomsAsync()
|
|
172
|
+
return new Map();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Get a client by ID
|
|
177
|
+
*/
|
|
178
|
+
async getClient(clientId: string): Promise<WsClientData | undefined> {
|
|
179
|
+
if (!this.storage) {
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
const client = await this.storage.getClient(clientId);
|
|
183
|
+
|
|
184
|
+
return client || undefined;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Get a room by name
|
|
189
|
+
*/
|
|
190
|
+
async getRoom(roomName: string): Promise<WsRoom | undefined> {
|
|
191
|
+
if (!this.storage) {
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
const room = await this.storage.getRoom(roomName);
|
|
195
|
+
|
|
196
|
+
return room || undefined;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Get all clients in a room
|
|
201
|
+
*/
|
|
202
|
+
async getClientsByRoom(roomName: string): Promise<WsClientData[]> {
|
|
203
|
+
if (!this.storage) {
|
|
204
|
+
return [];
|
|
205
|
+
}
|
|
206
|
+
const clientIds = await this.storage.getClientsInRoom(roomName);
|
|
207
|
+
const clients: WsClientData[] = [];
|
|
208
|
+
|
|
209
|
+
for (const id of clientIds) {
|
|
210
|
+
const client = await this.storage.getClient(id);
|
|
211
|
+
if (client) {
|
|
212
|
+
clients.push(client);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return clients;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Get all rooms matching a pattern
|
|
221
|
+
*/
|
|
222
|
+
async getRoomsByPattern(pattern: string): Promise<WsRoom[]> {
|
|
223
|
+
if (!this.storage) {
|
|
224
|
+
return [];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return await this.storage.getRoomsByPattern(pattern);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ============================================================================
|
|
231
|
+
// Emit Methods
|
|
232
|
+
// ============================================================================
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Send a message to a specific client
|
|
236
|
+
*/
|
|
237
|
+
emit(clientId: string, event: string, data: unknown): void {
|
|
238
|
+
this._localEmit(clientId, event, data);
|
|
239
|
+
|
|
240
|
+
// If using Redis, also publish for other instances
|
|
241
|
+
if (this.storage && isPubSubAdapter(this.storage)) {
|
|
242
|
+
this.storage.publish({
|
|
243
|
+
type: WsStorageEvent.CLIENT_MESSAGE,
|
|
244
|
+
sourceInstanceId: this.instanceId,
|
|
245
|
+
data: { clientId, event, message: data },
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Send to local client only
|
|
252
|
+
* @internal
|
|
253
|
+
*/
|
|
254
|
+
private _localEmit(clientId: string, event: string, data: unknown): void {
|
|
255
|
+
const socket = clientSockets.get(clientId);
|
|
256
|
+
if (socket) {
|
|
257
|
+
const message: WsMessage = { event, data };
|
|
258
|
+
socket.send(JSON.stringify(message));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Broadcast a message to all connected clients
|
|
264
|
+
*/
|
|
265
|
+
broadcast(event: string, data: unknown, excludeClientIds?: string[]): void {
|
|
266
|
+
this._localBroadcast(event, data, excludeClientIds);
|
|
267
|
+
|
|
268
|
+
// If using Redis, also publish for other instances
|
|
269
|
+
if (this.storage && isPubSubAdapter(this.storage)) {
|
|
270
|
+
this.storage.publish({
|
|
271
|
+
type: WsStorageEvent.BROADCAST,
|
|
272
|
+
sourceInstanceId: this.instanceId,
|
|
273
|
+
data: { event, message: data, excludeClientIds },
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Broadcast to local clients only
|
|
280
|
+
* @internal
|
|
281
|
+
*/
|
|
282
|
+
private _localBroadcast(event: string, data: unknown, excludeClientIds?: string[]): void {
|
|
283
|
+
const message = JSON.stringify({ event, data } as WsMessage);
|
|
284
|
+
const excludeSet = new Set(excludeClientIds || []);
|
|
285
|
+
|
|
286
|
+
for (const [clientId, socket] of clientSockets) {
|
|
287
|
+
if (!excludeSet.has(clientId)) {
|
|
288
|
+
socket.send(message);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Send a message to all clients in a room
|
|
295
|
+
*/
|
|
296
|
+
emitToRoom(roomName: string, event: string, data: unknown, excludeClientIds?: string[]): void {
|
|
297
|
+
this._localEmitToRoom(roomName, event, data, excludeClientIds);
|
|
298
|
+
|
|
299
|
+
// If using Redis, also publish for other instances
|
|
300
|
+
if (this.storage && isPubSubAdapter(this.storage)) {
|
|
301
|
+
this.storage.publish({
|
|
302
|
+
type: WsStorageEvent.ROOM_BROADCAST,
|
|
303
|
+
sourceInstanceId: this.instanceId,
|
|
304
|
+
data: {
|
|
305
|
+
roomName, event, message: data, excludeClientIds,
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Emit to room's local clients only
|
|
313
|
+
* @internal
|
|
314
|
+
*/
|
|
315
|
+
private async _localEmitToRoom(
|
|
316
|
+
roomName: string,
|
|
317
|
+
event: string,
|
|
318
|
+
data: unknown,
|
|
319
|
+
excludeClientIds?: string[],
|
|
320
|
+
): Promise<void> {
|
|
321
|
+
if (!this.storage) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const clientIds = await this.storage.getClientsInRoom(roomName);
|
|
326
|
+
const message = JSON.stringify({ event, data } as WsMessage);
|
|
327
|
+
const excludeSet = new Set(excludeClientIds || []);
|
|
328
|
+
|
|
329
|
+
for (const clientId of clientIds) {
|
|
330
|
+
if (!excludeSet.has(clientId)) {
|
|
331
|
+
const socket = clientSockets.get(clientId);
|
|
332
|
+
if (socket) {
|
|
333
|
+
socket.send(message);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Send a message to all clients in multiple rooms
|
|
341
|
+
*/
|
|
342
|
+
async emitToRooms(
|
|
343
|
+
roomNames: string[],
|
|
344
|
+
event: string,
|
|
345
|
+
data: unknown,
|
|
346
|
+
excludeClientIds?: string[],
|
|
347
|
+
): Promise<void> {
|
|
348
|
+
// Collect unique client IDs from all rooms
|
|
349
|
+
const clientIdsSet = new Set<string>();
|
|
350
|
+
|
|
351
|
+
for (const roomName of roomNames) {
|
|
352
|
+
if (this.storage) {
|
|
353
|
+
const ids = await this.storage.getClientsInRoom(roomName);
|
|
354
|
+
ids.forEach((id) => clientIdsSet.add(id));
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Send to each unique client
|
|
359
|
+
const message = JSON.stringify({ event, data } as WsMessage);
|
|
360
|
+
const excludeSet = new Set(excludeClientIds || []);
|
|
361
|
+
|
|
362
|
+
for (const clientId of clientIdsSet) {
|
|
363
|
+
if (!excludeSet.has(clientId)) {
|
|
364
|
+
const socket = clientSockets.get(clientId);
|
|
365
|
+
if (socket) {
|
|
366
|
+
socket.send(message);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Send a message to all clients in rooms matching a pattern
|
|
374
|
+
*/
|
|
375
|
+
async emitToRoomPattern(
|
|
376
|
+
pattern: string,
|
|
377
|
+
event: string,
|
|
378
|
+
data: unknown,
|
|
379
|
+
excludeClientIds?: string[],
|
|
380
|
+
): Promise<void> {
|
|
381
|
+
const rooms = await this.getRoomsByPattern(pattern);
|
|
382
|
+
const roomNames = rooms.map((r) => r.name);
|
|
383
|
+
await this.emitToRooms(roomNames, event, data, excludeClientIds);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// ============================================================================
|
|
387
|
+
// Connection Management
|
|
388
|
+
// ============================================================================
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Disconnect a specific client
|
|
392
|
+
*/
|
|
393
|
+
disconnectClient(clientId: string, reason?: string): void {
|
|
394
|
+
const socket = clientSockets.get(clientId);
|
|
395
|
+
if (socket) {
|
|
396
|
+
socket.close(1000, reason || 'Disconnected by server');
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Disconnect all clients
|
|
402
|
+
*/
|
|
403
|
+
disconnectAll(reason?: string): void {
|
|
404
|
+
for (const [_, socket] of clientSockets) {
|
|
405
|
+
socket.close(1000, reason || 'Server shutdown');
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Disconnect all clients in a room
|
|
411
|
+
*/
|
|
412
|
+
async disconnectRoom(roomName: string, reason?: string): Promise<void> {
|
|
413
|
+
if (!this.storage) {
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const clientIds = await this.storage.getClientsInRoom(roomName);
|
|
418
|
+
for (const clientId of clientIds) {
|
|
419
|
+
this.disconnectClient(clientId, reason);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Disconnect all clients in rooms matching a pattern
|
|
425
|
+
*/
|
|
426
|
+
async disconnectRoomPattern(pattern: string, reason?: string): Promise<void> {
|
|
427
|
+
const rooms = await this.getRoomsByPattern(pattern);
|
|
428
|
+
for (const room of rooms) {
|
|
429
|
+
await this.disconnectRoom(room.name, reason);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// ============================================================================
|
|
434
|
+
// Room Management
|
|
435
|
+
// ============================================================================
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Add a client to a room
|
|
439
|
+
*/
|
|
440
|
+
async joinRoom(clientId: string, roomName: string): Promise<void> {
|
|
441
|
+
if (!this.storage) {
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
await this.storage.addClientToRoom(clientId, roomName);
|
|
445
|
+
|
|
446
|
+
// Also subscribe to Bun's native pub/sub topic
|
|
447
|
+
const socket = clientSockets.get(clientId);
|
|
448
|
+
if (socket) {
|
|
449
|
+
socket.subscribe(roomName);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Remove a client from a room
|
|
455
|
+
*/
|
|
456
|
+
async leaveRoom(clientId: string, roomName: string): Promise<void> {
|
|
457
|
+
if (!this.storage) {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
await this.storage.removeClientFromRoom(clientId, roomName);
|
|
461
|
+
|
|
462
|
+
// Also unsubscribe from Bun's native pub/sub topic
|
|
463
|
+
const socket = clientSockets.get(clientId);
|
|
464
|
+
if (socket) {
|
|
465
|
+
socket.unsubscribe(roomName);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// ============================================================================
|
|
470
|
+
// Lifecycle Hooks (can be overridden)
|
|
471
|
+
// ============================================================================
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Called when a client connects
|
|
475
|
+
* Override in subclass to handle connection events
|
|
476
|
+
*/
|
|
477
|
+
protected onClientConnected(_client: WsClientData): void | Promise<void> {
|
|
478
|
+
// Default: no-op
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Called when a client disconnects
|
|
483
|
+
* Override in subclass to handle disconnection events
|
|
484
|
+
*/
|
|
485
|
+
protected onClientDisconnected(_client: WsClientData): void | Promise<void> {
|
|
486
|
+
// Default: no-op
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// ============================================================================
|
|
490
|
+
// Server Access
|
|
491
|
+
// ============================================================================
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Get WebSocket server wrapper
|
|
495
|
+
*/
|
|
496
|
+
getWsServer(): WsServer | null {
|
|
497
|
+
if (!this.server) {
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const server = this.server;
|
|
502
|
+
|
|
503
|
+
return {
|
|
504
|
+
server,
|
|
505
|
+
publish(topic: string, message: string | Buffer) {
|
|
506
|
+
server.publish(topic, message);
|
|
507
|
+
},
|
|
508
|
+
subscriberCount(_topic: string) {
|
|
509
|
+
// Bun doesn't expose this directly, so we count from our storage
|
|
510
|
+
return 0; // TODO: implement proper counting
|
|
511
|
+
},
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
}
|