@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.
@@ -0,0 +1,810 @@
1
+ const DEFAULT_MAX_PACKETS = 3;
2
+ const DEFAULT_MAX_QUEUE_BYTES = 12 * 1024 * 1024;
3
+ const DEFAULT_MAX_QUEUE_AGE_MS = 120;
4
+ const DEFAULT_DRAIN_BUDGET_PACKETS = 3;
5
+ const DEFAULT_MODE5_PACKETS_PER_STREAM = 2;
6
+
7
+ function boundedInteger(value, minimum, maximum, fallback) {
8
+ const parsed = Math.floor(Number(value));
9
+ if (!Number.isFinite(parsed)) {
10
+ return fallback;
11
+ }
12
+ return Math.max(minimum, Math.min(maximum, parsed));
13
+ }
14
+
15
+ function stringValue(value, maxLength = 160) {
16
+ return String(value ?? '').trim().slice(0, maxLength);
17
+ }
18
+
19
+ function bufferValue(value) {
20
+ if (Buffer.isBuffer(value)) {
21
+ return value;
22
+ }
23
+ if (value instanceof Uint8Array) {
24
+ return Buffer.from(value.buffer, value.byteOffset, value.byteLength);
25
+ }
26
+ return Buffer.from(value ?? []);
27
+ }
28
+
29
+ /**
30
+ * Append-only segmented byte storage for socket parsers.
31
+ *
32
+ * Appends retain the incoming immutable socket chunk instead of copying all
33
+ * historical bytes. Materialization always owns one exact-sized backing store,
34
+ * and discard/clear releases references to consumed chunks synchronously.
35
+ */
36
+ export class BoundedSegmentedBuffer {
37
+ constructor(maxBytes) {
38
+ this.maxBytes = boundedInteger(maxBytes, 1, Number.MAX_SAFE_INTEGER, 1);
39
+ this.chunks = [];
40
+ this.headIndex = 0;
41
+ this.headOffset = 0;
42
+ this.byteLength = 0;
43
+ this.highWaterBytes = 0;
44
+ this.appendedBytes = 0;
45
+ this.materializedBytes = 0;
46
+ this.rejectedBytes = 0;
47
+ }
48
+
49
+ get length() {
50
+ return this.byteLength;
51
+ }
52
+
53
+ get segmentCount() {
54
+ return Math.max(0, this.chunks.length - this.headIndex);
55
+ }
56
+
57
+ append(value) {
58
+ const chunk = bufferValue(value);
59
+ if (chunk.length === 0) {
60
+ return true;
61
+ }
62
+ if (chunk.length > this.maxBytes - this.byteLength) {
63
+ this.rejectedBytes += chunk.length;
64
+ return false;
65
+ }
66
+
67
+ this.chunks.push(chunk);
68
+ this.byteLength += chunk.length;
69
+ this.appendedBytes += chunk.length;
70
+ this.highWaterBytes = Math.max(this.highWaterBytes, this.byteLength);
71
+ return true;
72
+ }
73
+
74
+ byteAt(index) {
75
+ const target = Math.floor(Number(index));
76
+ if (!Number.isFinite(target) || target < 0 || target >= this.byteLength) {
77
+ return undefined;
78
+ }
79
+
80
+ let remaining = target;
81
+ for (let chunkIndex = this.headIndex; chunkIndex < this.chunks.length; chunkIndex += 1) {
82
+ const chunk = this.chunks[chunkIndex];
83
+ const start = chunkIndex === this.headIndex ? this.headOffset : 0;
84
+ const available = chunk.length - start;
85
+ if (remaining < available) {
86
+ return chunk[start + remaining];
87
+ }
88
+ remaining -= available;
89
+ }
90
+ return undefined;
91
+ }
92
+
93
+ readExact(startIndex, length) {
94
+ const start = Math.floor(Number(startIndex));
95
+ const requested = Math.floor(Number(length));
96
+ if (!Number.isFinite(start)
97
+ || !Number.isFinite(requested)
98
+ || start < 0
99
+ || requested < 0
100
+ || start + requested > this.byteLength) {
101
+ return null;
102
+ }
103
+ if (requested === 0) {
104
+ return Buffer.alloc(0);
105
+ }
106
+
107
+ // allocUnsafeSlow owns an exact ArrayBuffer instead of retaining a
108
+ // large socket chunk or a pooled backing store through subarray().
109
+ const output = Buffer.allocUnsafeSlow(requested);
110
+ let skip = start;
111
+ let written = 0;
112
+ for (let chunkIndex = this.headIndex; chunkIndex < this.chunks.length && written < requested; chunkIndex += 1) {
113
+ const chunk = this.chunks[chunkIndex];
114
+ const baseOffset = chunkIndex === this.headIndex ? this.headOffset : 0;
115
+ const available = chunk.length - baseOffset;
116
+ if (skip >= available) {
117
+ skip -= available;
118
+ continue;
119
+ }
120
+
121
+ const sourceStart = baseOffset + skip;
122
+ const copyLength = Math.min(requested - written, chunk.length - sourceStart);
123
+ chunk.copy(output, written, sourceStart, sourceStart + copyLength);
124
+ written += copyLength;
125
+ skip = 0;
126
+ }
127
+
128
+ if (written !== requested) {
129
+ return null;
130
+ }
131
+ this.materializedBytes += requested;
132
+ return output;
133
+ }
134
+
135
+ readUInt16BE(index) {
136
+ const first = this.byteAt(index);
137
+ const second = this.byteAt(Number(index) + 1);
138
+ if (first === undefined || second === undefined) {
139
+ return null;
140
+ }
141
+ return ((first << 8) | second) >>> 0;
142
+ }
143
+
144
+ readUInt32BE(index) {
145
+ const first = this.byteAt(index);
146
+ const second = this.byteAt(Number(index) + 1);
147
+ const third = this.byteAt(Number(index) + 2);
148
+ const fourth = this.byteAt(Number(index) + 3);
149
+ if (first === undefined
150
+ || second === undefined
151
+ || third === undefined
152
+ || fourth === undefined) {
153
+ return null;
154
+ }
155
+ return (((first << 24) >>> 0)
156
+ | (second << 16)
157
+ | (third << 8)
158
+ | fourth) >>> 0;
159
+ }
160
+
161
+ readBigUInt64BE(index) {
162
+ let value = 0n;
163
+ for (let offset = 0; offset < 8; offset += 1) {
164
+ const byte = this.byteAt(Number(index) + offset);
165
+ if (byte === undefined) {
166
+ return null;
167
+ }
168
+ value = (value << 8n) | BigInt(byte);
169
+ }
170
+ return value;
171
+ }
172
+
173
+ startsWith(sequence, startIndex = 0) {
174
+ const expected = bufferValue(sequence);
175
+ const start = Math.floor(Number(startIndex));
176
+ if (!Number.isFinite(start)
177
+ || start < 0
178
+ || expected.length === 0
179
+ || start + expected.length > this.byteLength) {
180
+ return false;
181
+ }
182
+ for (let index = 0; index < expected.length; index += 1) {
183
+ if (this.byteAt(start + index) !== expected[index]) {
184
+ return false;
185
+ }
186
+ }
187
+ return true;
188
+ }
189
+
190
+ indexOfByte(value, startIndex = 0) {
191
+ const needle = Number(value) & 0xff;
192
+ const start = Math.max(0, Math.floor(Number(startIndex) || 0));
193
+ if (start >= this.byteLength) {
194
+ return -1;
195
+ }
196
+
197
+ let logicalOffset = 0;
198
+ for (let chunkIndex = this.headIndex; chunkIndex < this.chunks.length; chunkIndex += 1) {
199
+ const chunk = this.chunks[chunkIndex];
200
+ const baseOffset = chunkIndex === this.headIndex ? this.headOffset : 0;
201
+ const available = chunk.length - baseOffset;
202
+ if (logicalOffset + available <= start) {
203
+ logicalOffset += available;
204
+ continue;
205
+ }
206
+ const localStart = baseOffset + Math.max(0, start - logicalOffset);
207
+ const found = chunk.indexOf(needle, localStart);
208
+ if (found >= 0) {
209
+ return logicalOffset + found - baseOffset;
210
+ }
211
+ logicalOffset += available;
212
+ }
213
+ return -1;
214
+ }
215
+
216
+ findIndex(predicate, startIndex = 0) {
217
+ if (typeof predicate !== 'function') {
218
+ return -1;
219
+ }
220
+ const start = Math.max(0, Math.floor(Number(startIndex) || 0));
221
+ if (start >= this.byteLength) {
222
+ return -1;
223
+ }
224
+
225
+ let logicalIndex = 0;
226
+ for (let chunkIndex = this.headIndex; chunkIndex < this.chunks.length; chunkIndex += 1) {
227
+ const chunk = this.chunks[chunkIndex];
228
+ const baseOffset = chunkIndex === this.headIndex ? this.headOffset : 0;
229
+ for (let offset = baseOffset; offset < chunk.length; offset += 1, logicalIndex += 1) {
230
+ if (logicalIndex >= start && predicate(chunk[offset], logicalIndex)) {
231
+ return logicalIndex;
232
+ }
233
+ }
234
+ }
235
+ return -1;
236
+ }
237
+
238
+ indexOfSequence(sequence, startIndex = 0) {
239
+ const needle = bufferValue(sequence);
240
+ const start = Math.max(0, Math.floor(Number(startIndex) || 0));
241
+ if (needle.length === 0) {
242
+ return Math.min(start, this.byteLength);
243
+ }
244
+ if (start >= this.byteLength || needle.length > this.byteLength - start) {
245
+ return -1;
246
+ }
247
+
248
+ const prefix = new Uint32Array(needle.length);
249
+ for (let index = 1, matched = 0; index < needle.length; index += 1) {
250
+ while (matched > 0 && needle[index] !== needle[matched]) {
251
+ matched = prefix[matched - 1];
252
+ }
253
+ if (needle[index] === needle[matched]) {
254
+ matched += 1;
255
+ }
256
+ prefix[index] = matched;
257
+ }
258
+
259
+ let logicalIndex = 0;
260
+ let matched = 0;
261
+ for (let chunkIndex = this.headIndex; chunkIndex < this.chunks.length; chunkIndex += 1) {
262
+ const chunk = this.chunks[chunkIndex];
263
+ const baseOffset = chunkIndex === this.headIndex ? this.headOffset : 0;
264
+ for (let offset = baseOffset; offset < chunk.length; offset += 1, logicalIndex += 1) {
265
+ if (logicalIndex < start) {
266
+ continue;
267
+ }
268
+ const current = chunk[offset];
269
+ while (matched > 0 && current !== needle[matched]) {
270
+ matched = prefix[matched - 1];
271
+ }
272
+ if (current === needle[matched]) {
273
+ matched += 1;
274
+ }
275
+ if (matched === needle.length) {
276
+ return logicalIndex - needle.length + 1;
277
+ }
278
+ }
279
+ }
280
+ return -1;
281
+ }
282
+
283
+ discard(length) {
284
+ let remaining = Math.max(0, Math.min(this.byteLength, Math.floor(Number(length) || 0)));
285
+ const discarded = remaining;
286
+ while (remaining > 0 && this.headIndex < this.chunks.length) {
287
+ const chunk = this.chunks[this.headIndex];
288
+ const available = chunk.length - this.headOffset;
289
+ if (remaining < available) {
290
+ this.headOffset += remaining;
291
+ remaining = 0;
292
+ break;
293
+ }
294
+ remaining -= available;
295
+ this.chunks[this.headIndex] = null;
296
+ this.headIndex += 1;
297
+ this.headOffset = 0;
298
+ }
299
+ this.byteLength -= discarded;
300
+
301
+ if (this.byteLength === 0) {
302
+ this.chunks = [];
303
+ this.headIndex = 0;
304
+ this.headOffset = 0;
305
+ } else if (this.headIndex >= 64 && this.headIndex * 2 >= this.chunks.length) {
306
+ this.chunks = this.chunks.slice(this.headIndex);
307
+ this.headIndex = 0;
308
+ }
309
+ return discarded;
310
+ }
311
+
312
+ consumeExact(length) {
313
+ const output = this.readExact(0, length);
314
+ if (!output) {
315
+ return null;
316
+ }
317
+ this.discard(length);
318
+ return output;
319
+ }
320
+
321
+ clear() {
322
+ this.chunks = [];
323
+ this.headIndex = 0;
324
+ this.headOffset = 0;
325
+ this.byteLength = 0;
326
+ }
327
+
328
+ getStatus() {
329
+ return {
330
+ bytes: this.byteLength,
331
+ segments: this.segmentCount,
332
+ maxBytes: this.maxBytes,
333
+ highWaterBytes: this.highWaterBytes,
334
+ appendedBytes: this.appendedBytes,
335
+ appendCopyBytes: 0,
336
+ materializedBytes: this.materializedBytes,
337
+ rejectedBytes: this.rejectedBytes
338
+ };
339
+ }
340
+ }
341
+
342
+ function normalizeFrameMode(header) {
343
+ return stringValue(header?.frameMode ?? header?.mode, 48).toLowerCase();
344
+ }
345
+
346
+ function isMode5Header(header) {
347
+ return normalizeFrameMode(header) === 'mode5-lzo-delta';
348
+ }
349
+
350
+ function isH264Header(header) {
351
+ const mode = normalizeFrameMode(header);
352
+ const codec = stringValue(
353
+ header?.codec
354
+ ?? header?.codecName
355
+ ?? header?.mimeType
356
+ ?? header?.format,
357
+ 96).toLowerCase();
358
+ return mode.includes('h264')
359
+ || codec.includes('h264')
360
+ || codec.includes('avc');
361
+ }
362
+
363
+ function isH264KeyHeader(header) {
364
+ const chunkType = stringValue(header?.chunkType, 24).toLowerCase();
365
+ return header?.isKeyFrame === true
366
+ || chunkType === 'key'
367
+ || chunkType === 'idr'
368
+ || chunkType === 'keyframe';
369
+ }
370
+
371
+ function ingressBindingKey(header) {
372
+ return [
373
+ stringValue(header?.deviceId, 160),
374
+ stringValue(header?.sessionId, 160),
375
+ stringValue(header?.streamId, 160),
376
+ stringValue(header?.commandId, 160),
377
+ stringValue(header?.streamPurpose ?? header?.purpose, 40),
378
+ stringValue(header?.monitorIndex, 24),
379
+ stringValue(header?.captureGeneration, 40)
380
+ ].join('\u001f');
381
+ }
382
+
383
+ function mode5StreamKey(header) {
384
+ return [
385
+ stringValue(header?.streamId, 160),
386
+ stringValue(header?.captureGeneration, 40)
387
+ ].join('\u001f');
388
+ }
389
+
390
+ /**
391
+ * One exact Agent socket owns one lane. The lane is packet- and payload-byte
392
+ * bounded, drains only a small budget per event-loop turn, and enters H.264
393
+ * key-frame recovery whenever queue pressure creates a dependency gap.
394
+ */
395
+ export function createBoundedAgentBinaryIngressLane(options = {}) {
396
+ const maxPackets = boundedInteger(
397
+ options.maxPackets,
398
+ 1,
399
+ 16,
400
+ DEFAULT_MAX_PACKETS);
401
+ const maxBytes = boundedInteger(
402
+ options.maxBytes,
403
+ 1,
404
+ 256 * 1024 * 1024,
405
+ DEFAULT_MAX_QUEUE_BYTES);
406
+ const drainBudgetPackets = boundedInteger(
407
+ options.drainBudgetPackets,
408
+ 1,
409
+ 4,
410
+ DEFAULT_DRAIN_BUDGET_PACKETS);
411
+ const maxQueueAgeMs = boundedInteger(
412
+ options.maxQueueAgeMs,
413
+ 80,
414
+ 150,
415
+ DEFAULT_MAX_QUEUE_AGE_MS);
416
+ const mode5PacketsPerStream = boundedInteger(
417
+ options.mode5PacketsPerStream,
418
+ 1,
419
+ 16,
420
+ DEFAULT_MODE5_PACKETS_PER_STREAM);
421
+ const maxRetainedPayloadBackingOverheadBytes = boundedInteger(
422
+ options.maxRetainedPayloadBackingOverheadBytes,
423
+ 0,
424
+ 1024 * 1024,
425
+ 0);
426
+ const consume = typeof options.consume === 'function' ? options.consume : () => {};
427
+ const onAccounting = typeof options.onAccounting === 'function' ? options.onAccounting : () => {};
428
+ const onDrop = typeof options.onDrop === 'function' ? options.onDrop : () => {};
429
+ const onError = typeof options.onError === 'function' ? options.onError : () => {};
430
+ const schedule = typeof options.schedule === 'function'
431
+ ? options.schedule
432
+ : callback => setImmediate(callback);
433
+ const cancelSchedule = typeof options.cancelSchedule === 'function'
434
+ ? options.cancelSchedule
435
+ : handle => clearImmediate(handle);
436
+ const isOwnerCurrent = typeof options.isOwnerCurrent === 'function'
437
+ ? options.isOwnerCurrent
438
+ : () => true;
439
+ const now = typeof options.now === 'function'
440
+ ? options.now
441
+ : () => Date.now();
442
+
443
+ let queue = [];
444
+ let queuedBytes = 0;
445
+ let queuedPackets = 0;
446
+ let queuedBytesHighWater = 0;
447
+ let queuedPacketsHighWater = 0;
448
+ let scheduled = false;
449
+ let scheduleHandle = null;
450
+ let draining = false;
451
+ let closed = false;
452
+ let underflowCount = 0;
453
+ let enqueuedPackets = 0;
454
+ let drainedPackets = 0;
455
+ let droppedPackets = 0;
456
+ let droppedBytes = 0;
457
+ let payloadBackingCopies = 0;
458
+ let payloadBackingCopyBytes = 0;
459
+ let retainedBackingOverheadBytesHighWater = 0;
460
+ const awaitingKeyFrames = new Set();
461
+
462
+ function emitAccounting(event, packetDelta = 0, byteDelta = 0, extra = {}) {
463
+ const nextPackets = queuedPackets + packetDelta;
464
+ const nextBytes = queuedBytes + byteDelta;
465
+ if (nextPackets < 0 || nextBytes < 0) {
466
+ underflowCount += 1;
467
+ }
468
+ queuedPackets = nextPackets;
469
+ queuedBytes = nextBytes;
470
+ queuedPacketsHighWater = Math.max(queuedPacketsHighWater, queuedPackets);
471
+ queuedBytesHighWater = Math.max(queuedBytesHighWater, queuedBytes);
472
+ onAccounting({
473
+ event,
474
+ queuedPacketsDelta: packetDelta,
475
+ queuedBytesDelta: byteDelta,
476
+ queuedPackets,
477
+ queuedBytes,
478
+ ...extra
479
+ });
480
+ }
481
+
482
+ function describeItem(header, payload) {
483
+ let ownedPayload = bufferValue(payload);
484
+ let retainedByteLength = Math.max(
485
+ ownedPayload.length,
486
+ Number(ownedPayload.buffer?.byteLength || ownedPayload.length));
487
+ let backingOverheadBytes = Math.max(0, retainedByteLength - ownedPayload.length);
488
+ if (backingOverheadBytes > maxRetainedPayloadBackingOverheadBytes) {
489
+ const exactPayload = Buffer.allocUnsafeSlow(ownedPayload.length);
490
+ ownedPayload.copy(exactPayload);
491
+ ownedPayload = exactPayload;
492
+ retainedByteLength = exactPayload.length;
493
+ backingOverheadBytes = 0;
494
+ payloadBackingCopies += 1;
495
+ payloadBackingCopyBytes += exactPayload.length;
496
+ }
497
+ retainedBackingOverheadBytesHighWater = Math.max(
498
+ retainedBackingOverheadBytesHighWater,
499
+ backingOverheadBytes);
500
+ const h264 = isH264Header(header);
501
+ return {
502
+ header,
503
+ payload: ownedPayload,
504
+ byteLength: retainedByteLength,
505
+ payloadByteLength: ownedPayload.length,
506
+ backingOverheadBytes,
507
+ h264,
508
+ keyFrame: h264 && isH264KeyHeader(header),
509
+ bindingKey: h264 ? ingressBindingKey(header) : '',
510
+ mode5: isMode5Header(header),
511
+ mode5Key: isMode5Header(header) ? mode5StreamKey(header) : '',
512
+ enqueuedAtMs: 0
513
+ };
514
+ }
515
+
516
+ function markDropped(item, reason) {
517
+ droppedPackets += 1;
518
+ droppedBytes += item.byteLength;
519
+ onDrop({ item, reason });
520
+ onAccounting({
521
+ event: 'drop',
522
+ reason,
523
+ queuedPacketsDelta: 0,
524
+ queuedBytesDelta: 0,
525
+ queuedPackets,
526
+ queuedBytes,
527
+ droppedPacketBytes: item.byteLength
528
+ });
529
+ }
530
+
531
+ function removeAt(index, reason, { dropped = true, recover = true } = {}) {
532
+ if (index < 0 || index >= queue.length) {
533
+ return null;
534
+ }
535
+ const [item] = queue.splice(index, 1);
536
+ emitAccounting(
537
+ dropped ? 'evict' : reason,
538
+ -1,
539
+ -item.byteLength,
540
+ { reason, packetBytes: item.byteLength });
541
+ if (dropped) {
542
+ markDropped(item, reason);
543
+ }
544
+ if (recover && dropped && item.h264) {
545
+ awaitingKeyFrames.add(item.bindingKey);
546
+ }
547
+ return item;
548
+ }
549
+
550
+ function retireH264Deltas(bindingKey, reason) {
551
+ for (let index = queue.length - 1; index >= 0; index -= 1) {
552
+ const item = queue[index];
553
+ if (item.h264 && !item.keyFrame && item.bindingKey === bindingKey) {
554
+ removeAt(index, reason, { dropped: true, recover: false });
555
+ }
556
+ }
557
+ }
558
+
559
+ function markH264Gap(item, reason) {
560
+ if (item.h264) {
561
+ awaitingKeyFrames.add(item.bindingKey);
562
+ retireH264Deltas(item.bindingKey, `${reason}-dependent`);
563
+ }
564
+ markDropped(item, reason);
565
+ }
566
+
567
+ function purgeOlderH264Binding(bindingKey) {
568
+ for (let index = queue.length - 1; index >= 0; index -= 1) {
569
+ const item = queue[index];
570
+ if (item.h264 && item.bindingKey === bindingKey) {
571
+ removeAt(index, 'superseded-by-latest-key', {
572
+ dropped: true,
573
+ recover: false
574
+ });
575
+ }
576
+ }
577
+ }
578
+
579
+ function chooseCapacityVictim() {
580
+ let index = queue.findIndex(item => !item.h264 && !item.mode5);
581
+ if (index >= 0) {
582
+ return index;
583
+ }
584
+ index = queue.findIndex(item => item.mode5);
585
+ if (index >= 0) {
586
+ return index;
587
+ }
588
+ index = queue.findIndex(item => item.h264 && !item.keyFrame);
589
+ if (index >= 0) {
590
+ return index;
591
+ }
592
+ return queue.length > 0 ? 0 : -1;
593
+ }
594
+
595
+ function makeCapacity(item) {
596
+ if (item.h264 && !item.keyFrame) {
597
+ return queue.length < maxPackets
598
+ && queuedBytes + item.byteLength <= maxBytes;
599
+ }
600
+
601
+ while (queue.length >= maxPackets || queuedBytes + item.byteLength > maxBytes) {
602
+ const victimIndex = chooseCapacityVictim();
603
+ if (victimIndex < 0) {
604
+ return false;
605
+ }
606
+ const victim = removeAt(victimIndex, 'ingress-capacity', {
607
+ dropped: true,
608
+ recover: true
609
+ });
610
+ if (victim?.h264) {
611
+ retireH264Deltas(victim.bindingKey, 'ingress-capacity-dependent');
612
+ }
613
+ }
614
+ return true;
615
+ }
616
+
617
+ function expireStaleItems(nowMs = now()) {
618
+ let expired = 0;
619
+ while (queue.length > 0) {
620
+ const item = queue[0];
621
+ const ageMs = Math.max(0, Number(nowMs) - Number(item.enqueuedAtMs || 0));
622
+ if (ageMs <= maxQueueAgeMs) {
623
+ break;
624
+ }
625
+ const removed = removeAt(0, 'ingress-age-limit', {
626
+ dropped: true,
627
+ recover: true
628
+ });
629
+ if (!removed) {
630
+ break;
631
+ }
632
+ expired += 1;
633
+ if (removed.h264) {
634
+ retireH264Deltas(removed.bindingKey, 'ingress-age-limit-dependent');
635
+ }
636
+ }
637
+ return expired;
638
+ }
639
+
640
+ function requestDrain() {
641
+ if (closed || scheduled || queue.length === 0) {
642
+ return;
643
+ }
644
+ scheduled = true;
645
+ scheduleHandle = schedule(() => {
646
+ scheduled = false;
647
+ scheduleHandle = null;
648
+ drain();
649
+ });
650
+ }
651
+
652
+ function drain() {
653
+ if (closed || draining) {
654
+ return;
655
+ }
656
+ if (!isOwnerCurrent()) {
657
+ close('stale-agent-binary-owner');
658
+ return;
659
+ }
660
+
661
+ draining = true;
662
+ let processed = 0;
663
+ try {
664
+ expireStaleItems();
665
+ while (!closed && queue.length > 0 && processed < drainBudgetPackets) {
666
+ const item = removeAt(0, 'drain', {
667
+ dropped: false,
668
+ recover: false
669
+ });
670
+ if (!item) {
671
+ break;
672
+ }
673
+ try {
674
+ consume(item);
675
+ } catch (error) {
676
+ onError(error);
677
+ }
678
+ drainedPackets += 1;
679
+ processed += 1;
680
+ }
681
+ } finally {
682
+ draining = false;
683
+ }
684
+ if (!closed && queue.length > 0) {
685
+ requestDrain();
686
+ }
687
+ }
688
+
689
+ function enqueue(header, payload) {
690
+ const item = describeItem(header, payload);
691
+ if (closed || !isOwnerCurrent()) {
692
+ markDropped(item, 'stale-agent-binary-owner');
693
+ return false;
694
+ }
695
+ if (item.byteLength <= 0 || item.byteLength > maxBytes) {
696
+ markH264Gap(item, 'ingress-packet-byte-limit');
697
+ return false;
698
+ }
699
+ expireStaleItems();
700
+
701
+ if (item.h264) {
702
+ if (item.keyFrame) {
703
+ purgeOlderH264Binding(item.bindingKey);
704
+ awaitingKeyFrames.delete(item.bindingKey);
705
+ } else if (awaitingKeyFrames.has(item.bindingKey)) {
706
+ markDropped(item, 'h264-awaiting-key');
707
+ return false;
708
+ }
709
+ }
710
+
711
+ if (item.mode5) {
712
+ let sameStreamCount = queue.filter(queued => queued.mode5 && queued.mode5Key === item.mode5Key).length;
713
+ while (sameStreamCount >= mode5PacketsPerStream) {
714
+ const index = queue.findIndex(queued => queued.mode5 && queued.mode5Key === item.mode5Key);
715
+ if (index < 0) {
716
+ break;
717
+ }
718
+ removeAt(index, 'mode5-latest-packet', {
719
+ dropped: true,
720
+ recover: false
721
+ });
722
+ sameStreamCount -= 1;
723
+ }
724
+ }
725
+
726
+ if (!makeCapacity(item)) {
727
+ markH264Gap(item, 'ingress-capacity');
728
+ return false;
729
+ }
730
+
731
+ item.enqueuedAtMs = now();
732
+ queue.push(item);
733
+ enqueuedPackets += 1;
734
+ emitAccounting('enqueue', 1, item.byteLength, {
735
+ packetBytes: item.byteLength,
736
+ h264: item.h264,
737
+ keyFrame: item.keyFrame
738
+ });
739
+ if (item.h264 && item.keyFrame) {
740
+ awaitingKeyFrames.delete(item.bindingKey);
741
+ }
742
+ requestDrain();
743
+ return true;
744
+ }
745
+
746
+ function close(reason = 'agent-binary-ingress-close') {
747
+ if (closed) {
748
+ return;
749
+ }
750
+ closed = true;
751
+ if (scheduled && scheduleHandle !== null && scheduleHandle !== undefined) {
752
+ try {
753
+ cancelSchedule(scheduleHandle);
754
+ } catch {
755
+ // The scheduled callback still observes closed=true.
756
+ }
757
+ }
758
+ scheduled = false;
759
+ scheduleHandle = null;
760
+ while (queue.length > 0) {
761
+ removeAt(0, reason, {
762
+ dropped: false,
763
+ recover: false
764
+ });
765
+ }
766
+ awaitingKeyFrames.clear();
767
+ }
768
+
769
+ function getStatus() {
770
+ const oldestAgeMs = queue.length > 0
771
+ ? Math.max(0, now() - Number(queue[0].enqueuedAtMs || 0))
772
+ : 0;
773
+ return {
774
+ closed,
775
+ scheduled,
776
+ draining,
777
+ queuedPackets,
778
+ queuedBytes,
779
+ queuedPacketsHighWater,
780
+ queuedBytesHighWater,
781
+ maxPackets,
782
+ maxBytes,
783
+ maxQueueAgeMs,
784
+ oldestAgeMs,
785
+ drainBudgetPackets,
786
+ mode5PacketsPerStream,
787
+ maxRetainedPayloadBackingOverheadBytes,
788
+ awaitingKeyFrameBindings: awaitingKeyFrames.size,
789
+ enqueuedPackets,
790
+ drainedPackets,
791
+ droppedPackets,
792
+ droppedBytes,
793
+ payloadBackingCopies,
794
+ payloadBackingCopyBytes,
795
+ retainedBackingOverheadBytesHighWater,
796
+ underflowCount,
797
+ terminalZero: queuedPackets === 0
798
+ && queuedBytes === 0
799
+ && scheduled === false
800
+ && draining === false
801
+ };
802
+ }
803
+
804
+ return {
805
+ enqueue,
806
+ drain,
807
+ close,
808
+ getStatus
809
+ };
810
+ }