@livedesk/hub 0.1.26 → 0.1.28
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 +2 -2
- package/src/agents/agent-manager.js +27 -11
- package/src/agents/codex-agent-runtime.js +325 -41
- package/src/frame-packet-contract.mjs +427 -0
- package/src/live-capture-transition-retry.mjs +297 -0
- package/src/mode4-atlas-pool.js +17 -3
- package/src/mode4-atlas-worker.js +134 -21
- package/src/mode4-atlas.js +180 -16
- package/src/remote-audio-subscription-contract.mjs +109 -0
- package/src/remote-audio-subscription-contract.test.mjs +72 -0
- package/src/remote-hub.js +6374 -3890
- package/src/server.js +3234 -2202
- package/src/transport/agent-binary-ingress.js +810 -0
- package/src/transport/relay-hub-control.js +372 -56
- package/src/transport/udp-hub-transport.js +215 -51
- package/src/transport/udp-protocol.js +302 -70
|
@@ -14,15 +14,19 @@ 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;
|
|
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
23
|
const UDP_MAX_HEADER_BYTES = 4096;
|
|
24
24
|
const UDP_MAX_PAYLOAD_BYTES = UDP_MAX_DATAGRAM_BYTES - UDP_FIXED_HEADER_BYTES - UDP_NONCE_BYTES - UDP_TAG_BYTES - 1;
|
|
25
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;
|
|
26
30
|
const UDP_MAX_FRAME_HEADER_BYTES = 64 * 1024;
|
|
27
31
|
|
|
28
32
|
function asBuffer(value) {
|
|
@@ -45,6 +49,106 @@ function normalizeSessionId(value) {
|
|
|
45
49
|
return sessionId;
|
|
46
50
|
}
|
|
47
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
|
+
|
|
48
152
|
function decodeFrameHeader(header) {
|
|
49
153
|
if (header?.frameHeader && typeof header.frameHeader === 'object') {
|
|
50
154
|
return header.frameHeader;
|
|
@@ -84,6 +188,72 @@ function decodeFramedPayload(payload, legacyHeader = null) {
|
|
|
84
188
|
return null;
|
|
85
189
|
}
|
|
86
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
|
+
}
|
|
87
257
|
|
|
88
258
|
function packetTypeCode(type) {
|
|
89
259
|
const code = typeof type === 'number' ? type : UDP_PACKET_TYPES[String(type || '').toLowerCase()];
|
|
@@ -99,11 +269,16 @@ function normalizeSequence(value) {
|
|
|
99
269
|
return sequence;
|
|
100
270
|
}
|
|
101
271
|
|
|
102
|
-
function encodeHeader(header) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
+
}
|
|
107
282
|
|
|
108
283
|
export function deriveUdpSessionKey(keyMaterial) {
|
|
109
284
|
return normalizeKey(keyMaterial);
|
|
@@ -116,12 +291,12 @@ export function createUdpSessionKey() {
|
|
|
116
291
|
export function encodeUdpPacket({ type, sessionId, key, sequence = 0, header = {}, payload = Buffer.alloc(0) }) {
|
|
117
292
|
const normalizedSessionId = normalizeSessionId(sessionId);
|
|
118
293
|
const sessionBytes = Buffer.from(normalizedSessionId, 'utf8');
|
|
119
|
-
const headerBytes = encodeHeader(header);
|
|
120
|
-
const payloadBytes = asBuffer(payload);
|
|
121
|
-
if (payloadBytes.length > UDP_MAX_PAYLOAD_BYTES) throw new Error('udp-packet-payload-too-large');
|
|
122
|
-
|
|
123
|
-
const preamble = Buffer.allocUnsafe(UDP_FIXED_HEADER_BYTES);
|
|
124
|
-
|
|
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);
|
|
125
300
|
preamble[4] = UDP_P2P_VERSION;
|
|
126
301
|
preamble[5] = packetTypeCode(type);
|
|
127
302
|
preamble[6] = sessionBytes.length;
|
|
@@ -139,11 +314,10 @@ export function encodeUdpPacket({ type, sessionId, key, sequence = 0, header = {
|
|
|
139
314
|
return packet;
|
|
140
315
|
}
|
|
141
316
|
|
|
142
|
-
export function decodeUdpPacket(packet, key) {
|
|
143
|
-
const bytes = asBuffer(packet);
|
|
144
|
-
if (bytes
|
|
145
|
-
|
|
146
|
-
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]);
|
|
147
321
|
const sessionLength = bytes[6];
|
|
148
322
|
const headerLength = bytes.readUInt16BE(7);
|
|
149
323
|
const payloadLength = bytes.readUInt16BE(9);
|
|
@@ -154,28 +328,36 @@ export function decodeUdpPacket(packet, key) {
|
|
|
154
328
|
|| headerLength > UDP_MAX_HEADER_BYTES || payloadLength > UDP_MAX_PAYLOAD_BYTES
|
|
155
329
|
|| expectedLength !== bytes.length) return null;
|
|
156
330
|
|
|
157
|
-
const associatedData = bytes.subarray(0, associatedLength);
|
|
158
|
-
const sessionStart = UDP_FIXED_HEADER_BYTES + UDP_NONCE_BYTES;
|
|
159
|
-
const
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
const
|
|
169
|
-
const
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
decipher.
|
|
174
|
-
decipher.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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;
|
|
179
361
|
}
|
|
180
362
|
}
|
|
181
363
|
|
|
@@ -221,54 +403,80 @@ export class UdpFrameReassembler {
|
|
|
221
403
|
this.timeoutMs = Math.max(20, Math.min(2000, Math.floor(Number(timeoutMs) || 500)));
|
|
222
404
|
this.maxFrameBytes = Math.max(64 * 1024, Math.min(16 * 1024 * 1024, Math.floor(Number(maxFrameBytes) || 8 * 1024 * 1024)));
|
|
223
405
|
this.pending = new Map();
|
|
406
|
+
this.pendingBytes = 0;
|
|
407
|
+
this.pendingChunkBuffers = 0;
|
|
408
|
+
this.pendingBytesHighWatermark = 0;
|
|
409
|
+
this.pendingChunkBuffersHighWatermark = 0;
|
|
224
410
|
this.droppedFrames = 0;
|
|
225
411
|
this.expiredFrames = 0;
|
|
226
412
|
this.evictedFrames = 0;
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
this.
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
if (!
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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) {
|
|
240
434
|
while (this.pending.size >= this.maxPending) {
|
|
241
435
|
const oldest = this.pending.keys().next().value;
|
|
242
436
|
if (oldest === undefined) break;
|
|
243
|
-
|
|
437
|
+
const evicted = this.#detach(oldest);
|
|
438
|
+
if (evicted) {
|
|
244
439
|
this.droppedFrames += 1;
|
|
245
440
|
this.evictedFrames += 1;
|
|
246
441
|
}
|
|
247
442
|
}
|
|
248
443
|
state = { createdAt: now, chunkCount, chunks: new Array(chunkCount), received: 0, totalBytes: 0, frameHeader: null };
|
|
249
444
|
this.pending.set(frameId, state);
|
|
250
|
-
}
|
|
251
|
-
if (state.chunkCount !== chunkCount || state.chunks[chunkIndex]) return null;
|
|
252
|
-
const payload = asBuffer(packet.payload);
|
|
253
|
-
state.chunks[chunkIndex] = payload;
|
|
254
|
-
state.received += 1;
|
|
255
|
-
state.totalBytes += payload.length;
|
|
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;
|
|
256
460
|
const decodedFrameHeader = decodeFrameHeader(header);
|
|
257
461
|
if (decodedFrameHeader) state.frameHeader = decodedFrameHeader;
|
|
258
462
|
if (state.totalBytes > this.maxFrameBytes) {
|
|
259
|
-
if (this
|
|
463
|
+
if (this.#detach(frameId)) this.droppedFrames += 1;
|
|
260
464
|
return null;
|
|
261
465
|
}
|
|
262
466
|
if (state.received !== state.chunkCount) return null;
|
|
263
|
-
this
|
|
264
|
-
const complete =
|
|
265
|
-
|
|
467
|
+
this.#detach(frameId);
|
|
468
|
+
const complete = decodeFramedChunks(
|
|
469
|
+
state.chunks,
|
|
470
|
+
state.totalBytes,
|
|
266
471
|
state.frameHeader || header.frameHeader || {}
|
|
267
472
|
);
|
|
268
473
|
if (!complete) {
|
|
269
474
|
this.droppedFrames += 1;
|
|
270
475
|
return null;
|
|
271
476
|
}
|
|
477
|
+
this.completedFrames += 1;
|
|
478
|
+
this.completedPayloadCopyCount += complete.payloadCopyCount;
|
|
479
|
+
this.completedPayloadCopyBytes += complete.payloadCopyBytes;
|
|
272
480
|
return {
|
|
273
481
|
frameId,
|
|
274
482
|
header: complete.header,
|
|
@@ -278,21 +486,45 @@ export class UdpFrameReassembler {
|
|
|
278
486
|
|
|
279
487
|
expire(now = Date.now()) {
|
|
280
488
|
for (const [frameId, state] of this.pending) {
|
|
281
|
-
if (now - state.createdAt > this.timeoutMs && this
|
|
489
|
+
if (now - state.createdAt > this.timeoutMs && this.#detach(frameId)) {
|
|
282
490
|
this.droppedFrames += 1;
|
|
283
491
|
this.expiredFrames += 1;
|
|
284
492
|
}
|
|
285
493
|
}
|
|
286
494
|
}
|
|
287
495
|
|
|
496
|
+
clear() {
|
|
497
|
+
this.pending.clear();
|
|
498
|
+
this.pendingBytes = 0;
|
|
499
|
+
this.pendingChunkBuffers = 0;
|
|
500
|
+
}
|
|
501
|
+
|
|
288
502
|
getStats() {
|
|
289
503
|
return {
|
|
290
504
|
pendingFrames: this.pending.size,
|
|
505
|
+
pendingBytes: this.pendingBytes,
|
|
506
|
+
pendingChunkBuffers: this.pendingChunkBuffers,
|
|
507
|
+
pendingBytesHighWatermark: this.pendingBytesHighWatermark,
|
|
508
|
+
pendingChunkBuffersHighWatermark: this.pendingChunkBuffersHighWatermark,
|
|
291
509
|
droppedFrames: this.droppedFrames,
|
|
292
510
|
expiredFrames: this.expiredFrames,
|
|
293
|
-
evictedFrames: this.evictedFrames
|
|
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
|
|
294
518
|
};
|
|
295
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
|
+
}
|
|
296
528
|
}
|
|
297
529
|
|
|
298
530
|
export function encodeRendezvousMessage(message) {
|