stelar-time-real 3.2.0 → 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/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 validatePayloadSize(payload, maxSize) {
51
- if (payload.length > maxSize) {
52
- throw new ProtocolError(`Payload exceeds max size (${maxSize} bytes)`, 'PAYLOAD_TOO_LARGE');
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
- const eventBuf = Buffer.from(event, 'utf8');
60
- const totalLen = HEADER_SIZE + eventBuf.length + payload.length;
61
- if (totalLen > maxFrameSize) {
62
- throw new ProtocolError(`Frame exceeds max size (${maxFrameSize} bytes)`, 'FRAME_TOO_LARGE');
63
- }
64
- const frame = Buffer.alloc(totalLen);
65
- frame.writeUInt32BE(totalLen, 0);
66
- frame[4] = type;
67
- frame.writeUInt16BE(eventBuf.length, 5);
68
- if (eventBuf.length > 0)
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
- export function encodeAckReqFrame(ackName, data, maxFrameSize) {
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] = FRAME_DISCONNECT;
48
+ f[4] = type;
121
49
  f.writeUInt16BE(0, 5);
122
50
  return f;
123
- }
124
- export function encodeJoinFrame(room, maxFrameSize) {
125
- const payload = Buffer.from(room, 'utf8');
126
- return encodeFrame(FRAME_JOIN, 'join-room', payload, maxFrameSize);
127
- }
128
- export function encodeLeaveFrame(room) {
129
- const payload = room ? Buffer.from(room, 'utf8') : Buffer.alloc(0);
130
- return encodeFrame(FRAME_LEAVE, 'leave-room', payload);
131
- }
132
- export function encodeErrorFrame(message) {
133
- return encodeFrame(FRAME_ERROR, 'error', Buffer.from(message, 'utf8'));
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(maxFrameSize = DEFAULT_MAX_FRAME_SIZE) {
64
+ constructor(max = DEFAULT_MAX_FRAME_SIZE) {
138
65
  this.buf = Buffer.alloc(0);
139
- this.totalBytesReceived = 0;
140
- this.maxFrameSize = maxFrameSize;
66
+ this.received = 0;
67
+ this.max = max;
141
68
  }
142
69
  feed(data) {
143
- this.totalBytesReceived += data.length;
70
+ this.received += data.length;
144
71
  this.buf = Buffer.concat([this.buf, data]);
145
- if (this.buf.length > this.maxFrameSize * 2) {
72
+ if (this.buf.length > this.max * 2) {
146
73
  this.buf = Buffer.alloc(0);
147
- throw new ProtocolError(`Input buffer exceeded limit (${this.maxFrameSize * 2} bytes)`, 'BUFFER_OVERFLOW');
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 totalLen = this.buf.readUInt32BE(0);
152
- if (totalLen < HEADER_SIZE) {
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(`Frame exceeds max size (${this.maxFrameSize} bytes)`, 'FRAME_TOO_LARGE');
81
+ throw new ProtocolError(`Invalid frame size: ${total}`, total < HEADER_SIZE ? 'INVALID_FRAME_SIZE' : 'FRAME_TOO_LARGE');
159
82
  }
160
- if (this.buf.length < totalLen)
83
+ if (this.buf.length < total)
161
84
  break;
162
- const type = this.buf[4];
163
- const eventLen = this.buf.readUInt16BE(5);
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(`Event name exceeds ${MAX_EVENT_LENGTH} bytes`, 'EVENT_TOO_LONG');
88
+ throw new ProtocolError('Invalid event length', 'INVALID_EVENT_LENGTH');
171
89
  }
172
- const event = eventLen > 0
173
- ? this.buf.subarray(HEADER_SIZE, HEADER_SIZE + eventLen).toString('utf8')
174
- : '';
175
- const payloadStart = HEADER_SIZE + eventLen;
176
- const payload = totalLen > payloadStart
177
- ? Buffer.from(this.buf.subarray(payloadStart, totalLen))
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
- this.buf = Buffer.alloc(0);
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 = 0x01;
11
- export const FRAME_BINARY = 0x02;
12
- export const FRAME_PING = 0x03;
13
- export const FRAME_PONG = 0x04;
14
- export const FRAME_ACK_REQ = 0x05;
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
- public code: string;
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
- throw new ProtocolError('Event name must be a string', 'INVALID_EVENT');
47
- }
48
- if (event.length === 0) {
49
- throw new ProtocolError('Event name cannot be empty', 'EMPTY_EVENT');
50
- }
51
- if (event.length > MAX_EVENT_LENGTH) {
52
- throw new ProtocolError(`Event name exceeds ${MAX_EVENT_LENGTH} bytes`, 'EVENT_TOO_LONG');
53
- }
54
- if (!/^[\w\-./:]+$/.test(event)) {
55
- throw new ProtocolError('Event name contains invalid characters', 'INVALID_EVENT_CHARS');
56
- }
57
- if (['ping', 'pong', 'connect', 'disconnect', 'error'].includes(event)) {
58
- throw new ProtocolError(`Event "${event}" is reserved`, 'RESERVED_EVENT');
59
- }
60
- }
61
-
62
- function validatePayloadSize(payload: Buffer, maxSize: number): void {
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
- export function encodePongFrame(): Buffer {
48
+ const emptyFrame = (type: number): Buffer => {
112
49
  const f = Buffer.alloc(HEADER_SIZE);
113
50
  f.writeUInt32BE(HEADER_SIZE, 0);
114
- f[4] = FRAME_PONG;
51
+ f[4] = type;
115
52
  f.writeUInt16BE(0, 5);
116
53
  return f;
117
- }
54
+ };
118
55
 
119
- export function encodeAckReqFrame(ackName: string, data: unknown, maxFrameSize?: number): Buffer {
120
- const payload = Buffer.from(JSON.stringify(data), 'utf8');
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 function encodeAckResFrame(ackName: string, data: unknown, maxFrameSize?: number): Buffer {
126
- const payload = Buffer.from(JSON.stringify(data), 'utf8');
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 function encodeConnectFrame(clientId: string): Buffer {
132
- return encodeFrame(FRAME_CONNECT, 'connect', Buffer.from(clientId, 'utf8'));
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 function encodeDisconnectFrame(): Buffer {
136
- const f = Buffer.alloc(HEADER_SIZE);
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 function encodeLeaveFrame(room: string): Buffer {
149
- const payload = room ? Buffer.from(room, 'utf8') : Buffer.alloc(0);
150
- return encodeFrame(FRAME_LEAVE, 'leave-room', payload);
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: Buffer = Buffer.alloc(0);
160
- private maxFrameSize: number;
161
- private totalBytesReceived = 0;
77
+ private buf = Buffer.alloc(0);
78
+ private max: number;
79
+ private received = 0;
162
80
 
163
- constructor(maxFrameSize = DEFAULT_MAX_FRAME_SIZE) {
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.totalBytesReceived += data.length;
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 totalLen = this.buf.readUInt32BE(0);
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: ${totalLen}`, 'INVALID_FRAME_SIZE');
95
+ throw new ProtocolError(`Invalid frame size: ${total}`, total < HEADER_SIZE ? 'INVALID_FRAME_SIZE' : 'FRAME_TOO_LARGE');
187
96
  }
188
-
189
- if (totalLen > this.maxFrameSize) {
190
- this.buf = Buffer.alloc(0);
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(`Event name exceeds ${MAX_EVENT_LENGTH} bytes`, 'EVENT_TOO_LONG');
101
+ throw new ProtocolError('Invalid event length', 'INVALID_EVENT_LENGTH');
210
102
  }
211
-
212
- const event = eventLen > 0
213
- ? this.buf.subarray(HEADER_SIZE, HEADER_SIZE + eventLen).toString('utf8')
214
- : '';
215
- const payloadStart = HEADER_SIZE + eventLen;
216
- const payload = totalLen > payloadStart
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(): void {
230
- this.buf = Buffer.alloc(0);
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
  }
@@ -1,65 +1,43 @@
1
1
  /**
2
- * @stelar-time-real WebSocket Protocol (RFC 6455)
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 OP_TEXT = 1;
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
- /** Compute Sec-WebSocket-Accept from client key per RFC 6455 Section 4.2.2 */
27
- export declare function computeAcceptKey(key: string): string;
28
- export declare function generateWSKey(): string;
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
- /** Parse a single WebSocket frame from buffer. Returns null if incomplete. */
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
- /** Create an unmasked WS frame (server-to-client per RFC 6455 Section 5.3) */
44
- export declare function createWSFrame(opcode: number, payload: Buffer | string): Buffer;
45
- /** Create a masked WS frame (client-to-server per RFC 6455 Section 5.3) */
46
- export declare function createWSFrameMasked(opcode: number, payload: Buffer | string): Buffer;
47
- export declare function createWSTextFrame(message: string): Buffer;
48
- export declare function createWSBinaryFrame(data: Buffer): Buffer;
49
- export declare function createWSCloseFrame(code?: number, reason?: string): Buffer;
50
- export declare function createWSPingFrame(data?: Buffer): Buffer;
51
- export declare function createWSPongFrame(data?: Buffer): Buffer;
52
- export declare function createWSTextFrameMasked(message: string): Buffer;
53
- export declare function createWSBinaryFrameMasked(data: Buffer): Buffer;
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 maxFrameSize;
61
- private totalBytesReceived;
62
- constructor(maxFrameSize?: number);
38
+ private max;
39
+ private received;
40
+ constructor(max?: number);
63
41
  feed(data: Buffer): WSFrame[];
64
42
  reset(): void;
65
43
  getBytesReceived(): number;