@livedesk/hub 0.1.41 → 0.1.43

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.
@@ -1,12 +1,12 @@
1
- import crypto from 'node:crypto';
2
- import { inflateRawSync } from 'node:zlib';
1
+ import crypto from 'node:crypto';
2
+ import { inflateRawSync } from 'node:zlib';
3
3
 
4
4
  export const UDP_P2P_PROTOCOL = 'livedesk.udp.p2p.v1';
5
- export const UDP_P2P_VERSION = 1;
6
- export const UDP_MAX_DATAGRAM_BYTES = 1200;
7
- const UDP_SERIAL_MODULUS = 0x1_0000_0000;
8
- const UDP_SERIAL_HALF_RANGE = 0x8000_0000;
9
- export const UDP_PACKET_TYPES = Object.freeze({
5
+ export const UDP_P2P_VERSION = 1;
6
+ export const UDP_MAX_DATAGRAM_BYTES = 1200;
7
+ const UDP_SERIAL_MODULUS = 0x1_0000_0000;
8
+ const UDP_SERIAL_HALF_RANGE = 0x8000_0000;
9
+ export const UDP_PACKET_TYPES = Object.freeze({
10
10
  probe: 1,
11
11
  'probe-ack': 2,
12
12
  ready: 3,
@@ -14,20 +14,20 @@ export const UDP_PACKET_TYPES = Object.freeze({
14
14
  pong: 5,
15
15
  frame: 6,
16
16
  close: 7
17
- });
18
- const UDP_PACKET_TYPE_NAMES = new Map(Object.entries(UDP_PACKET_TYPES).map(([name, value]) => [value, name]));
19
- const UDP_FIXED_HEADER_BYTES = 15;
20
- const UDP_NONCE_BYTES = 12;
21
- const UDP_TAG_BYTES = 16;
22
- const UDP_MAX_SESSION_ID_BYTES = 96;
23
- const UDP_MAX_HEADER_BYTES = 4096;
24
- const UDP_MAX_PAYLOAD_BYTES = UDP_MAX_DATAGRAM_BYTES - UDP_FIXED_HEADER_BYTES - UDP_NONCE_BYTES - UDP_TAG_BYTES - 1;
25
- const UDP_FRAME_PAYLOAD_MAGIC = Buffer.from('LDF1', 'ascii');
26
- const UDP_PACKET_MAGIC = Buffer.from('LDU1', 'ascii');
27
- const UDP_FRAME_CHUNK_MAGIC = Buffer.from('LDC1', 'ascii');
28
- export const UDP_FRAME_CHUNK_PROTOCOL = 'livedesk.udp.frame-chunk.v1';
29
- export const UDP_FRAME_CHUNK_HEADER_BYTES = 12;
30
- const UDP_MAX_FRAME_HEADER_BYTES = 64 * 1024;
17
+ });
18
+ const UDP_PACKET_TYPE_NAMES = new Map(Object.entries(UDP_PACKET_TYPES).map(([name, value]) => [value, name]));
19
+ const UDP_FIXED_HEADER_BYTES = 15;
20
+ const UDP_NONCE_BYTES = 12;
21
+ const UDP_TAG_BYTES = 16;
22
+ const UDP_MAX_SESSION_ID_BYTES = 96;
23
+ const UDP_MAX_HEADER_BYTES = 4096;
24
+ const UDP_MAX_PAYLOAD_BYTES = UDP_MAX_DATAGRAM_BYTES - UDP_FIXED_HEADER_BYTES - UDP_NONCE_BYTES - UDP_TAG_BYTES - 1;
25
+ const UDP_FRAME_PAYLOAD_MAGIC = Buffer.from('LDF1', 'ascii');
26
+ const UDP_PACKET_MAGIC = Buffer.from('LDU1', 'ascii');
27
+ const UDP_FRAME_CHUNK_MAGIC = Buffer.from('LDC1', 'ascii');
28
+ export const UDP_FRAME_CHUNK_PROTOCOL = 'livedesk.udp.frame-chunk.v1';
29
+ export const UDP_FRAME_CHUNK_HEADER_BYTES = 12;
30
+ const UDP_MAX_FRAME_HEADER_BYTES = 64 * 1024;
31
31
 
32
32
  function asBuffer(value) {
33
33
  if (Buffer.isBuffer(value)) return value;
@@ -41,244 +41,244 @@ function normalizeKey(key) {
41
41
  return value.length === 32 ? value : crypto.createHash('sha256').update(value).digest();
42
42
  }
43
43
 
44
- function normalizeSessionId(value) {
44
+ function normalizeSessionId(value) {
45
45
  const sessionId = String(value || '').trim();
46
46
  if (!sessionId || Buffer.byteLength(sessionId, 'utf8') > UDP_MAX_SESSION_ID_BYTES) {
47
47
  throw new Error('invalid-udp-session-id');
48
48
  }
49
49
  return sessionId;
50
- }
51
-
52
- export function isUdpBinaryDatagram(packet) {
53
- return Buffer.isBuffer(packet)
54
- && packet.length >= UDP_FIXED_HEADER_BYTES + UDP_NONCE_BYTES + UDP_TAG_BYTES
55
- && packet[0] === UDP_PACKET_MAGIC[0]
56
- && packet[1] === UDP_PACKET_MAGIC[1]
57
- && packet[2] === UDP_PACKET_MAGIC[2]
58
- && packet[3] === UDP_PACKET_MAGIC[3]
59
- && packet[4] === UDP_P2P_VERSION;
60
- }
61
-
62
- export function peekUdpSessionId(packet) {
63
- if (!isUdpBinaryDatagram(packet)) return '';
64
- const sessionLength = packet[6];
65
- const headerLength = packet.readUInt16BE(7);
66
- const payloadLength = packet.readUInt16BE(9);
67
- const associatedLength = UDP_FIXED_HEADER_BYTES + UDP_NONCE_BYTES + sessionLength + headerLength;
68
- const expectedLength = associatedLength + payloadLength + UDP_TAG_BYTES;
69
- if (sessionLength < 1 || sessionLength > UDP_MAX_SESSION_ID_BYTES
70
- || headerLength > UDP_MAX_HEADER_BYTES || payloadLength > UDP_MAX_PAYLOAD_BYTES
71
- || expectedLength !== packet.length) return '';
72
- return packet.subarray(
73
- UDP_FIXED_HEADER_BYTES + UDP_NONCE_BYTES,
74
- UDP_FIXED_HEADER_BYTES + UDP_NONCE_BYTES + sessionLength
75
- ).toString('utf8');
76
- }
77
-
78
- function routeKeyForBytes(bytes, offset = 0, length = bytes.length - offset) {
79
- let hash = 0x811c9dc5;
80
- const end = offset + length;
81
- for (let index = offset; index < end; index += 1) {
82
- hash = Math.imul(hash ^ bytes[index], 0x01000193) >>> 0;
83
- }
84
- return hash;
85
- }
86
-
87
- export function udpSessionRouteKey(sessionId) {
88
- return routeKeyForBytes(Buffer.from(normalizeSessionId(sessionId), 'utf8'));
89
- }
90
-
91
- export function peekUdpSessionRouteKey(packet) {
92
- if (!isUdpBinaryDatagram(packet)) return null;
93
- const sessionLength = packet[6];
94
- const headerLength = packet.readUInt16BE(7);
95
- const payloadLength = packet.readUInt16BE(9);
96
- const associatedLength = UDP_FIXED_HEADER_BYTES + UDP_NONCE_BYTES + sessionLength + headerLength;
97
- if (sessionLength < 1 || sessionLength > UDP_MAX_SESSION_ID_BYTES
98
- || headerLength > UDP_MAX_HEADER_BYTES || payloadLength > UDP_MAX_PAYLOAD_BYTES
99
- || associatedLength + payloadLength + UDP_TAG_BYTES !== packet.length) return null;
100
- return routeKeyForBytes(
101
- packet,
102
- UDP_FIXED_HEADER_BYTES + UDP_NONCE_BYTES,
103
- sessionLength
104
- );
105
- }
106
-
107
- function encodeFrameChunkHeader(header) {
108
- const frameSerial = Number(header?.frameSerial);
109
- const chunkIndex = Number(header?.chunkIndex);
110
- const chunkCount = Number(header?.chunkCount);
111
- if (!Number.isInteger(frameSerial) || frameSerial < 0 || frameSerial > 0xffff_ffff
112
- || !Number.isInteger(chunkIndex) || chunkIndex < 0 || chunkIndex > 0xffff
113
- || !Number.isInteger(chunkCount) || chunkCount < 1 || chunkCount > 4096
114
- || chunkIndex >= chunkCount) {
115
- throw new Error('invalid-udp-frame-chunk-header');
116
- }
117
- const bytes = Buffer.allocUnsafe(UDP_FRAME_CHUNK_HEADER_BYTES);
118
- UDP_FRAME_CHUNK_MAGIC.copy(bytes, 0);
119
- bytes.writeUInt32BE(frameSerial >>> 0, 4);
120
- bytes.writeUInt16BE(chunkIndex, 8);
121
- bytes.writeUInt16BE(chunkCount, 10);
122
- return bytes;
123
- }
124
-
125
- function decodePacketHeader(type, bytes) {
126
- if (type === 'frame'
127
- && bytes.length === UDP_FRAME_CHUNK_HEADER_BYTES
128
- && bytes[0] === UDP_FRAME_CHUNK_MAGIC[0]
129
- && bytes[1] === UDP_FRAME_CHUNK_MAGIC[1]
130
- && bytes[2] === UDP_FRAME_CHUNK_MAGIC[2]
131
- && bytes[3] === UDP_FRAME_CHUNK_MAGIC[3]) {
132
- const chunkIndex = bytes.readUInt16BE(8);
133
- const chunkCount = bytes.readUInt16BE(10);
134
- if (chunkCount < 1 || chunkCount > 4096 || chunkIndex >= chunkCount) return null;
135
- const frameSerial = bytes.readUInt32BE(4);
136
- return {
137
- frameId: frameSerial,
138
- frameSerial,
139
- chunkIndex,
140
- chunkCount,
141
- binaryChunkHeader: true
142
- };
143
- }
144
- try {
145
- const value = JSON.parse(bytes.toString('utf8'));
146
- return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
147
- } catch {
148
- return null;
149
- }
150
- }
151
-
152
- function decodeFrameHeader(header) {
153
- if (header?.frameHeader && typeof header.frameHeader === 'object') {
154
- return header.frameHeader;
155
- }
156
- const encoded = String(header?.frameHeaderDeflate || '');
157
- if (!encoded || encoded.length > 8192 || !/^[A-Za-z0-9+/]+={0,2}$/.test(encoded)) {
158
- return null;
159
- }
160
- try {
161
- const json = inflateRawSync(Buffer.from(encoded, 'base64'), { maxOutputLength: 64 * 1024 }).toString('utf8');
162
- const value = JSON.parse(json);
163
- return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
164
- } catch {
165
- return null;
166
- }
167
- }
168
-
169
- function decodeFramedPayload(payload, legacyHeader = null) {
170
- const bytes = asBuffer(payload);
171
- if (bytes.length < 8 || !bytes.subarray(0, 4).equals(UDP_FRAME_PAYLOAD_MAGIC)) {
172
- return { header: legacyHeader || {}, payload: bytes };
173
- }
174
- const compressedHeaderLength = bytes.readUInt32BE(4);
175
- if (compressedHeaderLength < 1 || compressedHeaderLength > UDP_MAX_FRAME_HEADER_BYTES
176
- || 8 + compressedHeaderLength > bytes.length) return null;
177
- try {
178
- const json = inflateRawSync(bytes.subarray(8, 8 + compressedHeaderLength), {
179
- maxOutputLength: UDP_MAX_FRAME_HEADER_BYTES
180
- }).toString('utf8');
181
- const header = JSON.parse(json);
182
- if (!header || typeof header !== 'object' || Array.isArray(header)) return null;
183
- return {
184
- header,
185
- payload: bytes.subarray(8 + compressedHeaderLength)
186
- };
187
- } catch {
188
- return null;
189
- }
190
- }
191
-
192
- function copyChunkRange(chunks, sourceOffset, length, target, targetOffset = 0) {
193
- let remaining = length;
194
- let skip = sourceOffset;
195
- let written = 0;
196
- for (const chunk of chunks) {
197
- if (!chunk || remaining <= 0) continue;
198
- if (skip >= chunk.length) {
199
- skip -= chunk.length;
200
- continue;
201
- }
202
- const available = Math.min(remaining, chunk.length - skip);
203
- chunk.copy(target, targetOffset + written, skip, skip + available);
204
- written += available;
205
- remaining -= available;
206
- skip = 0;
207
- }
208
- return written === length;
209
- }
210
-
211
- function decodeFramedChunks(chunks, totalBytes, legacyHeader = null) {
212
- if (chunks.length === 1) {
213
- const complete = decodeFramedPayload(chunks[0], legacyHeader);
214
- return complete
215
- ? { ...complete, payloadCopyCount: 0, payloadCopyBytes: 0 }
216
- : null;
217
- }
218
-
219
- const prefix = Buffer.allocUnsafe(Math.min(8, totalBytes));
220
- if (!copyChunkRange(chunks, 0, prefix.length, prefix)) return null;
221
- if (prefix.length < 8 || !prefix.subarray(0, 4).equals(UDP_FRAME_PAYLOAD_MAGIC)) {
222
- const payload = Buffer.allocUnsafe(totalBytes);
223
- if (!copyChunkRange(chunks, 0, totalBytes, payload)) return null;
224
- return {
225
- header: legacyHeader || {},
226
- payload,
227
- payloadCopyCount: 1,
228
- payloadCopyBytes: totalBytes
229
- };
230
- }
231
-
232
- const compressedHeaderLength = prefix.readUInt32BE(4);
233
- const payloadOffset = 8 + compressedHeaderLength;
234
- if (compressedHeaderLength < 1 || compressedHeaderLength > UDP_MAX_FRAME_HEADER_BYTES
235
- || payloadOffset > totalBytes) return null;
236
- try {
237
- const compressedHeader = Buffer.allocUnsafe(compressedHeaderLength);
238
- if (!copyChunkRange(chunks, 8, compressedHeaderLength, compressedHeader)) return null;
239
- const json = inflateRawSync(compressedHeader, {
240
- maxOutputLength: UDP_MAX_FRAME_HEADER_BYTES
241
- }).toString('utf8');
242
- const header = JSON.parse(json);
243
- if (!header || typeof header !== 'object' || Array.isArray(header)) return null;
244
- const payloadLength = totalBytes - payloadOffset;
245
- const payload = Buffer.allocUnsafe(payloadLength);
246
- if (!copyChunkRange(chunks, payloadOffset, payloadLength, payload)) return null;
247
- return {
248
- header,
249
- payload,
250
- payloadCopyCount: 1,
251
- payloadCopyBytes: payloadLength
252
- };
253
- } catch {
254
- return null;
255
- }
256
- }
257
-
258
- function packetTypeCode(type) {
50
+ }
51
+
52
+ export function isUdpBinaryDatagram(packet) {
53
+ return Buffer.isBuffer(packet)
54
+ && packet.length >= UDP_FIXED_HEADER_BYTES + UDP_NONCE_BYTES + UDP_TAG_BYTES
55
+ && packet[0] === UDP_PACKET_MAGIC[0]
56
+ && packet[1] === UDP_PACKET_MAGIC[1]
57
+ && packet[2] === UDP_PACKET_MAGIC[2]
58
+ && packet[3] === UDP_PACKET_MAGIC[3]
59
+ && packet[4] === UDP_P2P_VERSION;
60
+ }
61
+
62
+ export function peekUdpSessionId(packet) {
63
+ if (!isUdpBinaryDatagram(packet)) return '';
64
+ const sessionLength = packet[6];
65
+ const headerLength = packet.readUInt16BE(7);
66
+ const payloadLength = packet.readUInt16BE(9);
67
+ const associatedLength = UDP_FIXED_HEADER_BYTES + UDP_NONCE_BYTES + sessionLength + headerLength;
68
+ const expectedLength = associatedLength + payloadLength + UDP_TAG_BYTES;
69
+ if (sessionLength < 1 || sessionLength > UDP_MAX_SESSION_ID_BYTES
70
+ || headerLength > UDP_MAX_HEADER_BYTES || payloadLength > UDP_MAX_PAYLOAD_BYTES
71
+ || expectedLength !== packet.length) return '';
72
+ return packet.subarray(
73
+ UDP_FIXED_HEADER_BYTES + UDP_NONCE_BYTES,
74
+ UDP_FIXED_HEADER_BYTES + UDP_NONCE_BYTES + sessionLength
75
+ ).toString('utf8');
76
+ }
77
+
78
+ function routeKeyForBytes(bytes, offset = 0, length = bytes.length - offset) {
79
+ let hash = 0x811c9dc5;
80
+ const end = offset + length;
81
+ for (let index = offset; index < end; index += 1) {
82
+ hash = Math.imul(hash ^ bytes[index], 0x01000193) >>> 0;
83
+ }
84
+ return hash;
85
+ }
86
+
87
+ export function udpSessionRouteKey(sessionId) {
88
+ return routeKeyForBytes(Buffer.from(normalizeSessionId(sessionId), 'utf8'));
89
+ }
90
+
91
+ export function peekUdpSessionRouteKey(packet) {
92
+ if (!isUdpBinaryDatagram(packet)) return null;
93
+ const sessionLength = packet[6];
94
+ const headerLength = packet.readUInt16BE(7);
95
+ const payloadLength = packet.readUInt16BE(9);
96
+ const associatedLength = UDP_FIXED_HEADER_BYTES + UDP_NONCE_BYTES + sessionLength + headerLength;
97
+ if (sessionLength < 1 || sessionLength > UDP_MAX_SESSION_ID_BYTES
98
+ || headerLength > UDP_MAX_HEADER_BYTES || payloadLength > UDP_MAX_PAYLOAD_BYTES
99
+ || associatedLength + payloadLength + UDP_TAG_BYTES !== packet.length) return null;
100
+ return routeKeyForBytes(
101
+ packet,
102
+ UDP_FIXED_HEADER_BYTES + UDP_NONCE_BYTES,
103
+ sessionLength
104
+ );
105
+ }
106
+
107
+ function encodeFrameChunkHeader(header) {
108
+ const frameSerial = Number(header?.frameSerial);
109
+ const chunkIndex = Number(header?.chunkIndex);
110
+ const chunkCount = Number(header?.chunkCount);
111
+ if (!Number.isInteger(frameSerial) || frameSerial < 0 || frameSerial > 0xffff_ffff
112
+ || !Number.isInteger(chunkIndex) || chunkIndex < 0 || chunkIndex > 0xffff
113
+ || !Number.isInteger(chunkCount) || chunkCount < 1 || chunkCount > 4096
114
+ || chunkIndex >= chunkCount) {
115
+ throw new Error('invalid-udp-frame-chunk-header');
116
+ }
117
+ const bytes = Buffer.allocUnsafe(UDP_FRAME_CHUNK_HEADER_BYTES);
118
+ UDP_FRAME_CHUNK_MAGIC.copy(bytes, 0);
119
+ bytes.writeUInt32BE(frameSerial >>> 0, 4);
120
+ bytes.writeUInt16BE(chunkIndex, 8);
121
+ bytes.writeUInt16BE(chunkCount, 10);
122
+ return bytes;
123
+ }
124
+
125
+ function decodePacketHeader(type, bytes) {
126
+ if (type === 'frame'
127
+ && bytes.length === UDP_FRAME_CHUNK_HEADER_BYTES
128
+ && bytes[0] === UDP_FRAME_CHUNK_MAGIC[0]
129
+ && bytes[1] === UDP_FRAME_CHUNK_MAGIC[1]
130
+ && bytes[2] === UDP_FRAME_CHUNK_MAGIC[2]
131
+ && bytes[3] === UDP_FRAME_CHUNK_MAGIC[3]) {
132
+ const chunkIndex = bytes.readUInt16BE(8);
133
+ const chunkCount = bytes.readUInt16BE(10);
134
+ if (chunkCount < 1 || chunkCount > 4096 || chunkIndex >= chunkCount) return null;
135
+ const frameSerial = bytes.readUInt32BE(4);
136
+ return {
137
+ frameId: frameSerial,
138
+ frameSerial,
139
+ chunkIndex,
140
+ chunkCount,
141
+ binaryChunkHeader: true
142
+ };
143
+ }
144
+ try {
145
+ const value = JSON.parse(bytes.toString('utf8'));
146
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
147
+ } catch {
148
+ return null;
149
+ }
150
+ }
151
+
152
+ function decodeFrameHeader(header) {
153
+ if (header?.frameHeader && typeof header.frameHeader === 'object') {
154
+ return header.frameHeader;
155
+ }
156
+ const encoded = String(header?.frameHeaderDeflate || '');
157
+ if (!encoded || encoded.length > 8192 || !/^[A-Za-z0-9+/]+={0,2}$/.test(encoded)) {
158
+ return null;
159
+ }
160
+ try {
161
+ const json = inflateRawSync(Buffer.from(encoded, 'base64'), { maxOutputLength: 64 * 1024 }).toString('utf8');
162
+ const value = JSON.parse(json);
163
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
164
+ } catch {
165
+ return null;
166
+ }
167
+ }
168
+
169
+ function decodeFramedPayload(payload, legacyHeader = null) {
170
+ const bytes = asBuffer(payload);
171
+ if (bytes.length < 8 || !bytes.subarray(0, 4).equals(UDP_FRAME_PAYLOAD_MAGIC)) {
172
+ return { header: legacyHeader || {}, payload: bytes };
173
+ }
174
+ const compressedHeaderLength = bytes.readUInt32BE(4);
175
+ if (compressedHeaderLength < 1 || compressedHeaderLength > UDP_MAX_FRAME_HEADER_BYTES
176
+ || 8 + compressedHeaderLength > bytes.length) return null;
177
+ try {
178
+ const json = inflateRawSync(bytes.subarray(8, 8 + compressedHeaderLength), {
179
+ maxOutputLength: UDP_MAX_FRAME_HEADER_BYTES
180
+ }).toString('utf8');
181
+ const header = JSON.parse(json);
182
+ if (!header || typeof header !== 'object' || Array.isArray(header)) return null;
183
+ return {
184
+ header,
185
+ payload: bytes.subarray(8 + compressedHeaderLength)
186
+ };
187
+ } catch {
188
+ return null;
189
+ }
190
+ }
191
+
192
+ function copyChunkRange(chunks, sourceOffset, length, target, targetOffset = 0) {
193
+ let remaining = length;
194
+ let skip = sourceOffset;
195
+ let written = 0;
196
+ for (const chunk of chunks) {
197
+ if (!chunk || remaining <= 0) continue;
198
+ if (skip >= chunk.length) {
199
+ skip -= chunk.length;
200
+ continue;
201
+ }
202
+ const available = Math.min(remaining, chunk.length - skip);
203
+ chunk.copy(target, targetOffset + written, skip, skip + available);
204
+ written += available;
205
+ remaining -= available;
206
+ skip = 0;
207
+ }
208
+ return written === length;
209
+ }
210
+
211
+ function decodeFramedChunks(chunks, totalBytes, legacyHeader = null) {
212
+ if (chunks.length === 1) {
213
+ const complete = decodeFramedPayload(chunks[0], legacyHeader);
214
+ return complete
215
+ ? { ...complete, payloadCopyCount: 0, payloadCopyBytes: 0 }
216
+ : null;
217
+ }
218
+
219
+ const prefix = Buffer.allocUnsafe(Math.min(8, totalBytes));
220
+ if (!copyChunkRange(chunks, 0, prefix.length, prefix)) return null;
221
+ if (prefix.length < 8 || !prefix.subarray(0, 4).equals(UDP_FRAME_PAYLOAD_MAGIC)) {
222
+ const payload = Buffer.allocUnsafe(totalBytes);
223
+ if (!copyChunkRange(chunks, 0, totalBytes, payload)) return null;
224
+ return {
225
+ header: legacyHeader || {},
226
+ payload,
227
+ payloadCopyCount: 1,
228
+ payloadCopyBytes: totalBytes
229
+ };
230
+ }
231
+
232
+ const compressedHeaderLength = prefix.readUInt32BE(4);
233
+ const payloadOffset = 8 + compressedHeaderLength;
234
+ if (compressedHeaderLength < 1 || compressedHeaderLength > UDP_MAX_FRAME_HEADER_BYTES
235
+ || payloadOffset > totalBytes) return null;
236
+ try {
237
+ const compressedHeader = Buffer.allocUnsafe(compressedHeaderLength);
238
+ if (!copyChunkRange(chunks, 8, compressedHeaderLength, compressedHeader)) return null;
239
+ const json = inflateRawSync(compressedHeader, {
240
+ maxOutputLength: UDP_MAX_FRAME_HEADER_BYTES
241
+ }).toString('utf8');
242
+ const header = JSON.parse(json);
243
+ if (!header || typeof header !== 'object' || Array.isArray(header)) return null;
244
+ const payloadLength = totalBytes - payloadOffset;
245
+ const payload = Buffer.allocUnsafe(payloadLength);
246
+ if (!copyChunkRange(chunks, payloadOffset, payloadLength, payload)) return null;
247
+ return {
248
+ header,
249
+ payload,
250
+ payloadCopyCount: 1,
251
+ payloadCopyBytes: payloadLength
252
+ };
253
+ } catch {
254
+ return null;
255
+ }
256
+ }
257
+
258
+ function packetTypeCode(type) {
259
259
  const code = typeof type === 'number' ? type : UDP_PACKET_TYPES[String(type || '').toLowerCase()];
260
260
  if (!code || !UDP_PACKET_TYPE_NAMES.has(code)) throw new Error('invalid-udp-packet-type');
261
261
  return code;
262
- }
263
-
264
- function normalizeSequence(value) {
265
- const sequence = Number(value);
266
- if (!Number.isInteger(sequence) || sequence < 0 || sequence >= UDP_SERIAL_MODULUS) {
267
- throw new Error('invalid-udp-packet-sequence');
268
- }
269
- return sequence;
270
- }
271
-
272
- function encodeHeader(type, header) {
273
- if ((typeof type === 'number' ? type : UDP_PACKET_TYPES[String(type || '').toLowerCase()])
274
- === UDP_PACKET_TYPES.frame
275
- && Number.isInteger(Number(header?.frameSerial))) {
276
- return encodeFrameChunkHeader(header);
277
- }
278
- const bytes = Buffer.from(JSON.stringify(header && typeof header === 'object' ? header : {}), 'utf8');
279
- if (bytes.length > UDP_MAX_HEADER_BYTES) throw new Error('udp-packet-header-too-large');
280
- return bytes;
281
- }
262
+ }
263
+
264
+ function normalizeSequence(value) {
265
+ const sequence = Number(value);
266
+ if (!Number.isInteger(sequence) || sequence < 0 || sequence >= UDP_SERIAL_MODULUS) {
267
+ throw new Error('invalid-udp-packet-sequence');
268
+ }
269
+ return sequence;
270
+ }
271
+
272
+ function encodeHeader(type, header) {
273
+ if ((typeof type === 'number' ? type : UDP_PACKET_TYPES[String(type || '').toLowerCase()])
274
+ === UDP_PACKET_TYPES.frame
275
+ && Number.isInteger(Number(header?.frameSerial))) {
276
+ return encodeFrameChunkHeader(header);
277
+ }
278
+ const bytes = Buffer.from(JSON.stringify(header && typeof header === 'object' ? header : {}), 'utf8');
279
+ if (bytes.length > UDP_MAX_HEADER_BYTES) throw new Error('udp-packet-header-too-large');
280
+ return bytes;
281
+ }
282
282
 
283
283
  export function deriveUdpSessionKey(keyMaterial) {
284
284
  return normalizeKey(keyMaterial);
@@ -288,22 +288,22 @@ export function createUdpSessionKey() {
288
288
  return crypto.randomBytes(32);
289
289
  }
290
290
 
291
- export function encodeUdpPacket({ type, sessionId, key, sequence = 0, header = {}, payload = Buffer.alloc(0) }) {
292
- const normalizedSessionId = normalizeSessionId(sessionId);
293
- const sessionBytes = Buffer.from(normalizedSessionId, 'utf8');
294
- const headerBytes = encodeHeader(type, header);
295
- const payloadBytes = asBuffer(payload);
296
- if (payloadBytes.length > UDP_MAX_PAYLOAD_BYTES) throw new Error('udp-packet-payload-too-large');
297
-
298
- const preamble = Buffer.allocUnsafe(UDP_FIXED_HEADER_BYTES);
299
- UDP_PACKET_MAGIC.copy(preamble, 0);
291
+ export function encodeUdpPacket({ type, sessionId, key, sequence = 0, header = {}, payload = Buffer.alloc(0) }) {
292
+ const normalizedSessionId = normalizeSessionId(sessionId);
293
+ const sessionBytes = Buffer.from(normalizedSessionId, 'utf8');
294
+ const headerBytes = encodeHeader(type, header);
295
+ const payloadBytes = asBuffer(payload);
296
+ if (payloadBytes.length > UDP_MAX_PAYLOAD_BYTES) throw new Error('udp-packet-payload-too-large');
297
+
298
+ const preamble = Buffer.allocUnsafe(UDP_FIXED_HEADER_BYTES);
299
+ UDP_PACKET_MAGIC.copy(preamble, 0);
300
300
  preamble[4] = UDP_P2P_VERSION;
301
301
  preamble[5] = packetTypeCode(type);
302
302
  preamble[6] = sessionBytes.length;
303
303
  preamble.writeUInt16BE(headerBytes.length, 7);
304
304
  preamble.writeUInt16BE(payloadBytes.length, 9);
305
- preamble.writeUInt32BE((Number(sequence) >>> 0), 11);
306
- const nonce = crypto.randomBytes(UDP_NONCE_BYTES);
305
+ preamble.writeUInt32BE((Number(sequence) >>> 0), 11);
306
+ const nonce = crypto.randomBytes(UDP_NONCE_BYTES);
307
307
  const associatedData = Buffer.concat([preamble, nonce, sessionBytes, headerBytes]);
308
308
  const cipher = crypto.createCipheriv('aes-256-gcm', normalizeKey(key), nonce);
309
309
  cipher.setAAD(associatedData);
@@ -314,10 +314,10 @@ export function encodeUdpPacket({ type, sessionId, key, sequence = 0, header = {
314
314
  return packet;
315
315
  }
316
316
 
317
- export function decodeUdpPacket(packet, key, expectedSession = null) {
318
- const bytes = asBuffer(packet);
319
- if (!isUdpBinaryDatagram(bytes)) return null;
320
- const type = UDP_PACKET_TYPE_NAMES.get(bytes[5]);
317
+ export function decodeUdpPacket(packet, key, expectedSession = null) {
318
+ const bytes = asBuffer(packet);
319
+ if (!isUdpBinaryDatagram(bytes)) return null;
320
+ const type = UDP_PACKET_TYPE_NAMES.get(bytes[5]);
321
321
  const sessionLength = bytes[6];
322
322
  const headerLength = bytes.readUInt16BE(7);
323
323
  const payloadLength = bytes.readUInt16BE(9);
@@ -328,204 +328,204 @@ export function decodeUdpPacket(packet, key, expectedSession = null) {
328
328
  || headerLength > UDP_MAX_HEADER_BYTES || payloadLength > UDP_MAX_PAYLOAD_BYTES
329
329
  || expectedLength !== bytes.length) return null;
330
330
 
331
- const associatedData = bytes.subarray(0, associatedLength);
332
- const sessionStart = UDP_FIXED_HEADER_BYTES + UDP_NONCE_BYTES;
333
- const sessionSlice = bytes.subarray(sessionStart, sessionStart + sessionLength);
334
- const expectedSessionBytes = Buffer.isBuffer(expectedSession?.bytes)
335
- ? expectedSession.bytes
336
- : null;
337
- if (expectedSessionBytes && !sessionSlice.equals(expectedSessionBytes)) return null;
338
- const sessionId = expectedSessionBytes
339
- ? String(expectedSession?.id || '')
340
- : sessionSlice.toString('utf8');
341
- const headerStart = sessionStart + sessionLength;
342
- const nonce = bytes.subarray(UDP_FIXED_HEADER_BYTES, UDP_FIXED_HEADER_BYTES + UDP_NONCE_BYTES);
343
- const ciphertextStart = associatedLength;
344
- const ciphertext = bytes.subarray(ciphertextStart, ciphertextStart + payloadLength);
345
- const tag = bytes.subarray(ciphertextStart + payloadLength);
346
- try {
347
- const decipher = crypto.createDecipheriv('aes-256-gcm', normalizeKey(key), nonce);
348
- decipher.setAAD(associatedData);
349
- decipher.setAuthTag(tag);
350
- let payload = decipher.update(ciphertext);
351
- const final = decipher.final();
352
- if (final.length > 0) payload = Buffer.concat([payload, final], payload.length + final.length);
353
- const header = decodePacketHeader(
354
- type,
355
- bytes.subarray(headerStart, headerStart + headerLength)
356
- );
357
- if (!header) return null;
358
- return { type, sessionId, sequence, header, payload };
359
- } catch {
360
- return null;
331
+ const associatedData = bytes.subarray(0, associatedLength);
332
+ const sessionStart = UDP_FIXED_HEADER_BYTES + UDP_NONCE_BYTES;
333
+ const sessionSlice = bytes.subarray(sessionStart, sessionStart + sessionLength);
334
+ const expectedSessionBytes = Buffer.isBuffer(expectedSession?.bytes)
335
+ ? expectedSession.bytes
336
+ : null;
337
+ if (expectedSessionBytes && !sessionSlice.equals(expectedSessionBytes)) return null;
338
+ const sessionId = expectedSessionBytes
339
+ ? String(expectedSession?.id || '')
340
+ : sessionSlice.toString('utf8');
341
+ const headerStart = sessionStart + sessionLength;
342
+ const nonce = bytes.subarray(UDP_FIXED_HEADER_BYTES, UDP_FIXED_HEADER_BYTES + UDP_NONCE_BYTES);
343
+ const ciphertextStart = associatedLength;
344
+ const ciphertext = bytes.subarray(ciphertextStart, ciphertextStart + payloadLength);
345
+ const tag = bytes.subarray(ciphertextStart + payloadLength);
346
+ try {
347
+ const decipher = crypto.createDecipheriv('aes-256-gcm', normalizeKey(key), nonce);
348
+ decipher.setAAD(associatedData);
349
+ decipher.setAuthTag(tag);
350
+ let payload = decipher.update(ciphertext);
351
+ const final = decipher.final();
352
+ if (final.length > 0) payload = Buffer.concat([payload, final], payload.length + final.length);
353
+ const header = decodePacketHeader(
354
+ type,
355
+ bytes.subarray(headerStart, headerStart + headerLength)
356
+ );
357
+ if (!header) return null;
358
+ return { type, sessionId, sequence, header, payload };
359
+ } catch {
360
+ return null;
361
361
  }
362
362
  }
363
363
 
364
- export class UdpReplayWindow {
364
+ export class UdpReplayWindow {
365
365
  constructor(size = 512) {
366
366
  this.size = Math.max(32, Math.min(4096, Math.floor(Number(size) || 512)));
367
367
  this.highest = -1;
368
368
  this.seen = new Set();
369
369
  }
370
370
 
371
- accept(sequence) {
372
- let value;
373
- try {
374
- value = normalizeSequence(sequence);
375
- } catch {
376
- return false;
377
- }
378
- if (this.seen.has(value)) return false;
379
- if (this.highest >= 0) {
380
- const forward = (value - this.highest + UDP_SERIAL_MODULUS) % UDP_SERIAL_MODULUS;
381
- if (forward === 0 || forward === UDP_SERIAL_HALF_RANGE) return false;
382
- if (forward > UDP_SERIAL_HALF_RANGE) {
383
- const age = (this.highest - value + UDP_SERIAL_MODULUS) % UDP_SERIAL_MODULUS;
384
- if (age > this.size) return false;
385
- } else {
386
- this.highest = value;
387
- }
388
- } else {
389
- this.highest = value;
390
- }
391
- this.seen.add(value);
392
- for (const item of this.seen) {
393
- const age = (this.highest - item + UDP_SERIAL_MODULUS) % UDP_SERIAL_MODULUS;
394
- if (age > this.size) this.seen.delete(item);
395
- }
396
- return true;
397
- }
398
- }
399
-
400
- export class UdpFrameReassembler {
401
- constructor({ maxPending = 2, timeoutMs = 500, maxFrameBytes = 8 * 1024 * 1024 } = {}) {
402
- this.maxPending = Math.max(1, Math.min(16, Math.floor(Number(maxPending) || 2)));
403
- this.timeoutMs = Math.max(20, Math.min(2000, Math.floor(Number(timeoutMs) || 500)));
404
- this.maxFrameBytes = Math.max(64 * 1024, Math.min(16 * 1024 * 1024, Math.floor(Number(maxFrameBytes) || 8 * 1024 * 1024)));
405
- this.pending = new Map();
406
- this.pendingBytes = 0;
407
- this.pendingChunkBuffers = 0;
408
- this.pendingBytesHighWatermark = 0;
409
- this.pendingChunkBuffersHighWatermark = 0;
410
- this.droppedFrames = 0;
411
- this.expiredFrames = 0;
412
- this.evictedFrames = 0;
413
- this.completedFrames = 0;
414
- this.completedPayloadCopyCount = 0;
415
- this.completedPayloadCopyBytes = 0;
416
- this.fullWireDuplicateCopyCount = 0;
417
- this.binaryChunkHeaderCount = 0;
418
- this.legacyChunkHeaderCount = 0;
419
- }
420
-
421
- ingest(packet, now = Date.now()) {
422
- if (!packet || packet.type !== 'frame') return null;
423
- this.expire(now);
424
- const header = packet.header && typeof packet.header === 'object' ? packet.header : {};
425
- const frameId = Number.isInteger(header.frameSerial)
426
- ? header.frameSerial
427
- : String(header.frameId || '').slice(0, 160);
428
- const chunkIndex = Number(header.chunkIndex);
429
- const chunkCount = Number(header.chunkCount);
430
- if ((frameId === '' || frameId === null) || !Number.isInteger(chunkIndex) || !Number.isInteger(chunkCount)
431
- || chunkIndex < 0 || chunkCount < 1 || chunkCount > 4096 || chunkIndex >= chunkCount) return null;
432
- let state = this.pending.get(frameId);
433
- if (!state) {
434
- while (this.pending.size >= this.maxPending) {
435
- const oldest = this.pending.keys().next().value;
436
- if (oldest === undefined) break;
437
- const evicted = this.#detach(oldest);
438
- if (evicted) {
439
- this.droppedFrames += 1;
440
- this.evictedFrames += 1;
441
- }
442
- }
371
+ accept(sequence) {
372
+ let value;
373
+ try {
374
+ value = normalizeSequence(sequence);
375
+ } catch {
376
+ return false;
377
+ }
378
+ if (this.seen.has(value)) return false;
379
+ if (this.highest >= 0) {
380
+ const forward = (value - this.highest + UDP_SERIAL_MODULUS) % UDP_SERIAL_MODULUS;
381
+ if (forward === 0 || forward === UDP_SERIAL_HALF_RANGE) return false;
382
+ if (forward > UDP_SERIAL_HALF_RANGE) {
383
+ const age = (this.highest - value + UDP_SERIAL_MODULUS) % UDP_SERIAL_MODULUS;
384
+ if (age > this.size) return false;
385
+ } else {
386
+ this.highest = value;
387
+ }
388
+ } else {
389
+ this.highest = value;
390
+ }
391
+ this.seen.add(value);
392
+ for (const item of this.seen) {
393
+ const age = (this.highest - item + UDP_SERIAL_MODULUS) % UDP_SERIAL_MODULUS;
394
+ if (age > this.size) this.seen.delete(item);
395
+ }
396
+ return true;
397
+ }
398
+ }
399
+
400
+ export class UdpFrameReassembler {
401
+ constructor({ maxPending = 2, timeoutMs = 500, maxFrameBytes = 8 * 1024 * 1024 } = {}) {
402
+ this.maxPending = Math.max(1, Math.min(16, Math.floor(Number(maxPending) || 2)));
403
+ this.timeoutMs = Math.max(20, Math.min(2000, Math.floor(Number(timeoutMs) || 500)));
404
+ this.maxFrameBytes = Math.max(64 * 1024, Math.min(16 * 1024 * 1024, Math.floor(Number(maxFrameBytes) || 8 * 1024 * 1024)));
405
+ this.pending = new Map();
406
+ this.pendingBytes = 0;
407
+ this.pendingChunkBuffers = 0;
408
+ this.pendingBytesHighWatermark = 0;
409
+ this.pendingChunkBuffersHighWatermark = 0;
410
+ this.droppedFrames = 0;
411
+ this.expiredFrames = 0;
412
+ this.evictedFrames = 0;
413
+ this.completedFrames = 0;
414
+ this.completedPayloadCopyCount = 0;
415
+ this.completedPayloadCopyBytes = 0;
416
+ this.fullWireDuplicateCopyCount = 0;
417
+ this.binaryChunkHeaderCount = 0;
418
+ this.legacyChunkHeaderCount = 0;
419
+ }
420
+
421
+ ingest(packet, now = Date.now()) {
422
+ if (!packet || packet.type !== 'frame') return null;
423
+ this.expire(now);
424
+ const header = packet.header && typeof packet.header === 'object' ? packet.header : {};
425
+ const frameId = Number.isInteger(header.frameSerial)
426
+ ? header.frameSerial
427
+ : String(header.frameId || '').slice(0, 160);
428
+ const chunkIndex = Number(header.chunkIndex);
429
+ const chunkCount = Number(header.chunkCount);
430
+ if ((frameId === '' || frameId === null) || !Number.isInteger(chunkIndex) || !Number.isInteger(chunkCount)
431
+ || chunkIndex < 0 || chunkCount < 1 || chunkCount > 4096 || chunkIndex >= chunkCount) return null;
432
+ let state = this.pending.get(frameId);
433
+ if (!state) {
434
+ while (this.pending.size >= this.maxPending) {
435
+ const oldest = this.pending.keys().next().value;
436
+ if (oldest === undefined) break;
437
+ const evicted = this.#detach(oldest);
438
+ if (evicted) {
439
+ this.droppedFrames += 1;
440
+ this.evictedFrames += 1;
441
+ }
442
+ }
443
443
  state = { createdAt: now, chunkCount, chunks: new Array(chunkCount), received: 0, totalBytes: 0, frameHeader: null };
444
444
  this.pending.set(frameId, state);
445
- }
446
- if (state.chunkCount !== chunkCount || state.chunks[chunkIndex]) return null;
447
- const payload = asBuffer(packet.payload);
448
- state.chunks[chunkIndex] = payload;
449
- state.received += 1;
450
- state.totalBytes += payload.length;
451
- this.pendingBytes += payload.length;
452
- this.pendingChunkBuffers += 1;
453
- this.pendingBytesHighWatermark = Math.max(this.pendingBytesHighWatermark, this.pendingBytes);
454
- this.pendingChunkBuffersHighWatermark = Math.max(
455
- this.pendingChunkBuffersHighWatermark,
456
- this.pendingChunkBuffers
457
- );
458
- if (header.binaryChunkHeader === true) this.binaryChunkHeaderCount += 1;
459
- else this.legacyChunkHeaderCount += 1;
460
- const decodedFrameHeader = decodeFrameHeader(header);
461
- if (decodedFrameHeader) state.frameHeader = decodedFrameHeader;
462
- if (state.totalBytes > this.maxFrameBytes) {
463
- if (this.#detach(frameId)) this.droppedFrames += 1;
464
- return null;
465
- }
466
- if (state.received !== state.chunkCount) return null;
467
- this.#detach(frameId);
468
- const complete = decodeFramedChunks(
469
- state.chunks,
470
- state.totalBytes,
471
- state.frameHeader || header.frameHeader || {}
472
- );
473
- if (!complete) {
474
- this.droppedFrames += 1;
475
- return null;
476
- }
477
- this.completedFrames += 1;
478
- this.completedPayloadCopyCount += complete.payloadCopyCount;
479
- this.completedPayloadCopyBytes += complete.payloadCopyBytes;
480
- return {
481
- frameId,
482
- header: complete.header,
483
- payload: complete.payload
484
- };
485
- }
486
-
487
- expire(now = Date.now()) {
488
- for (const [frameId, state] of this.pending) {
489
- if (now - state.createdAt > this.timeoutMs && this.#detach(frameId)) {
490
- this.droppedFrames += 1;
491
- this.expiredFrames += 1;
492
- }
493
- }
494
- }
495
-
496
- clear() {
497
- this.pending.clear();
498
- this.pendingBytes = 0;
499
- this.pendingChunkBuffers = 0;
500
- }
501
-
502
- getStats() {
503
- return {
504
- pendingFrames: this.pending.size,
505
- pendingBytes: this.pendingBytes,
506
- pendingChunkBuffers: this.pendingChunkBuffers,
507
- pendingBytesHighWatermark: this.pendingBytesHighWatermark,
508
- pendingChunkBuffersHighWatermark: this.pendingChunkBuffersHighWatermark,
509
- droppedFrames: this.droppedFrames,
510
- expiredFrames: this.expiredFrames,
511
- evictedFrames: this.evictedFrames,
512
- completedFrames: this.completedFrames,
513
- completedPayloadCopyCount: this.completedPayloadCopyCount,
514
- completedPayloadCopyBytes: this.completedPayloadCopyBytes,
515
- fullWireDuplicateCopyCount: this.fullWireDuplicateCopyCount,
516
- binaryChunkHeaderCount: this.binaryChunkHeaderCount,
517
- legacyChunkHeaderCount: this.legacyChunkHeaderCount
518
- };
519
- }
520
-
521
- #detach(frameId) {
522
- const state = this.pending.get(frameId);
523
- if (!state || !this.pending.delete(frameId)) return null;
524
- this.pendingBytes = Math.max(0, this.pendingBytes - state.totalBytes);
525
- this.pendingChunkBuffers = Math.max(0, this.pendingChunkBuffers - state.received);
526
- return state;
527
- }
528
- }
445
+ }
446
+ if (state.chunkCount !== chunkCount || state.chunks[chunkIndex]) return null;
447
+ const payload = asBuffer(packet.payload);
448
+ state.chunks[chunkIndex] = payload;
449
+ state.received += 1;
450
+ state.totalBytes += payload.length;
451
+ this.pendingBytes += payload.length;
452
+ this.pendingChunkBuffers += 1;
453
+ this.pendingBytesHighWatermark = Math.max(this.pendingBytesHighWatermark, this.pendingBytes);
454
+ this.pendingChunkBuffersHighWatermark = Math.max(
455
+ this.pendingChunkBuffersHighWatermark,
456
+ this.pendingChunkBuffers
457
+ );
458
+ if (header.binaryChunkHeader === true) this.binaryChunkHeaderCount += 1;
459
+ else this.legacyChunkHeaderCount += 1;
460
+ const decodedFrameHeader = decodeFrameHeader(header);
461
+ if (decodedFrameHeader) state.frameHeader = decodedFrameHeader;
462
+ if (state.totalBytes > this.maxFrameBytes) {
463
+ if (this.#detach(frameId)) this.droppedFrames += 1;
464
+ return null;
465
+ }
466
+ if (state.received !== state.chunkCount) return null;
467
+ this.#detach(frameId);
468
+ const complete = decodeFramedChunks(
469
+ state.chunks,
470
+ state.totalBytes,
471
+ state.frameHeader || header.frameHeader || {}
472
+ );
473
+ if (!complete) {
474
+ this.droppedFrames += 1;
475
+ return null;
476
+ }
477
+ this.completedFrames += 1;
478
+ this.completedPayloadCopyCount += complete.payloadCopyCount;
479
+ this.completedPayloadCopyBytes += complete.payloadCopyBytes;
480
+ return {
481
+ frameId,
482
+ header: complete.header,
483
+ payload: complete.payload
484
+ };
485
+ }
486
+
487
+ expire(now = Date.now()) {
488
+ for (const [frameId, state] of this.pending) {
489
+ if (now - state.createdAt > this.timeoutMs && this.#detach(frameId)) {
490
+ this.droppedFrames += 1;
491
+ this.expiredFrames += 1;
492
+ }
493
+ }
494
+ }
495
+
496
+ clear() {
497
+ this.pending.clear();
498
+ this.pendingBytes = 0;
499
+ this.pendingChunkBuffers = 0;
500
+ }
501
+
502
+ getStats() {
503
+ return {
504
+ pendingFrames: this.pending.size,
505
+ pendingBytes: this.pendingBytes,
506
+ pendingChunkBuffers: this.pendingChunkBuffers,
507
+ pendingBytesHighWatermark: this.pendingBytesHighWatermark,
508
+ pendingChunkBuffersHighWatermark: this.pendingChunkBuffersHighWatermark,
509
+ droppedFrames: this.droppedFrames,
510
+ expiredFrames: this.expiredFrames,
511
+ evictedFrames: this.evictedFrames,
512
+ completedFrames: this.completedFrames,
513
+ completedPayloadCopyCount: this.completedPayloadCopyCount,
514
+ completedPayloadCopyBytes: this.completedPayloadCopyBytes,
515
+ fullWireDuplicateCopyCount: this.fullWireDuplicateCopyCount,
516
+ binaryChunkHeaderCount: this.binaryChunkHeaderCount,
517
+ legacyChunkHeaderCount: this.legacyChunkHeaderCount
518
+ };
519
+ }
520
+
521
+ #detach(frameId) {
522
+ const state = this.pending.get(frameId);
523
+ if (!state || !this.pending.delete(frameId)) return null;
524
+ this.pendingBytes = Math.max(0, this.pendingBytes - state.totalBytes);
525
+ this.pendingChunkBuffers = Math.max(0, this.pendingChunkBuffers - state.received);
526
+ return state;
527
+ }
528
+ }
529
529
 
530
530
  export function encodeRendezvousMessage(message) {
531
531
  const payload = Buffer.from(JSON.stringify(message && typeof message === 'object' ? message : {}), 'utf8');