stelar-time-real 2.0.4 → 3.2.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.
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @stelar-time-real Logger
3
+ * Zero-dependency structured logger with levels. Works in Node.js and browser.
4
+ */
5
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent';
6
+ export interface LoggerOptions {
7
+ level?: LogLevel;
8
+ timestamp?: boolean;
9
+ prefix?: string;
10
+ colorize?: boolean;
11
+ }
12
+ export declare class Logger {
13
+ private level;
14
+ private timestamp;
15
+ private prefix;
16
+ private colorize;
17
+ constructor(options?: LoggerOptions);
18
+ setLevel(level: LogLevel): this;
19
+ private shouldLog;
20
+ private format;
21
+ private _write;
22
+ debug(message: string, meta?: Record<string, unknown>): void;
23
+ info(message: string, meta?: Record<string, unknown>): void;
24
+ warn(message: string, meta?: Record<string, unknown>): void;
25
+ error(message: string, meta?: Record<string, unknown>): void;
26
+ }
27
+ /** No-op logger for zero overhead when logging is disabled */
28
+ export declare const NULL_LOGGER: Logger;
29
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["logger.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;AAUtE,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,QAAQ,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAYD,qBAAa,MAAM;IACjB,OAAO,CAAC,KAAK,CAAW;IACxB,OAAO,CAAC,SAAS,CAAU;IAC3B,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,QAAQ,CAAU;gBAEd,OAAO,GAAE,aAAkB;IAOvC,QAAQ,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI;IAK/B,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,MAAM;IA2Bd,OAAO,CAAC,MAAM;IAed,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAI5D,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAI3D,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAI3D,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;CAG7D;AAED,8DAA8D;AAC9D,eAAO,MAAM,WAAW,EAAE,MAAwC,CAAC"}
package/src/logger.js ADDED
@@ -0,0 +1,98 @@
1
+ /**
2
+ * @stelar-time-real Logger
3
+ * Zero-dependency structured logger with levels. Works in Node.js and browser.
4
+ */
5
+ const LEVEL_PRIORITY = {
6
+ debug: 0,
7
+ info: 1,
8
+ warn: 2,
9
+ error: 3,
10
+ silent: 4,
11
+ };
12
+ const COLORS = {
13
+ debug: '\x1b[36m',
14
+ info: '\x1b[32m',
15
+ warn: '\x1b[33m',
16
+ error: '\x1b[31m',
17
+ reset: '\x1b[0m',
18
+ };
19
+ const isBrowser = typeof window !== 'undefined' && typeof process === 'undefined';
20
+ export class Logger {
21
+ constructor(options = {}) {
22
+ this.level = options.level || 'info';
23
+ this.timestamp = options.timestamp !== false;
24
+ this.prefix = options.prefix || 'stelar';
25
+ this.colorize = isBrowser ? false : (options.colorize !== false);
26
+ }
27
+ setLevel(level) {
28
+ this.level = level;
29
+ return this;
30
+ }
31
+ shouldLog(level) {
32
+ return LEVEL_PRIORITY[level] >= LEVEL_PRIORITY[this.level];
33
+ }
34
+ format(level, message, meta) {
35
+ const parts = [];
36
+ if (this.timestamp) {
37
+ parts.push(new Date().toISOString());
38
+ }
39
+ if (this.colorize) {
40
+ const c = COLORS[level] || '';
41
+ parts.push(`${c}[${this.prefix}:${level}]${COLORS.reset}`);
42
+ }
43
+ else {
44
+ parts.push(`[${this.prefix}:${level}]`);
45
+ }
46
+ parts.push(message);
47
+ if (meta && Object.keys(meta).length > 0) {
48
+ try {
49
+ parts.push(JSON.stringify(meta));
50
+ }
51
+ catch {
52
+ parts.push('[meta: circular]');
53
+ }
54
+ }
55
+ return parts.join(' ');
56
+ }
57
+ _write(level, target, message, meta) {
58
+ const formatted = this.format(level, message, meta);
59
+ if (isBrowser) {
60
+ switch (level) {
61
+ case 'debug':
62
+ console.debug(formatted);
63
+ break;
64
+ case 'info':
65
+ console.info(formatted);
66
+ break;
67
+ case 'warn':
68
+ console.warn(formatted);
69
+ break;
70
+ case 'error':
71
+ console.error(formatted);
72
+ break;
73
+ }
74
+ }
75
+ else {
76
+ const stream = target === 'stderr' ? process.stderr : process.stdout;
77
+ stream.write(formatted + '\n');
78
+ }
79
+ }
80
+ debug(message, meta) {
81
+ if (this.shouldLog('debug'))
82
+ this._write('debug', 'stdout', message, meta);
83
+ }
84
+ info(message, meta) {
85
+ if (this.shouldLog('info'))
86
+ this._write('info', 'stdout', message, meta);
87
+ }
88
+ warn(message, meta) {
89
+ if (this.shouldLog('warn'))
90
+ this._write('warn', 'stderr', message, meta);
91
+ }
92
+ error(message, meta) {
93
+ if (this.shouldLog('error'))
94
+ this._write('error', 'stderr', message, meta);
95
+ }
96
+ }
97
+ /** No-op logger for zero overhead when logging is disabled */
98
+ export const NULL_LOGGER = new Logger({ level: 'silent' });
package/src/logger.ts ADDED
@@ -0,0 +1,115 @@
1
+ /**
2
+ * @stelar-time-real Logger
3
+ * Zero-dependency structured logger with levels. Works in Node.js and browser.
4
+ */
5
+
6
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent';
7
+
8
+ const LEVEL_PRIORITY: Record<LogLevel, number> = {
9
+ debug: 0,
10
+ info: 1,
11
+ warn: 2,
12
+ error: 3,
13
+ silent: 4,
14
+ };
15
+
16
+ export interface LoggerOptions {
17
+ level?: LogLevel;
18
+ timestamp?: boolean;
19
+ prefix?: string;
20
+ colorize?: boolean;
21
+ }
22
+
23
+ const COLORS: Record<string, string> = {
24
+ debug: '\x1b[36m',
25
+ info: '\x1b[32m',
26
+ warn: '\x1b[33m',
27
+ error: '\x1b[31m',
28
+ reset: '\x1b[0m',
29
+ };
30
+
31
+ const isBrowser = typeof window !== 'undefined' && typeof process === 'undefined';
32
+
33
+ export class Logger {
34
+ private level: LogLevel;
35
+ private timestamp: boolean;
36
+ private prefix: string;
37
+ private colorize: boolean;
38
+
39
+ constructor(options: LoggerOptions = {}) {
40
+ this.level = options.level || 'info';
41
+ this.timestamp = options.timestamp !== false;
42
+ this.prefix = options.prefix || 'stelar';
43
+ this.colorize = isBrowser ? false : (options.colorize !== false);
44
+ }
45
+
46
+ setLevel(level: LogLevel): this {
47
+ this.level = level;
48
+ return this;
49
+ }
50
+
51
+ private shouldLog(level: LogLevel): boolean {
52
+ return LEVEL_PRIORITY[level] >= LEVEL_PRIORITY[this.level];
53
+ }
54
+
55
+ private format(level: string, message: string, meta?: Record<string, unknown>): string {
56
+ const parts: string[] = [];
57
+
58
+ if (this.timestamp) {
59
+ parts.push(new Date().toISOString());
60
+ }
61
+
62
+ if (this.colorize) {
63
+ const c = COLORS[level] || '';
64
+ parts.push(`${c}[${this.prefix}:${level}]${COLORS.reset}`);
65
+ } else {
66
+ parts.push(`[${this.prefix}:${level}]`);
67
+ }
68
+
69
+ parts.push(message);
70
+
71
+ if (meta && Object.keys(meta).length > 0) {
72
+ try {
73
+ parts.push(JSON.stringify(meta));
74
+ } catch {
75
+ parts.push('[meta: circular]');
76
+ }
77
+ }
78
+
79
+ return parts.join(' ');
80
+ }
81
+
82
+ private _write(level: string, target: 'stdout' | 'stderr', message: string, meta?: Record<string, unknown>): void {
83
+ const formatted = this.format(level, message, meta);
84
+ if (isBrowser) {
85
+ switch (level) {
86
+ case 'debug': console.debug(formatted); break;
87
+ case 'info': console.info(formatted); break;
88
+ case 'warn': console.warn(formatted); break;
89
+ case 'error': console.error(formatted); break;
90
+ }
91
+ } else {
92
+ const stream = target === 'stderr' ? process.stderr : process.stdout;
93
+ stream.write(formatted + '\n');
94
+ }
95
+ }
96
+
97
+ debug(message: string, meta?: Record<string, unknown>): void {
98
+ if (this.shouldLog('debug')) this._write('debug', 'stdout', message, meta);
99
+ }
100
+
101
+ info(message: string, meta?: Record<string, unknown>): void {
102
+ if (this.shouldLog('info')) this._write('info', 'stdout', message, meta);
103
+ }
104
+
105
+ warn(message: string, meta?: Record<string, unknown>): void {
106
+ if (this.shouldLog('warn')) this._write('warn', 'stderr', message, meta);
107
+ }
108
+
109
+ error(message: string, meta?: Record<string, unknown>): void {
110
+ if (this.shouldLog('error')) this._write('error', 'stderr', message, meta);
111
+ }
112
+ }
113
+
114
+ /** No-op logger for zero overhead when logging is disabled */
115
+ export const NULL_LOGGER: Logger = new Logger({ level: 'silent' });
@@ -0,0 +1,57 @@
1
+ /**
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.
8
+ */
9
+ export declare const FRAME_JSON = 1;
10
+ export declare const FRAME_BINARY = 2;
11
+ export declare const FRAME_PING = 3;
12
+ export declare const FRAME_PONG = 4;
13
+ export declare const FRAME_ACK_REQ = 5;
14
+ export declare const FRAME_ACK_RES = 6;
15
+ export declare const FRAME_CONNECT = 7;
16
+ export declare const FRAME_DISCONNECT = 8;
17
+ export declare const FRAME_JOIN = 9;
18
+ export declare const FRAME_LEAVE = 10;
19
+ export declare const FRAME_ERROR = 11;
20
+ /** Max event name length in bytes */
21
+ export declare const MAX_EVENT_LENGTH = 256;
22
+ /** Default max frame size: 10 MB */
23
+ export declare const DEFAULT_MAX_FRAME_SIZE: number;
24
+ export declare const HEADER_SIZE = 7;
25
+ export interface ParsedFrame {
26
+ type: number;
27
+ event: string;
28
+ payload: Buffer;
29
+ }
30
+ export declare class ProtocolError extends Error {
31
+ code: string;
32
+ constructor(message: string, code?: string);
33
+ }
34
+ /** Validates event name format. Throws ProtocolError on invalid input. */
35
+ export declare function validateEventName(event: string): void;
36
+ export declare function encodeJsonFrame(event: string, data: unknown, maxFrameSize?: number): Buffer;
37
+ export declare function encodeBinaryFrame(event: string, data: Uint8Array | Buffer, maxFrameSize?: number): Buffer;
38
+ export declare function encodePingFrame(): Buffer;
39
+ export declare function encodePongFrame(): Buffer;
40
+ export declare function encodeAckReqFrame(ackName: string, data: unknown, maxFrameSize?: number): Buffer;
41
+ export declare function encodeAckResFrame(ackName: string, data: unknown, maxFrameSize?: number): Buffer;
42
+ export declare function encodeConnectFrame(clientId: string): Buffer;
43
+ export declare function encodeDisconnectFrame(): Buffer;
44
+ export declare function encodeJoinFrame(room: string, maxFrameSize?: number): Buffer;
45
+ export declare function encodeLeaveFrame(room: string): Buffer;
46
+ export declare function encodeErrorFrame(message: string): Buffer;
47
+ /** Streaming frame parser for TCP connections. Buffers partial data and emits complete frames. */
48
+ export declare class FrameParser {
49
+ private buf;
50
+ private maxFrameSize;
51
+ private totalBytesReceived;
52
+ constructor(maxFrameSize?: number);
53
+ feed(data: Buffer): ParsedFrame[];
54
+ reset(): void;
55
+ getBytesReceived(): number;
56
+ }
57
+ //# sourceMappingURL=protocol.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,eAAO,MAAM,UAAU,IAAa,CAAC;AACrC,eAAO,MAAM,YAAY,IAAW,CAAC;AACrC,eAAO,MAAM,UAAU,IAAa,CAAC;AACrC,eAAO,MAAM,UAAU,IAAa,CAAC;AACrC,eAAO,MAAM,aAAa,IAAU,CAAC;AACrC,eAAO,MAAM,aAAa,IAAU,CAAC;AACrC,eAAO,MAAM,aAAa,IAAU,CAAC;AACrC,eAAO,MAAM,gBAAgB,IAAO,CAAC;AACrC,eAAO,MAAM,UAAU,IAAa,CAAC;AACrC,eAAO,MAAM,WAAW,KAAY,CAAC;AACrC,eAAO,MAAM,WAAW,KAAY,CAAC;AAErC,qCAAqC;AACrC,eAAO,MAAM,gBAAgB,MAAM,CAAC;AACpC,oCAAoC;AACpC,eAAO,MAAM,sBAAsB,QAAmB,CAAC;AACvD,eAAO,MAAM,WAAW,IAAI,CAAC;AAE7B,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,aAAc,SAAQ,KAAK;IAC/B,IAAI,EAAE,MAAM,CAAC;gBACR,OAAO,EAAE,MAAM,EAAE,IAAI,SAAmB;CAKrD;AAED,0EAA0E;AAC1E,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAgBrD;AA6BD,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAK3F;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAKzG;AAED,wBAAgB,eAAe,IAAI,MAAM,CAMxC;AAED,wBAAgB,eAAe,IAAI,MAAM,CAMxC;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAI/F;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAI/F;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED,wBAAgB,qBAAqB,IAAI,MAAM,CAM9C;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAG3E;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAGrD;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,kGAAkG;AAClG,qBAAa,WAAW;IACtB,OAAO,CAAC,GAAG,CAA2B;IACtC,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,kBAAkB,CAAK;gBAEnB,YAAY,SAAyB;IAIjD,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,EAAE;IA8DjC,KAAK,IAAI,IAAI;IAKb,gBAAgB,IAAI,MAAM;CAG3B"}
@@ -0,0 +1,193 @@
1
+ /**
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.
8
+ */
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 */
21
+ export const MAX_EVENT_LENGTH = 256;
22
+ /** Default max frame size: 10 MB */
23
+ export const DEFAULT_MAX_FRAME_SIZE = 10 * 1024 * 1024;
24
+ export const HEADER_SIZE = 7;
25
+ export class ProtocolError extends Error {
26
+ constructor(message, code = 'PROTOCOL_ERROR') {
27
+ super(message);
28
+ this.name = 'ProtocolError';
29
+ this.code = code;
30
+ }
31
+ }
32
+ /** Validates event name format. Throws ProtocolError on invalid input. */
33
+ export function validateEventName(event) {
34
+ if (typeof event !== 'string') {
35
+ throw new ProtocolError('Event name must be a string', 'INVALID_EVENT');
36
+ }
37
+ if (event.length === 0) {
38
+ throw new ProtocolError('Event name cannot be empty', 'EMPTY_EVENT');
39
+ }
40
+ if (event.length > MAX_EVENT_LENGTH) {
41
+ throw new ProtocolError(`Event name exceeds ${MAX_EVENT_LENGTH} bytes`, 'EVENT_TOO_LONG');
42
+ }
43
+ if (!/^[\w\-./:]+$/.test(event)) {
44
+ throw new ProtocolError('Event name contains invalid characters', 'INVALID_EVENT_CHARS');
45
+ }
46
+ if (['ping', 'pong', 'connect', 'disconnect', 'error'].includes(event)) {
47
+ throw new ProtocolError(`Event "${event}" is reserved`, 'RESERVED_EVENT');
48
+ }
49
+ }
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) {
57
+ 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);
100
+ return f;
101
+ }
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() {
118
+ const f = Buffer.alloc(HEADER_SIZE);
119
+ f.writeUInt32BE(HEADER_SIZE, 0);
120
+ f[4] = FRAME_DISCONNECT;
121
+ f.writeUInt16BE(0, 5);
122
+ 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. */
136
+ export class FrameParser {
137
+ constructor(maxFrameSize = DEFAULT_MAX_FRAME_SIZE) {
138
+ this.buf = Buffer.alloc(0);
139
+ this.totalBytesReceived = 0;
140
+ this.maxFrameSize = maxFrameSize;
141
+ }
142
+ feed(data) {
143
+ this.totalBytesReceived += data.length;
144
+ this.buf = Buffer.concat([this.buf, data]);
145
+ if (this.buf.length > this.maxFrameSize * 2) {
146
+ this.buf = Buffer.alloc(0);
147
+ throw new ProtocolError(`Input buffer exceeded limit (${this.maxFrameSize * 2} bytes)`, 'BUFFER_OVERFLOW');
148
+ }
149
+ const frames = [];
150
+ 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) {
157
+ this.buf = Buffer.alloc(0);
158
+ throw new ProtocolError(`Frame exceeds max size (${this.maxFrameSize} bytes)`, 'FRAME_TOO_LARGE');
159
+ }
160
+ if (this.buf.length < totalLen)
161
+ 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) {
169
+ this.buf = Buffer.alloc(0);
170
+ throw new ProtocolError(`Event name exceeds ${MAX_EVENT_LENGTH} bytes`, 'EVENT_TOO_LONG');
171
+ }
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);
183
+ }
184
+ return frames;
185
+ }
186
+ reset() {
187
+ this.buf = Buffer.alloc(0);
188
+ this.totalBytesReceived = 0;
189
+ }
190
+ getBytesReceived() {
191
+ return this.totalBytesReceived;
192
+ }
193
+ }