Skip to content

Commit abfbd8c

Browse files
committed
desktop-app: Observe track search state
1 parent f9acb92 commit abfbd8c

File tree

6 files changed

+170
-37
lines changed

6 files changed

+170
-37
lines changed

crates/desktop-app/src/track/repo_search/mod.rs

+122-10
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ pub struct FetchedEntity {
2828
pub entity: Entity,
2929
}
3030

31+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
32+
pub struct FetchedEntitiesFingerprint {
33+
pub len: usize,
34+
pub last_offset_hash: u64,
35+
}
36+
3137
#[must_use]
3238
pub fn last_offset_hash_of_fetched_entities<'a>(
3339
fetched_entities: impl Into<Option<&'a [FetchedEntity]>>,
@@ -40,6 +46,32 @@ pub fn last_offset_hash_of_fetched_entities<'a>(
4046
})
4147
}
4248

49+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
50+
pub enum FetchStateTag {
51+
#[default]
52+
Initial,
53+
Ready,
54+
Pending,
55+
Failed,
56+
}
57+
58+
impl FetchStateTag {
59+
/// Check whether the state is stable.
60+
#[must_use]
61+
pub const fn is_idle(&self) -> bool {
62+
match self {
63+
Self::Initial | Self::Ready | Self::Failed => true,
64+
Self::Pending => false,
65+
}
66+
}
67+
}
68+
69+
#[derive(Debug, Clone, Default, PartialEq, Eq)]
70+
pub struct FetchStateLite {
71+
pub tag: FetchStateTag,
72+
pub fetched_entities_fingerprint: Option<FetchedEntitiesFingerprint>,
73+
}
74+
4375
#[derive(Debug, Default)]
4476
enum FetchState {
4577
#[default]
@@ -53,19 +85,42 @@ enum FetchState {
5385
},
5486
Failed {
5587
fetched_entities_before: Option<Vec<FetchedEntity>>,
56-
err: anyhow::Error,
88+
error: anyhow::Error,
5789
},
5890
}
5991

6092
impl FetchState {
6193
#[must_use]
62-
const fn is_idle(&self) -> bool {
94+
const fn state_tag(&self) -> FetchStateTag {
6395
match self {
64-
Self::Initial | Self::Ready { .. } | Self::Failed { .. } => true,
65-
Self::Pending { .. } => false,
96+
Self::Initial => FetchStateTag::Initial,
97+
Self::Ready { .. } => FetchStateTag::Ready,
98+
Self::Failed { .. } => FetchStateTag::Failed,
99+
Self::Pending { .. } => FetchStateTag::Pending,
100+
}
101+
}
102+
103+
#[must_use]
104+
fn clone_lite(&self) -> FetchStateLite {
105+
let tag = self.state_tag();
106+
let fetched_entities_fingerprint = self.fetched_entities_fingerprint();
107+
FetchStateLite {
108+
tag,
109+
fetched_entities_fingerprint,
66110
}
67111
}
68112

113+
#[must_use]
114+
fn equals_lite(&self, other: &FetchStateLite) -> bool {
115+
self.clone_lite() == *other
116+
}
117+
118+
/// Check whether the state is stable.
119+
#[must_use]
120+
const fn is_idle(&self) -> bool {
121+
self.state_tag().is_idle()
122+
}
123+
69124
#[must_use]
70125
fn fetched_entities(&self) -> Option<&[FetchedEntity]> {
71126
match self {
@@ -84,6 +139,17 @@ impl FetchState {
84139
}
85140
}
86141

142+
#[must_use]
143+
fn fetched_entities_fingerprint(&self) -> Option<FetchedEntitiesFingerprint> {
144+
let fetched_entities = self.fetched_entities()?;
145+
let len = fetched_entities.len();
146+
let last_offset_hash = last_offset_hash_of_fetched_entities(fetched_entities);
147+
Some(FetchedEntitiesFingerprint {
148+
len,
149+
last_offset_hash,
150+
})
151+
}
152+
87153
#[must_use]
88154
const fn should_prefetch(&self) -> bool {
89155
matches!(self, Self::Initial)
@@ -92,8 +158,8 @@ impl FetchState {
92158
#[must_use]
93159
const fn can_fetch_more(&self) -> Option<bool> {
94160
match self {
95-
Self::Initial => Some(true), // always
96-
Self::Pending { .. } | Self::Failed { .. } => None, // undefined
161+
Self::Initial => Some(true), // always, i.e. try at least once
162+
Self::Pending { .. } | Self::Failed { .. } => None, // undefined
97163
Self::Ready { can_fetch_more, .. } => Some(*can_fetch_more), // maybe
98164
}
99165
}
@@ -102,7 +168,7 @@ impl FetchState {
102168
const fn last_error(&self) -> Option<&anyhow::Error> {
103169
match self {
104170
Self::Initial | Self::Pending { .. } | Self::Ready { .. } => None,
105-
Self::Failed { err, .. } => Some(err),
171+
Self::Failed { error, .. } => Some(error),
106172
}
107173
}
108174

@@ -189,8 +255,8 @@ impl FetchState {
189255
}
190256

191257
#[allow(clippy::needless_pass_by_value)]
192-
fn fetch_more_failed(&mut self, err: anyhow::Error) -> bool {
193-
log::warn!("Fetching failed: {err}");
258+
fn fetch_more_failed(&mut self, error: anyhow::Error) -> bool {
259+
log::warn!("Fetching failed: {error}");
194260
let Self::Pending {
195261
fetched_entities_before,
196262
} = self
@@ -202,12 +268,19 @@ impl FetchState {
202268
let fetched_entities_before = std::mem::take(fetched_entities_before);
203269
*self = Self::Failed {
204270
fetched_entities_before,
205-
err,
271+
error,
206272
};
207273
true
208274
}
209275
}
210276

277+
#[derive(Debug, Clone, Default, PartialEq)]
278+
pub struct StateLite {
279+
pub default_params: Params,
280+
pub context: Context,
281+
pub fetch: FetchStateLite,
282+
}
283+
211284
#[derive(Debug)]
212285
pub struct State {
213286
default_params: Params,
@@ -271,6 +344,45 @@ impl State {
271344
self.fetch.fetched_entities()
272345
}
273346

347+
#[must_use]
348+
pub fn fetched_entities_fingerprint(&self) -> Option<FetchedEntitiesFingerprint> {
349+
self.fetch.fetched_entities_fingerprint()
350+
}
351+
352+
#[must_use]
353+
pub fn clone_lite(&self) -> StateLite {
354+
let Self {
355+
default_params,
356+
context,
357+
fetch,
358+
} = self;
359+
let default_params = default_params.clone();
360+
let context = context.clone();
361+
let fetch = fetch.clone_lite();
362+
StateLite {
363+
default_params,
364+
context,
365+
fetch,
366+
}
367+
}
368+
369+
#[must_use]
370+
pub fn equals_lite(&self, other: &StateLite) -> bool {
371+
let Self {
372+
default_params,
373+
context,
374+
fetch,
375+
} = self;
376+
let StateLite {
377+
default_params: other_default_params,
378+
context: other_context,
379+
fetch: other_fetch,
380+
} = other;
381+
default_params == other_default_params
382+
&& context == other_context
383+
&& fetch.equals_lite(other_fetch)
384+
}
385+
274386
pub fn reset(&mut self) -> bool {
275387
// Cloning the default params once for pre-creating the target state
276388
// is required to avoid redundant code for determining in advance if

demo-app/src/library/collection.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@ where
2828
{
2929
// The first event is always emitted immediately.
3030
loop {
31+
drop(subscriber.read_ack());
3132
let Some(event_emitter) = event_emitter.upgrade() else {
3233
log::info!("Stop watching collection state after event emitter has been dropped");
3334
break;
3435
};
35-
drop(subscriber.read_ack());
3636
event_emitter.emit_notification(LibraryNotification::CollectionStateChanged);
3737
if subscriber.changed().await.is_err() {
3838
log::info!("Stop watching collection state after publisher has been dropped");

demo-app/src/library/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ const TRACK_REPO_SEARCH_PREFETCH_LIMIT: NonZeroUsize =
3535
#[derive(Debug)]
3636
#[allow(clippy::enum_variant_names)] // common `...Changed` suffix
3737
pub enum LibraryNotification {
38-
SettingsStateChanged(settings::State),
38+
SettingsStateChanged,
3939
CollectionStateChanged,
4040
TrackSearchStateChanged,
4141
}

demo-app/src/library/settings.rs

+2-3
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,12 @@ where
1818
{
1919
// The first event is always emitted immediately.
2020
loop {
21+
drop(subscriber.read_ack());
2122
let Some(event_emitter) = event_emitter.upgrade() else {
2223
log::info!("Stop watching settings state after event emitter has been dropped");
2324
break;
2425
};
25-
// The lock is released immediately after cloning the state.
26-
let state = subscriber.read_ack().clone();
27-
event_emitter.emit_notification(LibraryNotification::SettingsStateChanged(state.clone()));
26+
event_emitter.emit_notification(LibraryNotification::SettingsStateChanged);
2827
if subscriber.changed().await.is_err() {
2928
log::info!("Stop watching settings state after publisher has been dropped");
3029
break;

demo-app/src/library/track_search.rs

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ where
3232
{
3333
// The first event is always emitted immediately.
3434
loop {
35+
drop(subscriber.read_ack());
3536
let Some(event_emitter) = event_emitter.upgrade() else {
3637
log::info!("Stop watching track search state after event emitter has been dropped");
3738
break;

demo-app/src/main.rs

+43-22
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,7 @@ struct AppModel {
328328
music_dir: Option<DirPath<'static>>,
329329
collection_state: CollectionState,
330330
track_search_input: String,
331+
track_search_state: aoide::desktop_app::track::repo_search::StateLite,
331332
}
332333

333334
impl AppModel {
@@ -337,6 +338,7 @@ impl AppModel {
337338
music_dir: Default::default(),
338339
collection_state: Default::default(),
339340
track_search_input: Default::default(),
341+
track_search_state: Default::default(),
340342
}
341343
}
342344
}
@@ -390,41 +392,60 @@ impl Model for AppModel {
390392
},
391393
AppEvent::Notification(notification) => match notification {
392394
AppNotification::Library(library) => match library {
393-
LibraryNotification::SettingsStateChanged(state) => {
394-
if state.music_dir == self.music_dir {
395-
log::info!(
396-
"Music directory unchanged: {music_dir:?}",
397-
music_dir = self.music_dir
398-
);
399-
return;
400-
}
401-
let new_music_dir = state.music_dir.clone();
402-
log::info!(
395+
LibraryNotification::SettingsStateChanged => {
396+
let new_music_dir = {
397+
let settings_state = self.app.library.state().settings().read_observable();
398+
if settings_state.music_dir == self.music_dir {
399+
log::debug!(
400+
"Music directory unchanged: {music_dir:?}",
401+
music_dir = self.music_dir,
402+
);
403+
return;
404+
}
405+
settings_state.music_dir.clone()
406+
};
407+
log::debug!(
403408
"Music directory changed: {old_music_dir:?} -> {new_music_dir:?}",
404409
old_music_dir = self.music_dir,
405410
);
406411
self.music_dir = new_music_dir;
407412
}
408413
LibraryNotification::CollectionStateChanged => {
409-
let new_collection_state = {
410-
let new_collection_state = self.app.library.state().collection().read_observable();
411-
if *new_collection_state == self.collection_state {
412-
log::info!(
413-
"Collection state unchanged: {old_collection_state:?}",
414-
old_collection_state = self.collection_state,
414+
let new_state = {
415+
let new_state = self.app.library.state().collection().read_observable();
416+
if *new_state == self.collection_state {
417+
log::debug!(
418+
"Collection state unchanged: {old_state:?}",
419+
old_state = self.collection_state,
415420
);
416421
return;
417422
}
418-
new_collection_state.clone()
423+
new_state.clone()
419424
};
420-
log::info!(
421-
"Collection state changed: {old_collection_state:?} -> {new_collection_state:?}",
422-
old_collection_state = self.collection_state,
425+
log::debug!(
426+
"Collection state changed: {old_state:?} -> {new_state:?}",
427+
old_state = self.collection_state,
423428
);
424-
self.collection_state = new_collection_state;
429+
self.collection_state = new_state;
425430
}
426431
LibraryNotification::TrackSearchStateChanged => {
427-
log::warn!("TODO: Track search state changed");
432+
let new_state = {
433+
let new_state = self.app.library.state().track_search().read_observable();
434+
if new_state.equals_lite(&self.track_search_state) {
435+
log::debug!(
436+
"Track search state unchanged: {old_state:?}",
437+
old_state = self.track_search_state,
438+
);
439+
return;
440+
}
441+
new_state.clone_lite()
442+
};
443+
log::debug!(
444+
"Track search state changed: {old_state:?} -> {new_state:?}",
445+
old_state = self.track_search_state,
446+
);
447+
self.track_search_state = new_state;
448+
log::warn!("TODO: Show fetched entities");
428449
}
429450
},
430451
},

0 commit comments

Comments
 (0)