1use std::num::NonZeroUsize;
2
3use bon::Builder;
4use kithara_assets::AssetStore;
5use kithara_bufpool::HasPool;
6use kithara_derive::Patch;
7use kithara_platform::{CancelToken, tokio::runtime::Handle as RuntimeHandle};
8use kithara_play::{CrossfadeSettings, PlayerImpl};
9
10use crate::{ActionAtItemEnd, PlaybackOrder};
11
12pub(crate) const DEFAULT_MAX_CONCURRENT_LOADS: NonZeroUsize = match NonZeroUsize::new(3) {
14 Some(n) => n,
15 None => unreachable!(),
16};
17
18pub(crate) const DEFAULT_PREFETCH_DURATION: f32 = 3.5;
22
23#[derive(Builder, derive_more::Debug, Patch)]
32#[builder(state_mod(vis = "pub"))]
33#[non_exhaustive]
34pub struct QueueConfig<S>
35where
36 S: HasPool<u8> + HasPool<f32> + Send + Sync + 'static,
37{
38 #[builder(default)]
39 pub action_at_item_end: ActionAtItemEnd,
40
41 #[builder(default)]
42 pub crossfade_settings: CrossfadeSettings,
43
44 #[builder(default = DEFAULT_MAX_CONCURRENT_LOADS)]
46 pub max_concurrent_loads: NonZeroUsize,
47
48 #[patch(skip)]
53 #[debug(skip)]
54 pub cancel: Option<CancelToken>,
55
56 #[patch(skip)]
58 #[debug(skip)]
59 pub store: Option<AssetStore<S>>,
60
61 #[builder(default)]
62 pub playback_order: PlaybackOrder,
63
64 #[patch(skip)]
69 #[debug(skip)]
70 pub runtime: Option<RuntimeHandle>,
71
72 #[patch(skip)]
74 #[debug(skip)]
75 pub player: PlayerImpl<S>,
76
77 #[builder(default = false)]
82 #[patch(skip)]
83 pub should_autoplay: bool,
84
85 #[builder(default = DEFAULT_PREFETCH_DURATION)]
92 pub prefetch_duration: f32,
93
94 #[builder(default = 100)]
98 pub max_history_size: usize,
99}
100
101#[cfg(test)]
102mod tests {
103 use kithara_play::{PlayWorker, PlayWorkerConfig, PlayerConfig};
104 use kithara_test_utils::kithara;
105
106 use super::*;
107 use crate::{
108 queue::{TEST_SAMPLE_RATE, test_session},
109 test_pools::pools,
110 };
111
112 pub(super) fn config() -> QueueConfig<crate::test_pools::TestPools> {
113 let worker = PlayWorker::new(PlayWorkerConfig::builder(pools()).build());
114 let player = PlayerImpl::new(
115 PlayerConfig::builder()
116 .sample_rate(TEST_SAMPLE_RATE)
117 .worker(worker)
118 .session(test_session())
119 .build(),
120 );
121 QueueConfig::builder().player(player).build()
122 }
123
124 #[kithara::test]
125 fn default_config_has_reasonable_loader_cap() {
126 let cfg = config();
127
128 assert_eq!(cfg.max_concurrent_loads.get(), 3);
129 assert!(cfg.store.is_none());
130 assert!((cfg.prefetch_duration - 3.5).abs() < f32::EPSILON);
131 }
132}
133
134#[cfg(all(test, not(target_arch = "wasm32")))]
135mod document_tests {
136 use kithara_test_utils::kithara;
137
138 use super::{QueueConfigPatch, tests::config};
139
140 #[kithara::test(native, flash(false))]
141 fn a_document_sets_the_load_cap_and_leaves_the_history_size() {
142 let patch: QueueConfigPatch =
143 serde_yaml_ng::from_str("max_concurrent_loads: 5\n").expect("the document types");
144 let mut config = config();
147 config.max_history_size = 37;
148
149 config.apply(patch);
150
151 assert_eq!(config.max_concurrent_loads.get(), 5);
152 assert_eq!(
153 config.max_history_size, 37,
154 "a key the document does not name must keep its seeded value"
155 );
156 }
157
158 #[kithara::test(native, flash(false))]
161 fn an_unknown_field_is_rejected_and_named() {
162 let error = serde_yaml_ng::from_str::<QueueConfigPatch>("concurrent_load_cap: 5\n")
163 .expect_err("a typo must not be silently ignored");
164
165 assert!(error.to_string().contains("concurrent_load_cap"), "{error}");
166 }
167}