@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,567 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Socket.IO Protocol Implementation
|
|
3
|
+
*
|
|
4
|
+
* Implements Engine.IO and Socket.IO protocols for compatibility with socket.io-client.
|
|
5
|
+
* Based on:
|
|
6
|
+
* - Engine.IO Protocol: https://socket.io/docs/v4/engine-io-protocol/
|
|
7
|
+
* - Socket.IO Protocol: https://socket.io/docs/v4/socket-io-protocol/
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Engine.IO Types
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Engine.IO packet types
|
|
16
|
+
*/
|
|
17
|
+
export enum EngineIOPacketType {
|
|
18
|
+
OPEN = 0,
|
|
19
|
+
CLOSE = 1,
|
|
20
|
+
PING = 2,
|
|
21
|
+
PONG = 3,
|
|
22
|
+
MESSAGE = 4,
|
|
23
|
+
UPGRADE = 5,
|
|
24
|
+
NOOP = 6,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Engine.IO packet
|
|
29
|
+
*/
|
|
30
|
+
export interface EngineIOPacket {
|
|
31
|
+
type: EngineIOPacketType;
|
|
32
|
+
data?: string | Buffer;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Engine.IO handshake data (sent with OPEN packet)
|
|
37
|
+
*/
|
|
38
|
+
export interface EngineIOHandshake {
|
|
39
|
+
/** Session ID */
|
|
40
|
+
sid: string;
|
|
41
|
+
/** Available upgrades (always ['websocket'] for our implementation) */
|
|
42
|
+
upgrades: string[];
|
|
43
|
+
/** Ping interval in milliseconds */
|
|
44
|
+
pingInterval: number;
|
|
45
|
+
/** Ping timeout in milliseconds */
|
|
46
|
+
pingTimeout: number;
|
|
47
|
+
/** Maximum payload size */
|
|
48
|
+
maxPayload: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ============================================================================
|
|
52
|
+
// Socket.IO Types
|
|
53
|
+
// ============================================================================
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Socket.IO packet types
|
|
57
|
+
*/
|
|
58
|
+
export enum SocketIOPacketType {
|
|
59
|
+
CONNECT = 0,
|
|
60
|
+
DISCONNECT = 1,
|
|
61
|
+
EVENT = 2,
|
|
62
|
+
ACK = 3,
|
|
63
|
+
CONNECT_ERROR = 4,
|
|
64
|
+
BINARY_EVENT = 5,
|
|
65
|
+
BINARY_ACK = 6,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Socket.IO packet
|
|
70
|
+
*/
|
|
71
|
+
export interface SocketIOPacket {
|
|
72
|
+
type: SocketIOPacketType;
|
|
73
|
+
/** Namespace */
|
|
74
|
+
nsp: string;
|
|
75
|
+
/** Event data (for EVENT and ACK types) */
|
|
76
|
+
data?: unknown[];
|
|
77
|
+
/** Acknowledgement ID */
|
|
78
|
+
id?: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Socket.IO connect error data
|
|
83
|
+
*/
|
|
84
|
+
export interface SocketIOConnectError {
|
|
85
|
+
message: string;
|
|
86
|
+
data?: unknown;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ============================================================================
|
|
90
|
+
// Default Configuration
|
|
91
|
+
// ============================================================================
|
|
92
|
+
|
|
93
|
+
export const DEFAULT_PING_INTERVAL = 25000; // 25 seconds
|
|
94
|
+
export const DEFAULT_PING_TIMEOUT = 20000; // 20 seconds
|
|
95
|
+
export const DEFAULT_MAX_PAYLOAD = 1024 * 1024; // 1 MB
|
|
96
|
+
|
|
97
|
+
// ============================================================================
|
|
98
|
+
// Engine.IO Protocol
|
|
99
|
+
// ============================================================================
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Encode an Engine.IO packet
|
|
103
|
+
*/
|
|
104
|
+
export function encodeEngineIOPacket(packet: EngineIOPacket): string {
|
|
105
|
+
if (packet.data === undefined) {
|
|
106
|
+
return String(packet.type);
|
|
107
|
+
}
|
|
108
|
+
if (typeof packet.data === 'string') {
|
|
109
|
+
return String(packet.type) + packet.data;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Buffer data - convert to base64
|
|
113
|
+
return String(packet.type) + 'b' + packet.data.toString('base64');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Decode an Engine.IO packet
|
|
118
|
+
*/
|
|
119
|
+
export function decodeEngineIOPacket(data: string | Buffer): EngineIOPacket {
|
|
120
|
+
const str = typeof data === 'string' ? data : data.toString();
|
|
121
|
+
|
|
122
|
+
if (str.length === 0) {
|
|
123
|
+
return { type: EngineIOPacketType.NOOP };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const type = parseInt(str.charAt(0), 10) as EngineIOPacketType;
|
|
127
|
+
|
|
128
|
+
if (str.length === 1) {
|
|
129
|
+
return { type };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const packetData = str.substring(1);
|
|
133
|
+
|
|
134
|
+
// Check for binary data (starts with 'b')
|
|
135
|
+
if (packetData.charAt(0) === 'b') {
|
|
136
|
+
return {
|
|
137
|
+
type,
|
|
138
|
+
data: Buffer.from(packetData.substring(1), 'base64'),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return { type, data: packetData };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Create handshake data for new connection
|
|
147
|
+
*/
|
|
148
|
+
export function createHandshake(
|
|
149
|
+
sid: string,
|
|
150
|
+
options?: Partial<EngineIOHandshake>,
|
|
151
|
+
): EngineIOHandshake {
|
|
152
|
+
return {
|
|
153
|
+
sid,
|
|
154
|
+
upgrades: ['websocket'],
|
|
155
|
+
pingInterval: options?.pingInterval ?? DEFAULT_PING_INTERVAL,
|
|
156
|
+
pingTimeout: options?.pingTimeout ?? DEFAULT_PING_TIMEOUT,
|
|
157
|
+
maxPayload: options?.maxPayload ?? DEFAULT_MAX_PAYLOAD,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Create OPEN packet with handshake data
|
|
163
|
+
*/
|
|
164
|
+
export function createOpenPacket(handshake: EngineIOHandshake): string {
|
|
165
|
+
return encodeEngineIOPacket({
|
|
166
|
+
type: EngineIOPacketType.OPEN,
|
|
167
|
+
data: JSON.stringify(handshake),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Create PING packet
|
|
173
|
+
*/
|
|
174
|
+
export function createPingPacket(data?: string): string {
|
|
175
|
+
return encodeEngineIOPacket({
|
|
176
|
+
type: EngineIOPacketType.PING,
|
|
177
|
+
data,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Create PONG packet
|
|
183
|
+
*/
|
|
184
|
+
export function createPongPacket(data?: string): string {
|
|
185
|
+
return encodeEngineIOPacket({
|
|
186
|
+
type: EngineIOPacketType.PONG,
|
|
187
|
+
data,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ============================================================================
|
|
192
|
+
// Socket.IO Protocol
|
|
193
|
+
// ============================================================================
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Encode a Socket.IO packet
|
|
197
|
+
*/
|
|
198
|
+
export function encodeSocketIOPacket(packet: SocketIOPacket): string {
|
|
199
|
+
let str = String(packet.type);
|
|
200
|
+
|
|
201
|
+
// Add namespace if not default
|
|
202
|
+
if (packet.nsp && packet.nsp !== '/') {
|
|
203
|
+
str += packet.nsp + ',';
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Add acknowledgement ID
|
|
207
|
+
if (packet.id !== undefined) {
|
|
208
|
+
str += String(packet.id);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Add data
|
|
212
|
+
if (packet.data !== undefined) {
|
|
213
|
+
str += JSON.stringify(packet.data);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return str;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Decode a Socket.IO packet
|
|
221
|
+
*/
|
|
222
|
+
export function decodeSocketIOPacket(data: string): SocketIOPacket {
|
|
223
|
+
let i = 0;
|
|
224
|
+
|
|
225
|
+
// Parse type
|
|
226
|
+
const type = parseInt(data.charAt(i++), 10) as SocketIOPacketType;
|
|
227
|
+
|
|
228
|
+
// Parse namespace
|
|
229
|
+
let nsp = '/';
|
|
230
|
+
if (data.charAt(i) === '/') {
|
|
231
|
+
const start = i;
|
|
232
|
+
while (i < data.length && data.charAt(i) !== ',') {
|
|
233
|
+
i++;
|
|
234
|
+
}
|
|
235
|
+
nsp = data.substring(start, i);
|
|
236
|
+
i++; // Skip comma
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Parse acknowledgement ID
|
|
240
|
+
let id: number | undefined;
|
|
241
|
+
if (i < data.length) {
|
|
242
|
+
const start = i;
|
|
243
|
+
while (i < data.length && !isNaN(parseInt(data.charAt(i), 10))) {
|
|
244
|
+
i++;
|
|
245
|
+
}
|
|
246
|
+
if (start !== i) {
|
|
247
|
+
id = parseInt(data.substring(start, i), 10);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Parse data
|
|
252
|
+
let packetData: unknown[] | undefined;
|
|
253
|
+
if (i < data.length) {
|
|
254
|
+
try {
|
|
255
|
+
packetData = JSON.parse(data.substring(i));
|
|
256
|
+
} catch {
|
|
257
|
+
// Invalid JSON, ignore data
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
type, nsp, id, data: packetData,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Create Socket.IO CONNECT packet
|
|
268
|
+
*/
|
|
269
|
+
export function createConnectPacket(nsp: string = '/', authData?: unknown): string {
|
|
270
|
+
const packet: SocketIOPacket = {
|
|
271
|
+
type: SocketIOPacketType.CONNECT,
|
|
272
|
+
nsp,
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
if (authData !== undefined) {
|
|
276
|
+
packet.data = [authData];
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return encodeSocketIOPacket(packet);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Create Socket.IO DISCONNECT packet
|
|
284
|
+
*/
|
|
285
|
+
export function createDisconnectPacket(nsp: string = '/'): string {
|
|
286
|
+
return encodeSocketIOPacket({
|
|
287
|
+
type: SocketIOPacketType.DISCONNECT,
|
|
288
|
+
nsp,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Create Socket.IO EVENT packet
|
|
294
|
+
*/
|
|
295
|
+
export function createEventPacket(
|
|
296
|
+
event: string,
|
|
297
|
+
data: unknown,
|
|
298
|
+
nsp: string = '/',
|
|
299
|
+
ackId?: number,
|
|
300
|
+
): string {
|
|
301
|
+
const packet: SocketIOPacket = {
|
|
302
|
+
type: SocketIOPacketType.EVENT,
|
|
303
|
+
nsp,
|
|
304
|
+
data: [event, data],
|
|
305
|
+
id: ackId,
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
return encodeSocketIOPacket(packet);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Create Socket.IO ACK packet
|
|
313
|
+
*/
|
|
314
|
+
export function createAckPacket(ackId: number, data: unknown, nsp: string = '/'): string {
|
|
315
|
+
return encodeSocketIOPacket({
|
|
316
|
+
type: SocketIOPacketType.ACK,
|
|
317
|
+
nsp,
|
|
318
|
+
id: ackId,
|
|
319
|
+
data: Array.isArray(data) ? data : [data],
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Create Socket.IO CONNECT_ERROR packet
|
|
325
|
+
*/
|
|
326
|
+
export function createConnectErrorPacket(
|
|
327
|
+
error: SocketIOConnectError,
|
|
328
|
+
nsp: string = '/',
|
|
329
|
+
): string {
|
|
330
|
+
return encodeSocketIOPacket({
|
|
331
|
+
type: SocketIOPacketType.CONNECT_ERROR,
|
|
332
|
+
nsp,
|
|
333
|
+
data: [error],
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ============================================================================
|
|
338
|
+
// Combined Message Handling
|
|
339
|
+
// ============================================================================
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Wrap Socket.IO packet in Engine.IO MESSAGE packet
|
|
343
|
+
*/
|
|
344
|
+
export function wrapInEngineIO(socketIOPacket: string): string {
|
|
345
|
+
return encodeEngineIOPacket({
|
|
346
|
+
type: EngineIOPacketType.MESSAGE,
|
|
347
|
+
data: socketIOPacket,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Extract Socket.IO packet from Engine.IO MESSAGE packet
|
|
353
|
+
*/
|
|
354
|
+
export function unwrapFromEngineIO(engineIOPacket: string): string | null {
|
|
355
|
+
const decoded = decodeEngineIOPacket(engineIOPacket);
|
|
356
|
+
|
|
357
|
+
if (decoded.type !== EngineIOPacketType.MESSAGE || typeof decoded.data !== 'string') {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return decoded.data;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Parse incoming message (Engine.IO wrapped Socket.IO)
|
|
366
|
+
*/
|
|
367
|
+
export function parseMessage(
|
|
368
|
+
data: string | Buffer,
|
|
369
|
+
): { engineIO: EngineIOPacket; socketIO?: SocketIOPacket } {
|
|
370
|
+
const engineIO = decodeEngineIOPacket(data);
|
|
371
|
+
|
|
372
|
+
if (engineIO.type === EngineIOPacketType.MESSAGE && typeof engineIO.data === 'string') {
|
|
373
|
+
const socketIO = decodeSocketIOPacket(engineIO.data);
|
|
374
|
+
|
|
375
|
+
return { engineIO, socketIO };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return { engineIO };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Create full message (Engine.IO + Socket.IO) for sending event
|
|
383
|
+
*/
|
|
384
|
+
export function createFullEventMessage(
|
|
385
|
+
event: string,
|
|
386
|
+
eventData: unknown,
|
|
387
|
+
nsp: string = '/',
|
|
388
|
+
ackId?: number,
|
|
389
|
+
): string {
|
|
390
|
+
const socketIOPacket = createEventPacket(event, eventData, nsp, ackId);
|
|
391
|
+
|
|
392
|
+
return wrapInEngineIO(socketIOPacket);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Create full message for acknowledgement
|
|
397
|
+
*/
|
|
398
|
+
export function createFullAckMessage(
|
|
399
|
+
ackId: number,
|
|
400
|
+
data: unknown,
|
|
401
|
+
nsp: string = '/',
|
|
402
|
+
): string {
|
|
403
|
+
const socketIOPacket = createAckPacket(ackId, data, nsp);
|
|
404
|
+
|
|
405
|
+
return wrapInEngineIO(socketIOPacket);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ============================================================================
|
|
409
|
+
// HTTP Polling Support (for Engine.IO)
|
|
410
|
+
// ============================================================================
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Parse Engine.IO HTTP request query parameters
|
|
414
|
+
*/
|
|
415
|
+
export function parseEngineIOQuery(url: URL): {
|
|
416
|
+
EIO: string;
|
|
417
|
+
transport: string;
|
|
418
|
+
sid?: string;
|
|
419
|
+
t?: string;
|
|
420
|
+
b64?: boolean;
|
|
421
|
+
} {
|
|
422
|
+
return {
|
|
423
|
+
EIO: url.searchParams.get('EIO') || '4',
|
|
424
|
+
transport: url.searchParams.get('transport') || 'polling',
|
|
425
|
+
sid: url.searchParams.get('sid') || undefined,
|
|
426
|
+
t: url.searchParams.get('t') || undefined,
|
|
427
|
+
b64: url.searchParams.get('b64') === '1',
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Check if request is Engine.IO polling request
|
|
433
|
+
*/
|
|
434
|
+
export function isPollingRequest(url: URL, basePath: string = '/socket.io'): boolean {
|
|
435
|
+
return url.pathname.startsWith(basePath) &&
|
|
436
|
+
url.searchParams.get('transport') === 'polling';
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Check if request is Engine.IO websocket upgrade
|
|
441
|
+
*/
|
|
442
|
+
export function isWebSocketUpgrade(url: URL, basePath: string = '/socket.io'): boolean {
|
|
443
|
+
return url.pathname.startsWith(basePath) &&
|
|
444
|
+
url.searchParams.get('transport') === 'websocket';
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Create polling response with multiple packets
|
|
449
|
+
*/
|
|
450
|
+
export function createPollingResponse(packets: string[]): string {
|
|
451
|
+
// Concatenate packets with length prefix
|
|
452
|
+
return packets.map((p) => `${p.length}:${p}`).join('');
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Parse polling request body with multiple packets
|
|
457
|
+
*/
|
|
458
|
+
export function parsePollingRequest(body: string): string[] {
|
|
459
|
+
const packets: string[] = [];
|
|
460
|
+
let i = 0;
|
|
461
|
+
|
|
462
|
+
while (i < body.length) {
|
|
463
|
+
// Find length separator
|
|
464
|
+
const colonIndex = body.indexOf(':', i);
|
|
465
|
+
if (colonIndex === -1) {
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const length = parseInt(body.substring(i, colonIndex), 10);
|
|
470
|
+
const start = colonIndex + 1;
|
|
471
|
+
const end = start + length;
|
|
472
|
+
|
|
473
|
+
packets.push(body.substring(start, end));
|
|
474
|
+
i = end;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return packets;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// ============================================================================
|
|
481
|
+
// Native WebSocket Message Handling (non-Socket.IO)
|
|
482
|
+
// ============================================================================
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Simple message format for native WebSocket clients
|
|
486
|
+
*/
|
|
487
|
+
export interface NativeWsMessage<T = unknown> {
|
|
488
|
+
event: string;
|
|
489
|
+
data: T;
|
|
490
|
+
ack?: number;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Check if message is in native format
|
|
495
|
+
*/
|
|
496
|
+
export function isNativeMessage(data: string): boolean {
|
|
497
|
+
try {
|
|
498
|
+
const parsed = JSON.parse(data);
|
|
499
|
+
|
|
500
|
+
return typeof parsed === 'object' &&
|
|
501
|
+
parsed !== null &&
|
|
502
|
+
'event' in parsed &&
|
|
503
|
+
typeof parsed.event === 'string';
|
|
504
|
+
} catch {
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Parse native WebSocket message
|
|
511
|
+
*/
|
|
512
|
+
export function parseNativeMessage(data: string): NativeWsMessage | null {
|
|
513
|
+
try {
|
|
514
|
+
const parsed = JSON.parse(data);
|
|
515
|
+
if (typeof parsed === 'object' &&
|
|
516
|
+
parsed !== null &&
|
|
517
|
+
'event' in parsed &&
|
|
518
|
+
typeof parsed.event === 'string') {
|
|
519
|
+
return parsed as NativeWsMessage;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return null;
|
|
523
|
+
} catch {
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Create native WebSocket message
|
|
530
|
+
*/
|
|
531
|
+
export function createNativeMessage<T>(event: string, data: T, ack?: number): string {
|
|
532
|
+
const message: NativeWsMessage<T> = { event, data };
|
|
533
|
+
if (ack !== undefined) {
|
|
534
|
+
message.ack = ack;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
return JSON.stringify(message);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// ============================================================================
|
|
541
|
+
// Protocol Detection
|
|
542
|
+
// ============================================================================
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Detected protocol type
|
|
546
|
+
*/
|
|
547
|
+
export type ProtocolType = 'socket.io' | 'native';
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Detect which protocol is being used
|
|
551
|
+
*/
|
|
552
|
+
export function detectProtocol(data: string | Buffer): ProtocolType {
|
|
553
|
+
const str = typeof data === 'string' ? data : data.toString();
|
|
554
|
+
|
|
555
|
+
// Socket.IO messages start with a digit (packet type)
|
|
556
|
+
if (/^\d/.test(str)) {
|
|
557
|
+
return 'socket.io';
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Try to parse as native JSON
|
|
561
|
+
if (isNativeMessage(str)) {
|
|
562
|
+
return 'native';
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Default to socket.io
|
|
566
|
+
return 'socket.io';
|
|
567
|
+
}
|