@editframe/elements 0.17.6-beta.0 → 0.18.7-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/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.browsertest.d.ts +0 -0
- package/dist/elements/EFMedia/AssetMediaEngine.d.ts +45 -0
- package/dist/elements/EFMedia/AssetMediaEngine.js +135 -0
- package/dist/elements/EFMedia/BaseMediaEngine.d.ts +55 -0
- package/dist/elements/EFMedia/BaseMediaEngine.js +115 -0
- package/dist/elements/EFMedia/BufferedSeekingInput.d.ts +43 -0
- package/dist/elements/EFMedia/BufferedSeekingInput.js +179 -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 +81 -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 +141 -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 +30 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.chunkboundary.regression.browsertest.d.ts +0 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.d.ts +7 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js +32 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.d.ts +4 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js +28 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.d.ts +4 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js +17 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.d.ts +3 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +107 -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/PrecisionUtils.d.ts +28 -0
- package/dist/elements/EFMedia/shared/PrecisionUtils.js +29 -0
- package/dist/elements/EFMedia/shared/RenditionHelpers.d.ts +19 -0
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.d.ts +18 -0
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js +60 -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 +34 -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 +28 -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 +17 -0
- package/dist/elements/EFMedia.browsertest.d.ts +1 -0
- package/dist/elements/EFMedia.d.ts +63 -111
- package/dist/elements/EFMedia.js +117 -1113
- 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 +83 -13
- package/dist/elements/EFVideo.d.ts +54 -32
- package/dist/elements/EFVideo.js +100 -207
- 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.js +2 -1
- package/dist/gui/ContextMixin.js +3 -2
- 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/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 -3
- package/src/elements/EFAudio.browsertest.ts +189 -49
- 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.browsertest.ts +100 -0
- package/src/elements/EFMedia/AssetMediaEngine.ts +255 -0
- package/src/elements/EFMedia/BaseMediaEngine.test.ts +164 -0
- package/src/elements/EFMedia/BaseMediaEngine.ts +219 -0
- package/src/elements/EFMedia/BufferedSeekingInput.browsertest.ts +481 -0
- package/src/elements/EFMedia/BufferedSeekingInput.ts +324 -0
- package/src/elements/EFMedia/JitMediaEngine.browsertest.ts +165 -0
- package/src/elements/EFMedia/JitMediaEngine.ts +166 -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 +250 -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 +43 -0
- package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.chunkboundary.regression.browsertest.ts +199 -0
- package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.ts +64 -0
- package/src/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.ts +45 -0
- package/src/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.ts +24 -0
- package/src/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.ts +183 -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/PrecisionUtils.ts +46 -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 +68 -0
- package/src/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.browsertest.ts +57 -0
- package/src/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.ts +43 -0
- package/src/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.browsertest.ts +56 -0
- package/src/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.ts +24 -0
- package/src/elements/EFMedia.browsertest.ts +706 -273
- package/src/elements/EFMedia.ts +136 -1769
- package/src/elements/EFTemporal.ts +3 -4
- package/src/elements/EFTimegroup.browsertest.ts +6 -3
- package/src/elements/EFTimegroup.ts +147 -21
- package/src/elements/EFVideo.browsertest.ts +980 -169
- package/src/elements/EFVideo.ts +113 -458
- package/src/elements/EFWaveform.ts +1 -1
- package/src/elements/MediaController.ts +2 -12
- package/src/elements/SampleBuffer.ts +95 -0
- package/src/gui/ContextMixin.ts +3 -6
- 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 +269 -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 +39 -30
- 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_1_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__9ed2d25c675aa6bb6ff5b3ae23887c71/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_audio_1_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__9ed2d25c675aa6bb6ff5b3ae23887c71/metadata.json +22 -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_2_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__d5a3309a2bf756dd6e304807eb402f56/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_audio_2_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__d5a3309a2bf756dd6e304807eb402f56/metadata.json +22 -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_3_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__773254bb671e3466fca8677139fb239e/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_audio_3_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__773254bb671e3466fca8677139fb239e/metadata.json +22 -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_4_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a6fb05a22b18d850f7f2950bbcdbdeed/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_high_4_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a6fb05a22b18d850f7f2950bbcdbdeed/metadata.json +21 -0
- package/test/__cache__/GET__api_v1_transcode_high_5_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a50058c7c3602e90879fe3428ed891f4/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_high_5_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a50058c7c3602e90879fe3428ed891f4/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 +352 -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.d.ts +0 -96
- 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/{DecoderResetFrequency.test.d.ts → elements/EFMedia/AssetIdMediaEngine.test.d.ts} +0 -0
- /package/dist/{DecoderResetRecovery.test.d.ts → elements/EFMedia/BaseMediaEngine.test.d.ts} +0 -0
- /package/dist/{JitTranscodingClient.browsertest.d.ts → elements/EFMedia/BufferedSeekingInput.browsertest.d.ts} +0 -0
- /package/dist/{JitTranscodingClient.test.d.ts → elements/EFMedia/shared/RenditionHelpers.browsertest.d.ts} +0 -0
- /package/dist/{ScrubTrackIntegration.test.d.ts → elements/EFMedia/tasks/makeMediaEngineTask.browsertest.d.ts} +0 -0
- /package/dist/{SegmentSwitchLoading.test.d.ts → elements/EFMedia/tasks/makeMediaEngineTask.test.d.ts} +0 -0
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { ReactiveController, ReactiveControllerHost } from "lit";
|
|
2
|
-
import { JitTranscodingClient } from "../JitTranscodingClient.ts";
|
|
3
2
|
|
|
4
3
|
export class MediaController implements ReactiveController {
|
|
5
4
|
#src: string | null = null;
|
|
@@ -29,10 +28,6 @@ export class MediaController implements ReactiveController {
|
|
|
29
28
|
return "asset";
|
|
30
29
|
}
|
|
31
30
|
|
|
32
|
-
if (JitTranscodingClient.isJitTranscodeEligible(this.host.src)) {
|
|
33
|
-
return "jit-transcode";
|
|
34
|
-
}
|
|
35
|
-
|
|
36
31
|
return "asset";
|
|
37
32
|
}
|
|
38
33
|
|
|
@@ -84,7 +79,6 @@ export class MediaController implements ReactiveController {
|
|
|
84
79
|
|
|
85
80
|
handleUpdate(changes: Map<string, { oldValue: any; newValue: any }>) {
|
|
86
81
|
if (changes.has("src") || changes.has("assetId") || changes.has("mode")) {
|
|
87
|
-
console.log("SRC/ASSETID/MODE changed");
|
|
88
82
|
}
|
|
89
83
|
}
|
|
90
84
|
|
|
@@ -98,11 +92,7 @@ export class MediaController implements ReactiveController {
|
|
|
98
92
|
this.assetId = this.host.assetId;
|
|
99
93
|
}
|
|
100
94
|
|
|
101
|
-
hostDisconnected() {
|
|
102
|
-
console.log("hostDisconnected");
|
|
103
|
-
}
|
|
95
|
+
hostDisconnected() {}
|
|
104
96
|
|
|
105
|
-
updated() {
|
|
106
|
-
console.log("updated");
|
|
107
|
-
}
|
|
97
|
+
updated() {}
|
|
108
98
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { AudioSample, VideoSample } from "mediabunny";
|
|
2
|
+
import { roundToMilliseconds } from "./EFMedia/shared/PrecisionUtils";
|
|
3
|
+
export type MediaSample = VideoSample | AudioSample;
|
|
4
|
+
|
|
5
|
+
// Generic sample buffer that works with both VideoSample and AudioSample
|
|
6
|
+
export class SampleBuffer {
|
|
7
|
+
private buffer: MediaSample[] = [];
|
|
8
|
+
private bufferSize: number;
|
|
9
|
+
|
|
10
|
+
constructor(bufferSize = 10) {
|
|
11
|
+
this.bufferSize = bufferSize;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
push(sample: MediaSample) {
|
|
15
|
+
// Defensive copy to avoid concurrent modification during iteration
|
|
16
|
+
const currentBuffer = [...this.buffer];
|
|
17
|
+
currentBuffer.push(sample);
|
|
18
|
+
|
|
19
|
+
if (currentBuffer.length > this.bufferSize) {
|
|
20
|
+
const shifted = currentBuffer.shift();
|
|
21
|
+
if (shifted) {
|
|
22
|
+
try {
|
|
23
|
+
shifted.close();
|
|
24
|
+
} catch (_error) {
|
|
25
|
+
// Sample already closed, continue
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Update buffer atomically
|
|
31
|
+
this.buffer = currentBuffer;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
clear() {
|
|
35
|
+
// Get current buffer and clear atomically
|
|
36
|
+
const toClose = this.buffer;
|
|
37
|
+
this.buffer = [];
|
|
38
|
+
|
|
39
|
+
// Close samples after clearing to avoid holding references
|
|
40
|
+
for (const sample of toClose) {
|
|
41
|
+
try {
|
|
42
|
+
sample.close();
|
|
43
|
+
} catch (_error) {
|
|
44
|
+
// Sample already closed, continue
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
peek(): MediaSample | undefined {
|
|
50
|
+
// Defensive read - get current buffer state
|
|
51
|
+
const currentBuffer = this.buffer;
|
|
52
|
+
return currentBuffer[0];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
find(desiredSeekTimeMs: number): MediaSample | undefined {
|
|
56
|
+
// Take snapshot to avoid concurrent modification during iteration
|
|
57
|
+
const currentBuffer = [...this.buffer];
|
|
58
|
+
|
|
59
|
+
if (currentBuffer.length === 0) return undefined;
|
|
60
|
+
|
|
61
|
+
// Use consistent precision handling across the entire pipeline
|
|
62
|
+
const targetTimeMs = roundToMilliseconds(desiredSeekTimeMs);
|
|
63
|
+
|
|
64
|
+
// Find the sample that contains the target time
|
|
65
|
+
for (const sample of currentBuffer) {
|
|
66
|
+
const sampleStartMs = roundToMilliseconds((sample.timestamp || 0) * 1000);
|
|
67
|
+
const sampleDurationMs = roundToMilliseconds(
|
|
68
|
+
(sample.duration || 0) * 1000,
|
|
69
|
+
);
|
|
70
|
+
const sampleEndMs = roundToMilliseconds(sampleStartMs + sampleDurationMs);
|
|
71
|
+
|
|
72
|
+
// Check if the desired time falls within this sample's time span [start, end], inclusive of end
|
|
73
|
+
if (targetTimeMs >= sampleStartMs && targetTimeMs <= sampleEndMs) {
|
|
74
|
+
return sample;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return undefined; // No sample contains the target time
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
get length() {
|
|
82
|
+
return this.buffer.length;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
get firstTimestamp() {
|
|
86
|
+
// Defensive read - get current buffer state
|
|
87
|
+
const currentBuffer = this.buffer;
|
|
88
|
+
return currentBuffer[0]?.timestamp || 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
getContents(): MediaSample[] {
|
|
92
|
+
// Defensive copy to avoid concurrent modification during iteration
|
|
93
|
+
return [...this.buffer];
|
|
94
|
+
}
|
|
95
|
+
}
|
package/src/gui/ContextMixin.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { consume, createContext, provide } from "@lit/context";
|
|
2
2
|
import type { LitElement } from "lit";
|
|
3
3
|
import { property, state } from "lit/decorators.js";
|
|
4
|
-
|
|
5
4
|
import type { EFTimegroup } from "../elements/EFTimegroup.js";
|
|
6
5
|
import {
|
|
7
6
|
type EFConfiguration,
|
|
@@ -161,9 +160,6 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
|
|
|
161
160
|
attributes: true,
|
|
162
161
|
});
|
|
163
162
|
|
|
164
|
-
// Preferrably we would use a resizeObserver, but it is difficult to get the first resize
|
|
165
|
-
// timed correctly. So we use requestAnimationFrame as a stop-gap.
|
|
166
|
-
// requestAnimationFrame(this.setStageScale);
|
|
167
163
|
if (this.playing) {
|
|
168
164
|
this.startPlayback();
|
|
169
165
|
}
|
|
@@ -212,7 +208,7 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
|
|
|
212
208
|
|
|
213
209
|
#playbackAudioContext: AudioContext | null = null;
|
|
214
210
|
#playbackAnimationFrameRequest: number | null = null;
|
|
215
|
-
#AUDIO_PLAYBACK_SLICE_MS = 1000;
|
|
211
|
+
#AUDIO_PLAYBACK_SLICE_MS = ((47 * 1024) / 48000) * 1000; // AAC-aligned: ~1002.67ms
|
|
216
212
|
|
|
217
213
|
#syncPlayheadToAudioContext(target: EFTimegroup, startMs: number) {
|
|
218
214
|
const rawTimeMs =
|
|
@@ -237,6 +233,7 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
|
|
|
237
233
|
cancelAnimationFrame(this.#playbackAnimationFrameRequest);
|
|
238
234
|
}
|
|
239
235
|
this.#playbackAudioContext = null;
|
|
236
|
+
this.#playbackAnimationFrameRequest = null;
|
|
240
237
|
}
|
|
241
238
|
|
|
242
239
|
private async startPlayback() {
|
|
@@ -277,7 +274,7 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
|
|
|
277
274
|
await playbackContext.suspend();
|
|
278
275
|
|
|
279
276
|
const fillBuffer = async () => {
|
|
280
|
-
if (bufferCount >
|
|
277
|
+
if (bufferCount > 2) {
|
|
281
278
|
return;
|
|
282
279
|
}
|
|
283
280
|
const canFillBuffer = await queueBufferSource();
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request deduplication utility
|
|
3
|
+
* Manages pending requests to prevent concurrent duplicate requests
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class RequestDeduplicator {
|
|
7
|
+
private pendingRequests = new Map<string, Promise<any>>();
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Execute a request with deduplication
|
|
11
|
+
* If a request with the same key is already pending, return the existing promise
|
|
12
|
+
* Otherwise, execute the request factory and track the promise
|
|
13
|
+
*/
|
|
14
|
+
async executeRequest<T>(
|
|
15
|
+
key: string,
|
|
16
|
+
requestFactory: () => Promise<T>,
|
|
17
|
+
): Promise<T> {
|
|
18
|
+
// Check if there's already a pending request for this key
|
|
19
|
+
const existingRequest = this.pendingRequests.get(key);
|
|
20
|
+
if (existingRequest) {
|
|
21
|
+
return existingRequest;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Create and track the new request
|
|
25
|
+
const requestPromise = requestFactory();
|
|
26
|
+
this.pendingRequests.set(key, requestPromise);
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const result = await requestPromise;
|
|
30
|
+
this.pendingRequests.delete(key);
|
|
31
|
+
return result;
|
|
32
|
+
} catch (error) {
|
|
33
|
+
this.pendingRequests.delete(key);
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Clear all pending requests (used in cache clearing)
|
|
40
|
+
*/
|
|
41
|
+
clear(): void {
|
|
42
|
+
this.pendingRequests.clear();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get number of pending requests
|
|
47
|
+
*/
|
|
48
|
+
getPendingCount(): number {
|
|
49
|
+
return this.pendingRequests.size;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Check if a request is pending
|
|
54
|
+
*/
|
|
55
|
+
isPending(key: string): boolean {
|
|
56
|
+
return this.pendingRequests.has(key);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get all pending request keys
|
|
61
|
+
*/
|
|
62
|
+
getPendingKeys(): string[] {
|
|
63
|
+
return Array.from(this.pendingRequests.keys());
|
|
64
|
+
}
|
|
65
|
+
}
|