@opentui/core 0.4.2 → 0.4.4
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/Renderable.d.ts +2 -7
- package/audio-stream/demuxer.d.ts +19 -0
- package/audio-stream/icy/demuxer.d.ts +21 -0
- package/audio-stream/icy/metadata.d.ts +1 -0
- package/audio.d.ts +201 -1
- package/{index-6xr3rbbe.js → index-7z5n7k9m.js} +148 -136
- package/index-7z5n7k9m.js.map +32 -0
- package/{index-x7n320jr.js → index-za1krqsf.js} +314 -72
- package/index-za1krqsf.js.map +58 -0
- package/index.d.ts +3 -0
- package/index.js +1489 -92
- package/index.js.map +14 -10
- package/package.json +10 -10
- package/renderables/EditBufferRenderable.d.ts +2 -1
- package/renderables/LineNumberRenderable.d.ts +2 -2
- package/renderables/ScrollBox.d.ts +3 -2
- package/renderables/Select.d.ts +5 -0
- package/renderables/Text.d.ts +1 -1
- package/renderables/TextBufferRenderable.d.ts +3 -2
- package/renderables/TextNode.d.ts +2 -1
- package/renderables/text-table-width.d.ts +1 -0
- package/testing/bun-test-node.d.ts +1 -0
- package/testing.js +2 -2
- package/yoga.d.ts +1 -4
- package/yoga.js +1 -1
- package/zig-structs.d.ts +50 -3
- package/zig.d.ts +55 -5
- package/index-6xr3rbbe.js.map +0 -32
- package/index-x7n320jr.js.map +0 -58
package/index.js
CHANGED
|
@@ -43,7 +43,7 @@ import {
|
|
|
43
43
|
mergeKeyAliases,
|
|
44
44
|
mergeKeyBindings,
|
|
45
45
|
wrapWithDelegates
|
|
46
|
-
} from "./index-
|
|
46
|
+
} from "./index-7z5n7k9m.js";
|
|
47
47
|
import {
|
|
48
48
|
ASCIIFontSelectionHelper,
|
|
49
49
|
ATTRIBUTE_BASE_BITS,
|
|
@@ -64,6 +64,14 @@ import {
|
|
|
64
64
|
LogLevel,
|
|
65
65
|
MacOSScrollAccel,
|
|
66
66
|
MouseParser,
|
|
67
|
+
NativeAudioStreamCloseReason,
|
|
68
|
+
NativeAudioStreamCloseReason1 as NativeAudioStreamCloseReason2,
|
|
69
|
+
NativeAudioStreamFormat,
|
|
70
|
+
NativeAudioStreamFormat1 as NativeAudioStreamFormat2,
|
|
71
|
+
NativeAudioStreamState,
|
|
72
|
+
NativeAudioStreamState1 as NativeAudioStreamState2,
|
|
73
|
+
NativeAudioStreamStateNames,
|
|
74
|
+
NativeMeasureTargetKind,
|
|
67
75
|
OptimizedBuffer,
|
|
68
76
|
PasteEvent,
|
|
69
77
|
RGBA,
|
|
@@ -184,7 +192,7 @@ import {
|
|
|
184
192
|
visualizeRenderableTree,
|
|
185
193
|
white,
|
|
186
194
|
yellow
|
|
187
|
-
} from "./index-
|
|
195
|
+
} from "./index-za1krqsf.js";
|
|
188
196
|
// src/post/effects.ts
|
|
189
197
|
function toU8(value) {
|
|
190
198
|
return Math.round(Math.max(0, Math.min(1, Number.isFinite(value) ? value : 0)) * 255);
|
|
@@ -2431,7 +2439,7 @@ class SlotRenderable extends Renderable {
|
|
|
2431
2439
|
}
|
|
2432
2440
|
_detachNodeFromMount(node) {
|
|
2433
2441
|
if (node.parent === this) {
|
|
2434
|
-
this.remove(node
|
|
2442
|
+
this.remove(node);
|
|
2435
2443
|
}
|
|
2436
2444
|
}
|
|
2437
2445
|
_cleanupInactivePluginNodes(nextActivePluginIds, registeredPluginIds) {
|
|
@@ -2504,7 +2512,7 @@ class SlotRenderable extends Renderable {
|
|
|
2504
2512
|
for (const node of this._mountedNodes) {
|
|
2505
2513
|
if (!desiredNodeSet.has(node)) {
|
|
2506
2514
|
if (node.parent === this) {
|
|
2507
|
-
this.remove(node
|
|
2515
|
+
this.remove(node);
|
|
2508
2516
|
}
|
|
2509
2517
|
}
|
|
2510
2518
|
}
|
|
@@ -2515,7 +2523,7 @@ class SlotRenderable extends Renderable {
|
|
|
2515
2523
|
continue;
|
|
2516
2524
|
}
|
|
2517
2525
|
const childAtIndex = this.getChildren()[index];
|
|
2518
|
-
if (childAtIndex
|
|
2526
|
+
if (childAtIndex !== node) {
|
|
2519
2527
|
this.add(node, index);
|
|
2520
2528
|
}
|
|
2521
2529
|
}
|
|
@@ -2525,40 +2533,1295 @@ class SlotRenderable extends Renderable {
|
|
|
2525
2533
|
// src/audio.ts
|
|
2526
2534
|
import { EventEmitter } from "events";
|
|
2527
2535
|
import { readFile } from "node:fs/promises";
|
|
2536
|
+
|
|
2537
|
+
// src/audio-stream/icy/metadata.ts
|
|
2538
|
+
function parseIcyMetadata(bytes, decoder) {
|
|
2539
|
+
const decoded = decoder.decode(bytes);
|
|
2540
|
+
const nul = decoded.indexOf("\x00");
|
|
2541
|
+
const text = nul === -1 ? decoded : decoded.slice(0, nul);
|
|
2542
|
+
if (text.length === 0)
|
|
2543
|
+
return null;
|
|
2544
|
+
const fields = Object.create(null);
|
|
2545
|
+
let found = false;
|
|
2546
|
+
let offset = 0;
|
|
2547
|
+
while (offset < text.length) {
|
|
2548
|
+
const separator = text.indexOf("='", offset);
|
|
2549
|
+
if (separator === -1)
|
|
2550
|
+
break;
|
|
2551
|
+
const key = text.slice(offset, separator);
|
|
2552
|
+
if (key.length === 0 || /[';=]/.test(key))
|
|
2553
|
+
break;
|
|
2554
|
+
const end = text.indexOf("';", separator + 2);
|
|
2555
|
+
if (end === -1)
|
|
2556
|
+
break;
|
|
2557
|
+
Object.defineProperty(fields, key, {
|
|
2558
|
+
configurable: true,
|
|
2559
|
+
enumerable: true,
|
|
2560
|
+
writable: true,
|
|
2561
|
+
value: text.slice(separator + 2, end)
|
|
2562
|
+
});
|
|
2563
|
+
found = true;
|
|
2564
|
+
offset = end + 2;
|
|
2565
|
+
}
|
|
2566
|
+
return found ? Object.freeze(fields) : null;
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
// src/audio-stream/icy/demuxer.ts
|
|
2570
|
+
var EMPTY_FIELDS = Object.freeze(Object.create(null));
|
|
2571
|
+
function copyHeaders(headers) {
|
|
2572
|
+
const copy = Object.create(null);
|
|
2573
|
+
for (const [name, value] of Object.entries(headers ?? {}))
|
|
2574
|
+
copy[name.toLowerCase()] = value;
|
|
2575
|
+
return Object.freeze(copy);
|
|
2576
|
+
}
|
|
2577
|
+
function fieldsEqual(left, right) {
|
|
2578
|
+
const keys = Object.keys(left);
|
|
2579
|
+
return keys.length === Object.keys(right).length && keys.every((key) => Object.prototype.hasOwnProperty.call(right, key) && left[key] === right[key]);
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
class IcyStreamDemuxer {
|
|
2583
|
+
initialMetadata;
|
|
2584
|
+
audioRemaining;
|
|
2585
|
+
metadata = null;
|
|
2586
|
+
metadataOffset = 0;
|
|
2587
|
+
fields = EMPTY_FIELDS;
|
|
2588
|
+
interval;
|
|
2589
|
+
decoder;
|
|
2590
|
+
headers;
|
|
2591
|
+
constructor(options) {
|
|
2592
|
+
if (!Number.isSafeInteger(options.metadataInterval) || options.metadataInterval < 0) {
|
|
2593
|
+
throw new TypeError("metadataInterval must be a non-negative safe integer");
|
|
2594
|
+
}
|
|
2595
|
+
this.interval = options.metadataInterval;
|
|
2596
|
+
try {
|
|
2597
|
+
this.decoder = new TextDecoder(options.metadataEncoding ?? "iso-8859-1");
|
|
2598
|
+
} catch {
|
|
2599
|
+
throw new TypeError(`Unsupported metadataEncoding: ${options.metadataEncoding}`);
|
|
2600
|
+
}
|
|
2601
|
+
this.headers = copyHeaders(options.headers);
|
|
2602
|
+
this.audioRemaining = this.interval;
|
|
2603
|
+
this.initialMetadata = Object.freeze({ format: "icy", headers: this.headers, fields: this.fields });
|
|
2604
|
+
}
|
|
2605
|
+
*push(chunk) {
|
|
2606
|
+
if (this.interval === 0) {
|
|
2607
|
+
if (chunk.byteLength > 0)
|
|
2608
|
+
yield { type: "audio", data: chunk };
|
|
2609
|
+
return;
|
|
2610
|
+
}
|
|
2611
|
+
let offset = 0;
|
|
2612
|
+
while (offset < chunk.byteLength) {
|
|
2613
|
+
if (this.audioRemaining > 0) {
|
|
2614
|
+
const length2 = Math.min(this.audioRemaining, chunk.byteLength - offset);
|
|
2615
|
+
yield { type: "audio", data: chunk.subarray(offset, offset + length2) };
|
|
2616
|
+
offset += length2;
|
|
2617
|
+
this.audioRemaining -= length2;
|
|
2618
|
+
continue;
|
|
2619
|
+
}
|
|
2620
|
+
if (this.metadata == null) {
|
|
2621
|
+
const metadataLength = (chunk[offset] ?? 0) * 16;
|
|
2622
|
+
offset += 1;
|
|
2623
|
+
if (metadataLength === 0) {
|
|
2624
|
+
this.audioRemaining = this.interval;
|
|
2625
|
+
continue;
|
|
2626
|
+
}
|
|
2627
|
+
this.metadata = new Uint8Array(metadataLength);
|
|
2628
|
+
this.metadataOffset = 0;
|
|
2629
|
+
}
|
|
2630
|
+
const metadata = this.metadata;
|
|
2631
|
+
const length = Math.min(metadata.byteLength - this.metadataOffset, chunk.byteLength - offset);
|
|
2632
|
+
metadata.set(chunk.subarray(offset, offset + length), this.metadataOffset);
|
|
2633
|
+
offset += length;
|
|
2634
|
+
this.metadataOffset += length;
|
|
2635
|
+
if (this.metadataOffset !== metadata.byteLength)
|
|
2636
|
+
continue;
|
|
2637
|
+
const fields = parseIcyMetadata(metadata, this.decoder);
|
|
2638
|
+
this.metadata = null;
|
|
2639
|
+
this.metadataOffset = 0;
|
|
2640
|
+
this.audioRemaining = this.interval;
|
|
2641
|
+
if (fields != null && !fieldsEqual(this.fields, fields)) {
|
|
2642
|
+
this.fields = fields;
|
|
2643
|
+
yield {
|
|
2644
|
+
type: "metadata",
|
|
2645
|
+
metadata: Object.freeze({ format: "icy", headers: this.headers, fields })
|
|
2646
|
+
};
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
*flush() {
|
|
2651
|
+
if (this.interval === 0)
|
|
2652
|
+
return;
|
|
2653
|
+
if (this.audioRemaining === 0 || this.metadata != null) {
|
|
2654
|
+
throw new Error("ICY stream ended inside a metadata block");
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
function createIcyStreamDemuxer(options) {
|
|
2659
|
+
return new IcyStreamDemuxer(options);
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
// src/audio-stream/demuxer.ts
|
|
2663
|
+
function selectAudioStreamDemuxer(options) {
|
|
2664
|
+
const icyHeaders = Object.create(null);
|
|
2665
|
+
options.headers.forEach((value2, name) => {
|
|
2666
|
+
if (name.toLowerCase().startsWith("icy-"))
|
|
2667
|
+
icyHeaders[name.toLowerCase()] = value2;
|
|
2668
|
+
});
|
|
2669
|
+
const headers = Object.freeze(icyHeaders);
|
|
2670
|
+
const rawInterval = options.headers.get("icy-metaint");
|
|
2671
|
+
if (rawInterval == null) {
|
|
2672
|
+
return Object.keys(headers).length === 0 ? null : createIcyStreamDemuxer({ metadataInterval: 0, metadataEncoding: options.metadataEncoding, headers });
|
|
2673
|
+
}
|
|
2674
|
+
const value = rawInterval.trim();
|
|
2675
|
+
if (!/^\d+$/.test(value))
|
|
2676
|
+
throw new Error(`Invalid icy-metaint response header: ${rawInterval}`);
|
|
2677
|
+
const interval = Number(value);
|
|
2678
|
+
if (!Number.isSafeInteger(interval))
|
|
2679
|
+
throw new Error(`Invalid icy-metaint response header: ${rawInterval}`);
|
|
2680
|
+
return createIcyStreamDemuxer({ metadataInterval: interval, metadataEncoding: options.metadataEncoding, headers });
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
// src/audio.ts
|
|
2684
|
+
class AudioInitializationError extends Error {
|
|
2685
|
+
action;
|
|
2686
|
+
status;
|
|
2687
|
+
constructor(action, message, status, cause) {
|
|
2688
|
+
super(message);
|
|
2689
|
+
this.name = "AudioInitializationError";
|
|
2690
|
+
this.action = action;
|
|
2691
|
+
this.status = status;
|
|
2692
|
+
if (cause !== undefined)
|
|
2693
|
+
this.cause = cause;
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2528
2696
|
function statusToError(action, status) {
|
|
2529
2697
|
return new Error(`Audio ${action} failed: ${status}`);
|
|
2530
2698
|
}
|
|
2531
2699
|
function toBytes(data) {
|
|
2532
2700
|
return data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
2533
2701
|
}
|
|
2702
|
+
var DEFAULT_AUDIO_SAMPLE_RATE = 48000;
|
|
2703
|
+
var DEFAULT_STREAM_PROBE_BYTES = 1024 * 1024;
|
|
2704
|
+
var STREAM_POLL_INTERVAL_MS = 5;
|
|
2705
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
2706
|
+
var MAX_U32 = 4294967295;
|
|
2707
|
+
var INVALID_STREAM_CHUNK_MESSAGE = "Audio stream chunks must be Uint8Array instances";
|
|
2708
|
+
|
|
2709
|
+
class AudioStreamError extends Error {
|
|
2710
|
+
context;
|
|
2711
|
+
constructor(message, context, cause) {
|
|
2712
|
+
super(message);
|
|
2713
|
+
this.name = "AudioStreamError";
|
|
2714
|
+
this.context = context;
|
|
2715
|
+
if (cause !== undefined)
|
|
2716
|
+
this.cause = cause;
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
class ClassifiedAudioStreamError extends AudioStreamError {
|
|
2721
|
+
retryable;
|
|
2722
|
+
retryAfterMs;
|
|
2723
|
+
constructor(message, context, retryable, retryAfterMs, cause) {
|
|
2724
|
+
super(message, context, cause);
|
|
2725
|
+
this.retryable = retryable;
|
|
2726
|
+
this.retryAfterMs = retryAfterMs;
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
function createAbortError() {
|
|
2730
|
+
return new DOMException("The operation was aborted", "AbortError");
|
|
2731
|
+
}
|
|
2732
|
+
var isU32 = (value) => Number.isInteger(value) && value >= 0 && value <= MAX_U32;
|
|
2733
|
+
function resolvePositiveU32(value, fallback, name) {
|
|
2734
|
+
const resolved = value ?? fallback;
|
|
2735
|
+
if (!Number.isFinite(resolved) || !Number.isInteger(resolved) || resolved <= 0) {
|
|
2736
|
+
throw new TypeError(`${name} must be a finite positive integer`);
|
|
2737
|
+
}
|
|
2738
|
+
if (resolved > MAX_U32)
|
|
2739
|
+
throw new RangeError(`${name} exceeds the supported limit`);
|
|
2740
|
+
return resolved;
|
|
2741
|
+
}
|
|
2742
|
+
function resolveReconnectOptions(options) {
|
|
2743
|
+
const maxRetries = options.maxRetries ?? Number.POSITIVE_INFINITY;
|
|
2744
|
+
const initialDelayMs = options.initialDelayMs ?? 1000;
|
|
2745
|
+
const maxDelayMs = options.maxDelayMs ?? 15000;
|
|
2746
|
+
const backoffFactor = options.backoffFactor ?? 2;
|
|
2747
|
+
const retryOnEnd = options.retryOnEnd ?? false;
|
|
2748
|
+
const retry = options.retry;
|
|
2749
|
+
if (maxRetries !== Number.POSITIVE_INFINITY && (!Number.isInteger(maxRetries) || maxRetries < 0)) {
|
|
2750
|
+
throw new TypeError("reconnect.maxRetries must be a non-negative integer or Infinity");
|
|
2751
|
+
}
|
|
2752
|
+
if (!Number.isFinite(initialDelayMs) || !Number.isInteger(initialDelayMs) || initialDelayMs < 0) {
|
|
2753
|
+
throw new TypeError("reconnect.initialDelayMs must be a finite non-negative integer");
|
|
2754
|
+
}
|
|
2755
|
+
if (!Number.isFinite(maxDelayMs) || !Number.isInteger(maxDelayMs) || maxDelayMs < 0) {
|
|
2756
|
+
throw new TypeError("reconnect.maxDelayMs must be a finite non-negative integer");
|
|
2757
|
+
}
|
|
2758
|
+
if (!Number.isFinite(backoffFactor) || backoffFactor < 1) {
|
|
2759
|
+
throw new TypeError("reconnect.backoffFactor must be a finite number greater than or equal to 1");
|
|
2760
|
+
}
|
|
2761
|
+
if (typeof retryOnEnd !== "boolean") {
|
|
2762
|
+
throw new TypeError("reconnect.retryOnEnd must be a boolean");
|
|
2763
|
+
}
|
|
2764
|
+
if (retry !== undefined && typeof retry !== "function") {
|
|
2765
|
+
throw new TypeError("reconnect.retry must be a function");
|
|
2766
|
+
}
|
|
2767
|
+
return {
|
|
2768
|
+
maxRetries,
|
|
2769
|
+
initialDelayMs,
|
|
2770
|
+
maxDelayMs,
|
|
2771
|
+
backoffFactor,
|
|
2772
|
+
retryOnEnd,
|
|
2773
|
+
retry: retry == null ? undefined : (error, context) => retry.call(options, error, context)
|
|
2774
|
+
};
|
|
2775
|
+
}
|
|
2776
|
+
function resolveAudioStreamOptions(options) {
|
|
2777
|
+
const format = resolveAudioStreamFormat(options.format);
|
|
2778
|
+
const capacityMs = resolvePositiveU32(options.buffer?.capacityMs, 2000, "buffer.capacityMs");
|
|
2779
|
+
const startupMs = resolvePositiveU32(options.buffer?.startupMs, 1000, "buffer.startupMs");
|
|
2780
|
+
const resumeMs = resolvePositiveU32(options.buffer?.resumeMs, 1000, "buffer.resumeMs");
|
|
2781
|
+
const maxProbeBytes = resolvePositiveU32(options.maxProbeBytes, DEFAULT_STREAM_PROBE_BYTES, "maxProbeBytes");
|
|
2782
|
+
if (startupMs > capacityMs)
|
|
2783
|
+
throw new RangeError("buffer.startupMs must not exceed buffer.capacityMs");
|
|
2784
|
+
if (resumeMs > capacityMs)
|
|
2785
|
+
throw new RangeError("buffer.resumeMs must not exceed buffer.capacityMs");
|
|
2786
|
+
return {
|
|
2787
|
+
format,
|
|
2788
|
+
capacityMs,
|
|
2789
|
+
startupMs,
|
|
2790
|
+
resumeMs,
|
|
2791
|
+
volume: options.volume ?? 1,
|
|
2792
|
+
pan: options.pan ?? 0,
|
|
2793
|
+
groupId: options.groupId ?? 0,
|
|
2794
|
+
maxProbeBytes,
|
|
2795
|
+
signal: options.signal,
|
|
2796
|
+
reconnect: options.reconnect === undefined ? undefined : resolveReconnectOptions(options.reconnect)
|
|
2797
|
+
};
|
|
2798
|
+
}
|
|
2799
|
+
function resolveAudioStreamConnector(connector) {
|
|
2800
|
+
const connect = connector?.connect;
|
|
2801
|
+
if (typeof connect !== "function")
|
|
2802
|
+
throw new TypeError("Audio stream connector must define connect()");
|
|
2803
|
+
return { connect: (context) => connect.call(connector, context) };
|
|
2804
|
+
}
|
|
2805
|
+
function runBoundedCleanup(cleanup, timeoutMs = 50) {
|
|
2806
|
+
let result;
|
|
2807
|
+
try {
|
|
2808
|
+
result = Promise.resolve(cleanup()).catch(() => {
|
|
2809
|
+
return;
|
|
2810
|
+
});
|
|
2811
|
+
} catch {
|
|
2812
|
+
return Promise.resolve();
|
|
2813
|
+
}
|
|
2814
|
+
return new Promise((resolve) => {
|
|
2815
|
+
const timer = setTimeout(resolve, timeoutMs);
|
|
2816
|
+
result.then(() => {
|
|
2817
|
+
clearTimeout(timer);
|
|
2818
|
+
resolve();
|
|
2819
|
+
});
|
|
2820
|
+
});
|
|
2821
|
+
}
|
|
2822
|
+
function waitForDelay(delayMs, signal) {
|
|
2823
|
+
if (signal.aborted)
|
|
2824
|
+
return Promise.reject(createAbortError());
|
|
2825
|
+
return new Promise((resolve, reject) => {
|
|
2826
|
+
let remainingMs = delayMs;
|
|
2827
|
+
let timer;
|
|
2828
|
+
const schedule = () => {
|
|
2829
|
+
const currentDelayMs = Math.min(remainingMs, MAX_TIMER_DELAY_MS);
|
|
2830
|
+
timer = setTimeout(() => {
|
|
2831
|
+
remainingMs -= currentDelayMs;
|
|
2832
|
+
if (remainingMs > 0)
|
|
2833
|
+
schedule();
|
|
2834
|
+
else {
|
|
2835
|
+
signal.removeEventListener("abort", onAbort);
|
|
2836
|
+
resolve();
|
|
2837
|
+
}
|
|
2838
|
+
}, currentDelayMs);
|
|
2839
|
+
};
|
|
2840
|
+
const onAbort = () => {
|
|
2841
|
+
clearTimeout(timer);
|
|
2842
|
+
reject(createAbortError());
|
|
2843
|
+
};
|
|
2844
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2845
|
+
schedule();
|
|
2846
|
+
});
|
|
2847
|
+
}
|
|
2848
|
+
function waitForPoll(signal) {
|
|
2849
|
+
return waitForDelay(STREAM_POLL_INTERVAL_MS, signal).then(() => true).catch(() => false);
|
|
2850
|
+
}
|
|
2851
|
+
function parseRetryAfter(value, maxDelayMs) {
|
|
2852
|
+
if (value == null)
|
|
2853
|
+
return;
|
|
2854
|
+
const seconds = Number(value.trim());
|
|
2855
|
+
if (Number.isFinite(seconds) && seconds >= 0)
|
|
2856
|
+
return Math.min(maxDelayMs, Math.ceil(seconds * 1000));
|
|
2857
|
+
const date = Date.parse(value);
|
|
2858
|
+
if (!Number.isFinite(date))
|
|
2859
|
+
return;
|
|
2860
|
+
return Math.min(maxDelayMs, Math.max(0, date - Date.now()));
|
|
2861
|
+
}
|
|
2862
|
+
function resolveAudioStreamFormat(value) {
|
|
2863
|
+
const format = value ?? "mp3";
|
|
2864
|
+
if (format !== "mp3" && format !== "flac")
|
|
2865
|
+
throw new TypeError(`Unsupported audio stream format: ${format}`);
|
|
2866
|
+
return format;
|
|
2867
|
+
}
|
|
2868
|
+
function toNativeAudioStreamFormat(format) {
|
|
2869
|
+
switch (format) {
|
|
2870
|
+
case "mp3":
|
|
2871
|
+
return NativeAudioStreamFormat.Mp3;
|
|
2872
|
+
case "flac":
|
|
2873
|
+
return NativeAudioStreamFormat.Flac;
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
function resolveContentTypePolicy(value, receiver) {
|
|
2877
|
+
const policy = value ?? "validate";
|
|
2878
|
+
if (policy !== "validate" && policy !== "ignore" && typeof policy !== "function") {
|
|
2879
|
+
throw new TypeError("contentTypePolicy must be 'validate', 'ignore', or a function");
|
|
2880
|
+
}
|
|
2881
|
+
return typeof policy === "function" ? (context) => policy.call(receiver, context) : policy;
|
|
2882
|
+
}
|
|
2883
|
+
function isAllowedContentType(format, value) {
|
|
2884
|
+
const contentType = value.split(";", 1)[0]?.trim().toLowerCase();
|
|
2885
|
+
switch (format) {
|
|
2886
|
+
case "mp3":
|
|
2887
|
+
return ["audio/mpeg", "audio/mp3", "application/octet-stream", "application/mp3"].includes(contentType ?? "");
|
|
2888
|
+
case "flac":
|
|
2889
|
+
return ["audio/flac", "audio/x-flac", "application/octet-stream"].includes(contentType ?? "");
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
function createAudioStreamUrlConnector(source, request, format, contentTypePolicy) {
|
|
2893
|
+
return {
|
|
2894
|
+
async connect({ signal, attempt }) {
|
|
2895
|
+
let response;
|
|
2896
|
+
try {
|
|
2897
|
+
const headers = new Headers(request?.headers);
|
|
2898
|
+
if (!headers.has("icy-metadata"))
|
|
2899
|
+
headers.set("Icy-MetaData", "1");
|
|
2900
|
+
response = await globalThis.fetch(source, { ...request, headers, signal });
|
|
2901
|
+
} catch (cause) {
|
|
2902
|
+
throw new ClassifiedAudioStreamError("Audio stream fetch failed", { action: "fetch", attempt }, true, undefined, cause);
|
|
2903
|
+
}
|
|
2904
|
+
if (!response.ok) {
|
|
2905
|
+
const retryAfterMs = parseRetryAfter(response.headers.get("retry-after"), Number.POSITIVE_INFINITY);
|
|
2906
|
+
await runBoundedCleanup(() => response.body?.cancel());
|
|
2907
|
+
const retryable = [408, 425, 429].includes(response.status) || response.status >= 500 && response.status <= 599;
|
|
2908
|
+
throw new ClassifiedAudioStreamError(`Audio stream request failed with HTTP ${response.status}`, { action: "response", status: response.status, attempt }, retryable, retryAfterMs);
|
|
2909
|
+
}
|
|
2910
|
+
const contentType = response.headers.get("content-type");
|
|
2911
|
+
let contentTypeAccepted = contentType == null || contentTypePolicy === "ignore";
|
|
2912
|
+
if (!contentTypeAccepted && contentTypePolicy === "validate") {
|
|
2913
|
+
contentTypeAccepted = isAllowedContentType(format, contentType);
|
|
2914
|
+
} else if (typeof contentTypePolicy === "function") {
|
|
2915
|
+
try {
|
|
2916
|
+
const result = contentTypePolicy(Object.freeze({
|
|
2917
|
+
format,
|
|
2918
|
+
contentType,
|
|
2919
|
+
status: response.status,
|
|
2920
|
+
url: response.url || String(source)
|
|
2921
|
+
}));
|
|
2922
|
+
if (typeof result !== "boolean")
|
|
2923
|
+
throw new TypeError("contentTypePolicy must return a boolean");
|
|
2924
|
+
contentTypeAccepted = result;
|
|
2925
|
+
} catch (cause) {
|
|
2926
|
+
await runBoundedCleanup(() => response.body?.cancel());
|
|
2927
|
+
throw new ClassifiedAudioStreamError(cause instanceof Error ? cause.message : "Audio stream content type policy failed", { action: "response", status: response.status, attempt }, false, undefined, cause);
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
if (!contentTypeAccepted) {
|
|
2931
|
+
await runBoundedCleanup(() => response.body?.cancel());
|
|
2932
|
+
throw new ClassifiedAudioStreamError(`Unsupported audio stream Content-Type: ${contentType}`, { action: "response", status: response.status, attempt }, false);
|
|
2933
|
+
}
|
|
2934
|
+
if (response.body == null) {
|
|
2935
|
+
throw new ClassifiedAudioStreamError("Audio stream response has no body", { action: "response", status: response.status, attempt }, true);
|
|
2936
|
+
}
|
|
2937
|
+
return {
|
|
2938
|
+
body: response.body,
|
|
2939
|
+
info: { headers: response.headers, status: response.status }
|
|
2940
|
+
};
|
|
2941
|
+
}
|
|
2942
|
+
};
|
|
2943
|
+
}
|
|
2944
|
+
function resolveMetadataEncoding(value) {
|
|
2945
|
+
try {
|
|
2946
|
+
return new TextDecoder(value ?? "iso-8859-1").encoding;
|
|
2947
|
+
} catch {
|
|
2948
|
+
throw new TypeError(`Unsupported metadataEncoding: ${value}`);
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
function resolveAudioStreamRequest(request) {
|
|
2952
|
+
if (request === undefined)
|
|
2953
|
+
return;
|
|
2954
|
+
const { body: _body, signal: _signal, ...safeRequest } = request;
|
|
2955
|
+
return safeRequest;
|
|
2956
|
+
}
|
|
2957
|
+
function isReadableStreamSource(source) {
|
|
2958
|
+
try {
|
|
2959
|
+
return typeof source?.getReader === "function";
|
|
2960
|
+
} catch {
|
|
2961
|
+
return false;
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
function isAsyncIterableSource(source) {
|
|
2965
|
+
try {
|
|
2966
|
+
return typeof source?.[Symbol.asyncIterator] === "function";
|
|
2967
|
+
} catch {
|
|
2968
|
+
return false;
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
function isUint8Array(value) {
|
|
2972
|
+
return ArrayBuffer.isView(value) && Object.prototype.toString.call(value) === "[object Uint8Array]";
|
|
2973
|
+
}
|
|
2974
|
+
var createAudioStream;
|
|
2975
|
+
var openAudioStream;
|
|
2976
|
+
|
|
2977
|
+
class AudioStream extends EventEmitter {
|
|
2978
|
+
closed;
|
|
2979
|
+
format;
|
|
2980
|
+
lib;
|
|
2981
|
+
engine;
|
|
2982
|
+
connector;
|
|
2983
|
+
demuxerFactory;
|
|
2984
|
+
readAction;
|
|
2985
|
+
options;
|
|
2986
|
+
removeFromOwner;
|
|
2987
|
+
lifecycleController = new AbortController;
|
|
2988
|
+
nativeStreamId = null;
|
|
2989
|
+
nativeStats = null;
|
|
2990
|
+
activeAttempt = null;
|
|
2991
|
+
pendingCleanup = null;
|
|
2992
|
+
reconnectAttempts = 0;
|
|
2993
|
+
consecutiveReconnectAttempts = 0;
|
|
2994
|
+
disposed = false;
|
|
2995
|
+
exposed = false;
|
|
2996
|
+
terminalError = null;
|
|
2997
|
+
metadata = null;
|
|
2998
|
+
pendingMetadataEvent = false;
|
|
2999
|
+
metadataEventScheduled = false;
|
|
3000
|
+
terminalEventScheduled = false;
|
|
3001
|
+
setupResolve;
|
|
3002
|
+
setupReject;
|
|
3003
|
+
closedResolve;
|
|
3004
|
+
setupPromise;
|
|
3005
|
+
overallAbortListener = () => this.dispose();
|
|
3006
|
+
static {
|
|
3007
|
+
createAudioStream = (init) => new AudioStream(init);
|
|
3008
|
+
openAudioStream = (stream) => stream.open();
|
|
3009
|
+
}
|
|
3010
|
+
constructor(init) {
|
|
3011
|
+
super();
|
|
3012
|
+
this.lib = init.lib;
|
|
3013
|
+
this.engine = init.engine;
|
|
3014
|
+
this.connector = init.connector;
|
|
3015
|
+
this.demuxerFactory = init.demuxer;
|
|
3016
|
+
this.readAction = init.readAction;
|
|
3017
|
+
this.options = resolveAudioStreamOptions(init.options);
|
|
3018
|
+
this.format = this.options.format;
|
|
3019
|
+
this.removeFromOwner = init.removeFromOwner;
|
|
3020
|
+
this.setupPromise = new Promise((resolve, reject) => (this.setupResolve = resolve, this.setupReject = reject));
|
|
3021
|
+
this.closed = new Promise((resolve) => this.closedResolve = resolve);
|
|
3022
|
+
this.options.signal?.addEventListener("abort", this.overallAbortListener, { once: true });
|
|
3023
|
+
}
|
|
3024
|
+
get state() {
|
|
3025
|
+
if (this.disposed)
|
|
3026
|
+
return "disposed";
|
|
3027
|
+
if (this.terminalError != null)
|
|
3028
|
+
return "errored";
|
|
3029
|
+
return this.nativeStats == null ? "initializing" : NativeAudioStreamStateNames[this.nativeStats.state] ?? "errored";
|
|
3030
|
+
}
|
|
3031
|
+
async open() {
|
|
3032
|
+
if (this.options.signal?.aborted)
|
|
3033
|
+
this.dispose();
|
|
3034
|
+
else
|
|
3035
|
+
this.runLifecycle();
|
|
3036
|
+
await this.setupPromise;
|
|
3037
|
+
if (this.lifecycleController.signal.aborted && this.state !== "ended")
|
|
3038
|
+
throw this.terminalError ?? createAbortError();
|
|
3039
|
+
this.exposed = true;
|
|
3040
|
+
if (this.pendingMetadataEvent && this.metadata != null)
|
|
3041
|
+
this.emitMetadata();
|
|
3042
|
+
this.pendingMetadataEvent = false;
|
|
3043
|
+
if (this.state === "ended")
|
|
3044
|
+
this.emitTerminal("ended");
|
|
3045
|
+
}
|
|
3046
|
+
getStats() {
|
|
3047
|
+
const stats = this.readNativeStats();
|
|
3048
|
+
if (!this.lifecycleController.signal.aborted && this.nativeStreamId != null) {
|
|
3049
|
+
const error = this.snapshotError(stats);
|
|
3050
|
+
const ended = stats?.state === NativeAudioStreamState.Ended && !this.options.reconnect?.retryOnEnd;
|
|
3051
|
+
if (error != null || ended) {
|
|
3052
|
+
queueMicrotask(() => {
|
|
3053
|
+
if (this.lifecycleController.signal.aborted)
|
|
3054
|
+
return;
|
|
3055
|
+
const reason = error == null || error.context.action === "decoder" ? NativeAudioStreamCloseReason.PreserveNativeTerminal : NativeAudioStreamCloseReason.TransportError;
|
|
3056
|
+
this.finish(reason, error ?? undefined);
|
|
3057
|
+
});
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
3060
|
+
return this.toPublicStats();
|
|
3061
|
+
}
|
|
3062
|
+
getMetadata() {
|
|
3063
|
+
return this.metadata;
|
|
3064
|
+
}
|
|
3065
|
+
setVolume(volume) {
|
|
3066
|
+
return this.control("setVolume", (streamId) => this.lib.audioSetStreamVolume(this.engine, streamId, volume));
|
|
3067
|
+
}
|
|
3068
|
+
setPan(pan) {
|
|
3069
|
+
return this.control("setPan", (streamId) => this.lib.audioSetStreamPan(this.engine, streamId, pan));
|
|
3070
|
+
}
|
|
3071
|
+
setGroup(groupId) {
|
|
3072
|
+
if (!isU32(groupId)) {
|
|
3073
|
+
const context = { action: "setGroup" };
|
|
3074
|
+
if (this.exposed) {
|
|
3075
|
+
this.emitAsync("error", new AudioStreamError("Invalid audio stream group", context), context);
|
|
3076
|
+
}
|
|
3077
|
+
return false;
|
|
3078
|
+
}
|
|
3079
|
+
return this.control("setGroup", (streamId) => this.lib.audioSetStreamGroup(this.engine, streamId, groupId));
|
|
3080
|
+
}
|
|
3081
|
+
control(action, call) {
|
|
3082
|
+
const streamId = this.nativeStreamId;
|
|
3083
|
+
if (this.disposed || this.lifecycleController.signal.aborted || streamId == null)
|
|
3084
|
+
return false;
|
|
3085
|
+
const status = call(streamId);
|
|
3086
|
+
if (status !== 0) {
|
|
3087
|
+
const context = { action, status };
|
|
3088
|
+
if (this.exposed) {
|
|
3089
|
+
this.emitAsync("error", new AudioStreamError(`Audio stream ${action} failed: ${status}`, context), context);
|
|
3090
|
+
}
|
|
3091
|
+
return false;
|
|
3092
|
+
}
|
|
3093
|
+
return true;
|
|
3094
|
+
}
|
|
3095
|
+
dispose() {
|
|
3096
|
+
if (this.disposed) {
|
|
3097
|
+
if (this.nativeStreamId != null && this.closeNativeStream(NativeAudioStreamCloseReason.Disposed) === 0)
|
|
3098
|
+
this.removeOwner();
|
|
3099
|
+
return;
|
|
3100
|
+
}
|
|
3101
|
+
this.disposed = true;
|
|
3102
|
+
const wasExposed = this.exposed;
|
|
3103
|
+
this.lifecycleController.abort();
|
|
3104
|
+
const cleanup = this.stopSource();
|
|
3105
|
+
this.setupReject(createAbortError());
|
|
3106
|
+
if (this.closeNativeStream(NativeAudioStreamCloseReason.Disposed) === 0)
|
|
3107
|
+
this.removeOwner();
|
|
3108
|
+
cleanup.finally(() => {
|
|
3109
|
+
if (wasExposed && !this.terminalEventScheduled)
|
|
3110
|
+
this.emitTerminal("disposed");
|
|
3111
|
+
else if (!this.terminalEventScheduled)
|
|
3112
|
+
this.closedResolve();
|
|
3113
|
+
});
|
|
3114
|
+
}
|
|
3115
|
+
async runLifecycle() {
|
|
3116
|
+
while (!this.lifecycleController.signal.aborted) {
|
|
3117
|
+
const attempt = {
|
|
3118
|
+
controller: new AbortController,
|
|
3119
|
+
body: null,
|
|
3120
|
+
closeConnection: null,
|
|
3121
|
+
reader: null,
|
|
3122
|
+
iterator: null,
|
|
3123
|
+
demuxer: null,
|
|
3124
|
+
demuxerFinished: false,
|
|
3125
|
+
sourceReleased: false,
|
|
3126
|
+
sourceAcquisitionAttempted: false,
|
|
3127
|
+
connectionClosed: false,
|
|
3128
|
+
demuxerAborted: false,
|
|
3129
|
+
resourceAcquisition: null,
|
|
3130
|
+
cleanupPromise: null
|
|
3131
|
+
};
|
|
3132
|
+
this.activeAttempt = attempt;
|
|
3133
|
+
let connection;
|
|
3134
|
+
try {
|
|
3135
|
+
let returnedConnection;
|
|
3136
|
+
const finishConnectionAcquisition = this.beginResourceAcquisition(attempt);
|
|
3137
|
+
try {
|
|
3138
|
+
returnedConnection = await this.connector.connect({
|
|
3139
|
+
signal: attempt.controller.signal,
|
|
3140
|
+
attempt: this.consecutiveReconnectAttempts
|
|
3141
|
+
});
|
|
3142
|
+
} finally {
|
|
3143
|
+
finishConnectionAcquisition();
|
|
3144
|
+
}
|
|
3145
|
+
connection = this.resolveConnection(returnedConnection, attempt);
|
|
3146
|
+
} catch (failure) {
|
|
3147
|
+
const active = this.isAttemptActive(attempt);
|
|
3148
|
+
await this.stopSource(attempt);
|
|
3149
|
+
if (!active)
|
|
3150
|
+
return;
|
|
3151
|
+
const context = {
|
|
3152
|
+
action: this.readAction,
|
|
3153
|
+
attempt: this.consecutiveReconnectAttempts
|
|
3154
|
+
};
|
|
3155
|
+
const error2 = failure instanceof AudioStreamError ? failure : new AudioStreamError(failure instanceof Error ? failure.message : "Audio stream connection failed", context, failure);
|
|
3156
|
+
if (await this.retry(error2, attempt, "connect"))
|
|
3157
|
+
continue;
|
|
3158
|
+
return;
|
|
3159
|
+
}
|
|
3160
|
+
if (!this.isAttemptActive(attempt)) {
|
|
3161
|
+
await this.stopSource(attempt);
|
|
3162
|
+
return;
|
|
3163
|
+
}
|
|
3164
|
+
if (!isReadableStreamSource(connection.body) && !isAsyncIterableSource(connection.body)) {
|
|
3165
|
+
await this.stopSource(attempt);
|
|
3166
|
+
const context = { action: "source" };
|
|
3167
|
+
await this.finish(NativeAudioStreamCloseReason.TransportError, new AudioStreamError("Audio stream connection body must be a ReadableStream or AsyncIterable", context));
|
|
3168
|
+
return;
|
|
3169
|
+
}
|
|
3170
|
+
if (!this.isAttemptActive(attempt)) {
|
|
3171
|
+
await this.stopSource(attempt);
|
|
3172
|
+
return;
|
|
3173
|
+
}
|
|
3174
|
+
let initialMetadata = null;
|
|
3175
|
+
let demuxerFailed = false;
|
|
3176
|
+
let demuxerFailure;
|
|
3177
|
+
const finishDemuxerAcquisition = this.beginResourceAcquisition(attempt);
|
|
3178
|
+
try {
|
|
3179
|
+
attempt.demuxer = this.demuxerFactory?.(connection.info) ?? null;
|
|
3180
|
+
initialMetadata = attempt.demuxer?.initialMetadata ?? null;
|
|
3181
|
+
} catch (cause) {
|
|
3182
|
+
demuxerFailed = true;
|
|
3183
|
+
demuxerFailure = cause;
|
|
3184
|
+
} finally {
|
|
3185
|
+
finishDemuxerAcquisition();
|
|
3186
|
+
}
|
|
3187
|
+
if (demuxerFailed) {
|
|
3188
|
+
await this.stopSource(attempt);
|
|
3189
|
+
const error2 = demuxerFailure instanceof AudioStreamError ? demuxerFailure : new AudioStreamError(demuxerFailure instanceof Error ? demuxerFailure.message : "Audio stream demuxer creation failed", { action: "demuxer" }, demuxerFailure);
|
|
3190
|
+
await this.finish(NativeAudioStreamCloseReason.TransportError, error2);
|
|
3191
|
+
return;
|
|
3192
|
+
}
|
|
3193
|
+
if (!this.isAttemptActive(attempt)) {
|
|
3194
|
+
await this.stopSource(attempt);
|
|
3195
|
+
return;
|
|
3196
|
+
}
|
|
3197
|
+
this.publishMetadata(initialMetadata, attempt);
|
|
3198
|
+
try {
|
|
3199
|
+
this.createNativeStream();
|
|
3200
|
+
} catch (cause) {
|
|
3201
|
+
await this.stopSource(attempt);
|
|
3202
|
+
const error2 = cause instanceof AudioStreamError ? cause : new AudioStreamError("Audio stream create failed", { action: "create" }, cause);
|
|
3203
|
+
await this.finish(NativeAudioStreamCloseReason.TransportError, error2);
|
|
3204
|
+
return;
|
|
3205
|
+
}
|
|
3206
|
+
try {
|
|
3207
|
+
if (!await this.consumeSource(connection, attempt))
|
|
3208
|
+
return;
|
|
3209
|
+
} catch (cause) {
|
|
3210
|
+
if (this.lifecycleController.signal.aborted)
|
|
3211
|
+
return;
|
|
3212
|
+
const error2 = cause instanceof AudioStreamError ? cause : cause instanceof TypeError && cause.message === INVALID_STREAM_CHUNK_MESSAGE ? cause : new AudioStreamError("Audio stream source failed", { action: "source" }, cause);
|
|
3213
|
+
if (error2 instanceof ClassifiedAudioStreamError) {
|
|
3214
|
+
if (await this.retry(error2, attempt, "read"))
|
|
3215
|
+
continue;
|
|
3216
|
+
return;
|
|
3217
|
+
}
|
|
3218
|
+
const context = error2 instanceof AudioStreamError ? error2.context : { action: "source" };
|
|
3219
|
+
await this.finish(NativeAudioStreamCloseReason.TransportError, error2, context);
|
|
3220
|
+
return;
|
|
3221
|
+
}
|
|
3222
|
+
if (!this.options.reconnect?.retryOnEnd) {
|
|
3223
|
+
await this.finish(NativeAudioStreamCloseReason.PreserveNativeTerminal);
|
|
3224
|
+
return;
|
|
3225
|
+
}
|
|
3226
|
+
const error = new AudioStreamError("Audio stream source ended", { action: "source" });
|
|
3227
|
+
if (await this.retry(error, attempt, undefined, true))
|
|
3228
|
+
continue;
|
|
3229
|
+
return;
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
createNativeStream() {
|
|
3233
|
+
if (this.nativeStreamId != null)
|
|
3234
|
+
return;
|
|
3235
|
+
const created = this.lib.audioCreateStream(this.engine, {
|
|
3236
|
+
capacityMs: this.options.capacityMs,
|
|
3237
|
+
startupMs: this.options.startupMs,
|
|
3238
|
+
resumeMs: this.options.resumeMs,
|
|
3239
|
+
maxProbeBytes: this.options.maxProbeBytes,
|
|
3240
|
+
volume: this.options.volume,
|
|
3241
|
+
pan: this.options.pan,
|
|
3242
|
+
groupId: this.options.groupId,
|
|
3243
|
+
format: toNativeAudioStreamFormat(this.options.format)
|
|
3244
|
+
});
|
|
3245
|
+
if (created.status !== 0 || created.streamId == null) {
|
|
3246
|
+
const context = { action: "create", status: created.status };
|
|
3247
|
+
throw new AudioStreamError(`Audio stream create failed: ${created.status}`, context);
|
|
3248
|
+
}
|
|
3249
|
+
this.nativeStreamId = created.streamId;
|
|
3250
|
+
}
|
|
3251
|
+
async consumeSource(connection, attempt) {
|
|
3252
|
+
const initial = await this.pollNativeSnapshot(attempt);
|
|
3253
|
+
if (initial == null || !this.isAttemptActive(attempt))
|
|
3254
|
+
return false;
|
|
3255
|
+
const decoderReady = this.awaitReady(attempt, initial.readyGeneration);
|
|
3256
|
+
try {
|
|
3257
|
+
await this.pumpSource(connection, attempt);
|
|
3258
|
+
} catch (cause) {
|
|
3259
|
+
this.observeReady(this.readNativeStats(), initial.readyGeneration);
|
|
3260
|
+
await this.stopSource(attempt);
|
|
3261
|
+
await decoderReady;
|
|
3262
|
+
if (!this.lifecycleController.signal.aborted)
|
|
3263
|
+
throw cause;
|
|
3264
|
+
return false;
|
|
3265
|
+
}
|
|
3266
|
+
if (this.lifecycleController.signal.aborted)
|
|
3267
|
+
return false;
|
|
3268
|
+
const status = this.lib.audioEndStream(this.engine, this.nativeStreamId);
|
|
3269
|
+
if (status !== 0) {
|
|
3270
|
+
const nativeError = this.snapshotError(this.readNativeStats());
|
|
3271
|
+
if (nativeError?.context.action === "decoder")
|
|
3272
|
+
throw nativeError;
|
|
3273
|
+
const context = { action: "end", status };
|
|
3274
|
+
throw new AudioStreamError(`Audio stream end failed: ${status}`, context);
|
|
3275
|
+
}
|
|
3276
|
+
if (!await decoderReady || this.lifecycleController.signal.aborted)
|
|
3277
|
+
return false;
|
|
3278
|
+
if (!await this.awaitEnded(attempt))
|
|
3279
|
+
return false;
|
|
3280
|
+
await this.stopSource(attempt);
|
|
3281
|
+
return !this.lifecycleController.signal.aborted;
|
|
3282
|
+
}
|
|
3283
|
+
async pumpSource(connection, attempt) {
|
|
3284
|
+
const source = connection.body;
|
|
3285
|
+
if (!this.isAttemptActive(attempt)) {
|
|
3286
|
+
await this.runBoundedAttemptCleanup(attempt);
|
|
3287
|
+
return;
|
|
3288
|
+
}
|
|
3289
|
+
const release = () => {
|
|
3290
|
+
if (attempt.sourceReleased)
|
|
3291
|
+
return;
|
|
3292
|
+
if (attempt.reader == null) {
|
|
3293
|
+
attempt.sourceReleased = true;
|
|
3294
|
+
return;
|
|
3295
|
+
}
|
|
3296
|
+
try {
|
|
3297
|
+
attempt.reader.releaseLock();
|
|
3298
|
+
attempt.sourceReleased = true;
|
|
3299
|
+
} catch {}
|
|
3300
|
+
};
|
|
3301
|
+
const next = () => attempt.reader == null ? attempt.iterator.next() : attempt.reader.read();
|
|
3302
|
+
attempt.sourceAcquisitionAttempted = true;
|
|
3303
|
+
const finishSourceAcquisition = this.beginResourceAcquisition(attempt);
|
|
3304
|
+
try {
|
|
3305
|
+
attempt.reader = isReadableStreamSource(source) ? source.getReader() : null;
|
|
3306
|
+
attempt.iterator = attempt.reader == null ? source[Symbol.asyncIterator]() : null;
|
|
3307
|
+
} catch (cause) {
|
|
3308
|
+
const context = { action: this.readAction };
|
|
3309
|
+
throw new ClassifiedAudioStreamError("Audio stream source failed", context, true, undefined, cause);
|
|
3310
|
+
} finally {
|
|
3311
|
+
finishSourceAcquisition();
|
|
3312
|
+
}
|
|
3313
|
+
if (!this.isAttemptActive(attempt)) {
|
|
3314
|
+
await this.runBoundedAttemptCleanup(attempt);
|
|
3315
|
+
return;
|
|
3316
|
+
}
|
|
3317
|
+
while (this.isAttemptActive(attempt)) {
|
|
3318
|
+
let result;
|
|
3319
|
+
try {
|
|
3320
|
+
result = await next();
|
|
3321
|
+
} catch (cause) {
|
|
3322
|
+
const context = { action: this.readAction };
|
|
3323
|
+
throw new ClassifiedAudioStreamError("Audio stream source failed", context, true, undefined, cause);
|
|
3324
|
+
}
|
|
3325
|
+
if (!this.isAttemptActive(attempt))
|
|
3326
|
+
return;
|
|
3327
|
+
if (result.done) {
|
|
3328
|
+
release();
|
|
3329
|
+
if (attempt.demuxer != null) {
|
|
3330
|
+
try {
|
|
3331
|
+
await this.processDemuxOutput(attempt.demuxer.flush(), attempt);
|
|
3332
|
+
} catch (cause) {
|
|
3333
|
+
if (cause instanceof AudioStreamError)
|
|
3334
|
+
throw cause;
|
|
3335
|
+
const context = { action: this.readAction };
|
|
3336
|
+
throw new ClassifiedAudioStreamError(cause instanceof Error ? cause.message : "Audio stream demuxer flush failed", context, true, undefined, cause);
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
attempt.demuxerFinished = true;
|
|
3340
|
+
await this.runBoundedAttemptCleanup(attempt);
|
|
3341
|
+
return;
|
|
3342
|
+
}
|
|
3343
|
+
const chunk = result.value;
|
|
3344
|
+
if (!isUint8Array(chunk))
|
|
3345
|
+
throw new TypeError(INVALID_STREAM_CHUNK_MESSAGE);
|
|
3346
|
+
if (chunk.byteLength === 0) {
|
|
3347
|
+
await waitForDelay(0, attempt.controller.signal);
|
|
3348
|
+
continue;
|
|
3349
|
+
}
|
|
3350
|
+
if (attempt.demuxer == null) {
|
|
3351
|
+
await this.writeStreamChunk(chunk, attempt);
|
|
3352
|
+
continue;
|
|
3353
|
+
}
|
|
3354
|
+
try {
|
|
3355
|
+
await this.processDemuxOutput(attempt.demuxer.push(chunk), attempt);
|
|
3356
|
+
} catch (cause) {
|
|
3357
|
+
if (cause instanceof AudioStreamError)
|
|
3358
|
+
throw cause;
|
|
3359
|
+
throw new AudioStreamError(cause instanceof Error ? cause.message : "Audio stream demuxer failed", { action: "demuxer" }, cause);
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
3362
|
+
}
|
|
3363
|
+
async processDemuxOutput(outputs, attempt) {
|
|
3364
|
+
for (const output of outputs) {
|
|
3365
|
+
if (!this.isAttemptActive(attempt))
|
|
3366
|
+
return;
|
|
3367
|
+
if (output.type === "audio") {
|
|
3368
|
+
if (!isUint8Array(output.data)) {
|
|
3369
|
+
throw new AudioStreamError("Audio stream demuxer audio output must be a Uint8Array", {
|
|
3370
|
+
action: "demuxer"
|
|
3371
|
+
});
|
|
3372
|
+
}
|
|
3373
|
+
await this.writeStreamChunk(output.data, attempt);
|
|
3374
|
+
} else if (output.type === "metadata") {
|
|
3375
|
+
this.publishMetadata(output.metadata, attempt);
|
|
3376
|
+
} else {
|
|
3377
|
+
throw new AudioStreamError("Audio stream demuxer returned an invalid output", { action: "demuxer" });
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
}
|
|
3381
|
+
async writeStreamChunk(chunk, attempt) {
|
|
3382
|
+
let offset = 0;
|
|
3383
|
+
while (offset < chunk.byteLength && this.isAttemptActive(attempt)) {
|
|
3384
|
+
const streamId = this.nativeStreamId;
|
|
3385
|
+
if (streamId == null)
|
|
3386
|
+
return;
|
|
3387
|
+
let accepted;
|
|
3388
|
+
try {
|
|
3389
|
+
accepted = this.lib.audioWriteStream(this.engine, streamId, chunk.subarray(offset));
|
|
3390
|
+
} catch (cause) {
|
|
3391
|
+
const context = { action: "write" };
|
|
3392
|
+
throw new AudioStreamError("Audio stream write failed", context, cause);
|
|
3393
|
+
}
|
|
3394
|
+
if (accepted < 0) {
|
|
3395
|
+
const context = { action: "write", status: accepted };
|
|
3396
|
+
throw new AudioStreamError(`Audio stream write failed: ${accepted}`, context);
|
|
3397
|
+
}
|
|
3398
|
+
if (accepted === 0) {
|
|
3399
|
+
if (await this.pollNativeSnapshot(attempt) == null)
|
|
3400
|
+
return;
|
|
3401
|
+
await waitForDelay(STREAM_POLL_INTERVAL_MS, attempt.controller.signal);
|
|
3402
|
+
continue;
|
|
3403
|
+
}
|
|
3404
|
+
offset += accepted;
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
resolveConnection(connection, attempt) {
|
|
3408
|
+
const finishAcquisition = this.beginResourceAcquisition(attempt);
|
|
3409
|
+
try {
|
|
3410
|
+
const close = connection.close;
|
|
3411
|
+
if (close !== undefined && typeof close !== "function") {
|
|
3412
|
+
throw new TypeError("Audio stream connection close must be a function");
|
|
3413
|
+
}
|
|
3414
|
+
attempt.closeConnection = close == null ? null : () => close.call(connection);
|
|
3415
|
+
const info = connection.info;
|
|
3416
|
+
const body = connection.body;
|
|
3417
|
+
attempt.body = body;
|
|
3418
|
+
return { body, info };
|
|
3419
|
+
} finally {
|
|
3420
|
+
finishAcquisition();
|
|
3421
|
+
}
|
|
3422
|
+
}
|
|
3423
|
+
beginResourceAcquisition(attempt) {
|
|
3424
|
+
let resolve;
|
|
3425
|
+
const acquisition = new Promise((done) => {
|
|
3426
|
+
resolve = done;
|
|
3427
|
+
});
|
|
3428
|
+
attempt.resourceAcquisition = acquisition;
|
|
3429
|
+
return () => {
|
|
3430
|
+
if (attempt.resourceAcquisition === acquisition)
|
|
3431
|
+
attempt.resourceAcquisition = null;
|
|
3432
|
+
resolve();
|
|
3433
|
+
};
|
|
3434
|
+
}
|
|
3435
|
+
cleanupAttempt(attempt) {
|
|
3436
|
+
if (attempt.cleanupPromise != null)
|
|
3437
|
+
return attempt.cleanupPromise;
|
|
3438
|
+
let resolveCleanup;
|
|
3439
|
+
let rejectCleanup;
|
|
3440
|
+
const cleanup = new Promise((resolve, reject) => {
|
|
3441
|
+
resolveCleanup = resolve;
|
|
3442
|
+
rejectCleanup = reject;
|
|
3443
|
+
});
|
|
3444
|
+
attempt.cleanupPromise = cleanup;
|
|
3445
|
+
this.performAttemptCleanup(attempt).then(resolveCleanup, rejectCleanup);
|
|
3446
|
+
const clearCleanup = () => {
|
|
3447
|
+
if (attempt.cleanupPromise === cleanup)
|
|
3448
|
+
attempt.cleanupPromise = null;
|
|
3449
|
+
};
|
|
3450
|
+
cleanup.then(clearCleanup, clearCleanup);
|
|
3451
|
+
return cleanup;
|
|
3452
|
+
}
|
|
3453
|
+
async performAttemptCleanup(attempt) {
|
|
3454
|
+
if (attempt.resourceAcquisition != null)
|
|
3455
|
+
await attempt.resourceAcquisition;
|
|
3456
|
+
const pending = [];
|
|
3457
|
+
const reason = createAbortError();
|
|
3458
|
+
if (!attempt.demuxerFinished && !attempt.demuxerAborted && attempt.demuxer != null) {
|
|
3459
|
+
attempt.demuxerAborted = true;
|
|
3460
|
+
try {
|
|
3461
|
+
attempt.demuxer.abort?.(reason);
|
|
3462
|
+
} catch {}
|
|
3463
|
+
}
|
|
3464
|
+
if (!attempt.sourceReleased) {
|
|
3465
|
+
if (attempt.reader != null) {
|
|
3466
|
+
attempt.sourceReleased = true;
|
|
3467
|
+
try {
|
|
3468
|
+
const result = attempt.reader.cancel(reason);
|
|
3469
|
+
try {
|
|
3470
|
+
attempt.reader.releaseLock();
|
|
3471
|
+
} catch {}
|
|
3472
|
+
pending.push(result);
|
|
3473
|
+
} catch {}
|
|
3474
|
+
} else if (attempt.iterator != null) {
|
|
3475
|
+
attempt.sourceReleased = true;
|
|
3476
|
+
try {
|
|
3477
|
+
pending.push(Promise.resolve(attempt.iterator.return?.()));
|
|
3478
|
+
} catch {}
|
|
3479
|
+
} else if (attempt.body != null && !attempt.sourceAcquisitionAttempted) {
|
|
3480
|
+
attempt.sourceReleased = true;
|
|
3481
|
+
try {
|
|
3482
|
+
if (isReadableStreamSource(attempt.body)) {
|
|
3483
|
+
pending.push(attempt.body.cancel(reason));
|
|
3484
|
+
} else if (isAsyncIterableSource(attempt.body)) {
|
|
3485
|
+
attempt.iterator = attempt.body[Symbol.asyncIterator]();
|
|
3486
|
+
pending.push(Promise.resolve(attempt.iterator.return?.()));
|
|
3487
|
+
}
|
|
3488
|
+
} catch {}
|
|
3489
|
+
}
|
|
3490
|
+
}
|
|
3491
|
+
if (!attempt.connectionClosed && attempt.closeConnection != null) {
|
|
3492
|
+
attempt.connectionClosed = true;
|
|
3493
|
+
try {
|
|
3494
|
+
pending.push(Promise.resolve(attempt.closeConnection()));
|
|
3495
|
+
} catch {}
|
|
3496
|
+
}
|
|
3497
|
+
await Promise.allSettled(pending);
|
|
3498
|
+
}
|
|
3499
|
+
runBoundedAttemptCleanup(attempt) {
|
|
3500
|
+
if (this.pendingCleanup != null)
|
|
3501
|
+
return this.pendingCleanup;
|
|
3502
|
+
let resolveCleanup;
|
|
3503
|
+
let rejectCleanup;
|
|
3504
|
+
const pendingCleanup = new Promise((resolve, reject) => {
|
|
3505
|
+
resolveCleanup = resolve;
|
|
3506
|
+
rejectCleanup = reject;
|
|
3507
|
+
});
|
|
3508
|
+
this.pendingCleanup = pendingCleanup;
|
|
3509
|
+
const cleanup = runBoundedCleanup(() => this.cleanupAttempt(attempt));
|
|
3510
|
+
cleanup.then(resolveCleanup, rejectCleanup);
|
|
3511
|
+
const clearCleanup = () => {
|
|
3512
|
+
if (this.pendingCleanup === pendingCleanup)
|
|
3513
|
+
this.pendingCleanup = null;
|
|
3514
|
+
};
|
|
3515
|
+
pendingCleanup.then(clearCleanup, clearCleanup);
|
|
3516
|
+
return pendingCleanup;
|
|
3517
|
+
}
|
|
3518
|
+
async retry(error, attempt, phase, cleanEnd = false) {
|
|
3519
|
+
if (this.lifecycleController.signal.aborted)
|
|
3520
|
+
return false;
|
|
3521
|
+
const reconnect = this.options.reconnect;
|
|
3522
|
+
const retryable = phase == null || !(error instanceof ClassifiedAudioStreamError) || error.retryable;
|
|
3523
|
+
if (reconnect == null || this.consecutiveReconnectAttempts >= reconnect.maxRetries) {
|
|
3524
|
+
if (cleanEnd)
|
|
3525
|
+
await this.finish(NativeAudioStreamCloseReason.PreserveNativeTerminal);
|
|
3526
|
+
else
|
|
3527
|
+
await this.finish(NativeAudioStreamCloseReason.TransportError, error);
|
|
3528
|
+
return false;
|
|
3529
|
+
}
|
|
3530
|
+
let retryDelayMs = error instanceof ClassifiedAudioStreamError ? error.retryAfterMs : undefined;
|
|
3531
|
+
if (phase != null && reconnect.retry != null) {
|
|
3532
|
+
let decision;
|
|
3533
|
+
try {
|
|
3534
|
+
decision = reconnect.retry(error, {
|
|
3535
|
+
attempt: this.consecutiveReconnectAttempts + 1,
|
|
3536
|
+
maxRetries: reconnect.maxRetries,
|
|
3537
|
+
phase
|
|
3538
|
+
});
|
|
3539
|
+
if (decision !== false && (decision == null || typeof decision !== "object")) {
|
|
3540
|
+
throw new TypeError("Audio stream retry policy must return false or a retry decision");
|
|
3541
|
+
}
|
|
3542
|
+
} catch (cause) {
|
|
3543
|
+
await this.finish(NativeAudioStreamCloseReason.TransportError, new AudioStreamError("Audio stream retry policy failed", { action: "source" }, cause));
|
|
3544
|
+
return false;
|
|
3545
|
+
}
|
|
3546
|
+
if (decision === false) {
|
|
3547
|
+
await this.finish(NativeAudioStreamCloseReason.TransportError, error);
|
|
3548
|
+
return false;
|
|
3549
|
+
}
|
|
3550
|
+
if (this.lifecycleController.signal.aborted)
|
|
3551
|
+
return false;
|
|
3552
|
+
let policyDelayMs;
|
|
3553
|
+
try {
|
|
3554
|
+
policyDelayMs = decision.delayMs;
|
|
3555
|
+
} catch (cause) {
|
|
3556
|
+
await this.finish(NativeAudioStreamCloseReason.TransportError, new AudioStreamError("Audio stream retry policy failed", { action: "source" }, cause));
|
|
3557
|
+
return false;
|
|
3558
|
+
}
|
|
3559
|
+
if (policyDelayMs !== undefined && (!Number.isFinite(policyDelayMs) || !Number.isInteger(policyDelayMs) || policyDelayMs < 0)) {
|
|
3560
|
+
await this.finish(NativeAudioStreamCloseReason.TransportError, new AudioStreamError("Audio stream retry delay must be a finite non-negative integer", {
|
|
3561
|
+
action: "source"
|
|
3562
|
+
}));
|
|
3563
|
+
return false;
|
|
3564
|
+
}
|
|
3565
|
+
if (policyDelayMs !== undefined)
|
|
3566
|
+
retryDelayMs = Math.min(reconnect.maxDelayMs, policyDelayMs);
|
|
3567
|
+
} else if (!retryable) {
|
|
3568
|
+
await this.finish(NativeAudioStreamCloseReason.TransportError, error);
|
|
3569
|
+
return false;
|
|
3570
|
+
}
|
|
3571
|
+
if (retryDelayMs !== undefined)
|
|
3572
|
+
retryDelayMs = Math.min(reconnect.maxDelayMs, retryDelayMs);
|
|
3573
|
+
await this.cleanupAttempt(attempt);
|
|
3574
|
+
if (this.lifecycleController.signal.aborted)
|
|
3575
|
+
return false;
|
|
3576
|
+
if (this.nativeStreamId != null) {
|
|
3577
|
+
const nativeError = this.snapshotError(this.readNativeStats());
|
|
3578
|
+
if (nativeError != null) {
|
|
3579
|
+
const reason = nativeError.context.action === "decoder" ? NativeAudioStreamCloseReason.PreserveNativeTerminal : NativeAudioStreamCloseReason.TransportError;
|
|
3580
|
+
await this.finish(reason, nativeError);
|
|
3581
|
+
return false;
|
|
3582
|
+
}
|
|
3583
|
+
const restartStatus = this.lib.audioRestartStream(this.engine, this.nativeStreamId);
|
|
3584
|
+
if (restartStatus !== 0) {
|
|
3585
|
+
const restartContext = { action: "restart", status: restartStatus };
|
|
3586
|
+
await this.finish(NativeAudioStreamCloseReason.TransportError, new AudioStreamError("Audio stream restart failed during reconnect", restartContext));
|
|
3587
|
+
return false;
|
|
3588
|
+
}
|
|
3589
|
+
this.readNativeStats();
|
|
3590
|
+
}
|
|
3591
|
+
this.reconnectAttempts += 1;
|
|
3592
|
+
this.consecutiveReconnectAttempts += 1;
|
|
3593
|
+
const delayMs = retryDelayMs ?? Math.min(reconnect.maxDelayMs, reconnect.initialDelayMs * reconnect.backoffFactor ** (this.consecutiveReconnectAttempts - 1));
|
|
3594
|
+
if (this.exposed) {
|
|
3595
|
+
this.emitAsync("reconnecting", {
|
|
3596
|
+
attempt: this.consecutiveReconnectAttempts,
|
|
3597
|
+
delayMs,
|
|
3598
|
+
maxRetries: reconnect.maxRetries,
|
|
3599
|
+
error
|
|
3600
|
+
});
|
|
3601
|
+
}
|
|
3602
|
+
return waitForDelay(delayMs, this.lifecycleController.signal).then(() => true).catch(() => false);
|
|
3603
|
+
}
|
|
3604
|
+
async awaitReady(attempt, previousGeneration) {
|
|
3605
|
+
while (this.isAttemptActive(attempt)) {
|
|
3606
|
+
const stats = await this.pollNativeSnapshot(attempt);
|
|
3607
|
+
if (stats == null)
|
|
3608
|
+
return false;
|
|
3609
|
+
if (this.observeReady(stats, previousGeneration))
|
|
3610
|
+
return true;
|
|
3611
|
+
if (!await waitForPoll(attempt.controller.signal))
|
|
3612
|
+
return false;
|
|
3613
|
+
}
|
|
3614
|
+
return false;
|
|
3615
|
+
}
|
|
3616
|
+
observeReady(stats, previousGeneration) {
|
|
3617
|
+
if (stats == null || stats.readyGeneration === previousGeneration)
|
|
3618
|
+
return false;
|
|
3619
|
+
this.consecutiveReconnectAttempts = 0;
|
|
3620
|
+
this.setupResolve();
|
|
3621
|
+
return true;
|
|
3622
|
+
}
|
|
3623
|
+
async awaitEnded(attempt) {
|
|
3624
|
+
while (this.isAttemptActive(attempt)) {
|
|
3625
|
+
const stats = await this.pollNativeSnapshot(attempt);
|
|
3626
|
+
if (stats == null)
|
|
3627
|
+
return false;
|
|
3628
|
+
if (stats.state === NativeAudioStreamState.Ended)
|
|
3629
|
+
return true;
|
|
3630
|
+
if (!await waitForPoll(attempt.controller.signal))
|
|
3631
|
+
return false;
|
|
3632
|
+
}
|
|
3633
|
+
return false;
|
|
3634
|
+
}
|
|
3635
|
+
async pollNativeSnapshot(attempt) {
|
|
3636
|
+
if (!this.isAttemptActive(attempt))
|
|
3637
|
+
return null;
|
|
3638
|
+
const stats = this.readNativeStats();
|
|
3639
|
+
const error = this.snapshotError(stats);
|
|
3640
|
+
if (error != null) {
|
|
3641
|
+
const reason = error.context.action === "decoder" ? NativeAudioStreamCloseReason.PreserveNativeTerminal : NativeAudioStreamCloseReason.TransportError;
|
|
3642
|
+
await this.finish(reason, error);
|
|
3643
|
+
return null;
|
|
3644
|
+
}
|
|
3645
|
+
return stats;
|
|
3646
|
+
}
|
|
3647
|
+
snapshotError(stats) {
|
|
3648
|
+
if (stats == null)
|
|
3649
|
+
return new AudioStreamError("Audio stream stats failed", { action: "stats" });
|
|
3650
|
+
if (NativeAudioStreamStateNames[stats.state] == null) {
|
|
3651
|
+
return new AudioStreamError(`Unknown native audio stream state: ${stats.state}`, { action: "stats" });
|
|
3652
|
+
}
|
|
3653
|
+
if (stats.state !== NativeAudioStreamState.Failed && stats.state !== NativeAudioStreamState.Cancelled)
|
|
3654
|
+
return null;
|
|
3655
|
+
const context = { action: "decoder", errorCode: stats.errorCode };
|
|
3656
|
+
return new AudioStreamError(stats.state === NativeAudioStreamState.Failed ? `Audio stream decoder failed: ${stats.errorCode}` : "Audio stream was cancelled by the decoder", context);
|
|
3657
|
+
}
|
|
3658
|
+
async finish(reason, error, context) {
|
|
3659
|
+
if (this.lifecycleController.signal.aborted)
|
|
3660
|
+
return;
|
|
3661
|
+
if (error instanceof AudioStreamError)
|
|
3662
|
+
context = error.context;
|
|
3663
|
+
this.lifecycleController.abort();
|
|
3664
|
+
this.terminalError = error ?? null;
|
|
3665
|
+
const cleanup = this.stopSource();
|
|
3666
|
+
const closeStatus = this.closeNativeStream(reason);
|
|
3667
|
+
if (error == null && closeStatus !== 0) {
|
|
3668
|
+
context = { action: "destroy", status: closeStatus };
|
|
3669
|
+
error = new AudioStreamError("Audio stream destroy failed after end", context);
|
|
3670
|
+
this.terminalError = error;
|
|
3671
|
+
}
|
|
3672
|
+
await cleanup;
|
|
3673
|
+
if (error != null)
|
|
3674
|
+
this.setupReject(error);
|
|
3675
|
+
else
|
|
3676
|
+
this.setupResolve();
|
|
3677
|
+
if (closeStatus === 0)
|
|
3678
|
+
this.removeOwner();
|
|
3679
|
+
if (!this.disposed && this.exposed) {
|
|
3680
|
+
if (error != null)
|
|
3681
|
+
this.emitTerminal("error", error, context);
|
|
3682
|
+
else
|
|
3683
|
+
this.emitTerminal("ended");
|
|
3684
|
+
} else if (error != null && !this.disposed)
|
|
3685
|
+
this.closedResolve();
|
|
3686
|
+
}
|
|
3687
|
+
publishMetadata(metadata, attempt) {
|
|
3688
|
+
if (!this.isAttemptActive(attempt) || Object.is(this.metadata, metadata))
|
|
3689
|
+
return;
|
|
3690
|
+
this.metadata = metadata;
|
|
3691
|
+
if (!this.exposed) {
|
|
3692
|
+
this.pendingMetadataEvent = true;
|
|
3693
|
+
return;
|
|
3694
|
+
}
|
|
3695
|
+
this.emitMetadata();
|
|
3696
|
+
}
|
|
3697
|
+
emitMetadata() {
|
|
3698
|
+
if (this.metadataEventScheduled)
|
|
3699
|
+
return;
|
|
3700
|
+
this.metadataEventScheduled = true;
|
|
3701
|
+
setTimeout(() => {
|
|
3702
|
+
this.metadataEventScheduled = false;
|
|
3703
|
+
if (!this.disposed)
|
|
3704
|
+
EventEmitter.prototype.emit.call(this, "metadata", this.metadata);
|
|
3705
|
+
}, 0);
|
|
3706
|
+
}
|
|
3707
|
+
emitAsync(event, ...args) {
|
|
3708
|
+
setTimeout(() => EventEmitter.prototype.emit.call(this, event, ...args), 0);
|
|
3709
|
+
}
|
|
3710
|
+
emitTerminal(event, ...args) {
|
|
3711
|
+
if (this.terminalEventScheduled)
|
|
3712
|
+
return;
|
|
3713
|
+
this.terminalEventScheduled = true;
|
|
3714
|
+
setTimeout(() => {
|
|
3715
|
+
try {
|
|
3716
|
+
EventEmitter.prototype.emit.call(this, event, ...args);
|
|
3717
|
+
} finally {
|
|
3718
|
+
this.closedResolve();
|
|
3719
|
+
}
|
|
3720
|
+
}, 0);
|
|
3721
|
+
}
|
|
3722
|
+
isAttemptActive(attempt) {
|
|
3723
|
+
return !this.lifecycleController.signal.aborted && this.activeAttempt === attempt;
|
|
3724
|
+
}
|
|
3725
|
+
stopSource(attempt = this.activeAttempt) {
|
|
3726
|
+
if (attempt == null)
|
|
3727
|
+
return this.pendingCleanup ?? Promise.resolve();
|
|
3728
|
+
if (this.activeAttempt === attempt)
|
|
3729
|
+
this.activeAttempt = null;
|
|
3730
|
+
attempt.controller.abort();
|
|
3731
|
+
return this.runBoundedAttemptCleanup(attempt);
|
|
3732
|
+
}
|
|
3733
|
+
closeNativeStream(reason) {
|
|
3734
|
+
const streamId = this.nativeStreamId;
|
|
3735
|
+
if (streamId == null)
|
|
3736
|
+
return 0;
|
|
3737
|
+
const result = this.lib.audioCloseStream(this.engine, streamId, reason);
|
|
3738
|
+
if (result.status !== 0 || result.stats == null)
|
|
3739
|
+
return result.status === 0 ? -1 : result.status;
|
|
3740
|
+
this.nativeStats = result.stats;
|
|
3741
|
+
this.nativeStreamId = null;
|
|
3742
|
+
return 0;
|
|
3743
|
+
}
|
|
3744
|
+
readNativeStats() {
|
|
3745
|
+
if (this.nativeStreamId == null)
|
|
3746
|
+
return this.nativeStats;
|
|
3747
|
+
const stats = this.lib.audioGetStreamStats(this.engine, this.nativeStreamId);
|
|
3748
|
+
if (stats != null)
|
|
3749
|
+
this.nativeStats = stats;
|
|
3750
|
+
return stats;
|
|
3751
|
+
}
|
|
3752
|
+
toPublicStats() {
|
|
3753
|
+
const stats = this.nativeStats;
|
|
3754
|
+
const sampleRate = stats?.sampleRate ?? 0;
|
|
3755
|
+
const bufferedFrames = stats?.bufferedFrames ?? 0;
|
|
3756
|
+
return {
|
|
3757
|
+
state: this.state,
|
|
3758
|
+
sampleRate,
|
|
3759
|
+
channels: stats?.channels ?? 0,
|
|
3760
|
+
bufferedFrames,
|
|
3761
|
+
capacityFrames: stats?.capacityFrames ?? 0,
|
|
3762
|
+
bufferedDurationMs: sampleRate === 0 ? 0 : bufferedFrames * 1000 / sampleRate,
|
|
3763
|
+
bytesReceived: stats?.bytesReceived ?? 0n,
|
|
3764
|
+
framesDecoded: stats?.framesDecoded ?? 0n,
|
|
3765
|
+
framesPlayed: stats?.framesPlayed ?? 0n,
|
|
3766
|
+
underruns: stats?.underruns ?? 0,
|
|
3767
|
+
reconnectAttempts: this.reconnectAttempts
|
|
3768
|
+
};
|
|
3769
|
+
}
|
|
3770
|
+
removeOwner() {
|
|
3771
|
+
this.options.signal?.removeEventListener("abort", this.overallAbortListener);
|
|
3772
|
+
this.removeFromOwner();
|
|
3773
|
+
}
|
|
3774
|
+
}
|
|
2534
3775
|
|
|
2535
3776
|
class Audio extends EventEmitter {
|
|
2536
3777
|
static create(options = {}) {
|
|
2537
|
-
|
|
3778
|
+
let lib;
|
|
3779
|
+
try {
|
|
3780
|
+
lib = resolveRenderLib();
|
|
3781
|
+
} catch (cause) {
|
|
3782
|
+
throw new AudioInitializationError("resolveRenderLib", "Failed to resolve the native audio library", undefined, cause);
|
|
3783
|
+
}
|
|
3784
|
+
return new Audio(lib, options);
|
|
2538
3785
|
}
|
|
3786
|
+
sampleRate;
|
|
2539
3787
|
lib;
|
|
2540
3788
|
defaultStartOptions;
|
|
2541
3789
|
engine = null;
|
|
2542
3790
|
groups = new Map;
|
|
3791
|
+
streams = new Set;
|
|
2543
3792
|
playbackStarted = false;
|
|
2544
3793
|
mixerStarted = false;
|
|
3794
|
+
disposing = false;
|
|
2545
3795
|
constructor(lib, options) {
|
|
2546
3796
|
super();
|
|
2547
3797
|
this.lib = lib;
|
|
2548
3798
|
this.defaultStartOptions = options.startOptions;
|
|
3799
|
+
const normalizedSampleRate = options.sampleRate == null || !Number.isFinite(options.sampleRate) ? 0 : Math.min(MAX_U32, Math.max(0, Math.trunc(options.sampleRate)));
|
|
3800
|
+
this.sampleRate = normalizedSampleRate || DEFAULT_AUDIO_SAMPLE_RATE;
|
|
2549
3801
|
const createOptions = options.sampleRate == null && options.playbackChannels == null ? undefined : {
|
|
2550
|
-
sampleRate: options.sampleRate == null ? undefined :
|
|
3802
|
+
sampleRate: options.sampleRate == null ? undefined : normalizedSampleRate,
|
|
2551
3803
|
playbackChannels: options.playbackChannels == null ? undefined : Math.max(0, Math.trunc(options.playbackChannels))
|
|
2552
3804
|
};
|
|
2553
3805
|
this.engine = this.lib.createAudioEngine(createOptions);
|
|
2554
3806
|
if (!this.engine) {
|
|
2555
|
-
|
|
2556
|
-
return;
|
|
3807
|
+
throw new AudioInitializationError("createAudioEngine", "Audio createAudioEngine returned null");
|
|
2557
3808
|
}
|
|
2558
3809
|
if (options.autoStart ?? false) {
|
|
2559
|
-
this.
|
|
3810
|
+
const status = this.lib.audioStart(this.engine, this.defaultStartOptions);
|
|
3811
|
+
if (status !== 0) {
|
|
3812
|
+
this.throwAfterInitializationCleanup(new AudioInitializationError("start", `Audio auto-start failed: ${status}`, status));
|
|
3813
|
+
}
|
|
3814
|
+
this.playbackStarted = true;
|
|
3815
|
+
this.mixerStarted = true;
|
|
2560
3816
|
}
|
|
2561
3817
|
}
|
|
3818
|
+
throwAfterInitializationCleanup(error) {
|
|
3819
|
+
const engine2 = this.engine;
|
|
3820
|
+
this.engine = null;
|
|
3821
|
+
if (engine2)
|
|
3822
|
+
this.lib.destroyAudioEngine(engine2);
|
|
3823
|
+
throw error;
|
|
3824
|
+
}
|
|
2562
3825
|
emitError(action, status, message, cause) {
|
|
2563
3826
|
const error = message ? new Error(message) : statusToError(action, status ?? -1);
|
|
2564
3827
|
if (cause)
|
|
@@ -2697,6 +3960,74 @@ class Audio extends EventEmitter {
|
|
|
2697
3960
|
}
|
|
2698
3961
|
return result.voiceId;
|
|
2699
3962
|
}
|
|
3963
|
+
async playStream(source, options = {}) {
|
|
3964
|
+
const urlOptions = options;
|
|
3965
|
+
if (urlOptions.request !== undefined || urlOptions.reconnect !== undefined || urlOptions.metadataEncoding !== undefined || urlOptions.contentTypePolicy !== undefined) {
|
|
3966
|
+
return Promise.reject(new TypeError("request, reconnect, metadataEncoding, and contentTypePolicy options are only supported by playStreamUrl()"));
|
|
3967
|
+
}
|
|
3968
|
+
if (!isReadableStreamSource(source) && !isAsyncIterableSource(source)) {
|
|
3969
|
+
return Promise.reject(new TypeError("Audio stream source must be a ReadableStream or AsyncIterable"));
|
|
3970
|
+
}
|
|
3971
|
+
const { demuxer, ...streamOptions } = options;
|
|
3972
|
+
const connector = {
|
|
3973
|
+
async connect() {
|
|
3974
|
+
return { body: source, info: undefined };
|
|
3975
|
+
}
|
|
3976
|
+
};
|
|
3977
|
+
return this.openStream(connector, demuxer == null ? undefined : () => demuxer(), streamOptions, "source");
|
|
3978
|
+
}
|
|
3979
|
+
async playStreamUrl(source, options = {}) {
|
|
3980
|
+
if (options.demuxer !== undefined) {
|
|
3981
|
+
throw new TypeError("demuxer is only supported by playStream() and playStreamSource()");
|
|
3982
|
+
}
|
|
3983
|
+
if (typeof source !== "string" && Object.prototype.toString.call(source) !== "[object URL]") {
|
|
3984
|
+
return Promise.reject(new TypeError("Audio stream URL source must be a string or URL"));
|
|
3985
|
+
}
|
|
3986
|
+
const { request, metadataEncoding, reconnect, format, contentTypePolicy, ...streamOptions } = options;
|
|
3987
|
+
const resolvedFormat = resolveAudioStreamFormat(format);
|
|
3988
|
+
const resolvedContentTypePolicy = resolveContentTypePolicy(contentTypePolicy, options);
|
|
3989
|
+
const encoding = resolveMetadataEncoding(metadataEncoding);
|
|
3990
|
+
const connector = createAudioStreamUrlConnector(source, resolveAudioStreamRequest(request), resolvedFormat, resolvedContentTypePolicy);
|
|
3991
|
+
return this.openStream(connector, (info) => {
|
|
3992
|
+
try {
|
|
3993
|
+
return selectAudioStreamDemuxer({ headers: info.headers, metadataEncoding: encoding });
|
|
3994
|
+
} catch (cause) {
|
|
3995
|
+
throw new AudioStreamError(cause instanceof Error ? cause.message : "Invalid audio stream metadata response", { action: "response", status: info.status }, cause);
|
|
3996
|
+
}
|
|
3997
|
+
}, { ...streamOptions, reconnect, format: resolvedFormat }, "fetch");
|
|
3998
|
+
}
|
|
3999
|
+
async playStreamSource(connector, options = {}) {
|
|
4000
|
+
const urlOptions = options;
|
|
4001
|
+
if (urlOptions.request !== undefined || urlOptions.metadataEncoding !== undefined || urlOptions.contentTypePolicy !== undefined) {
|
|
4002
|
+
return Promise.reject(new TypeError("request, metadataEncoding, and contentTypePolicy are only supported by playStreamUrl()"));
|
|
4003
|
+
}
|
|
4004
|
+
const { demuxer, ...streamOptions } = options;
|
|
4005
|
+
return this.openStream(connector, demuxer, streamOptions, "source");
|
|
4006
|
+
}
|
|
4007
|
+
async openStream(connector, demuxer, options, readAction) {
|
|
4008
|
+
const engine2 = this.engine;
|
|
4009
|
+
if (!engine2)
|
|
4010
|
+
throw new Error("Audio engine unavailable during stream playback");
|
|
4011
|
+
const resolvedConnector = resolveAudioStreamConnector(connector);
|
|
4012
|
+
let stream;
|
|
4013
|
+
stream = createAudioStream({
|
|
4014
|
+
lib: this.lib,
|
|
4015
|
+
engine: engine2,
|
|
4016
|
+
connector: resolvedConnector,
|
|
4017
|
+
demuxer,
|
|
4018
|
+
options,
|
|
4019
|
+
readAction,
|
|
4020
|
+
removeFromOwner: () => this.streams.delete(stream)
|
|
4021
|
+
});
|
|
4022
|
+
this.streams.add(stream);
|
|
4023
|
+
try {
|
|
4024
|
+
await openAudioStream(stream);
|
|
4025
|
+
return stream;
|
|
4026
|
+
} catch (error) {
|
|
4027
|
+
stream.dispose();
|
|
4028
|
+
throw error;
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
2700
4031
|
stopVoice(voice) {
|
|
2701
4032
|
const engine2 = this.engine;
|
|
2702
4033
|
if (!engine2) {
|
|
@@ -2864,15 +4195,22 @@ class Audio extends EventEmitter {
|
|
|
2864
4195
|
return stats;
|
|
2865
4196
|
}
|
|
2866
4197
|
dispose() {
|
|
2867
|
-
if (!this.engine)
|
|
4198
|
+
if (!this.engine || this.disposing)
|
|
2868
4199
|
return;
|
|
2869
|
-
|
|
2870
|
-
|
|
4200
|
+
this.disposing = true;
|
|
4201
|
+
try {
|
|
4202
|
+
for (const stream of [...this.streams])
|
|
4203
|
+
stream.dispose();
|
|
4204
|
+
if (this.mixerStarted) {
|
|
4205
|
+
this.stop();
|
|
4206
|
+
}
|
|
4207
|
+
this.groups.clear();
|
|
4208
|
+
this.lib.destroyAudioEngine(this.engine);
|
|
4209
|
+
this.engine = null;
|
|
4210
|
+
this.emit("disposed");
|
|
4211
|
+
} finally {
|
|
4212
|
+
this.disposing = false;
|
|
2871
4213
|
}
|
|
2872
|
-
this.groups.clear();
|
|
2873
|
-
this.lib.destroyAudioEngine(this.engine);
|
|
2874
|
-
this.engine = null;
|
|
2875
|
-
this.emit("disposed");
|
|
2876
4214
|
}
|
|
2877
4215
|
}
|
|
2878
4216
|
function setupAudio(options = {}) {
|
|
@@ -3435,10 +4773,10 @@ class LineNumberRenderable extends Renderable {
|
|
|
3435
4773
|
return;
|
|
3436
4774
|
if (this.target) {
|
|
3437
4775
|
this.target.off("line-info-change", this.handleLineInfoChange);
|
|
3438
|
-
super.remove(this.target
|
|
4776
|
+
super.remove(this.target);
|
|
3439
4777
|
}
|
|
3440
4778
|
if (this.gutter) {
|
|
3441
|
-
super.remove(this.gutter
|
|
4779
|
+
super.remove(this.gutter);
|
|
3442
4780
|
this.gutter = null;
|
|
3443
4781
|
}
|
|
3444
4782
|
this.target = target;
|
|
@@ -3467,18 +4805,18 @@ class LineNumberRenderable extends Renderable {
|
|
|
3467
4805
|
}
|
|
3468
4806
|
return -1;
|
|
3469
4807
|
}
|
|
3470
|
-
remove(
|
|
4808
|
+
remove(child) {
|
|
3471
4809
|
if (this._isDestroying) {
|
|
3472
|
-
super.remove(
|
|
4810
|
+
super.remove(child);
|
|
3473
4811
|
return;
|
|
3474
4812
|
}
|
|
3475
|
-
if (this.gutter &&
|
|
4813
|
+
if (this.gutter && child === this.gutter) {
|
|
3476
4814
|
throw new Error("LineNumberRenderable: Cannot remove gutter directly.");
|
|
3477
4815
|
}
|
|
3478
|
-
if (this.target &&
|
|
4816
|
+
if (this.target && child === this.target) {
|
|
3479
4817
|
throw new Error("LineNumberRenderable: Cannot remove target directly. Use clearTarget() instead.");
|
|
3480
4818
|
}
|
|
3481
|
-
super.remove(
|
|
4819
|
+
super.remove(child);
|
|
3482
4820
|
}
|
|
3483
4821
|
destroyRecursively() {
|
|
3484
4822
|
this._isDestroying = true;
|
|
@@ -3492,11 +4830,11 @@ class LineNumberRenderable extends Renderable {
|
|
|
3492
4830
|
clearTarget() {
|
|
3493
4831
|
if (this.target) {
|
|
3494
4832
|
this.target.off("line-info-change", this.handleLineInfoChange);
|
|
3495
|
-
super.remove(this.target
|
|
4833
|
+
super.remove(this.target);
|
|
3496
4834
|
this.target = null;
|
|
3497
4835
|
}
|
|
3498
4836
|
if (this.gutter) {
|
|
3499
|
-
super.remove(this.gutter
|
|
4837
|
+
super.remove(this.gutter);
|
|
3500
4838
|
this.gutter = null;
|
|
3501
4839
|
}
|
|
3502
4840
|
}
|
|
@@ -4209,11 +5547,11 @@ class DiffRenderable extends Renderable {
|
|
|
4209
5547
|
buildErrorView() {
|
|
4210
5548
|
this.flexDirection = "column";
|
|
4211
5549
|
if (this.leftSide && this.leftSideAdded) {
|
|
4212
|
-
super.remove(this.leftSide
|
|
5550
|
+
super.remove(this.leftSide);
|
|
4213
5551
|
this.leftSideAdded = false;
|
|
4214
5552
|
}
|
|
4215
5553
|
if (this.rightSide && this.rightSideAdded) {
|
|
4216
|
-
super.remove(this.rightSide
|
|
5554
|
+
super.remove(this.rightSide);
|
|
4217
5555
|
this.rightSideAdded = false;
|
|
4218
5556
|
}
|
|
4219
5557
|
const errorMessage = `Error parsing diff: ${this._parseError?.message || "Unknown error"}
|
|
@@ -4361,13 +5699,13 @@ class DiffRenderable extends Renderable {
|
|
|
4361
5699
|
if (this.errorTextRenderable) {
|
|
4362
5700
|
const errorTextIndex = this.getChildren().indexOf(this.errorTextRenderable);
|
|
4363
5701
|
if (errorTextIndex !== -1) {
|
|
4364
|
-
super.remove(this.errorTextRenderable
|
|
5702
|
+
super.remove(this.errorTextRenderable);
|
|
4365
5703
|
}
|
|
4366
5704
|
}
|
|
4367
5705
|
if (this.errorCodeRenderable) {
|
|
4368
5706
|
const errorCodeIndex = this.getChildren().indexOf(this.errorCodeRenderable);
|
|
4369
5707
|
if (errorCodeIndex !== -1) {
|
|
4370
|
-
super.remove(this.errorCodeRenderable
|
|
5708
|
+
super.remove(this.errorCodeRenderable);
|
|
4371
5709
|
}
|
|
4372
5710
|
}
|
|
4373
5711
|
const contentLines = [];
|
|
@@ -4442,7 +5780,7 @@ class DiffRenderable extends Renderable {
|
|
|
4442
5780
|
this.attachLineInfoListeners();
|
|
4443
5781
|
this.createOrUpdateSide("left", codeRenderable, lineColors, lineSigns, lineNumbers, new Set, "100%");
|
|
4444
5782
|
if (this.rightSide && this.rightSideAdded) {
|
|
4445
|
-
super.remove(this.rightSide
|
|
5783
|
+
super.remove(this.rightSide);
|
|
4446
5784
|
this.rightSideAdded = false;
|
|
4447
5785
|
}
|
|
4448
5786
|
}
|
|
@@ -4453,13 +5791,13 @@ class DiffRenderable extends Renderable {
|
|
|
4453
5791
|
if (this.errorTextRenderable) {
|
|
4454
5792
|
const errorTextIndex = this.getChildren().indexOf(this.errorTextRenderable);
|
|
4455
5793
|
if (errorTextIndex !== -1) {
|
|
4456
|
-
super.remove(this.errorTextRenderable
|
|
5794
|
+
super.remove(this.errorTextRenderable);
|
|
4457
5795
|
}
|
|
4458
5796
|
}
|
|
4459
5797
|
if (this.errorCodeRenderable) {
|
|
4460
5798
|
const errorCodeIndex = this.getChildren().indexOf(this.errorCodeRenderable);
|
|
4461
5799
|
if (errorCodeIndex !== -1) {
|
|
4462
|
-
super.remove(this.errorCodeRenderable
|
|
5800
|
+
super.remove(this.errorCodeRenderable);
|
|
4463
5801
|
}
|
|
4464
5802
|
}
|
|
4465
5803
|
const leftLogicalLines = [];
|
|
@@ -6770,6 +8108,83 @@ var Nt = d.parseInline;
|
|
|
6770
8108
|
var Ft = b.parse;
|
|
6771
8109
|
var jt = x.lex;
|
|
6772
8110
|
|
|
8111
|
+
// src/renderables/text-table-width.ts
|
|
8112
|
+
function comparePriority(leftGrowth, leftCapacity, rightGrowth, rightCapacity) {
|
|
8113
|
+
const left = leftGrowth * leftGrowth * rightCapacity;
|
|
8114
|
+
const right = rightGrowth * rightGrowth * leftCapacity;
|
|
8115
|
+
if (Number.isSafeInteger(left) && Number.isSafeInteger(right)) {
|
|
8116
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
8117
|
+
}
|
|
8118
|
+
const exactLeft = BigInt(leftGrowth) * BigInt(leftGrowth) * BigInt(rightCapacity);
|
|
8119
|
+
const exactRight = BigInt(rightGrowth) * BigInt(rightGrowth) * BigInt(leftCapacity);
|
|
8120
|
+
return exactLeft < exactRight ? -1 : exactLeft > exactRight ? 1 : 0;
|
|
8121
|
+
}
|
|
8122
|
+
function allocateProportionalColumnWidths(widths, targetWidth, minWidth) {
|
|
8123
|
+
const baseWidths = widths.map((width) => Math.max(minWidth, Math.floor(width)));
|
|
8124
|
+
const totalBaseWidth = baseWidths.reduce((sum, width) => sum + width, 0);
|
|
8125
|
+
const capacity = baseWidths.map((width) => width - minWidth);
|
|
8126
|
+
const growth = new Array(baseWidths.length).fill(0);
|
|
8127
|
+
const available = Math.min(Math.max(0, targetWidth - minWidth * baseWidths.length), totalBaseWidth - minWidth * baseWidths.length);
|
|
8128
|
+
if (available === 0)
|
|
8129
|
+
return growth.map(() => minWidth);
|
|
8130
|
+
if (available === capacity.reduce((sum, width) => sum + width, 0))
|
|
8131
|
+
return baseWidths;
|
|
8132
|
+
const weights = capacity.map(Math.sqrt);
|
|
8133
|
+
const active = capacity.map((width, idx) => ({ idx, width, weight: weights[idx] })).filter((column) => column.width > 0).sort((a, b2) => a.weight - b2.weight);
|
|
8134
|
+
if (active.length === capacity.length && capacity.every((width) => width === capacity[0])) {
|
|
8135
|
+
const sharedGrowth = Math.floor(available / capacity.length);
|
|
8136
|
+
const remainder = available % capacity.length;
|
|
8137
|
+
return growth.map((_2, idx) => minWidth + sharedGrowth + (idx < remainder ? 1 : 0));
|
|
8138
|
+
}
|
|
8139
|
+
let remaining = available;
|
|
8140
|
+
let totalWeight = active.reduce((sum, column) => sum + column.weight, 0);
|
|
8141
|
+
for (const column of active) {
|
|
8142
|
+
if (remaining / totalWeight <= column.weight)
|
|
8143
|
+
break;
|
|
8144
|
+
growth[column.idx] = column.width;
|
|
8145
|
+
remaining -= column.width;
|
|
8146
|
+
totalWeight -= column.weight;
|
|
8147
|
+
}
|
|
8148
|
+
const level = remaining / totalWeight;
|
|
8149
|
+
for (const column of active) {
|
|
8150
|
+
if (growth[column.idx] === column.width)
|
|
8151
|
+
continue;
|
|
8152
|
+
growth[column.idx] = Math.min(column.width, Math.floor(level * column.weight));
|
|
8153
|
+
}
|
|
8154
|
+
let allocatedGrowth = growth.reduce((sum, width) => sum + width, 0);
|
|
8155
|
+
while (allocatedGrowth > available) {
|
|
8156
|
+
let worstIdx = -1;
|
|
8157
|
+
for (let idx = 0;idx < baseWidths.length; idx++) {
|
|
8158
|
+
if (growth[idx] === 0)
|
|
8159
|
+
continue;
|
|
8160
|
+
const comparison = worstIdx === -1 ? 1 : comparePriority(growth[idx], capacity[idx], growth[worstIdx], capacity[worstIdx]);
|
|
8161
|
+
if (comparison > 0 || comparison === 0 && idx > worstIdx) {
|
|
8162
|
+
worstIdx = idx;
|
|
8163
|
+
}
|
|
8164
|
+
}
|
|
8165
|
+
if (worstIdx === -1)
|
|
8166
|
+
break;
|
|
8167
|
+
growth[worstIdx] -= 1;
|
|
8168
|
+
allocatedGrowth -= 1;
|
|
8169
|
+
}
|
|
8170
|
+
while (allocatedGrowth < available) {
|
|
8171
|
+
let bestIdx = -1;
|
|
8172
|
+
for (let idx = 0;idx < baseWidths.length; idx++) {
|
|
8173
|
+
if (growth[idx] >= capacity[idx])
|
|
8174
|
+
continue;
|
|
8175
|
+
const comparison = bestIdx === -1 ? -1 : comparePriority(growth[idx] + 1, capacity[idx], growth[bestIdx] + 1, capacity[bestIdx]);
|
|
8176
|
+
if (comparison < 0) {
|
|
8177
|
+
bestIdx = idx;
|
|
8178
|
+
}
|
|
8179
|
+
}
|
|
8180
|
+
if (bestIdx === -1)
|
|
8181
|
+
break;
|
|
8182
|
+
growth[bestIdx] += 1;
|
|
8183
|
+
allocatedGrowth += 1;
|
|
8184
|
+
}
|
|
8185
|
+
return growth.map((width) => width + minWidth);
|
|
8186
|
+
}
|
|
8187
|
+
|
|
6773
8188
|
// src/renderables/TextTable.ts
|
|
6774
8189
|
var MEASURE_HEIGHT = 1e4;
|
|
6775
8190
|
|
|
@@ -7323,54 +8738,7 @@ class TextTableRenderable extends Renderable {
|
|
|
7323
8738
|
}
|
|
7324
8739
|
fitColumnWidthsProportional(widths, targetContentWidth) {
|
|
7325
8740
|
const minWidth = 1 + this.getHorizontalCellPadding();
|
|
7326
|
-
|
|
7327
|
-
const baseWidths = widths.map((width) => Math.max(1, Math.floor(width)));
|
|
7328
|
-
const preferredMinWidths = baseWidths.map((width) => Math.min(width, minWidth + 1));
|
|
7329
|
-
const preferredMinTotal = preferredMinWidths.reduce((sum, width) => sum + width, 0);
|
|
7330
|
-
const floorWidths = preferredMinTotal <= targetContentWidth ? preferredMinWidths : hardMinWidths;
|
|
7331
|
-
const floorTotal = floorWidths.reduce((sum, width) => sum + width, 0);
|
|
7332
|
-
const clampedTarget = Math.max(floorTotal, targetContentWidth);
|
|
7333
|
-
const totalBaseWidth = baseWidths.reduce((sum, width) => sum + width, 0);
|
|
7334
|
-
if (totalBaseWidth <= clampedTarget) {
|
|
7335
|
-
return baseWidths;
|
|
7336
|
-
}
|
|
7337
|
-
const shrinkable = baseWidths.map((width, idx) => width - floorWidths[idx]);
|
|
7338
|
-
const totalShrinkable = shrinkable.reduce((sum, value) => sum + value, 0);
|
|
7339
|
-
if (totalShrinkable <= 0) {
|
|
7340
|
-
return [...floorWidths];
|
|
7341
|
-
}
|
|
7342
|
-
const targetShrink = totalBaseWidth - clampedTarget;
|
|
7343
|
-
const integerShrink = new Array(baseWidths.length).fill(0);
|
|
7344
|
-
const fractions = new Array(baseWidths.length).fill(0);
|
|
7345
|
-
let usedShrink = 0;
|
|
7346
|
-
for (let idx = 0;idx < baseWidths.length; idx++) {
|
|
7347
|
-
if (shrinkable[idx] <= 0)
|
|
7348
|
-
continue;
|
|
7349
|
-
const exact = shrinkable[idx] / totalShrinkable * targetShrink;
|
|
7350
|
-
const whole = Math.min(shrinkable[idx], Math.floor(exact));
|
|
7351
|
-
integerShrink[idx] = whole;
|
|
7352
|
-
fractions[idx] = exact - whole;
|
|
7353
|
-
usedShrink += whole;
|
|
7354
|
-
}
|
|
7355
|
-
let remainingShrink = targetShrink - usedShrink;
|
|
7356
|
-
while (remainingShrink > 0) {
|
|
7357
|
-
let bestIdx = -1;
|
|
7358
|
-
let bestFraction = -1;
|
|
7359
|
-
for (let idx = 0;idx < baseWidths.length; idx++) {
|
|
7360
|
-
if (shrinkable[idx] - integerShrink[idx] <= 0)
|
|
7361
|
-
continue;
|
|
7362
|
-
if (fractions[idx] > bestFraction) {
|
|
7363
|
-
bestFraction = fractions[idx];
|
|
7364
|
-
bestIdx = idx;
|
|
7365
|
-
}
|
|
7366
|
-
}
|
|
7367
|
-
if (bestIdx === -1)
|
|
7368
|
-
break;
|
|
7369
|
-
integerShrink[bestIdx] += 1;
|
|
7370
|
-
fractions[bestIdx] = 0;
|
|
7371
|
-
remainingShrink -= 1;
|
|
7372
|
-
}
|
|
7373
|
-
return baseWidths.map((width, idx) => Math.max(floorWidths[idx], width - integerShrink[idx]));
|
|
8741
|
+
return allocateProportionalColumnWidths(widths, targetContentWidth, minWidth);
|
|
7374
8742
|
}
|
|
7375
8743
|
fitColumnWidthsBalanced(widths, targetContentWidth) {
|
|
7376
8744
|
const minWidth = 1 + this.getHorizontalCellPadding();
|
|
@@ -10375,12 +11743,19 @@ class ScrollBoxRenderable extends BoxRenderable {
|
|
|
10375
11743
|
insertBefore(obj, anchor) {
|
|
10376
11744
|
return this.content.insertBefore(obj, anchor);
|
|
10377
11745
|
}
|
|
10378
|
-
remove(
|
|
10379
|
-
this
|
|
11746
|
+
remove(child) {
|
|
11747
|
+
if (child.parent === this) {
|
|
11748
|
+
super.remove(child);
|
|
11749
|
+
return;
|
|
11750
|
+
}
|
|
11751
|
+
this.content.remove(child);
|
|
10380
11752
|
}
|
|
10381
11753
|
getChildren() {
|
|
10382
11754
|
return this.content.getChildren();
|
|
10383
11755
|
}
|
|
11756
|
+
getRenderable(id) {
|
|
11757
|
+
return this.content.getRenderable(id);
|
|
11758
|
+
}
|
|
10384
11759
|
onMouseEvent(event) {
|
|
10385
11760
|
if (event.type === "scroll") {
|
|
10386
11761
|
let dir = event.scroll?.direction;
|
|
@@ -10701,6 +12076,7 @@ class SelectRenderable extends Renderable {
|
|
|
10701
12076
|
_showScrollIndicator;
|
|
10702
12077
|
_wrapSelection;
|
|
10703
12078
|
_showDescription;
|
|
12079
|
+
_showSelectionIndicator;
|
|
10704
12080
|
_font;
|
|
10705
12081
|
_itemSpacing;
|
|
10706
12082
|
linesPerItem;
|
|
@@ -10722,6 +12098,7 @@ class SelectRenderable extends Renderable {
|
|
|
10722
12098
|
showScrollIndicator: false,
|
|
10723
12099
|
wrapSelection: false,
|
|
10724
12100
|
showDescription: true,
|
|
12101
|
+
showSelectionIndicator: true,
|
|
10725
12102
|
itemSpacing: 0,
|
|
10726
12103
|
fastScrollStep: 5
|
|
10727
12104
|
};
|
|
@@ -10737,6 +12114,7 @@ class SelectRenderable extends Renderable {
|
|
|
10737
12114
|
this._showScrollIndicator = options.showScrollIndicator ?? this._defaultOptions.showScrollIndicator;
|
|
10738
12115
|
this._wrapSelection = options.wrapSelection ?? this._defaultOptions.wrapSelection;
|
|
10739
12116
|
this._showDescription = options.showDescription ?? this._defaultOptions.showDescription;
|
|
12117
|
+
this._showSelectionIndicator = options.showSelectionIndicator ?? this._defaultOptions.showSelectionIndicator;
|
|
10740
12118
|
this._font = options.font;
|
|
10741
12119
|
this._itemSpacing = options.itemSpacing || this._defaultOptions.itemSpacing;
|
|
10742
12120
|
this.fontHeight = this._font ? measureText({ text: "A", font: this._font }).height : 1;
|
|
@@ -10785,29 +12163,30 @@ class SelectRenderable extends Renderable {
|
|
|
10785
12163
|
const contentHeight2 = this.linesPerItem - this._itemSpacing;
|
|
10786
12164
|
this.frameBuffer.fillRect(contentX, itemY, contentWidth, contentHeight2, this._selectedBackgroundColor);
|
|
10787
12165
|
}
|
|
10788
|
-
const
|
|
12166
|
+
const indicator = this._showSelectionIndicator ? isSelected ? "▶ " : " " : "";
|
|
12167
|
+
const indicatorWidth = this._showSelectionIndicator ? 2 : 0;
|
|
12168
|
+
const nameContent = `${indicator}${option.name}`;
|
|
10789
12169
|
const baseTextColor = this._focused ? this._focusedTextColor : this._textColor;
|
|
10790
12170
|
const nameColor = isSelected ? this._selectedTextColor : baseTextColor;
|
|
10791
|
-
|
|
12171
|
+
const textX = contentX + 1 + indicatorWidth;
|
|
10792
12172
|
if (this._font) {
|
|
10793
|
-
|
|
10794
|
-
|
|
10795
|
-
|
|
12173
|
+
if (indicator) {
|
|
12174
|
+
this.frameBuffer.drawText(indicator, contentX + 1, itemY, nameColor);
|
|
12175
|
+
}
|
|
10796
12176
|
renderFontToFrameBuffer(this.frameBuffer, {
|
|
10797
12177
|
text: option.name,
|
|
10798
|
-
x:
|
|
12178
|
+
x: textX,
|
|
10799
12179
|
y: itemY,
|
|
10800
12180
|
color: nameColor,
|
|
10801
12181
|
backgroundColor: isSelected ? this._selectedBackgroundColor : bgColor,
|
|
10802
12182
|
font: this._font
|
|
10803
12183
|
});
|
|
10804
|
-
descX = contentX + 1 + indicatorWidth;
|
|
10805
12184
|
} else {
|
|
10806
12185
|
this.frameBuffer.drawText(nameContent, contentX + 1, itemY, nameColor);
|
|
10807
12186
|
}
|
|
10808
12187
|
if (this._showDescription && itemY + this.fontHeight < contentY + contentHeight) {
|
|
10809
12188
|
const descColor = isSelected ? this._selectedDescriptionColor : this._descriptionColor;
|
|
10810
|
-
this.frameBuffer.drawText(option.description,
|
|
12189
|
+
this.frameBuffer.drawText(option.description, textX, itemY + this.fontHeight, descColor);
|
|
10811
12190
|
}
|
|
10812
12191
|
}
|
|
10813
12192
|
if (this._showScrollIndicator && this._options.length > this.maxVisibleItems) {
|
|
@@ -10937,6 +12316,16 @@ class SelectRenderable extends Renderable {
|
|
|
10937
12316
|
this.requestRender();
|
|
10938
12317
|
}
|
|
10939
12318
|
}
|
|
12319
|
+
get showSelectionIndicator() {
|
|
12320
|
+
return this._showSelectionIndicator;
|
|
12321
|
+
}
|
|
12322
|
+
set showSelectionIndicator(show) {
|
|
12323
|
+
const next = show ?? this._defaultOptions.showSelectionIndicator;
|
|
12324
|
+
if (this._showSelectionIndicator !== next) {
|
|
12325
|
+
this._showSelectionIndicator = next;
|
|
12326
|
+
this.requestRender();
|
|
12327
|
+
}
|
|
12328
|
+
}
|
|
10940
12329
|
get wrapSelection() {
|
|
10941
12330
|
return this._wrapSelection;
|
|
10942
12331
|
}
|
|
@@ -11525,6 +12914,7 @@ export {
|
|
|
11525
12914
|
createTerminalPalette,
|
|
11526
12915
|
createSlotRegistry,
|
|
11527
12916
|
createMarkdownCodeBlockRenderer,
|
|
12917
|
+
createIcyStreamDemuxer,
|
|
11528
12918
|
createExtmarksController,
|
|
11529
12919
|
createCoreSlotRegistry,
|
|
11530
12920
|
createCliRenderer,
|
|
@@ -11622,6 +13012,10 @@ export {
|
|
|
11622
13012
|
PROTANOPIA_COMP_MATRIX,
|
|
11623
13013
|
OptimizedBuffer,
|
|
11624
13014
|
NativeSpanFeed,
|
|
13015
|
+
NativeMeasureTargetKind,
|
|
13016
|
+
NativeAudioStreamState2 as NativeAudioStreamState,
|
|
13017
|
+
NativeAudioStreamFormat2 as NativeAudioStreamFormat,
|
|
13018
|
+
NativeAudioStreamCloseReason2 as NativeAudioStreamCloseReason,
|
|
11625
13019
|
MouseParser,
|
|
11626
13020
|
MouseEvent,
|
|
11627
13021
|
MouseButton,
|
|
@@ -11672,6 +13066,9 @@ export {
|
|
|
11672
13066
|
BorderCharArrays,
|
|
11673
13067
|
BloomEffect,
|
|
11674
13068
|
BaseRenderable,
|
|
13069
|
+
AudioStreamError,
|
|
13070
|
+
AudioStream,
|
|
13071
|
+
AudioInitializationError,
|
|
11675
13072
|
Audio,
|
|
11676
13073
|
ArrowRenderable,
|
|
11677
13074
|
ATTRIBUTE_BASE_MASK,
|
|
@@ -11682,5 +13079,5 @@ export {
|
|
|
11682
13079
|
ACHROMATOPSIA_MATRIX
|
|
11683
13080
|
};
|
|
11684
13081
|
|
|
11685
|
-
//# debugId=
|
|
13082
|
+
//# debugId=EF989EBD255C13EC64756E2164756E21
|
|
11686
13083
|
//# sourceMappingURL=index.js.map
|