stelar-time-real 3.3.0 → 3.3.1
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 +10 -3
- package/src/client.d.ts.map +1 -1
- package/src/client.js +105 -58
- package/src/client.ts +92 -42
- package/src/index.d.ts +8 -6
- package/src/index.d.ts.map +1 -1
- package/src/index.js +161 -105
- package/src/index.ts +127 -79
- package/src/logger.d.ts +0 -1
- package/src/logger.d.ts.map +1 -1
- package/src/logger.js +5 -8
- package/src/logger.ts +6 -10
- package/src/protocol.d.ts +5 -5
- package/src/protocol.d.ts.map +1 -1
- package/src/protocol.js +39 -25
- package/src/protocol.ts +31 -41
- package/src/websocket.d.ts +14 -8
- package/src/websocket.d.ts.map +1 -1
- package/src/websocket.js +81 -32
- package/src/websocket.ts +68 -30
package/src/protocol.js
CHANGED
|
@@ -1,17 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @stelar-time-real Binary Protocol
|
|
3
|
-
* Frame: [4B totalLen BE][1B type][2B eventLen BE][event][payload]
|
|
4
|
-
*/
|
|
1
|
+
/** @stelar-time-real Binary Protocol — Frame: [4B totalLen BE][1B type][2B eventLen BE][event][payload] */
|
|
5
2
|
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;
|
|
6
3
|
export const MAX_EVENT_LENGTH = 256;
|
|
7
4
|
export const DEFAULT_MAX_FRAME_SIZE = 10 * 1024 * 1024;
|
|
8
5
|
export const HEADER_SIZE = 7;
|
|
9
6
|
export class ProtocolError extends Error {
|
|
10
|
-
constructor(message, code = 'PROTOCOL_ERROR') {
|
|
11
|
-
super(message);
|
|
12
|
-
this.name = 'ProtocolError';
|
|
13
|
-
this.code = code;
|
|
14
|
-
}
|
|
7
|
+
constructor(message, code = 'PROTOCOL_ERROR') { super(message); this.name = 'ProtocolError'; this.code = code; }
|
|
15
8
|
}
|
|
16
9
|
export function validateEventName(event) {
|
|
17
10
|
if (typeof event !== 'string')
|
|
@@ -31,7 +24,7 @@ function encode(type, event, payload, max = DEFAULT_MAX_FRAME_SIZE) {
|
|
|
31
24
|
throw new ProtocolError(`Event name exceeds ${MAX_EVENT_LENGTH} bytes`, 'EVENT_TOO_LONG');
|
|
32
25
|
const total = HEADER_SIZE + eb.length + payload.length;
|
|
33
26
|
if (total > max)
|
|
34
|
-
throw new ProtocolError(`Frame exceeds max size (${max}
|
|
27
|
+
throw new ProtocolError(`Frame exceeds max size (${max})`, 'FRAME_TOO_LARGE');
|
|
35
28
|
const f = Buffer.alloc(total);
|
|
36
29
|
f.writeUInt32BE(total, 0);
|
|
37
30
|
f[4] = type;
|
|
@@ -60,42 +53,63 @@ export const encodeDisconnectFrame = () => emptyFrame(FRAME_DISCONNECT);
|
|
|
60
53
|
export const encodeJoinFrame = (room, max) => encode(FRAME_JOIN, 'join-room', Buffer.from(room, 'utf8'), max);
|
|
61
54
|
export const encodeLeaveFrame = (room) => encode(FRAME_LEAVE, 'leave-room', room ? Buffer.from(room, 'utf8') : Buffer.alloc(0));
|
|
62
55
|
export const encodeErrorFrame = (msg) => encode(FRAME_ERROR, 'error', Buffer.from(msg, 'utf8'));
|
|
56
|
+
/** O(1) append streaming parser — avoids Buffer.concat O(n²) on many small chunks */
|
|
63
57
|
export class FrameParser {
|
|
64
58
|
constructor(max = DEFAULT_MAX_FRAME_SIZE) {
|
|
65
|
-
this.
|
|
59
|
+
this.chunks = [];
|
|
60
|
+
this.len = 0;
|
|
66
61
|
this.received = 0;
|
|
67
62
|
this.max = max;
|
|
68
63
|
}
|
|
64
|
+
_compact() {
|
|
65
|
+
if (this.chunks.length <= 1)
|
|
66
|
+
return this.chunks[0] || Buffer.alloc(0);
|
|
67
|
+
const buf = Buffer.concat(this.chunks);
|
|
68
|
+
this.chunks = [buf];
|
|
69
|
+
return buf;
|
|
70
|
+
}
|
|
69
71
|
feed(data) {
|
|
70
72
|
this.received += data.length;
|
|
71
|
-
this.
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
this.chunks.push(data);
|
|
74
|
+
this.len += data.length;
|
|
75
|
+
if (this.len > this.max * 2) {
|
|
76
|
+
this.chunks = [];
|
|
77
|
+
this.len = 0;
|
|
74
78
|
throw new ProtocolError(`Buffer overflow (${this.max * 2})`, 'BUFFER_OVERFLOW');
|
|
75
79
|
}
|
|
76
80
|
const frames = [];
|
|
77
|
-
while (this.
|
|
78
|
-
const
|
|
81
|
+
while (this.len >= HEADER_SIZE) {
|
|
82
|
+
const buf = this._compact();
|
|
83
|
+
const total = buf.readUInt32BE(0);
|
|
79
84
|
if (total < HEADER_SIZE || total > this.max) {
|
|
80
|
-
this.
|
|
85
|
+
this.chunks = [];
|
|
86
|
+
this.len = 0;
|
|
81
87
|
throw new ProtocolError(`Invalid frame size: ${total}`, total < HEADER_SIZE ? 'INVALID_FRAME_SIZE' : 'FRAME_TOO_LARGE');
|
|
82
88
|
}
|
|
83
|
-
if (
|
|
89
|
+
if (buf.length < total)
|
|
84
90
|
break;
|
|
85
|
-
const el =
|
|
91
|
+
const el = buf.readUInt16BE(5);
|
|
86
92
|
if (HEADER_SIZE + el > total || el > MAX_EVENT_LENGTH) {
|
|
87
|
-
this.
|
|
93
|
+
this.chunks = [];
|
|
94
|
+
this.len = 0;
|
|
88
95
|
throw new ProtocolError('Invalid event length', 'INVALID_EVENT_LENGTH');
|
|
89
96
|
}
|
|
90
97
|
frames.push({
|
|
91
|
-
type:
|
|
92
|
-
event: el ?
|
|
93
|
-
payload: total > HEADER_SIZE + el ? Buffer.from(
|
|
98
|
+
type: buf[4],
|
|
99
|
+
event: el ? buf.subarray(HEADER_SIZE, HEADER_SIZE + el).toString('utf8') : '',
|
|
100
|
+
payload: total > HEADER_SIZE + el ? Buffer.from(buf.subarray(HEADER_SIZE + el, total)) : Buffer.alloc(0),
|
|
94
101
|
});
|
|
95
|
-
|
|
102
|
+
if (total < buf.length) {
|
|
103
|
+
this.chunks = [Buffer.from(buf.subarray(total))];
|
|
104
|
+
this.len = this.chunks[0].length;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
this.chunks = [];
|
|
108
|
+
this.len = 0;
|
|
109
|
+
}
|
|
96
110
|
}
|
|
97
111
|
return frames;
|
|
98
112
|
}
|
|
99
|
-
reset() { this.
|
|
113
|
+
reset() { this.chunks = []; this.len = 0; this.received = 0; }
|
|
100
114
|
getBytesReceived() { return this.received; }
|
|
101
115
|
}
|
package/src/protocol.ts
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @stelar-time-real Binary Protocol
|
|
3
|
-
* Frame: [4B totalLen BE][1B type][2B eventLen BE][event][payload]
|
|
4
|
-
*/
|
|
1
|
+
/** @stelar-time-real Binary Protocol — Frame: [4B totalLen BE][1B type][2B eventLen BE][event][payload] */
|
|
5
2
|
|
|
6
3
|
export const FRAME_JSON = 0x01, FRAME_BINARY = 0x02, FRAME_PING = 0x03,
|
|
7
4
|
FRAME_PONG = 0x04, FRAME_ACK_REQ = 0x05, FRAME_ACK_RES = 0x06,
|
|
@@ -16,11 +13,7 @@ export interface ParsedFrame { type: number; event: string; payload: Buffer; }
|
|
|
16
13
|
|
|
17
14
|
export class ProtocolError extends Error {
|
|
18
15
|
code: string;
|
|
19
|
-
constructor(message: string, code = 'PROTOCOL_ERROR') {
|
|
20
|
-
super(message);
|
|
21
|
-
this.name = 'ProtocolError';
|
|
22
|
-
this.code = code;
|
|
23
|
-
}
|
|
16
|
+
constructor(message: string, code = 'PROTOCOL_ERROR') { super(message); this.name = 'ProtocolError'; this.code = code; }
|
|
24
17
|
}
|
|
25
18
|
|
|
26
19
|
export function validateEventName(event: string): void {
|
|
@@ -35,11 +28,9 @@ function encode(type: number, event: string, payload: Buffer, max = DEFAULT_MAX_
|
|
|
35
28
|
const eb = Buffer.from(event, 'utf8');
|
|
36
29
|
if (eb.length > MAX_EVENT_LENGTH) throw new ProtocolError(`Event name exceeds ${MAX_EVENT_LENGTH} bytes`, 'EVENT_TOO_LONG');
|
|
37
30
|
const total = HEADER_SIZE + eb.length + payload.length;
|
|
38
|
-
if (total > max) throw new ProtocolError(`Frame exceeds max size (${max}
|
|
31
|
+
if (total > max) throw new ProtocolError(`Frame exceeds max size (${max})`, 'FRAME_TOO_LARGE');
|
|
39
32
|
const f = Buffer.alloc(total);
|
|
40
|
-
f.writeUInt32BE(total, 0);
|
|
41
|
-
f[4] = type;
|
|
42
|
-
f.writeUInt16BE(eb.length, 5);
|
|
33
|
+
f.writeUInt32BE(total, 0); f[4] = type; f.writeUInt16BE(eb.length, 5);
|
|
43
34
|
if (eb.length) eb.copy(f, HEADER_SIZE);
|
|
44
35
|
if (payload.length) payload.copy(f, HEADER_SIZE + eb.length);
|
|
45
36
|
return f;
|
|
@@ -47,9 +38,7 @@ function encode(type: number, event: string, payload: Buffer, max = DEFAULT_MAX_
|
|
|
47
38
|
|
|
48
39
|
const emptyFrame = (type: number): Buffer => {
|
|
49
40
|
const f = Buffer.alloc(HEADER_SIZE);
|
|
50
|
-
f.writeUInt32BE(HEADER_SIZE, 0);
|
|
51
|
-
f[4] = type;
|
|
52
|
-
f.writeUInt16BE(0, 5);
|
|
41
|
+
f.writeUInt32BE(HEADER_SIZE, 0); f[4] = type; f.writeUInt16BE(0, 5);
|
|
53
42
|
return f;
|
|
54
43
|
};
|
|
55
44
|
|
|
@@ -63,53 +52,54 @@ export const encodePingFrame = () => emptyFrame(FRAME_PING);
|
|
|
63
52
|
export const encodePongFrame = () => emptyFrame(FRAME_PONG);
|
|
64
53
|
export const encodeAckReqFrame = (name: string, data: unknown, max?: number) =>
|
|
65
54
|
encode(FRAME_ACK_REQ, name, Buffer.from(JSON.stringify(data), 'utf8'), max);
|
|
66
|
-
|
|
67
55
|
export const encodeAckResFrame = (name: string, data: unknown, max?: number) =>
|
|
68
56
|
encode(FRAME_ACK_RES, name, Buffer.from(JSON.stringify(data), 'utf8'), max);
|
|
69
|
-
|
|
70
57
|
export const encodeConnectFrame = (id: string) => encode(FRAME_CONNECT, 'connect', Buffer.from(id, 'utf8'));
|
|
71
58
|
export const encodeDisconnectFrame = () => emptyFrame(FRAME_DISCONNECT);
|
|
72
59
|
export const encodeJoinFrame = (room: string, max?: number) => encode(FRAME_JOIN, 'join-room', Buffer.from(room, 'utf8'), max);
|
|
73
60
|
export const encodeLeaveFrame = (room: string) => encode(FRAME_LEAVE, 'leave-room', room ? Buffer.from(room, 'utf8') : Buffer.alloc(0));
|
|
74
61
|
export const encodeErrorFrame = (msg: string) => encode(FRAME_ERROR, 'error', Buffer.from(msg, 'utf8'));
|
|
75
62
|
|
|
63
|
+
/** O(1) append streaming parser — avoids Buffer.concat O(n²) on many small chunks */
|
|
76
64
|
export class FrameParser {
|
|
77
|
-
private
|
|
65
|
+
private chunks: Buffer[] = [];
|
|
66
|
+
private len = 0;
|
|
78
67
|
private max: number;
|
|
79
68
|
private received = 0;
|
|
80
69
|
|
|
81
70
|
constructor(max = DEFAULT_MAX_FRAME_SIZE) { this.max = max; }
|
|
82
71
|
|
|
72
|
+
private _compact(): Buffer {
|
|
73
|
+
if (this.chunks.length <= 1) return this.chunks[0] || Buffer.alloc(0);
|
|
74
|
+
const buf = Buffer.concat(this.chunks);
|
|
75
|
+
this.chunks = [buf];
|
|
76
|
+
return buf;
|
|
77
|
+
}
|
|
78
|
+
|
|
83
79
|
feed(data: Buffer): ParsedFrame[] {
|
|
84
80
|
this.received += data.length;
|
|
85
|
-
this.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
throw new ProtocolError(`Buffer overflow (${this.max * 2})`, 'BUFFER_OVERFLOW');
|
|
89
|
-
}
|
|
81
|
+
this.chunks.push(data);
|
|
82
|
+
this.len += data.length;
|
|
83
|
+
if (this.len > this.max * 2) { this.chunks = []; this.len = 0; throw new ProtocolError(`Buffer overflow (${this.max * 2})`, 'BUFFER_OVERFLOW'); }
|
|
90
84
|
const frames: ParsedFrame[] = [];
|
|
91
|
-
while (this.
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (this.
|
|
98
|
-
const el = this.buf.readUInt16BE(5);
|
|
99
|
-
if (HEADER_SIZE + el > total || el > MAX_EVENT_LENGTH) {
|
|
100
|
-
this.buf = Buffer.alloc(0);
|
|
101
|
-
throw new ProtocolError('Invalid event length', 'INVALID_EVENT_LENGTH');
|
|
102
|
-
}
|
|
85
|
+
while (this.len >= HEADER_SIZE) {
|
|
86
|
+
const buf = this._compact();
|
|
87
|
+
const total = buf.readUInt32BE(0);
|
|
88
|
+
if (total < HEADER_SIZE || total > this.max) { this.chunks = []; this.len = 0; throw new ProtocolError(`Invalid frame size: ${total}`, total < HEADER_SIZE ? 'INVALID_FRAME_SIZE' : 'FRAME_TOO_LARGE'); }
|
|
89
|
+
if (buf.length < total) break;
|
|
90
|
+
const el = buf.readUInt16BE(5);
|
|
91
|
+
if (HEADER_SIZE + el > total || el > MAX_EVENT_LENGTH) { this.chunks = []; this.len = 0; throw new ProtocolError('Invalid event length', 'INVALID_EVENT_LENGTH'); }
|
|
103
92
|
frames.push({
|
|
104
|
-
type:
|
|
105
|
-
event: el ?
|
|
106
|
-
payload: total > HEADER_SIZE + el ? Buffer.from(
|
|
93
|
+
type: buf[4],
|
|
94
|
+
event: el ? buf.subarray(HEADER_SIZE, HEADER_SIZE + el).toString('utf8') : '',
|
|
95
|
+
payload: total > HEADER_SIZE + el ? Buffer.from(buf.subarray(HEADER_SIZE + el, total)) : Buffer.alloc(0),
|
|
107
96
|
});
|
|
108
|
-
|
|
97
|
+
if (total < buf.length) { this.chunks = [Buffer.from(buf.subarray(total))]; this.len = this.chunks[0].length; }
|
|
98
|
+
else { this.chunks = []; this.len = 0; }
|
|
109
99
|
}
|
|
110
100
|
return frames;
|
|
111
101
|
}
|
|
112
102
|
|
|
113
|
-
reset() { this.
|
|
103
|
+
reset() { this.chunks = []; this.len = 0; this.received = 0; }
|
|
114
104
|
getBytesReceived() { return this.received; }
|
|
115
105
|
}
|
package/src/websocket.d.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @stelar-time-real WebSocket (RFC 6455)
|
|
3
|
-
*/
|
|
1
|
+
/** @stelar-time-real WebSocket (RFC 6455) + permessage-deflate (RFC 7692) */
|
|
4
2
|
export declare const DEFAULT_MAX_WS_FRAME_SIZE: number;
|
|
5
3
|
export declare const OP_CONTINUATION = 0, OP_TEXT = 1, OP_BINARY = 2, OP_CLOSE = 8, OP_PING = 9, OP_PONG = 10;
|
|
6
4
|
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;
|
|
@@ -11,33 +9,41 @@ export declare class WebSocketError extends Error {
|
|
|
11
9
|
export declare const computeAcceptKey: (key: string) => string;
|
|
12
10
|
export declare const generateWSKey: () => string;
|
|
13
11
|
export declare const validateWSKey: (key: string) => boolean;
|
|
14
|
-
export declare function buildUpgradeResponse(key: string, headers?: Record<string, string
|
|
12
|
+
export declare function buildUpgradeResponse(key: string, headers?: Record<string, string>, compress?: boolean): string;
|
|
13
|
+
/** Parse client's Sec-WebSocket-Extensions header to check for permessage-deflate */
|
|
14
|
+
export declare function clientWantsCompression(extensionsHeader: string | undefined): boolean;
|
|
15
15
|
export interface WSFrame {
|
|
16
16
|
fin: boolean;
|
|
17
17
|
opcode: number;
|
|
18
18
|
payload: Buffer;
|
|
19
19
|
masked: boolean;
|
|
20
|
+
rsv1: boolean;
|
|
20
21
|
}
|
|
22
|
+
export declare function wsCompress(data: Buffer): Buffer;
|
|
23
|
+
export declare function wsDecompress(data: Buffer): Buffer;
|
|
21
24
|
export declare function parseWSFrame(buf: Buffer, max?: number): {
|
|
22
25
|
frame: WSFrame;
|
|
23
26
|
consumed: number;
|
|
24
27
|
} | null;
|
|
25
|
-
export declare function createWSFrame(opcode: number, payload: Buffer | string, masked?: boolean): Buffer;
|
|
26
|
-
export declare const createWSTextFrame: (msg: string) => Buffer<ArrayBufferLike>;
|
|
28
|
+
export declare function createWSFrame(opcode: number, payload: Buffer | string, masked?: boolean, compress?: boolean): Buffer;
|
|
29
|
+
export declare const createWSTextFrame: (msg: string, compress?: boolean) => Buffer<ArrayBufferLike>;
|
|
27
30
|
export declare const createWSBinaryFrame: (data: Buffer) => Buffer<ArrayBufferLike>;
|
|
28
31
|
export declare const createWSCloseFrame: (code?: number, reason?: string) => Buffer<ArrayBufferLike>;
|
|
29
32
|
export declare const createWSPingFrame: (data?: Buffer) => Buffer<ArrayBufferLike>;
|
|
30
33
|
export declare const createWSPongFrame: (data?: Buffer) => Buffer<ArrayBufferLike>;
|
|
31
|
-
export declare const createWSTextFrameMasked: (msg: string) => Buffer<ArrayBufferLike>;
|
|
34
|
+
export declare const createWSTextFrameMasked: (msg: string, compress?: boolean) => Buffer<ArrayBufferLike>;
|
|
32
35
|
export declare const createWSBinaryFrameMasked: (data: Buffer) => Buffer<ArrayBufferLike>;
|
|
33
36
|
export declare const createWSCloseFrameMasked: (code?: number, reason?: string) => Buffer<ArrayBufferLike>;
|
|
34
37
|
export declare const createWSPingFrameMasked: () => Buffer<ArrayBufferLike>;
|
|
35
38
|
export declare const createWSPongFrameMasked: () => Buffer<ArrayBufferLike>;
|
|
39
|
+
/** Streaming WS frame parser — O(1) append, avoids Buffer.concat O(n²) */
|
|
36
40
|
export declare class WSFrameParser {
|
|
37
|
-
private
|
|
41
|
+
private chunks;
|
|
42
|
+
private len;
|
|
38
43
|
private max;
|
|
39
44
|
private received;
|
|
40
45
|
constructor(max?: number);
|
|
46
|
+
private _compact;
|
|
41
47
|
feed(data: Buffer): WSFrame[];
|
|
42
48
|
reset(): void;
|
|
43
49
|
getBytesReceived(): number;
|
package/src/websocket.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"websocket.d.ts","sourceRoot":"","sources":["websocket.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"websocket.d.ts","sourceRoot":"","sources":["websocket.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAM7E,eAAO,MAAM,yBAAyB,QAAmB,CAAC;AAE1D,eAAO,MAAM,eAAe,IAAM,EAAE,OAAO,IAAM,EAAE,SAAS,IAAM,EAChE,QAAQ,IAAM,EAAE,OAAO,IAAM,EAAE,OAAO,KAAM,CAAC;AAE/C,eAAO,MAAM,YAAY,OAAO,EAAE,gBAAgB,OAAO,EAAE,oBAAoB,OAAO,EACpF,iBAAiB,OAAO,EAAE,qBAAqB,OAAO,EAAE,sBAAsB,OAAO,EACrF,qBAAqB,OAAO,EAAE,oBAAoB,OAAO,CAAC;AAE5D,qBAAa,cAAe,SAAQ,KAAK;IACvC,IAAI,EAAE,MAAM,CAAC;gBACD,OAAO,EAAE,MAAM,EAAE,IAAI,SAAuB;CACzD;AAED,eAAO,MAAM,gBAAgB,GAAI,KAAK,MAAM,WAA+D,CAAC;AAC5G,eAAO,MAAM,aAAa,cAA2C,CAAC;AACtE,eAAO,MAAM,aAAa,GAAI,KAAK,MAAM,KAAG,OAA8E,CAAC;AAE3H,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,QAAQ,UAAQ,GAAG,MAAM,CAM5G;AAED,qFAAqF;AACrF,wBAAgB,sBAAsB,CAAC,gBAAgB,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAGpF;AAED,MAAM,WAAW,OAAO;IAAG,GAAG,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC;CAAE;AAI3G,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAG/C;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,SAA4B,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAetH;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,MAAM,UAAQ,EAAE,QAAQ,UAAQ,GAAG,MAAM,CAqBhH;AAGD,eAAO,MAAM,iBAAiB,GAAI,KAAK,MAAM,EAAE,kBAAgB,4BAAiD,CAAC;AACjH,eAAO,MAAM,mBAAmB,GAAI,MAAM,MAAM,4BAAmC,CAAC;AACpF,eAAO,MAAM,kBAAkB,GAAI,aAAmB,EAAE,eAAW,4BAElE,CAAC;AACF,eAAO,MAAM,iBAAiB,GAAI,OAAO,MAAM,4BAAoD,CAAC;AACpG,eAAO,MAAM,iBAAiB,GAAI,OAAO,MAAM,4BAAoD,CAAC;AAGpG,eAAO,MAAM,uBAAuB,GAAI,KAAK,MAAM,EAAE,kBAAgB,4BAAgD,CAAC;AACtH,eAAO,MAAM,yBAAyB,GAAI,MAAM,MAAM,4BAAyC,CAAC;AAChG,eAAO,MAAM,wBAAwB,GAAI,aAAmB,EAAE,eAAW,4BAExE,CAAC;AACF,eAAO,MAAM,uBAAuB,+BAAsD,CAAC;AAC3F,eAAO,MAAM,uBAAuB,+BAAsD,CAAC;AAE3F,0EAA0E;AAC1E,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,GAAG,CAAK;IAChB,OAAO,CAAC,GAAG,CAAS;IACpB,OAAO,CAAC,QAAQ,CAAK;gBAET,GAAG,SAA4B;IAE3C,OAAO,CAAC,QAAQ;IAOhB,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,EAAE;IAyB7B,KAAK;IACL,gBAAgB;CACjB"}
|
package/src/websocket.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @stelar-time-real WebSocket (RFC 6455)
|
|
3
|
-
*/
|
|
1
|
+
/** @stelar-time-real WebSocket (RFC 6455) + permessage-deflate (RFC 7692) */
|
|
4
2
|
import { createHash, randomBytes } from 'crypto';
|
|
3
|
+
import { deflateRawSync, inflateRawSync } from 'zlib';
|
|
5
4
|
const WS_MAGIC = '258EAFA5-E914-47DA-95CA-5AB5A7E3A741';
|
|
6
5
|
export const DEFAULT_MAX_WS_FRAME_SIZE = 10 * 1024 * 1024;
|
|
7
6
|
export const OP_CONTINUATION = 0x0, OP_TEXT = 0x1, OP_BINARY = 0x2, OP_CLOSE = 0x8, OP_PING = 0x9, OP_PONG = 0xA;
|
|
@@ -12,22 +11,38 @@ export class WebSocketError extends Error {
|
|
|
12
11
|
export const computeAcceptKey = (key) => createHash('sha1').update(key + WS_MAGIC).digest('base64');
|
|
13
12
|
export const generateWSKey = () => randomBytes(16).toString('base64');
|
|
14
13
|
export const validateWSKey = (key) => typeof key === 'string' && Buffer.from(key, 'base64').length === 16;
|
|
15
|
-
export function buildUpgradeResponse(key, headers) {
|
|
14
|
+
export function buildUpgradeResponse(key, headers, compress = false) {
|
|
16
15
|
const lines = ['HTTP/1.1 101 Switching Protocols', 'Upgrade: websocket', 'Connection: Upgrade', `Sec-WebSocket-Accept: ${computeAcceptKey(key)}`];
|
|
16
|
+
if (compress)
|
|
17
|
+
lines.push('Sec-WebSocket-Extensions: permessage-deflate; server_no_context_takeover; client_no_context_takeover');
|
|
17
18
|
if (headers)
|
|
18
19
|
for (const [k, v] of Object.entries(headers))
|
|
19
20
|
lines.push(`${k}: ${v}`);
|
|
20
21
|
lines.push('', '');
|
|
21
22
|
return lines.join('\r\n');
|
|
22
23
|
}
|
|
24
|
+
/** Parse client's Sec-WebSocket-Extensions header to check for permessage-deflate */
|
|
25
|
+
export function clientWantsCompression(extensionsHeader) {
|
|
26
|
+
if (!extensionsHeader)
|
|
27
|
+
return false;
|
|
28
|
+
return /permessage-deflate/.test(extensionsHeader);
|
|
29
|
+
}
|
|
30
|
+
const TAIL = Buffer.from([0x00, 0x00, 0xFF, 0xFF]);
|
|
31
|
+
export function wsCompress(data) {
|
|
32
|
+
const deflated = deflateRawSync(data);
|
|
33
|
+
return deflated.subarray(0, deflated.length - 4);
|
|
34
|
+
}
|
|
35
|
+
export function wsDecompress(data) {
|
|
36
|
+
return inflateRawSync(Buffer.concat([data, TAIL]));
|
|
37
|
+
}
|
|
23
38
|
export function parseWSFrame(buf, max = DEFAULT_MAX_WS_FRAME_SIZE) {
|
|
24
39
|
if (buf.length < 2)
|
|
25
40
|
return null;
|
|
26
41
|
const b0 = buf[0], b1 = buf[1];
|
|
27
|
-
const fin = !!(b0 & 0x80), rsv = b0 &
|
|
42
|
+
const fin = !!(b0 & 0x80), rsv1 = !!(b0 & 0x40), rsv = b0 & 0x30, opcode = b0 & 0x0F, masked = !!(b1 & 0x80);
|
|
28
43
|
let len = b1 & 0x7F, off = 2;
|
|
29
44
|
if (rsv)
|
|
30
|
-
throw new WebSocketError('
|
|
45
|
+
throw new WebSocketError('RSV2/RSV3 bits set', CLOSE_PROTOCOL_ERROR);
|
|
31
46
|
if (len === 126) {
|
|
32
47
|
if (buf.length < 4)
|
|
33
48
|
return null;
|
|
@@ -55,44 +70,47 @@ export function parseWSFrame(buf, max = DEFAULT_MAX_WS_FRAME_SIZE) {
|
|
|
55
70
|
if (masked && mk)
|
|
56
71
|
for (let i = 0; i < len; i++)
|
|
57
72
|
payload[i] ^= mk[i & 3];
|
|
58
|
-
return { frame: { fin, opcode, payload, masked }, consumed: off + len };
|
|
73
|
+
return { frame: { fin, opcode, payload, masked, rsv1 }, consumed: off + len };
|
|
59
74
|
}
|
|
60
|
-
export function createWSFrame(opcode, payload, masked = false) {
|
|
75
|
+
export function createWSFrame(opcode, payload, masked = false, compress = false) {
|
|
61
76
|
const d = typeof payload === 'string' ? Buffer.from(payload, 'utf8') : payload;
|
|
77
|
+
const compressed = compress && (opcode === OP_TEXT || opcode === OP_BINARY) ? wsCompress(d) : d;
|
|
78
|
+
const actualOpcode = compressed !== d ? opcode : opcode;
|
|
62
79
|
const mk = masked ? randomBytes(4) : undefined;
|
|
63
80
|
const base = masked ? 0x80 : 0;
|
|
81
|
+
const rsv1Bit = (compress && (opcode === OP_TEXT || opcode === OP_BINARY) && compressed !== d) ? 0x40 : 0;
|
|
64
82
|
let h;
|
|
65
|
-
if (
|
|
83
|
+
if (compressed.length < 126) {
|
|
66
84
|
h = Buffer.alloc(masked ? 6 : 2);
|
|
67
|
-
h[0] = 0x80 |
|
|
68
|
-
h[1] = base |
|
|
85
|
+
h[0] = 0x80 | rsv1Bit | actualOpcode;
|
|
86
|
+
h[1] = base | compressed.length;
|
|
69
87
|
if (mk)
|
|
70
88
|
mk.copy(h, 2);
|
|
71
89
|
}
|
|
72
|
-
else if (
|
|
90
|
+
else if (compressed.length < 65536) {
|
|
73
91
|
h = Buffer.alloc(masked ? 8 : 4);
|
|
74
|
-
h[0] = 0x80 |
|
|
92
|
+
h[0] = 0x80 | rsv1Bit | actualOpcode;
|
|
75
93
|
h[1] = base | 126;
|
|
76
|
-
h.writeUInt16BE(
|
|
94
|
+
h.writeUInt16BE(compressed.length, 2);
|
|
77
95
|
if (mk)
|
|
78
96
|
mk.copy(h, 4);
|
|
79
97
|
}
|
|
80
98
|
else {
|
|
81
99
|
h = Buffer.alloc(masked ? 14 : 10);
|
|
82
|
-
h[0] = 0x80 |
|
|
100
|
+
h[0] = 0x80 | rsv1Bit | actualOpcode;
|
|
83
101
|
h[1] = base | 127;
|
|
84
|
-
h.writeUInt32BE(Math.floor(
|
|
85
|
-
h.writeUInt32BE(
|
|
102
|
+
h.writeUInt32BE(Math.floor(compressed.length / 0x100000000), 2);
|
|
103
|
+
h.writeUInt32BE(compressed.length & 0xFFFFFFFF, 6);
|
|
86
104
|
if (mk)
|
|
87
105
|
mk.copy(h, 10);
|
|
88
106
|
}
|
|
89
107
|
if (mk)
|
|
90
|
-
for (let i = 0; i <
|
|
91
|
-
|
|
92
|
-
return Buffer.concat([h,
|
|
108
|
+
for (let i = 0; i < compressed.length; i++)
|
|
109
|
+
compressed[i] ^= mk[i & 3];
|
|
110
|
+
return Buffer.concat([h, compressed]);
|
|
93
111
|
}
|
|
94
112
|
/* Server (unmasked) */
|
|
95
|
-
export const createWSTextFrame = (msg) => createWSFrame(OP_TEXT, msg);
|
|
113
|
+
export const createWSTextFrame = (msg, compress = false) => createWSFrame(OP_TEXT, msg, false, compress);
|
|
96
114
|
export const createWSBinaryFrame = (data) => createWSFrame(OP_BINARY, data);
|
|
97
115
|
export const createWSCloseFrame = (code = CLOSE_NORMAL, reason = '') => {
|
|
98
116
|
const b = Buffer.alloc(2 + Buffer.byteLength(reason));
|
|
@@ -104,7 +122,7 @@ export const createWSCloseFrame = (code = CLOSE_NORMAL, reason = '') => {
|
|
|
104
122
|
export const createWSPingFrame = (data) => createWSFrame(OP_PING, data || Buffer.alloc(0));
|
|
105
123
|
export const createWSPongFrame = (data) => createWSFrame(OP_PONG, data || Buffer.alloc(0));
|
|
106
124
|
/* Client (masked) */
|
|
107
|
-
export const createWSTextFrameMasked = (msg) => createWSFrame(OP_TEXT, msg, true);
|
|
125
|
+
export const createWSTextFrameMasked = (msg, compress = false) => createWSFrame(OP_TEXT, msg, true, compress);
|
|
108
126
|
export const createWSBinaryFrameMasked = (data) => createWSFrame(OP_BINARY, data, true);
|
|
109
127
|
export const createWSCloseFrameMasked = (code = CLOSE_NORMAL, reason = '') => {
|
|
110
128
|
const b = Buffer.alloc(2 + Buffer.byteLength(reason));
|
|
@@ -115,36 +133,67 @@ export const createWSCloseFrameMasked = (code = CLOSE_NORMAL, reason = '') => {
|
|
|
115
133
|
};
|
|
116
134
|
export const createWSPingFrameMasked = () => createWSFrame(OP_PING, Buffer.alloc(0), true);
|
|
117
135
|
export const createWSPongFrameMasked = () => createWSFrame(OP_PONG, Buffer.alloc(0), true);
|
|
136
|
+
/** Streaming WS frame parser — O(1) append, avoids Buffer.concat O(n²) */
|
|
118
137
|
export class WSFrameParser {
|
|
119
138
|
constructor(max = DEFAULT_MAX_WS_FRAME_SIZE) {
|
|
120
|
-
this.
|
|
139
|
+
this.chunks = [];
|
|
140
|
+
this.len = 0;
|
|
121
141
|
this.received = 0;
|
|
122
142
|
this.max = max;
|
|
123
143
|
}
|
|
144
|
+
_compact() {
|
|
145
|
+
if (this.chunks.length <= 1)
|
|
146
|
+
return this.chunks[0] || Buffer.alloc(0);
|
|
147
|
+
const buf = Buffer.concat(this.chunks);
|
|
148
|
+
this.chunks = [buf];
|
|
149
|
+
return buf;
|
|
150
|
+
}
|
|
124
151
|
feed(data) {
|
|
125
152
|
this.received += data.length;
|
|
126
|
-
this.
|
|
127
|
-
|
|
128
|
-
|
|
153
|
+
this.chunks.push(data);
|
|
154
|
+
this.len += data.length;
|
|
155
|
+
if (this.len > this.max * 2) {
|
|
156
|
+
this.chunks = [];
|
|
157
|
+
this.len = 0;
|
|
129
158
|
throw new WebSocketError('Buffer overflow', CLOSE_POLICY_VIOLATION);
|
|
130
159
|
}
|
|
131
160
|
const frames = [];
|
|
132
|
-
while (this.
|
|
161
|
+
while (this.len > 0) {
|
|
162
|
+
const buf = this._compact();
|
|
133
163
|
try {
|
|
134
|
-
const r = parseWSFrame(
|
|
164
|
+
const r = parseWSFrame(buf, this.max);
|
|
135
165
|
if (!r)
|
|
136
166
|
break;
|
|
137
|
-
|
|
138
|
-
if (
|
|
167
|
+
const consumed = r.consumed;
|
|
168
|
+
if (consumed < buf.length) {
|
|
169
|
+
this.chunks = [Buffer.from(buf.subarray(consumed))];
|
|
170
|
+
this.len = this.chunks[0].length;
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
this.chunks = [];
|
|
174
|
+
this.len = 0;
|
|
175
|
+
}
|
|
176
|
+
if (r.frame.opcode !== OP_CONTINUATION) {
|
|
177
|
+
if (r.frame.rsv1 && (r.frame.opcode === OP_TEXT || r.frame.opcode === OP_BINARY)) {
|
|
178
|
+
try {
|
|
179
|
+
r.frame.payload = wsDecompress(r.frame.payload);
|
|
180
|
+
r.frame.rsv1 = false;
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
throw new WebSocketError('Decompression failed', CLOSE_INVALID_PAYLOAD);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
139
186
|
frames.push(r.frame);
|
|
187
|
+
}
|
|
140
188
|
}
|
|
141
189
|
catch (e) {
|
|
142
|
-
this.
|
|
190
|
+
this.chunks = [];
|
|
191
|
+
this.len = 0;
|
|
143
192
|
throw e;
|
|
144
193
|
}
|
|
145
194
|
}
|
|
146
195
|
return frames;
|
|
147
196
|
}
|
|
148
|
-
reset() { this.
|
|
197
|
+
reset() { this.chunks = []; this.len = 0; this.received = 0; }
|
|
149
198
|
getBytesReceived() { return this.received; }
|
|
150
199
|
}
|