@editframe/elements 0.17.6-beta.0 → 0.18.3-beta.0
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/dist/EF_FRAMEGEN.js +1 -1
- package/dist/ScrubTrackManager.d.ts +2 -2
- package/dist/elements/EFAudio.d.ts +21 -2
- package/dist/elements/EFAudio.js +41 -11
- package/dist/elements/EFImage.d.ts +1 -0
- package/dist/elements/EFImage.js +11 -3
- package/dist/elements/EFMedia/AssetIdMediaEngine.d.ts +18 -0
- package/dist/elements/EFMedia/AssetIdMediaEngine.js +41 -0
- package/dist/elements/EFMedia/AssetMediaEngine.d.ts +47 -0
- package/dist/elements/EFMedia/AssetMediaEngine.js +116 -0
- package/dist/elements/EFMedia/BaseMediaEngine.d.ts +55 -0
- package/dist/elements/EFMedia/BaseMediaEngine.js +96 -0
- package/dist/elements/EFMedia/BufferedSeekingInput.d.ts +43 -0
- package/dist/elements/EFMedia/BufferedSeekingInput.js +159 -0
- package/dist/elements/EFMedia/JitMediaEngine.browsertest.d.ts +0 -0
- package/dist/elements/EFMedia/JitMediaEngine.d.ts +31 -0
- package/dist/elements/EFMedia/JitMediaEngine.js +62 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.browsertest.d.ts +9 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.d.ts +16 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +48 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.d.ts +3 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +138 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.browsertest.d.ts +9 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.d.ts +4 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js +16 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.browsertest.d.ts +9 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.d.ts +3 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js +22 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.d.ts +7 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js +24 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.d.ts +4 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js +18 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.d.ts +4 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js +16 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.d.ts +3 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +104 -0
- package/dist/elements/EFMedia/services/AudioElementFactory.d.ts +22 -0
- package/dist/elements/EFMedia/services/AudioElementFactory.js +72 -0
- package/dist/elements/EFMedia/services/MediaSourceService.browsertest.d.ts +1 -0
- package/dist/elements/EFMedia/services/MediaSourceService.d.ts +47 -0
- package/dist/elements/EFMedia/services/MediaSourceService.js +73 -0
- package/dist/elements/EFMedia/shared/AudioSpanUtils.d.ts +7 -0
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js +54 -0
- package/dist/elements/EFMedia/shared/BufferUtils.d.ts +70 -0
- package/dist/elements/EFMedia/shared/BufferUtils.js +89 -0
- package/dist/elements/EFMedia/shared/MediaTaskUtils.d.ts +23 -0
- package/dist/elements/EFMedia/shared/RenditionHelpers.browsertest.d.ts +1 -0
- package/dist/elements/EFMedia/shared/RenditionHelpers.d.ts +19 -0
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.browsertest.d.ts +1 -0
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.d.ts +18 -0
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js +60 -0
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.test.d.ts +1 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.browsertest.d.ts +9 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.d.ts +16 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js +46 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.browsertest.d.ts +9 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.d.ts +4 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.js +16 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoInputTask.browsertest.d.ts +9 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoInputTask.d.ts +3 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoInputTask.js +27 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoSeekTask.d.ts +7 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoSeekTask.js +25 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.browsertest.d.ts +9 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.d.ts +4 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.js +18 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.browsertest.d.ts +9 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.d.ts +4 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.js +16 -0
- package/dist/elements/EFMedia.browsertest.d.ts +1 -0
- package/dist/elements/EFMedia.d.ts +75 -111
- package/dist/elements/EFMedia.js +141 -1111
- package/dist/elements/EFTemporal.d.ts +1 -1
- package/dist/elements/EFTemporal.js +1 -1
- package/dist/elements/EFTimegroup.d.ts +11 -0
- package/dist/elements/EFTimegroup.js +88 -13
- package/dist/elements/EFVideo.d.ts +60 -29
- package/dist/elements/EFVideo.js +103 -203
- package/dist/elements/EFWaveform.js +2 -2
- package/dist/elements/SampleBuffer.d.ts +14 -0
- package/dist/elements/SampleBuffer.js +52 -0
- package/dist/getRenderInfo.d.ts +2 -2
- package/dist/getRenderInfo.js +2 -1
- package/dist/gui/ContextMixin.js +17 -70
- package/dist/gui/EFFilmstrip.d.ts +3 -3
- package/dist/gui/EFFilmstrip.js +1 -1
- package/dist/gui/EFFitScale.d.ts +2 -2
- package/dist/gui/TWMixin.js +1 -1
- package/dist/gui/services/ElementConnectionManager.browsertest.d.ts +1 -0
- package/dist/gui/services/ElementConnectionManager.d.ts +59 -0
- package/dist/gui/services/ElementConnectionManager.js +128 -0
- package/dist/gui/services/PlaybackController.browsertest.d.ts +1 -0
- package/dist/gui/services/PlaybackController.d.ts +103 -0
- package/dist/gui/services/PlaybackController.js +290 -0
- package/dist/services/MediaSourceManager.d.ts +62 -0
- package/dist/services/MediaSourceManager.js +211 -0
- package/dist/style.css +1 -1
- package/dist/transcoding/cache/CacheManager.d.ts +73 -0
- package/dist/transcoding/cache/RequestDeduplicator.d.ts +29 -0
- package/dist/transcoding/cache/RequestDeduplicator.js +53 -0
- package/dist/transcoding/cache/RequestDeduplicator.test.d.ts +1 -0
- package/dist/transcoding/types/index.d.ts +242 -0
- package/dist/transcoding/utils/MediaUtils.d.ts +9 -0
- package/dist/transcoding/utils/UrlGenerator.d.ts +26 -0
- package/dist/transcoding/utils/UrlGenerator.js +45 -0
- package/dist/transcoding/utils/constants.d.ts +27 -0
- package/dist/utils/LRUCache.d.ts +34 -0
- package/dist/utils/LRUCache.js +115 -0
- package/package.json +3 -2
- package/src/elements/EFAudio.browsertest.ts +183 -43
- package/src/elements/EFAudio.ts +59 -13
- package/src/elements/EFImage.browsertest.ts +42 -0
- package/src/elements/EFImage.ts +23 -3
- package/src/elements/EFMedia/AssetIdMediaEngine.test.ts +222 -0
- package/src/elements/EFMedia/AssetIdMediaEngine.ts +70 -0
- package/src/elements/EFMedia/AssetMediaEngine.ts +210 -0
- package/src/elements/EFMedia/BaseMediaEngine.test.ts +164 -0
- package/src/elements/EFMedia/BaseMediaEngine.ts +170 -0
- package/src/elements/EFMedia/BufferedSeekingInput.browsertest.ts +400 -0
- package/src/elements/EFMedia/BufferedSeekingInput.ts +267 -0
- package/src/elements/EFMedia/JitMediaEngine.browsertest.ts +165 -0
- package/src/elements/EFMedia/JitMediaEngine.ts +110 -0
- package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.browsertest.ts +554 -0
- package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.ts +81 -0
- package/src/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.ts +241 -0
- package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.browsertest.ts +59 -0
- package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.ts +23 -0
- package/src/elements/EFMedia/audioTasks/makeAudioInputTask.browsertest.ts +55 -0
- package/src/elements/EFMedia/audioTasks/makeAudioInputTask.ts +35 -0
- package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.ts +42 -0
- package/src/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.ts +34 -0
- package/src/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.ts +23 -0
- package/src/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.ts +174 -0
- package/src/elements/EFMedia/services/AudioElementFactory.browsertest.ts +325 -0
- package/src/elements/EFMedia/services/AudioElementFactory.ts +119 -0
- package/src/elements/EFMedia/services/MediaSourceService.browsertest.ts +257 -0
- package/src/elements/EFMedia/services/MediaSourceService.ts +102 -0
- package/src/elements/EFMedia/shared/AudioSpanUtils.ts +128 -0
- package/src/elements/EFMedia/shared/BufferUtils.ts +310 -0
- package/src/elements/EFMedia/shared/MediaTaskUtils.ts +44 -0
- package/src/elements/EFMedia/shared/RenditionHelpers.browsertest.ts +247 -0
- package/src/elements/EFMedia/shared/RenditionHelpers.ts +79 -0
- package/src/elements/EFMedia/tasks/makeMediaEngineTask.browsertest.ts +128 -0
- package/src/elements/EFMedia/tasks/makeMediaEngineTask.test.ts +233 -0
- package/src/elements/EFMedia/tasks/makeMediaEngineTask.ts +89 -0
- package/src/elements/EFMedia/videoTasks/makeVideoBufferTask.browsertest.ts +555 -0
- package/src/elements/EFMedia/videoTasks/makeVideoBufferTask.ts +79 -0
- package/src/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.browsertest.ts +59 -0
- package/src/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.ts +23 -0
- package/src/elements/EFMedia/videoTasks/makeVideoInputTask.browsertest.ts +55 -0
- package/src/elements/EFMedia/videoTasks/makeVideoInputTask.ts +45 -0
- package/src/elements/EFMedia/videoTasks/makeVideoSeekTask.ts +44 -0
- package/src/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.browsertest.ts +57 -0
- package/src/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.ts +32 -0
- package/src/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.browsertest.ts +56 -0
- package/src/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.ts +23 -0
- package/src/elements/EFMedia.browsertest.ts +658 -265
- package/src/elements/EFMedia.ts +173 -1763
- package/src/elements/EFTemporal.ts +3 -4
- package/src/elements/EFTimegroup.browsertest.ts +6 -3
- package/src/elements/EFTimegroup.ts +152 -21
- package/src/elements/EFVideo.browsertest.ts +115 -37
- package/src/elements/EFVideo.ts +123 -452
- package/src/elements/EFWaveform.ts +1 -1
- package/src/elements/MediaController.ts +2 -12
- package/src/elements/SampleBuffer.ts +97 -0
- package/src/gui/ContextMixin.ts +23 -104
- package/src/gui/services/ElementConnectionManager.browsertest.ts +263 -0
- package/src/gui/services/ElementConnectionManager.ts +224 -0
- package/src/gui/services/PlaybackController.browsertest.ts +437 -0
- package/src/gui/services/PlaybackController.ts +521 -0
- package/src/services/MediaSourceManager.ts +333 -0
- package/src/transcoding/cache/CacheManager.ts +208 -0
- package/src/transcoding/cache/RequestDeduplicator.test.ts +170 -0
- package/src/transcoding/cache/RequestDeduplicator.ts +65 -0
- package/src/transcoding/types/index.ts +265 -0
- package/src/transcoding/utils/MediaUtils.ts +63 -0
- package/src/transcoding/utils/UrlGenerator.ts +68 -0
- package/src/transcoding/utils/constants.ts +36 -0
- package/src/utils/LRUCache.ts +153 -0
- package/test/EFVideo.framegen.browsertest.ts +38 -29
- package/test/__cache__/GET__api_v1_transcode_audio_1_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__32da3954ba60c96ad732020c65a08ebc/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_audio_1_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__32da3954ba60c96ad732020c65a08ebc/metadata.json +21 -0
- package/test/__cache__/GET__api_v1_transcode_audio_2_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__b0b2b07efcf607de8ee0f650328c32f7/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_audio_2_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__b0b2b07efcf607de8ee0f650328c32f7/metadata.json +21 -0
- package/test/__cache__/GET__api_v1_transcode_audio_3_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a75c2252b542e0c152c780e9a8d7b154/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_audio_3_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a75c2252b542e0c152c780e9a8d7b154/metadata.json +21 -0
- package/test/__cache__/GET__api_v1_transcode_audio_4_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a64ff1cfb1b52cae14df4b5dfa1e222b/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_audio_4_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a64ff1cfb1b52cae14df4b5dfa1e222b/metadata.json +21 -0
- package/test/__cache__/GET__api_v1_transcode_audio_5_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__91e8a522f950809b9f09f4173113b4b0/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_audio_5_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__91e8a522f950809b9f09f4173113b4b0/metadata.json +21 -0
- package/test/__cache__/GET__api_v1_transcode_audio_init_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__e66d2c831d951e74ad0aeaa6489795d0/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_audio_init_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__e66d2c831d951e74ad0aeaa6489795d0/metadata.json +21 -0
- package/test/__cache__/GET__api_v1_transcode_high_1_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__26197f6f7c46cacb0a71134131c3f775/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_high_1_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__26197f6f7c46cacb0a71134131c3f775/metadata.json +21 -0
- package/test/__cache__/GET__api_v1_transcode_high_2_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__4cb6774cd3650ccf59c8f8dc6678c0b9/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_high_2_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__4cb6774cd3650ccf59c8f8dc6678c0b9/metadata.json +21 -0
- package/test/__cache__/GET__api_v1_transcode_high_3_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__0b3b2b1c8933f7fcf8a9ecaa88d58b41/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_high_3_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__0b3b2b1c8933f7fcf8a9ecaa88d58b41/metadata.json +21 -0
- package/test/__cache__/GET__api_v1_transcode_high_init_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__0798c479b44aaeef850609a430f6e613/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_high_init_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__0798c479b44aaeef850609a430f6e613/metadata.json +21 -0
- package/test/__cache__/GET__api_v1_transcode_manifest_json_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__3be92a0437de726b431ed5af2369158a/data.bin +1 -0
- package/test/__cache__/GET__api_v1_transcode_manifest_json_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__3be92a0437de726b431ed5af2369158a/metadata.json +19 -0
- package/test/createJitTestClips.ts +320 -188
- package/test/recordReplayProxyPlugin.js +302 -0
- package/test/useAssetMSW.ts +1 -1
- package/test/useMSW.ts +35 -22
- package/types.json +1 -1
- package/dist/JitTranscodingClient.d.ts +0 -167
- package/dist/JitTranscodingClient.js +0 -373
- package/dist/ScrubTrackManager.js +0 -216
- package/dist/elements/printTaskStatus.js +0 -11
- package/src/elements/__screenshots__/EFMedia.browsertest.ts/EFMedia-JIT-audio-playback-audioBufferTask-should-work-in-JIT-mode-without-URL-errors-1.png +0 -0
- package/test/EFVideo.frame-tasks.browsertest.ts +0 -524
- /package/dist/{JitTranscodingClient.browsertest.d.ts → elements/EFMedia/AssetIdMediaEngine.test.d.ts} +0 -0
- /package/dist/{JitTranscodingClient.test.d.ts → elements/EFMedia/BaseMediaEngine.test.d.ts} +0 -0
- /package/dist/{ScrubTrackIntegration.test.d.ts → elements/EFMedia/BufferedSeekingInput.browsertest.d.ts} +0 -0
- /package/dist/{SegmentSwitchLoading.test.d.ts → elements/EFMedia/services/AudioElementFactory.browsertest.d.ts} +0 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
export interface MediaSourceManagerOptions {
|
|
2
|
+
onError?: (error: Error) => void;
|
|
3
|
+
onReady?: () => void;
|
|
4
|
+
onUpdateEnd?: () => void;
|
|
5
|
+
timeout?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Manages MediaSource for audio streaming
|
|
10
|
+
*/
|
|
11
|
+
export class MediaSourceManager {
|
|
12
|
+
private mediaSource: MediaSource | null = null;
|
|
13
|
+
private audioElement: HTMLAudioElement | null = null;
|
|
14
|
+
private sourceBuffer: SourceBuffer | null = null;
|
|
15
|
+
private mediaSourceReady = false;
|
|
16
|
+
private pendingSegments: ArrayBuffer[] = [];
|
|
17
|
+
private options: MediaSourceManagerOptions;
|
|
18
|
+
|
|
19
|
+
constructor(options: MediaSourceManagerOptions = {}) {
|
|
20
|
+
this.options = {
|
|
21
|
+
timeout: 10000,
|
|
22
|
+
...options,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Initialize MediaSource for audio streaming
|
|
28
|
+
*/
|
|
29
|
+
async initialize(): Promise<void> {
|
|
30
|
+
this.cleanup(true);
|
|
31
|
+
|
|
32
|
+
this.mediaSource = new MediaSource();
|
|
33
|
+
this.audioElement = document.createElement("audio");
|
|
34
|
+
|
|
35
|
+
// Add error event listeners to the audio element
|
|
36
|
+
this.audioElement.addEventListener("error", (event) => {
|
|
37
|
+
const error = this.audioElement?.error;
|
|
38
|
+
console.error("🎵 [AUDIO_ELEMENT_ERROR] Audio element error:", {
|
|
39
|
+
code: error?.code,
|
|
40
|
+
message: error?.message,
|
|
41
|
+
event,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (this.options.onError) {
|
|
45
|
+
this.options.onError(
|
|
46
|
+
new Error(`Audio element error: ${error?.message}`),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
this.audioElement.src = URL.createObjectURL(this.mediaSource);
|
|
52
|
+
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
this.mediaSource?.addEventListener("sourceopen", () => {
|
|
55
|
+
try {
|
|
56
|
+
const sourceBuffer = this.createSourceBuffer();
|
|
57
|
+
if (!sourceBuffer) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
"Failed to create SourceBuffer with any supported codec",
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this.sourceBuffer = sourceBuffer;
|
|
64
|
+
this.setupSourceBufferListeners();
|
|
65
|
+
|
|
66
|
+
this.mediaSourceReady = true;
|
|
67
|
+
|
|
68
|
+
if (this.options.onReady) {
|
|
69
|
+
this.options.onReady();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
resolve();
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error(
|
|
75
|
+
"🎵 [MEDIA_SOURCE_ERROR] Failed to create SourceBuffer:",
|
|
76
|
+
error,
|
|
77
|
+
);
|
|
78
|
+
reject(error);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
this.mediaSource?.addEventListener("error", (error) => {
|
|
83
|
+
console.error("🎵 [MEDIA_SOURCE_ERROR] MediaSource error:", error);
|
|
84
|
+
reject(error);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Add timeout for MediaSource opening
|
|
88
|
+
setTimeout(() => {
|
|
89
|
+
if (!this.mediaSourceReady) {
|
|
90
|
+
const timeoutError = new Error(
|
|
91
|
+
"MediaSource failed to open within timeout",
|
|
92
|
+
);
|
|
93
|
+
console.error(
|
|
94
|
+
"🎵 [MEDIA_SOURCE_TIMEOUT] MediaSource initialization timeout",
|
|
95
|
+
);
|
|
96
|
+
reject(timeoutError);
|
|
97
|
+
}
|
|
98
|
+
}, 4000);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Create SourceBuffer with codec fallback
|
|
104
|
+
*/
|
|
105
|
+
private createSourceBuffer(): SourceBuffer | undefined {
|
|
106
|
+
const codecOptions = [
|
|
107
|
+
'audio/mp4; codecs="mp4a.40.2"', // AAC-LC (most common)
|
|
108
|
+
'audio/mp4; codecs="mp4a.40.5"', // AAC-HE
|
|
109
|
+
"audio/mp4", // Generic MP4 audio
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
let sourceBuffer: SourceBuffer | undefined;
|
|
113
|
+
let lastError: Error | undefined;
|
|
114
|
+
|
|
115
|
+
for (const codec of codecOptions) {
|
|
116
|
+
try {
|
|
117
|
+
if (MediaSource.isTypeSupported(codec)) {
|
|
118
|
+
sourceBuffer = this.mediaSource?.addSourceBuffer(codec);
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error(
|
|
123
|
+
`🎵 [CODEC_ERROR] Failed to create SourceBuffer with ${codec}:`,
|
|
124
|
+
error,
|
|
125
|
+
);
|
|
126
|
+
lastError = error as Error;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!sourceBuffer && lastError) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`Failed to create SourceBuffer with any supported codec. Last error: ${lastError.message}`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return sourceBuffer;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Setup SourceBuffer event listeners
|
|
141
|
+
*/
|
|
142
|
+
private setupSourceBufferListeners(): void {
|
|
143
|
+
if (!this.sourceBuffer) return;
|
|
144
|
+
|
|
145
|
+
this.sourceBuffer.addEventListener("updateend", () => {
|
|
146
|
+
this.processPendingSegments();
|
|
147
|
+
|
|
148
|
+
if (this.options.onUpdateEnd) {
|
|
149
|
+
this.options.onUpdateEnd();
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
this.sourceBuffer.addEventListener("error", (event) => {
|
|
154
|
+
console.error(
|
|
155
|
+
"🎵 [SOURCE_BUFFER_EVENT_ERROR] SourceBuffer error event:",
|
|
156
|
+
event,
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
if (this.options.onError) {
|
|
160
|
+
this.options.onError(new Error("SourceBuffer error"));
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Feed audio segments directly to MediaSource SourceBuffer
|
|
167
|
+
*/
|
|
168
|
+
async feedSegment(segmentBuffer: ArrayBuffer): Promise<void> {
|
|
169
|
+
if (!this.mediaSourceReady || !this.sourceBuffer) {
|
|
170
|
+
this.pendingSegments.push(segmentBuffer);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (this.sourceBuffer.updating) {
|
|
175
|
+
this.pendingSegments.push(segmentBuffer);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Check for HTMLMediaElement errors before appending
|
|
180
|
+
if (this.audioElement?.error) {
|
|
181
|
+
const error = this.audioElement.error;
|
|
182
|
+
console.error(
|
|
183
|
+
"🎵 [MEDIA_ELEMENT_ERROR] HTMLMediaElement error detected:",
|
|
184
|
+
{
|
|
185
|
+
code: error.code,
|
|
186
|
+
message: error.message,
|
|
187
|
+
MEDIA_ERR_ABORTED: error.code === MediaError.MEDIA_ERR_ABORTED,
|
|
188
|
+
MEDIA_ERR_NETWORK: error.code === MediaError.MEDIA_ERR_NETWORK,
|
|
189
|
+
MEDIA_ERR_DECODE: error.code === MediaError.MEDIA_ERR_DECODE,
|
|
190
|
+
MEDIA_ERR_SRC_NOT_SUPPORTED:
|
|
191
|
+
error.code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED,
|
|
192
|
+
},
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
// Reset the audio element to try to recover
|
|
196
|
+
this.audioElement.load();
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
this.sourceBuffer.appendBuffer(segmentBuffer);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
console.error(
|
|
204
|
+
"🎵 [SOURCE_BUFFER_ERROR] Failed to append segment:",
|
|
205
|
+
error,
|
|
206
|
+
);
|
|
207
|
+
this.logDebugInfo();
|
|
208
|
+
this.pendingSegments.push(segmentBuffer);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Process any queued segments when SourceBuffer becomes available
|
|
214
|
+
*/
|
|
215
|
+
private processPendingSegments(): void {
|
|
216
|
+
if (
|
|
217
|
+
!this.sourceBuffer ||
|
|
218
|
+
this.sourceBuffer.updating ||
|
|
219
|
+
this.pendingSegments.length === 0
|
|
220
|
+
) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const nextSegment = this.pendingSegments.shift();
|
|
225
|
+
if (nextSegment) {
|
|
226
|
+
this.feedSegment(nextSegment);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Log debug information for troubleshooting
|
|
232
|
+
*/
|
|
233
|
+
private logDebugInfo(): void {
|
|
234
|
+
console.error("🎵 [SOURCE_BUFFER_DEBUG] SourceBuffer state:", {
|
|
235
|
+
updating: this.sourceBuffer?.updating,
|
|
236
|
+
buffered: this.sourceBuffer?.buffered
|
|
237
|
+
? Array.from(
|
|
238
|
+
{ length: this.sourceBuffer.buffered.length },
|
|
239
|
+
(_, i) =>
|
|
240
|
+
`${this.sourceBuffer?.buffered.start(i)}-${this.sourceBuffer?.buffered.end(i)}`,
|
|
241
|
+
)
|
|
242
|
+
: [],
|
|
243
|
+
mode: this.sourceBuffer?.mode,
|
|
244
|
+
timestampOffset: this.sourceBuffer?.timestampOffset,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
console.error("🎵 [MEDIA_SOURCE_DEBUG] MediaSource state:", {
|
|
248
|
+
readyState: this.mediaSource?.readyState,
|
|
249
|
+
sourceBuffers: this.mediaSource?.sourceBuffers.length,
|
|
250
|
+
duration: this.mediaSource?.duration,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
console.error("🎵 [AUDIO_ELEMENT_DEBUG] Audio element state:", {
|
|
254
|
+
readyState: this.audioElement?.readyState,
|
|
255
|
+
networkState: this.audioElement?.networkState,
|
|
256
|
+
error: this.audioElement?.error?.code,
|
|
257
|
+
src: `${this.audioElement?.src.substring(0, 50)}...`,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Set audio element current time
|
|
263
|
+
*/
|
|
264
|
+
setCurrentTime(timeMs: number): void {
|
|
265
|
+
if (this.audioElement) {
|
|
266
|
+
this.audioElement.currentTime = timeMs / 1000;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Get the audio element for MediaElementSource
|
|
272
|
+
*/
|
|
273
|
+
getAudioElement(): HTMLAudioElement | null {
|
|
274
|
+
return this.audioElement;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Check if MediaSource is ready
|
|
279
|
+
*/
|
|
280
|
+
isReady(): boolean {
|
|
281
|
+
return this.mediaSourceReady;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Get buffered time ranges
|
|
286
|
+
*/
|
|
287
|
+
getBuffered(): TimeRanges | null {
|
|
288
|
+
return this.sourceBuffer?.buffered || null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Clean up MediaSource resources
|
|
293
|
+
*/
|
|
294
|
+
cleanup(_preserveCache = false): void {
|
|
295
|
+
// Clean up existing MediaSource
|
|
296
|
+
if (
|
|
297
|
+
this.sourceBuffer &&
|
|
298
|
+
this.mediaSource &&
|
|
299
|
+
this.mediaSource.readyState === "open"
|
|
300
|
+
) {
|
|
301
|
+
try {
|
|
302
|
+
this.mediaSource.removeSourceBuffer(this.sourceBuffer);
|
|
303
|
+
} catch (error) {
|
|
304
|
+
console.warn("🎵 [CLEANUP_ERROR] Error removing SourceBuffer:", error);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (this.mediaSource) {
|
|
309
|
+
try {
|
|
310
|
+
if (this.mediaSource.readyState === "open") {
|
|
311
|
+
this.mediaSource.endOfStream();
|
|
312
|
+
}
|
|
313
|
+
} catch (error) {
|
|
314
|
+
console.warn("🎵 [CLEANUP_ERROR] Error ending MediaSource:", error);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (this.audioElement) {
|
|
319
|
+
try {
|
|
320
|
+
URL.revokeObjectURL(this.audioElement.src);
|
|
321
|
+
} catch (error) {
|
|
322
|
+
console.warn("🎵 [CLEANUP_ERROR] Error revoking URL:", error);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Reset state
|
|
327
|
+
this.mediaSource = null;
|
|
328
|
+
this.audioElement = null;
|
|
329
|
+
this.sourceBuffer = null;
|
|
330
|
+
this.mediaSourceReady = false;
|
|
331
|
+
this.pendingSegments = [];
|
|
332
|
+
}
|
|
333
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache manager for handling multiple cache types with LRU eviction and statistics
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
CacheStats,
|
|
7
|
+
ManifestResponse,
|
|
8
|
+
VideoMetadata,
|
|
9
|
+
} from "../types/index.js";
|
|
10
|
+
|
|
11
|
+
export class CacheManager {
|
|
12
|
+
private segmentCache = new Map<string, ArrayBuffer>();
|
|
13
|
+
private metadataCache = new Map<string, VideoMetadata>();
|
|
14
|
+
private manifestCache = new Map<string, ManifestResponse>();
|
|
15
|
+
private initSegmentCache = new Map<string, ArrayBuffer>();
|
|
16
|
+
private cacheAccessOrder: string[] = [];
|
|
17
|
+
|
|
18
|
+
// Cache performance tracking
|
|
19
|
+
private cacheHits = 0;
|
|
20
|
+
private cacheMisses = 0;
|
|
21
|
+
private totalRequests = 0;
|
|
22
|
+
|
|
23
|
+
constructor(private maxSize: number) {}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Cache a segment with LRU eviction
|
|
27
|
+
*/
|
|
28
|
+
cacheSegment(cacheKey: string, buffer: ArrayBuffer): void {
|
|
29
|
+
// Implement LRU eviction
|
|
30
|
+
if (this.segmentCache.size >= this.maxSize) {
|
|
31
|
+
const firstKey = this.segmentCache.keys().next().value;
|
|
32
|
+
if (firstKey) {
|
|
33
|
+
this.segmentCache.delete(firstKey);
|
|
34
|
+
// Remove from access order tracking
|
|
35
|
+
const index = this.cacheAccessOrder.indexOf(firstKey);
|
|
36
|
+
if (index > -1) {
|
|
37
|
+
this.cacheAccessOrder.splice(index, 1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
this.segmentCache.set(cacheKey, buffer);
|
|
43
|
+
|
|
44
|
+
// Track access order for LRU analytics
|
|
45
|
+
const existingIndex = this.cacheAccessOrder.indexOf(cacheKey);
|
|
46
|
+
if (existingIndex > -1) {
|
|
47
|
+
this.cacheAccessOrder.splice(existingIndex, 1);
|
|
48
|
+
}
|
|
49
|
+
this.cacheAccessOrder.push(cacheKey);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get a segment from cache
|
|
54
|
+
*/
|
|
55
|
+
getSegment(cacheKey: string): ArrayBuffer | undefined {
|
|
56
|
+
const cached = this.segmentCache.get(cacheKey);
|
|
57
|
+
if (cached) {
|
|
58
|
+
this.cacheHits++;
|
|
59
|
+
// Update access order for LRU
|
|
60
|
+
const index = this.cacheAccessOrder.indexOf(cacheKey);
|
|
61
|
+
if (index > -1) {
|
|
62
|
+
this.cacheAccessOrder.splice(index, 1);
|
|
63
|
+
this.cacheAccessOrder.push(cacheKey);
|
|
64
|
+
}
|
|
65
|
+
return cached;
|
|
66
|
+
}
|
|
67
|
+
this.cacheMisses++;
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Check if a segment exists in cache
|
|
73
|
+
*/
|
|
74
|
+
hasSegment(cacheKey: string): boolean {
|
|
75
|
+
return this.segmentCache.has(cacheKey);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Cache metadata
|
|
80
|
+
*/
|
|
81
|
+
cacheMetadata(url: string, metadata: VideoMetadata): void {
|
|
82
|
+
this.metadataCache.set(url, metadata);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get metadata from cache
|
|
87
|
+
*/
|
|
88
|
+
getMetadata(url: string): VideoMetadata | undefined {
|
|
89
|
+
const cached = this.metadataCache.get(url);
|
|
90
|
+
if (cached) {
|
|
91
|
+
this.cacheHits++;
|
|
92
|
+
return cached;
|
|
93
|
+
}
|
|
94
|
+
this.cacheMisses++;
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Cache manifest
|
|
100
|
+
*/
|
|
101
|
+
cacheManifest(url: string, manifest: ManifestResponse): void {
|
|
102
|
+
this.manifestCache.set(url, manifest);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get manifest from cache
|
|
107
|
+
*/
|
|
108
|
+
getManifest(url: string): ManifestResponse | undefined {
|
|
109
|
+
const cached = this.manifestCache.get(url);
|
|
110
|
+
if (cached) {
|
|
111
|
+
this.cacheHits++;
|
|
112
|
+
return cached;
|
|
113
|
+
}
|
|
114
|
+
this.cacheMisses++;
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Cache init segment
|
|
120
|
+
*/
|
|
121
|
+
cacheInitSegment(cacheKey: string, buffer: ArrayBuffer): void {
|
|
122
|
+
this.initSegmentCache.set(cacheKey, buffer);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get init segment from cache
|
|
127
|
+
*/
|
|
128
|
+
getInitSegment(cacheKey: string): ArrayBuffer | undefined {
|
|
129
|
+
const cached = this.initSegmentCache.get(cacheKey);
|
|
130
|
+
if (cached) {
|
|
131
|
+
this.cacheHits++;
|
|
132
|
+
return cached;
|
|
133
|
+
}
|
|
134
|
+
this.cacheMisses++;
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get comprehensive cache statistics
|
|
140
|
+
*/
|
|
141
|
+
getCacheStats(): CacheStats {
|
|
142
|
+
this.totalRequests = this.cacheHits + this.cacheMisses;
|
|
143
|
+
const hitRate =
|
|
144
|
+
this.totalRequests > 0 ? this.cacheHits / this.totalRequests : 0;
|
|
145
|
+
const efficiency =
|
|
146
|
+
this.segmentCache.size > 0 ? this.cacheHits / this.segmentCache.size : 0;
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
size: this.segmentCache.size,
|
|
150
|
+
maxSize: this.maxSize,
|
|
151
|
+
hitRate: hitRate,
|
|
152
|
+
efficiency: efficiency,
|
|
153
|
+
totalRequests: this.totalRequests,
|
|
154
|
+
recentKeys: this.cacheAccessOrder.slice(-5), // Last 5 accessed keys
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Clear all caches
|
|
160
|
+
*/
|
|
161
|
+
clearAll(): void {
|
|
162
|
+
this.segmentCache.clear();
|
|
163
|
+
this.metadataCache.clear();
|
|
164
|
+
this.manifestCache.clear();
|
|
165
|
+
this.initSegmentCache.clear();
|
|
166
|
+
this.cacheAccessOrder = [];
|
|
167
|
+
this.cacheHits = 0;
|
|
168
|
+
this.cacheMisses = 0;
|
|
169
|
+
this.totalRequests = 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Get cache sizes for each cache type
|
|
174
|
+
*/
|
|
175
|
+
getCacheSizes(): {
|
|
176
|
+
segments: number;
|
|
177
|
+
metadata: number;
|
|
178
|
+
manifests: number;
|
|
179
|
+
initSegments: number;
|
|
180
|
+
} {
|
|
181
|
+
return {
|
|
182
|
+
segments: this.segmentCache.size,
|
|
183
|
+
metadata: this.metadataCache.size,
|
|
184
|
+
manifests: this.manifestCache.size,
|
|
185
|
+
initSegments: this.initSegmentCache.size,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Clear specific cache type
|
|
191
|
+
*/
|
|
192
|
+
clearSegmentCache(): void {
|
|
193
|
+
this.segmentCache.clear();
|
|
194
|
+
this.cacheAccessOrder = [];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
clearMetadataCache(): void {
|
|
198
|
+
this.metadataCache.clear();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
clearManifestCache(): void {
|
|
202
|
+
this.manifestCache.clear();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
clearInitSegmentCache(): void {
|
|
206
|
+
this.initSegmentCache.clear();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { RequestDeduplicator } from "./RequestDeduplicator.js";
|
|
3
|
+
|
|
4
|
+
describe("RequestDeduplicator", () => {
|
|
5
|
+
let deduplicator: RequestDeduplicator;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
deduplicator = new RequestDeduplicator();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe("executeRequest", () => {
|
|
12
|
+
it("should execute request and return result for new key", async () => {
|
|
13
|
+
const mockFactory = vi.fn().mockResolvedValue("result");
|
|
14
|
+
|
|
15
|
+
const result = await deduplicator.executeRequest("key1", mockFactory);
|
|
16
|
+
|
|
17
|
+
expect(result).toBe("result");
|
|
18
|
+
expect(mockFactory).toHaveBeenCalledTimes(1);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("should return same promise for concurrent requests with same key", async () => {
|
|
22
|
+
const mockFactory = vi.fn().mockResolvedValue("result");
|
|
23
|
+
|
|
24
|
+
const [result1, result2] = await Promise.all([
|
|
25
|
+
deduplicator.executeRequest("key1", mockFactory),
|
|
26
|
+
deduplicator.executeRequest("key1", mockFactory),
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
expect(result1).toBe("result");
|
|
30
|
+
expect(result2).toBe("result");
|
|
31
|
+
expect(mockFactory).toHaveBeenCalledTimes(1); // Should only be called once
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("should allow separate requests for different keys", async () => {
|
|
35
|
+
const mockFactory1 = vi.fn().mockResolvedValue("result1");
|
|
36
|
+
const mockFactory2 = vi.fn().mockResolvedValue("result2");
|
|
37
|
+
|
|
38
|
+
const [result1, result2] = await Promise.all([
|
|
39
|
+
deduplicator.executeRequest("key1", mockFactory1),
|
|
40
|
+
deduplicator.executeRequest("key2", mockFactory2),
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
expect(result1).toBe("result1");
|
|
44
|
+
expect(result2).toBe("result2");
|
|
45
|
+
expect(mockFactory1).toHaveBeenCalledTimes(1);
|
|
46
|
+
expect(mockFactory2).toHaveBeenCalledTimes(1);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("should handle request failures and clean up", async () => {
|
|
50
|
+
const error = new Error("Request failed");
|
|
51
|
+
const mockFactory = vi.fn().mockRejectedValue(error);
|
|
52
|
+
|
|
53
|
+
await expect(
|
|
54
|
+
deduplicator.executeRequest("key1", mockFactory),
|
|
55
|
+
).rejects.toThrow("Request failed");
|
|
56
|
+
|
|
57
|
+
// Should allow new request with same key after failure
|
|
58
|
+
const mockFactory2 = vi.fn().mockResolvedValue("success");
|
|
59
|
+
const result = await deduplicator.executeRequest("key1", mockFactory2);
|
|
60
|
+
|
|
61
|
+
expect(result).toBe("success");
|
|
62
|
+
expect(mockFactory).toHaveBeenCalledTimes(1);
|
|
63
|
+
expect(mockFactory2).toHaveBeenCalledTimes(1);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should clean up pending requests after success", async () => {
|
|
67
|
+
const mockFactory = vi.fn().mockResolvedValue("result");
|
|
68
|
+
|
|
69
|
+
await deduplicator.executeRequest("key1", mockFactory);
|
|
70
|
+
|
|
71
|
+
expect(deduplicator.isPending("key1")).toBe(false);
|
|
72
|
+
expect(deduplicator.getPendingCount()).toBe(0);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("isPending", () => {
|
|
77
|
+
it("should return true for pending requests", async () => {
|
|
78
|
+
const mockFactory = vi.fn().mockImplementation(
|
|
79
|
+
() =>
|
|
80
|
+
new Promise((resolve) => {
|
|
81
|
+
setTimeout(() => resolve("result"), 100);
|
|
82
|
+
}),
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const promise = deduplicator.executeRequest("key1", mockFactory);
|
|
86
|
+
|
|
87
|
+
expect(deduplicator.isPending("key1")).toBe(true);
|
|
88
|
+
|
|
89
|
+
await promise;
|
|
90
|
+
|
|
91
|
+
expect(deduplicator.isPending("key1")).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should return false for non-existent keys", () => {
|
|
95
|
+
expect(deduplicator.isPending("nonexistent")).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("getPendingCount", () => {
|
|
100
|
+
it("should return 0 initially", () => {
|
|
101
|
+
expect(deduplicator.getPendingCount()).toBe(0);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("should track pending request count", async () => {
|
|
105
|
+
const mockFactory = vi.fn().mockImplementation(
|
|
106
|
+
() =>
|
|
107
|
+
new Promise((resolve) => {
|
|
108
|
+
setTimeout(() => resolve("result"), 100);
|
|
109
|
+
}),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const promise1 = deduplicator.executeRequest("key1", mockFactory);
|
|
113
|
+
const promise2 = deduplicator.executeRequest("key2", mockFactory);
|
|
114
|
+
|
|
115
|
+
expect(deduplicator.getPendingCount()).toBe(2);
|
|
116
|
+
|
|
117
|
+
await Promise.all([promise1, promise2]);
|
|
118
|
+
|
|
119
|
+
expect(deduplicator.getPendingCount()).toBe(0);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("getPendingKeys", () => {
|
|
124
|
+
it("should return empty array initially", () => {
|
|
125
|
+
expect(deduplicator.getPendingKeys()).toEqual([]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("should return pending keys", async () => {
|
|
129
|
+
const mockFactory = vi.fn().mockImplementation(
|
|
130
|
+
() =>
|
|
131
|
+
new Promise((resolve) => {
|
|
132
|
+
setTimeout(() => resolve("result"), 100);
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const promise1 = deduplicator.executeRequest("key1", mockFactory);
|
|
137
|
+
const promise2 = deduplicator.executeRequest("key2", mockFactory);
|
|
138
|
+
|
|
139
|
+
const pendingKeys = deduplicator.getPendingKeys();
|
|
140
|
+
expect(pendingKeys).toHaveLength(2);
|
|
141
|
+
expect(pendingKeys).toContain("key1");
|
|
142
|
+
expect(pendingKeys).toContain("key2");
|
|
143
|
+
|
|
144
|
+
await Promise.all([promise1, promise2]);
|
|
145
|
+
|
|
146
|
+
expect(deduplicator.getPendingKeys()).toEqual([]);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("clear", () => {
|
|
151
|
+
it("should clear all pending requests", async () => {
|
|
152
|
+
const mockFactory = vi.fn().mockImplementation(
|
|
153
|
+
() =>
|
|
154
|
+
new Promise((resolve) => {
|
|
155
|
+
setTimeout(() => resolve("result"), 100);
|
|
156
|
+
}),
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
deduplicator.executeRequest("key1", mockFactory);
|
|
160
|
+
deduplicator.executeRequest("key2", mockFactory);
|
|
161
|
+
|
|
162
|
+
expect(deduplicator.getPendingCount()).toBe(2);
|
|
163
|
+
|
|
164
|
+
deduplicator.clear();
|
|
165
|
+
|
|
166
|
+
expect(deduplicator.getPendingCount()).toBe(0);
|
|
167
|
+
expect(deduplicator.getPendingKeys()).toEqual([]);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
});
|