stelar-time-real 3.2.1 → 3.3.0
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/package.json +1 -1
- package/src/client.d.ts +47 -59
- package/src/client.d.ts.map +1 -1
- package/src/client.js +406 -728
- package/src/client.ts +317 -908
- package/src/index.d.ts +84 -124
- package/src/index.d.ts.map +1 -1
- package/src/index.js +740 -1165
- package/src/index.ts +552 -1574
- package/src/logger.d.ts +12 -17
- package/src/logger.d.ts.map +1 -1
- package/src/logger.js +34 -90
- package/src/logger.ts +31 -98
- package/src/protocol.d.ts +16 -34
- package/src/protocol.d.ts.map +1 -1
- package/src/protocol.js +56 -148
- package/src/protocol.ts +66 -188
- package/src/websocket.d.ts +21 -43
- package/src/websocket.d.ts.map +1 -1
- package/src/websocket.js +106 -216
- package/src/websocket.ts +78 -279
package/src/protocol.js
CHANGED
|
@@ -1,25 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @stelar-time-real Binary Protocol
|
|
3
|
-
*
|
|
4
|
-
* Frame format:
|
|
5
|
-
* [4B totalLen BE][1B type][2B eventLen BE][eventLen bytes event][payload]
|
|
6
|
-
*
|
|
7
|
-
* Min frame: 7 bytes (header only). Max event name: 256 bytes.
|
|
3
|
+
* Frame: [4B totalLen BE][1B type][2B eventLen BE][event][payload]
|
|
8
4
|
*/
|
|
9
|
-
export const FRAME_JSON = 0x01;
|
|
10
|
-
export const FRAME_BINARY = 0x02;
|
|
11
|
-
export const FRAME_PING = 0x03;
|
|
12
|
-
export const FRAME_PONG = 0x04;
|
|
13
|
-
export const FRAME_ACK_REQ = 0x05;
|
|
14
|
-
export const FRAME_ACK_RES = 0x06;
|
|
15
|
-
export const FRAME_CONNECT = 0x07;
|
|
16
|
-
export const FRAME_DISCONNECT = 0x08;
|
|
17
|
-
export const FRAME_JOIN = 0x09;
|
|
18
|
-
export const FRAME_LEAVE = 0x0A;
|
|
19
|
-
export const FRAME_ERROR = 0x0B;
|
|
20
|
-
/** Max event name length in bytes */
|
|
5
|
+
export const FRAME_JSON = 0x01, FRAME_BINARY = 0x02, FRAME_PING = 0x03, FRAME_PONG = 0x04, FRAME_ACK_REQ = 0x05, FRAME_ACK_RES = 0x06, FRAME_CONNECT = 0x07, FRAME_DISCONNECT = 0x08, FRAME_JOIN = 0x09, FRAME_LEAVE = 0x0A, FRAME_ERROR = 0x0B;
|
|
21
6
|
export const MAX_EVENT_LENGTH = 256;
|
|
22
|
-
/** Default max frame size: 10 MB */
|
|
23
7
|
export const DEFAULT_MAX_FRAME_SIZE = 10 * 1024 * 1024;
|
|
24
8
|
export const HEADER_SIZE = 7;
|
|
25
9
|
export class ProtocolError extends Error {
|
|
@@ -29,165 +13,89 @@ export class ProtocolError extends Error {
|
|
|
29
13
|
this.code = code;
|
|
30
14
|
}
|
|
31
15
|
}
|
|
32
|
-
/** Validates event name format. Throws ProtocolError on invalid input. */
|
|
33
16
|
export function validateEventName(event) {
|
|
34
|
-
if (typeof event !== 'string')
|
|
17
|
+
if (typeof event !== 'string')
|
|
35
18
|
throw new ProtocolError('Event name must be a string', 'INVALID_EVENT');
|
|
36
|
-
|
|
37
|
-
if (event.length === 0) {
|
|
19
|
+
if (!event)
|
|
38
20
|
throw new ProtocolError('Event name cannot be empty', 'EMPTY_EVENT');
|
|
39
|
-
|
|
40
|
-
if (event.length > MAX_EVENT_LENGTH) {
|
|
21
|
+
if (event.length > MAX_EVENT_LENGTH)
|
|
41
22
|
throw new ProtocolError(`Event name exceeds ${MAX_EVENT_LENGTH} bytes`, 'EVENT_TOO_LONG');
|
|
42
|
-
|
|
43
|
-
if (!/^[\w\-./:]+$/.test(event)) {
|
|
23
|
+
if (!/^[a-zA-Z0-9\-./:]+$/.test(event))
|
|
44
24
|
throw new ProtocolError('Event name contains invalid characters', 'INVALID_EVENT_CHARS');
|
|
45
|
-
|
|
46
|
-
if (['ping', 'pong', 'connect', 'disconnect', 'error'].includes(event)) {
|
|
25
|
+
if (['ping', 'pong', 'connect', 'disconnect', 'error'].includes(event))
|
|
47
26
|
throw new ProtocolError(`Event "${event}" is reserved`, 'RESERVED_EVENT');
|
|
48
|
-
}
|
|
49
27
|
}
|
|
50
|
-
function
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
function encodeFrame(type, event, payload, maxFrameSize = DEFAULT_MAX_FRAME_SIZE) {
|
|
56
|
-
if (event.length > MAX_EVENT_LENGTH) {
|
|
28
|
+
function encode(type, event, payload, max = DEFAULT_MAX_FRAME_SIZE) {
|
|
29
|
+
const eb = Buffer.from(event, 'utf8');
|
|
30
|
+
if (eb.length > MAX_EVENT_LENGTH)
|
|
57
31
|
throw new ProtocolError(`Event name exceeds ${MAX_EVENT_LENGTH} bytes`, 'EVENT_TOO_LONG');
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
eventBuf.copy(frame, HEADER_SIZE);
|
|
70
|
-
if (payload.length > 0)
|
|
71
|
-
payload.copy(frame, HEADER_SIZE + eventBuf.length);
|
|
72
|
-
return frame;
|
|
73
|
-
}
|
|
74
|
-
export function encodeJsonFrame(event, data, maxFrameSize) {
|
|
75
|
-
validateEventName(event);
|
|
76
|
-
const payload = Buffer.from(JSON.stringify(data), 'utf8');
|
|
77
|
-
if (maxFrameSize)
|
|
78
|
-
validatePayloadSize(payload, maxFrameSize);
|
|
79
|
-
return encodeFrame(FRAME_JSON, event, payload, maxFrameSize);
|
|
80
|
-
}
|
|
81
|
-
export function encodeBinaryFrame(event, data, maxFrameSize) {
|
|
82
|
-
validateEventName(event);
|
|
83
|
-
const payload = Buffer.from(data);
|
|
84
|
-
if (maxFrameSize)
|
|
85
|
-
validatePayloadSize(payload, maxFrameSize);
|
|
86
|
-
return encodeFrame(FRAME_BINARY, event, payload, maxFrameSize);
|
|
87
|
-
}
|
|
88
|
-
export function encodePingFrame() {
|
|
89
|
-
const f = Buffer.alloc(HEADER_SIZE);
|
|
90
|
-
f.writeUInt32BE(HEADER_SIZE, 0);
|
|
91
|
-
f[4] = FRAME_PING;
|
|
92
|
-
f.writeUInt16BE(0, 5);
|
|
93
|
-
return f;
|
|
94
|
-
}
|
|
95
|
-
export function encodePongFrame() {
|
|
96
|
-
const f = Buffer.alloc(HEADER_SIZE);
|
|
97
|
-
f.writeUInt32BE(HEADER_SIZE, 0);
|
|
98
|
-
f[4] = FRAME_PONG;
|
|
99
|
-
f.writeUInt16BE(0, 5);
|
|
32
|
+
const total = HEADER_SIZE + eb.length + payload.length;
|
|
33
|
+
if (total > max)
|
|
34
|
+
throw new ProtocolError(`Frame exceeds max size (${max} bytes)`, 'FRAME_TOO_LARGE');
|
|
35
|
+
const f = Buffer.alloc(total);
|
|
36
|
+
f.writeUInt32BE(total, 0);
|
|
37
|
+
f[4] = type;
|
|
38
|
+
f.writeUInt16BE(eb.length, 5);
|
|
39
|
+
if (eb.length)
|
|
40
|
+
eb.copy(f, HEADER_SIZE);
|
|
41
|
+
if (payload.length)
|
|
42
|
+
payload.copy(f, HEADER_SIZE + eb.length);
|
|
100
43
|
return f;
|
|
101
44
|
}
|
|
102
|
-
|
|
103
|
-
const payload = Buffer.from(JSON.stringify(data), 'utf8');
|
|
104
|
-
if (maxFrameSize)
|
|
105
|
-
validatePayloadSize(payload, maxFrameSize);
|
|
106
|
-
return encodeFrame(FRAME_ACK_REQ, ackName, payload, maxFrameSize);
|
|
107
|
-
}
|
|
108
|
-
export function encodeAckResFrame(ackName, data, maxFrameSize) {
|
|
109
|
-
const payload = Buffer.from(JSON.stringify(data), 'utf8');
|
|
110
|
-
if (maxFrameSize)
|
|
111
|
-
validatePayloadSize(payload, maxFrameSize);
|
|
112
|
-
return encodeFrame(FRAME_ACK_RES, ackName, payload, maxFrameSize);
|
|
113
|
-
}
|
|
114
|
-
export function encodeConnectFrame(clientId) {
|
|
115
|
-
return encodeFrame(FRAME_CONNECT, 'connect', Buffer.from(clientId, 'utf8'));
|
|
116
|
-
}
|
|
117
|
-
export function encodeDisconnectFrame() {
|
|
45
|
+
const emptyFrame = (type) => {
|
|
118
46
|
const f = Buffer.alloc(HEADER_SIZE);
|
|
119
47
|
f.writeUInt32BE(HEADER_SIZE, 0);
|
|
120
|
-
f[4] =
|
|
48
|
+
f[4] = type;
|
|
121
49
|
f.writeUInt16BE(0, 5);
|
|
122
50
|
return f;
|
|
123
|
-
}
|
|
124
|
-
export
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
export
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
export
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
/** Streaming frame parser for TCP connections. Buffers partial data and emits complete frames. */
|
|
51
|
+
};
|
|
52
|
+
export const encodeJsonFrame = (event, data, max) => (validateEventName(event), encode(FRAME_JSON, event, Buffer.from(JSON.stringify(data), 'utf8'), max));
|
|
53
|
+
export const encodeBinaryFrame = (event, data, max) => (validateEventName(event), encode(FRAME_BINARY, event, Buffer.from(data), max));
|
|
54
|
+
export const encodePingFrame = () => emptyFrame(FRAME_PING);
|
|
55
|
+
export const encodePongFrame = () => emptyFrame(FRAME_PONG);
|
|
56
|
+
export const encodeAckReqFrame = (name, data, max) => encode(FRAME_ACK_REQ, name, Buffer.from(JSON.stringify(data), 'utf8'), max);
|
|
57
|
+
export const encodeAckResFrame = (name, data, max) => encode(FRAME_ACK_RES, name, Buffer.from(JSON.stringify(data), 'utf8'), max);
|
|
58
|
+
export const encodeConnectFrame = (id) => encode(FRAME_CONNECT, 'connect', Buffer.from(id, 'utf8'));
|
|
59
|
+
export const encodeDisconnectFrame = () => emptyFrame(FRAME_DISCONNECT);
|
|
60
|
+
export const encodeJoinFrame = (room, max) => encode(FRAME_JOIN, 'join-room', Buffer.from(room, 'utf8'), max);
|
|
61
|
+
export const encodeLeaveFrame = (room) => encode(FRAME_LEAVE, 'leave-room', room ? Buffer.from(room, 'utf8') : Buffer.alloc(0));
|
|
62
|
+
export const encodeErrorFrame = (msg) => encode(FRAME_ERROR, 'error', Buffer.from(msg, 'utf8'));
|
|
136
63
|
export class FrameParser {
|
|
137
|
-
constructor(
|
|
64
|
+
constructor(max = DEFAULT_MAX_FRAME_SIZE) {
|
|
138
65
|
this.buf = Buffer.alloc(0);
|
|
139
|
-
this.
|
|
140
|
-
this.
|
|
66
|
+
this.received = 0;
|
|
67
|
+
this.max = max;
|
|
141
68
|
}
|
|
142
69
|
feed(data) {
|
|
143
|
-
this.
|
|
70
|
+
this.received += data.length;
|
|
144
71
|
this.buf = Buffer.concat([this.buf, data]);
|
|
145
|
-
if (this.buf.length > this.
|
|
72
|
+
if (this.buf.length > this.max * 2) {
|
|
146
73
|
this.buf = Buffer.alloc(0);
|
|
147
|
-
throw new ProtocolError(`
|
|
74
|
+
throw new ProtocolError(`Buffer overflow (${this.max * 2})`, 'BUFFER_OVERFLOW');
|
|
148
75
|
}
|
|
149
76
|
const frames = [];
|
|
150
77
|
while (this.buf.length >= HEADER_SIZE) {
|
|
151
|
-
const
|
|
152
|
-
if (
|
|
153
|
-
this.buf = Buffer.alloc(0);
|
|
154
|
-
throw new ProtocolError(`Invalid frame size: ${totalLen}`, 'INVALID_FRAME_SIZE');
|
|
155
|
-
}
|
|
156
|
-
if (totalLen > this.maxFrameSize) {
|
|
78
|
+
const total = this.buf.readUInt32BE(0);
|
|
79
|
+
if (total < HEADER_SIZE || total > this.max) {
|
|
157
80
|
this.buf = Buffer.alloc(0);
|
|
158
|
-
throw new ProtocolError(`
|
|
81
|
+
throw new ProtocolError(`Invalid frame size: ${total}`, total < HEADER_SIZE ? 'INVALID_FRAME_SIZE' : 'FRAME_TOO_LARGE');
|
|
159
82
|
}
|
|
160
|
-
if (this.buf.length <
|
|
83
|
+
if (this.buf.length < total)
|
|
161
84
|
break;
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
if (HEADER_SIZE + eventLen > totalLen) {
|
|
165
|
-
this.buf = Buffer.alloc(0);
|
|
166
|
-
throw new ProtocolError('Event length exceeds frame bounds', 'INVALID_EVENT_LENGTH');
|
|
167
|
-
}
|
|
168
|
-
if (eventLen > MAX_EVENT_LENGTH) {
|
|
85
|
+
const el = this.buf.readUInt16BE(5);
|
|
86
|
+
if (HEADER_SIZE + el > total || el > MAX_EVENT_LENGTH) {
|
|
169
87
|
this.buf = Buffer.alloc(0);
|
|
170
|
-
throw new ProtocolError(
|
|
88
|
+
throw new ProtocolError('Invalid event length', 'INVALID_EVENT_LENGTH');
|
|
171
89
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
: ''
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
: Buffer.alloc(0);
|
|
179
|
-
frames.push({ type, event, payload });
|
|
180
|
-
this.buf = totalLen < this.buf.length
|
|
181
|
-
? Buffer.from(this.buf.subarray(totalLen))
|
|
182
|
-
: Buffer.alloc(0);
|
|
90
|
+
frames.push({
|
|
91
|
+
type: this.buf[4],
|
|
92
|
+
event: el ? this.buf.subarray(HEADER_SIZE, HEADER_SIZE + el).toString('utf8') : '',
|
|
93
|
+
payload: total > HEADER_SIZE + el ? Buffer.from(this.buf.subarray(HEADER_SIZE + el, total)) : Buffer.alloc(0),
|
|
94
|
+
});
|
|
95
|
+
this.buf = total < this.buf.length ? Buffer.from(this.buf.subarray(total)) : Buffer.alloc(0);
|
|
183
96
|
}
|
|
184
97
|
return frames;
|
|
185
98
|
}
|
|
186
|
-
reset() {
|
|
187
|
-
|
|
188
|
-
this.totalBytesReceived = 0;
|
|
189
|
-
}
|
|
190
|
-
getBytesReceived() {
|
|
191
|
-
return this.totalBytesReceived;
|
|
192
|
-
}
|
|
99
|
+
reset() { this.buf = Buffer.alloc(0); this.received = 0; }
|
|
100
|
+
getBytesReceived() { return this.received; }
|
|
193
101
|
}
|
package/src/protocol.ts
CHANGED
|
@@ -1,38 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @stelar-time-real Binary Protocol
|
|
3
|
-
*
|
|
4
|
-
* Frame format:
|
|
5
|
-
* [4B totalLen BE][1B type][2B eventLen BE][eventLen bytes event][payload]
|
|
6
|
-
*
|
|
7
|
-
* Min frame: 7 bytes (header only). Max event name: 256 bytes.
|
|
3
|
+
* Frame: [4B totalLen BE][1B type][2B eventLen BE][event][payload]
|
|
8
4
|
*/
|
|
9
5
|
|
|
10
|
-
export const FRAME_JSON
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
export const FRAME_ACK_RES = 0x06;
|
|
16
|
-
export const FRAME_CONNECT = 0x07;
|
|
17
|
-
export const FRAME_DISCONNECT = 0x08;
|
|
18
|
-
export const FRAME_JOIN = 0x09;
|
|
19
|
-
export const FRAME_LEAVE = 0x0A;
|
|
20
|
-
export const FRAME_ERROR = 0x0B;
|
|
21
|
-
|
|
22
|
-
/** Max event name length in bytes */
|
|
6
|
+
export const FRAME_JSON = 0x01, FRAME_BINARY = 0x02, FRAME_PING = 0x03,
|
|
7
|
+
FRAME_PONG = 0x04, FRAME_ACK_REQ = 0x05, FRAME_ACK_RES = 0x06,
|
|
8
|
+
FRAME_CONNECT = 0x07, FRAME_DISCONNECT = 0x08, FRAME_JOIN = 0x09,
|
|
9
|
+
FRAME_LEAVE = 0x0A, FRAME_ERROR = 0x0B;
|
|
10
|
+
|
|
23
11
|
export const MAX_EVENT_LENGTH = 256;
|
|
24
|
-
/** Default max frame size: 10 MB */
|
|
25
12
|
export const DEFAULT_MAX_FRAME_SIZE = 10 * 1024 * 1024;
|
|
26
13
|
export const HEADER_SIZE = 7;
|
|
27
14
|
|
|
28
|
-
export interface ParsedFrame {
|
|
29
|
-
type: number;
|
|
30
|
-
event: string;
|
|
31
|
-
payload: Buffer;
|
|
32
|
-
}
|
|
15
|
+
export interface ParsedFrame { type: number; event: string; payload: Buffer; }
|
|
33
16
|
|
|
34
17
|
export class ProtocolError extends Error {
|
|
35
|
-
|
|
18
|
+
code: string;
|
|
36
19
|
constructor(message: string, code = 'PROTOCOL_ERROR') {
|
|
37
20
|
super(message);
|
|
38
21
|
this.name = 'ProtocolError';
|
|
@@ -40,198 +23,93 @@ export class ProtocolError extends Error {
|
|
|
40
23
|
}
|
|
41
24
|
}
|
|
42
25
|
|
|
43
|
-
/** Validates event name format. Throws ProtocolError on invalid input. */
|
|
44
26
|
export function validateEventName(event: string): void {
|
|
45
|
-
if (typeof event !== 'string')
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
if (event
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (payload.length > maxSize) {
|
|
64
|
-
throw new ProtocolError(`Payload exceeds max size (${maxSize} bytes)`, 'PAYLOAD_TOO_LARGE');
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function encodeFrame(type: number, event: string, payload: Buffer, maxFrameSize = DEFAULT_MAX_FRAME_SIZE): Buffer {
|
|
69
|
-
if (event.length > MAX_EVENT_LENGTH) {
|
|
70
|
-
throw new ProtocolError(`Event name exceeds ${MAX_EVENT_LENGTH} bytes`, 'EVENT_TOO_LONG');
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const eventBuf = Buffer.from(event, 'utf8');
|
|
74
|
-
const totalLen = HEADER_SIZE + eventBuf.length + payload.length;
|
|
75
|
-
|
|
76
|
-
if (totalLen > maxFrameSize) {
|
|
77
|
-
throw new ProtocolError(`Frame exceeds max size (${maxFrameSize} bytes)`, 'FRAME_TOO_LARGE');
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const frame = Buffer.alloc(totalLen);
|
|
81
|
-
frame.writeUInt32BE(totalLen, 0);
|
|
82
|
-
frame[4] = type;
|
|
83
|
-
frame.writeUInt16BE(eventBuf.length, 5);
|
|
84
|
-
if (eventBuf.length > 0) eventBuf.copy(frame, HEADER_SIZE);
|
|
85
|
-
if (payload.length > 0) payload.copy(frame, HEADER_SIZE + eventBuf.length);
|
|
86
|
-
return frame;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export function encodeJsonFrame(event: string, data: unknown, maxFrameSize?: number): Buffer {
|
|
90
|
-
validateEventName(event);
|
|
91
|
-
const payload = Buffer.from(JSON.stringify(data), 'utf8');
|
|
92
|
-
if (maxFrameSize) validatePayloadSize(payload, maxFrameSize);
|
|
93
|
-
return encodeFrame(FRAME_JSON, event, payload, maxFrameSize);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function encodeBinaryFrame(event: string, data: Uint8Array | Buffer, maxFrameSize?: number): Buffer {
|
|
97
|
-
validateEventName(event);
|
|
98
|
-
const payload = Buffer.from(data);
|
|
99
|
-
if (maxFrameSize) validatePayloadSize(payload, maxFrameSize);
|
|
100
|
-
return encodeFrame(FRAME_BINARY, event, payload, maxFrameSize);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export function encodePingFrame(): Buffer {
|
|
104
|
-
const f = Buffer.alloc(HEADER_SIZE);
|
|
105
|
-
f.writeUInt32BE(HEADER_SIZE, 0);
|
|
106
|
-
f[4] = FRAME_PING;
|
|
107
|
-
f.writeUInt16BE(0, 5);
|
|
27
|
+
if (typeof event !== 'string') throw new ProtocolError('Event name must be a string', 'INVALID_EVENT');
|
|
28
|
+
if (!event) throw new ProtocolError('Event name cannot be empty', 'EMPTY_EVENT');
|
|
29
|
+
if (event.length > MAX_EVENT_LENGTH) throw new ProtocolError(`Event name exceeds ${MAX_EVENT_LENGTH} bytes`, 'EVENT_TOO_LONG');
|
|
30
|
+
if (!/^[a-zA-Z0-9\-./:]+$/.test(event)) throw new ProtocolError('Event name contains invalid characters', 'INVALID_EVENT_CHARS');
|
|
31
|
+
if (['ping', 'pong', 'connect', 'disconnect', 'error'].includes(event)) throw new ProtocolError(`Event "${event}" is reserved`, 'RESERVED_EVENT');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function encode(type: number, event: string, payload: Buffer, max = DEFAULT_MAX_FRAME_SIZE): Buffer {
|
|
35
|
+
const eb = Buffer.from(event, 'utf8');
|
|
36
|
+
if (eb.length > MAX_EVENT_LENGTH) throw new ProtocolError(`Event name exceeds ${MAX_EVENT_LENGTH} bytes`, 'EVENT_TOO_LONG');
|
|
37
|
+
const total = HEADER_SIZE + eb.length + payload.length;
|
|
38
|
+
if (total > max) throw new ProtocolError(`Frame exceeds max size (${max} bytes)`, 'FRAME_TOO_LARGE');
|
|
39
|
+
const f = Buffer.alloc(total);
|
|
40
|
+
f.writeUInt32BE(total, 0);
|
|
41
|
+
f[4] = type;
|
|
42
|
+
f.writeUInt16BE(eb.length, 5);
|
|
43
|
+
if (eb.length) eb.copy(f, HEADER_SIZE);
|
|
44
|
+
if (payload.length) payload.copy(f, HEADER_SIZE + eb.length);
|
|
108
45
|
return f;
|
|
109
46
|
}
|
|
110
47
|
|
|
111
|
-
|
|
48
|
+
const emptyFrame = (type: number): Buffer => {
|
|
112
49
|
const f = Buffer.alloc(HEADER_SIZE);
|
|
113
50
|
f.writeUInt32BE(HEADER_SIZE, 0);
|
|
114
|
-
f[4] =
|
|
51
|
+
f[4] = type;
|
|
115
52
|
f.writeUInt16BE(0, 5);
|
|
116
53
|
return f;
|
|
117
|
-
}
|
|
54
|
+
};
|
|
118
55
|
|
|
119
|
-
export
|
|
120
|
-
|
|
121
|
-
if (maxFrameSize) validatePayloadSize(payload, maxFrameSize);
|
|
122
|
-
return encodeFrame(FRAME_ACK_REQ, ackName, payload, maxFrameSize);
|
|
123
|
-
}
|
|
56
|
+
export const encodeJsonFrame = (event: string, data: unknown, max?: number) =>
|
|
57
|
+
(validateEventName(event), encode(FRAME_JSON, event, Buffer.from(JSON.stringify(data), 'utf8'), max));
|
|
124
58
|
|
|
125
|
-
export
|
|
126
|
-
|
|
127
|
-
if (maxFrameSize) validatePayloadSize(payload, maxFrameSize);
|
|
128
|
-
return encodeFrame(FRAME_ACK_RES, ackName, payload, maxFrameSize);
|
|
129
|
-
}
|
|
59
|
+
export const encodeBinaryFrame = (event: string, data: Uint8Array | Buffer, max?: number) =>
|
|
60
|
+
(validateEventName(event), encode(FRAME_BINARY, event, Buffer.from(data), max));
|
|
130
61
|
|
|
131
|
-
export
|
|
132
|
-
|
|
133
|
-
|
|
62
|
+
export const encodePingFrame = () => emptyFrame(FRAME_PING);
|
|
63
|
+
export const encodePongFrame = () => emptyFrame(FRAME_PONG);
|
|
64
|
+
export const encodeAckReqFrame = (name: string, data: unknown, max?: number) =>
|
|
65
|
+
encode(FRAME_ACK_REQ, name, Buffer.from(JSON.stringify(data), 'utf8'), max);
|
|
134
66
|
|
|
135
|
-
export
|
|
136
|
-
|
|
137
|
-
f.writeUInt32BE(HEADER_SIZE, 0);
|
|
138
|
-
f[4] = FRAME_DISCONNECT;
|
|
139
|
-
f.writeUInt16BE(0, 5);
|
|
140
|
-
return f;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export function encodeJoinFrame(room: string, maxFrameSize?: number): Buffer {
|
|
144
|
-
const payload = Buffer.from(room, 'utf8');
|
|
145
|
-
return encodeFrame(FRAME_JOIN, 'join-room', payload, maxFrameSize);
|
|
146
|
-
}
|
|
67
|
+
export const encodeAckResFrame = (name: string, data: unknown, max?: number) =>
|
|
68
|
+
encode(FRAME_ACK_RES, name, Buffer.from(JSON.stringify(data), 'utf8'), max);
|
|
147
69
|
|
|
148
|
-
export
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
70
|
+
export const encodeConnectFrame = (id: string) => encode(FRAME_CONNECT, 'connect', Buffer.from(id, 'utf8'));
|
|
71
|
+
export const encodeDisconnectFrame = () => emptyFrame(FRAME_DISCONNECT);
|
|
72
|
+
export const encodeJoinFrame = (room: string, max?: number) => encode(FRAME_JOIN, 'join-room', Buffer.from(room, 'utf8'), max);
|
|
73
|
+
export const encodeLeaveFrame = (room: string) => encode(FRAME_LEAVE, 'leave-room', room ? Buffer.from(room, 'utf8') : Buffer.alloc(0));
|
|
74
|
+
export const encodeErrorFrame = (msg: string) => encode(FRAME_ERROR, 'error', Buffer.from(msg, 'utf8'));
|
|
152
75
|
|
|
153
|
-
export function encodeErrorFrame(message: string): Buffer {
|
|
154
|
-
return encodeFrame(FRAME_ERROR, 'error', Buffer.from(message, 'utf8'));
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/** Streaming frame parser for TCP connections. Buffers partial data and emits complete frames. */
|
|
158
76
|
export class FrameParser {
|
|
159
|
-
private buf
|
|
160
|
-
private
|
|
161
|
-
private
|
|
77
|
+
private buf = Buffer.alloc(0);
|
|
78
|
+
private max: number;
|
|
79
|
+
private received = 0;
|
|
162
80
|
|
|
163
|
-
constructor(
|
|
164
|
-
this.maxFrameSize = maxFrameSize;
|
|
165
|
-
}
|
|
81
|
+
constructor(max = DEFAULT_MAX_FRAME_SIZE) { this.max = max; }
|
|
166
82
|
|
|
167
83
|
feed(data: Buffer): ParsedFrame[] {
|
|
168
|
-
this.
|
|
84
|
+
this.received += data.length;
|
|
169
85
|
this.buf = Buffer.concat([this.buf, data]);
|
|
170
|
-
|
|
171
|
-
if (this.buf.length > this.maxFrameSize * 2) {
|
|
86
|
+
if (this.buf.length > this.max * 2) {
|
|
172
87
|
this.buf = Buffer.alloc(0);
|
|
173
|
-
throw new ProtocolError(
|
|
174
|
-
`Input buffer exceeded limit (${this.maxFrameSize * 2} bytes)`,
|
|
175
|
-
'BUFFER_OVERFLOW'
|
|
176
|
-
);
|
|
88
|
+
throw new ProtocolError(`Buffer overflow (${this.max * 2})`, 'BUFFER_OVERFLOW');
|
|
177
89
|
}
|
|
178
|
-
|
|
179
90
|
const frames: ParsedFrame[] = [];
|
|
180
|
-
|
|
181
91
|
while (this.buf.length >= HEADER_SIZE) {
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
if (totalLen < HEADER_SIZE) {
|
|
92
|
+
const total = this.buf.readUInt32BE(0);
|
|
93
|
+
if (total < HEADER_SIZE || total > this.max) {
|
|
185
94
|
this.buf = Buffer.alloc(0);
|
|
186
|
-
throw new ProtocolError(`Invalid frame size: ${
|
|
95
|
+
throw new ProtocolError(`Invalid frame size: ${total}`, total < HEADER_SIZE ? 'INVALID_FRAME_SIZE' : 'FRAME_TOO_LARGE');
|
|
187
96
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
throw new ProtocolError(
|
|
192
|
-
`Frame exceeds max size (${this.maxFrameSize} bytes)`,
|
|
193
|
-
'FRAME_TOO_LARGE'
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (this.buf.length < totalLen) break;
|
|
198
|
-
|
|
199
|
-
const type = this.buf[4];
|
|
200
|
-
const eventLen = this.buf.readUInt16BE(5);
|
|
201
|
-
|
|
202
|
-
if (HEADER_SIZE + eventLen > totalLen) {
|
|
203
|
-
this.buf = Buffer.alloc(0);
|
|
204
|
-
throw new ProtocolError('Event length exceeds frame bounds', 'INVALID_EVENT_LENGTH');
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (eventLen > MAX_EVENT_LENGTH) {
|
|
97
|
+
if (this.buf.length < total) break;
|
|
98
|
+
const el = this.buf.readUInt16BE(5);
|
|
99
|
+
if (HEADER_SIZE + el > total || el > MAX_EVENT_LENGTH) {
|
|
208
100
|
this.buf = Buffer.alloc(0);
|
|
209
|
-
throw new ProtocolError(
|
|
101
|
+
throw new ProtocolError('Invalid event length', 'INVALID_EVENT_LENGTH');
|
|
210
102
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
? this.buf.subarray(HEADER_SIZE, HEADER_SIZE +
|
|
214
|
-
:
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
? Buffer.from(this.buf.subarray(payloadStart, totalLen))
|
|
218
|
-
: Buffer.alloc(0);
|
|
219
|
-
|
|
220
|
-
frames.push({ type, event, payload });
|
|
221
|
-
this.buf = totalLen < this.buf.length
|
|
222
|
-
? Buffer.from(this.buf.subarray(totalLen))
|
|
223
|
-
: Buffer.alloc(0);
|
|
103
|
+
frames.push({
|
|
104
|
+
type: this.buf[4],
|
|
105
|
+
event: el ? this.buf.subarray(HEADER_SIZE, HEADER_SIZE + el).toString('utf8') : '',
|
|
106
|
+
payload: total > HEADER_SIZE + el ? Buffer.from(this.buf.subarray(HEADER_SIZE + el, total)) : Buffer.alloc(0),
|
|
107
|
+
});
|
|
108
|
+
this.buf = total < this.buf.length ? Buffer.from(this.buf.subarray(total)) : Buffer.alloc(0);
|
|
224
109
|
}
|
|
225
|
-
|
|
226
110
|
return frames;
|
|
227
111
|
}
|
|
228
112
|
|
|
229
|
-
reset()
|
|
230
|
-
|
|
231
|
-
this.totalBytesReceived = 0;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
getBytesReceived(): number {
|
|
235
|
-
return this.totalBytesReceived;
|
|
236
|
-
}
|
|
113
|
+
reset() { this.buf = Buffer.alloc(0); this.received = 0; }
|
|
114
|
+
getBytesReceived() { return this.received; }
|
|
237
115
|
}
|
package/src/websocket.d.ts
CHANGED
|
@@ -1,65 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @stelar-time-real WebSocket
|
|
3
|
-
*
|
|
4
|
-
* Hand-crafted implementation with no external dependencies.
|
|
5
|
-
* Uses Node.js built-in crypto for handshake and frame masking.
|
|
2
|
+
* @stelar-time-real WebSocket (RFC 6455)
|
|
6
3
|
*/
|
|
7
4
|
export declare const DEFAULT_MAX_WS_FRAME_SIZE: number;
|
|
8
|
-
export declare const OP_CONTINUATION = 0;
|
|
9
|
-
export declare const
|
|
10
|
-
export declare const OP_BINARY = 2;
|
|
11
|
-
export declare const OP_CLOSE = 8;
|
|
12
|
-
export declare const OP_PING = 9;
|
|
13
|
-
export declare const OP_PONG = 10;
|
|
14
|
-
export declare const CLOSE_NORMAL = 1000;
|
|
15
|
-
export declare const CLOSE_GOING_AWAY = 1001;
|
|
16
|
-
export declare const CLOSE_PROTOCOL_ERROR = 1002;
|
|
17
|
-
export declare const CLOSE_UNSUPPORTED = 1003;
|
|
18
|
-
export declare const CLOSE_INVALID_PAYLOAD = 1007;
|
|
19
|
-
export declare const CLOSE_POLICY_VIOLATION = 1008;
|
|
20
|
-
export declare const CLOSE_MESSAGE_TOO_BIG = 1009;
|
|
21
|
-
export declare const CLOSE_INTERNAL_ERROR = 1011;
|
|
5
|
+
export declare const OP_CONTINUATION = 0, OP_TEXT = 1, OP_BINARY = 2, OP_CLOSE = 8, OP_PING = 9, OP_PONG = 10;
|
|
6
|
+
export declare const CLOSE_NORMAL = 1000, CLOSE_GOING_AWAY = 1001, CLOSE_PROTOCOL_ERROR = 1002, CLOSE_UNSUPPORTED = 1003, CLOSE_INVALID_PAYLOAD = 1007, CLOSE_POLICY_VIOLATION = 1008, CLOSE_MESSAGE_TOO_BIG = 1009, CLOSE_INTERNAL_ERROR = 1011;
|
|
22
7
|
export declare class WebSocketError extends Error {
|
|
23
8
|
code: number;
|
|
24
9
|
constructor(message: string, code?: number);
|
|
25
10
|
}
|
|
26
|
-
|
|
27
|
-
export declare
|
|
28
|
-
export declare
|
|
11
|
+
export declare const computeAcceptKey: (key: string) => string;
|
|
12
|
+
export declare const generateWSKey: () => string;
|
|
13
|
+
export declare const validateWSKey: (key: string) => boolean;
|
|
29
14
|
export declare function buildUpgradeResponse(key: string, headers?: Record<string, string>): string;
|
|
30
|
-
/** Validate Sec-WebSocket-Key: must be 16 bytes base64 encoded */
|
|
31
|
-
export declare function validateWSKey(key: string): boolean;
|
|
32
15
|
export interface WSFrame {
|
|
33
16
|
fin: boolean;
|
|
34
17
|
opcode: number;
|
|
35
18
|
payload: Buffer;
|
|
36
19
|
masked: boolean;
|
|
37
20
|
}
|
|
38
|
-
|
|
39
|
-
export declare function parseWSFrame(buf: Buffer, maxFrameSize?: number): {
|
|
21
|
+
export declare function parseWSFrame(buf: Buffer, max?: number): {
|
|
40
22
|
frame: WSFrame;
|
|
41
23
|
consumed: number;
|
|
42
24
|
} | null;
|
|
43
|
-
|
|
44
|
-
export declare
|
|
45
|
-
|
|
46
|
-
export declare
|
|
47
|
-
export declare
|
|
48
|
-
export declare
|
|
49
|
-
export declare
|
|
50
|
-
export declare
|
|
51
|
-
export declare
|
|
52
|
-
export declare
|
|
53
|
-
export declare
|
|
54
|
-
export declare function createWSCloseFrameMasked(code?: number, reason?: string): Buffer;
|
|
55
|
-
export declare function createWSPingFrameMasked(): Buffer;
|
|
56
|
-
export declare function createWSPongFrameMasked(): Buffer;
|
|
57
|
-
/** Streaming parser for WebSocket frames. Buffers partial data and emits complete frames. */
|
|
25
|
+
export declare function createWSFrame(opcode: number, payload: Buffer | string, masked?: boolean): Buffer;
|
|
26
|
+
export declare const createWSTextFrame: (msg: string) => Buffer<ArrayBufferLike>;
|
|
27
|
+
export declare const createWSBinaryFrame: (data: Buffer) => Buffer<ArrayBufferLike>;
|
|
28
|
+
export declare const createWSCloseFrame: (code?: number, reason?: string) => Buffer<ArrayBufferLike>;
|
|
29
|
+
export declare const createWSPingFrame: (data?: Buffer) => Buffer<ArrayBufferLike>;
|
|
30
|
+
export declare const createWSPongFrame: (data?: Buffer) => Buffer<ArrayBufferLike>;
|
|
31
|
+
export declare const createWSTextFrameMasked: (msg: string) => Buffer<ArrayBufferLike>;
|
|
32
|
+
export declare const createWSBinaryFrameMasked: (data: Buffer) => Buffer<ArrayBufferLike>;
|
|
33
|
+
export declare const createWSCloseFrameMasked: (code?: number, reason?: string) => Buffer<ArrayBufferLike>;
|
|
34
|
+
export declare const createWSPingFrameMasked: () => Buffer<ArrayBufferLike>;
|
|
35
|
+
export declare const createWSPongFrameMasked: () => Buffer<ArrayBufferLike>;
|
|
58
36
|
export declare class WSFrameParser {
|
|
59
37
|
private buf;
|
|
60
|
-
private
|
|
61
|
-
private
|
|
62
|
-
constructor(
|
|
38
|
+
private max;
|
|
39
|
+
private received;
|
|
40
|
+
constructor(max?: number);
|
|
63
41
|
feed(data: Buffer): WSFrame[];
|
|
64
42
|
reset(): void;
|
|
65
43
|
getBytesReceived(): number;
|