Skip to content

Commit d64142f

Browse files
committed
Split collection observers for data vs state updates
Phase 4 of MetadataService implementation: Enable granular observer control so UI can show loading indicators separately from data refreshes. Changes: - Split `is_relevant_update` into `is_relevant_data_update` and `is_relevant_state_update` in `MetadataCollection` - Add relevance checking methods to `ListMetadataReader` trait: - `get_list_metadata_id()` - get DB rowid for a key - `is_item_row_for_key()` - check if item row belongs to key - `is_state_row_for_list()` - check if state row belongs to list - `get_sync_state()` - get current ListState for a key - Add `sync_state()` method to query current sync state (Idle, FetchingFirstPage, FetchingNextPage, Error) - Add repository methods for relevance checking in `wp_mobile_cache` - Implement all trait methods in `MetadataService` The original `is_relevant_update()` still works (combines both checks) for backwards compatibility. Phase 4.2 (Kotlin wrapper split observers) not included - requires platform-specific updates.
1 parent d8c836b commit d64142f

File tree

5 files changed

+320
-13
lines changed

5 files changed

+320
-13
lines changed

wp_mobile/src/collection/post_metadata_collection.rs

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -186,14 +186,49 @@ impl PostMetadataCollectionWithEditContext {
186186
self.collection.total_pages()
187187
}
188188

189-
/// Check if a database update is relevant to this collection.
189+
/// Get the current sync state for this collection.
190190
///
191-
/// Returns `true` if the update is to a table this collection monitors.
192-
/// Platform layers use this to determine when to notify observers.
191+
/// Returns the current `ListState`:
192+
/// - `Idle` - No sync in progress
193+
/// - `FetchingFirstPage` - Refresh in progress
194+
/// - `FetchingNextPage` - Load more in progress
195+
/// - `Error` - Last sync failed
196+
///
197+
/// Use this to show loading indicators in the UI. Observe state changes
198+
/// via `is_relevant_state_update`.
199+
pub fn sync_state(&self) -> wp_mobile_cache::list_metadata::ListState {
200+
self.collection.sync_state()
201+
}
202+
203+
/// Check if a database update is relevant to this collection (either data or state).
204+
///
205+
/// Returns `true` if the update affects either data or state.
206+
/// For more granular control, use `is_relevant_data_update` or `is_relevant_state_update`.
193207
pub fn is_relevant_update(&self, hook: &UpdateHook) -> bool {
194208
self.collection.is_relevant_update(hook)
195209
}
196210

211+
/// Check if a database update affects this collection's data.
212+
///
213+
/// Returns `true` if the update is to:
214+
/// - An entity table this collection monitors (PostsEditContext, TermRelationships)
215+
/// - The ListMetadataItems table for this collection's key
216+
///
217+
/// Use this for data observers that should refresh list contents.
218+
pub fn is_relevant_data_update(&self, hook: &UpdateHook) -> bool {
219+
self.collection.is_relevant_data_update(hook)
220+
}
221+
222+
/// Check if a database update affects this collection's sync state.
223+
///
224+
/// Returns `true` if the update is to the ListMetadataState table
225+
/// for this collection's specific list.
226+
///
227+
/// Use this for state observers that should update loading indicators.
228+
pub fn is_relevant_state_update(&self, hook: &UpdateHook) -> bool {
229+
self.collection.is_relevant_state_update(hook)
230+
}
231+
197232
/// Get the filter for this collection.
198233
pub fn filter(&self) -> AnyPostFilter {
199234
self.filter.clone()

wp_mobile/src/service/metadata.rs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,16 +302,87 @@ impl MetadataService {
302302
})?;
303303
Ok(())
304304
}
305+
306+
// ============================================
307+
// Relevance checking for update hooks
308+
// ============================================
309+
310+
/// Get the list_metadata_id (rowid) for a given key.
311+
///
312+
/// Returns None if no list exists for this key yet.
313+
/// Used by collections to cache the ID for state update matching.
314+
pub fn get_list_metadata_id(&self, key: &str) -> Option<i64> {
315+
self.cache
316+
.execute(|conn| self.repo.get_list_metadata_id(conn, &self.db_site, key))
317+
.ok()
318+
.flatten()
319+
.map(i64::from) // Convert RowId to i64 for trait interface
320+
}
321+
322+
/// Check if a list_metadata_state row belongs to a specific list_metadata_id.
323+
///
324+
/// Given a rowid from the list_metadata_state table (from an UpdateHook),
325+
/// returns true if that state row belongs to the given list_metadata_id.
326+
pub fn is_state_row_for_list(&self, state_row_id: i64, list_metadata_id: i64) -> bool {
327+
use wp_mobile_cache::RowId;
328+
329+
self.cache
330+
.execute(|conn| {
331+
self.repo
332+
.get_list_metadata_id_for_state_row(conn, RowId::from(state_row_id))
333+
})
334+
.ok()
335+
.flatten()
336+
.is_some_and(|id| i64::from(id) == list_metadata_id)
337+
}
338+
339+
/// Check if a list_metadata_items row belongs to a specific key.
340+
///
341+
/// Given a rowid from the list_metadata_items table (from an UpdateHook),
342+
/// returns true if that item row belongs to this service's site and the given key.
343+
pub fn is_item_row_for_key(&self, item_row_id: i64, key: &str) -> bool {
344+
use wp_mobile_cache::RowId;
345+
346+
self.cache
347+
.execute(|conn| {
348+
self.repo
349+
.is_item_row_for_key(conn, &self.db_site, key, RowId::from(item_row_id))
350+
})
351+
.unwrap_or(false)
352+
}
305353
}
306354

307355
/// Implement ListMetadataReader for database-backed metadata.
308356
///
309357
/// This allows MetadataCollection to read list structure from the database
310358
/// through the same trait interface it uses for in-memory stores.
359+
///
360+
/// Unlike the in-memory implementation, this also supports relevance checking
361+
/// methods for split observers (data vs state updates).
311362
impl ListMetadataReader for MetadataService {
312363
fn get(&self, key: &str) -> Option<Vec<EntityMetadata>> {
313364
self.get_metadata(key).ok().flatten()
314365
}
366+
367+
fn get_list_metadata_id(&self, key: &str) -> Option<i64> {
368+
// Delegate to our existing method
369+
MetadataService::get_list_metadata_id(self, key)
370+
}
371+
372+
fn is_item_row_for_key(&self, item_row_id: i64, key: &str) -> bool {
373+
// Delegate to our existing method
374+
MetadataService::is_item_row_for_key(self, item_row_id, key)
375+
}
376+
377+
fn is_state_row_for_list(&self, state_row_id: i64, list_metadata_id: i64) -> bool {
378+
// Delegate to our existing method
379+
MetadataService::is_state_row_for_list(self, state_row_id, list_metadata_id)
380+
}
381+
382+
fn get_sync_state(&self, key: &str) -> wp_mobile_cache::list_metadata::ListState {
383+
// Delegate to our existing method, default to Idle on error
384+
self.get_state(key).unwrap_or_default()
385+
}
315386
}
316387

317388
/// Pagination info for a list.

wp_mobile/src/sync/list_metadata_store.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,60 @@ use super::EntityMetadata;
77
///
88
/// This trait allows components (like `MetadataCollection`) to read list structure
99
/// without being able to modify it. Only the service layer should write metadata.
10+
///
11+
/// # Relevance Checking
12+
///
13+
/// The trait also provides methods for checking if database update hooks are relevant
14+
/// to a specific collection. These are used to implement split observers for data vs
15+
/// state updates.
16+
///
17+
/// Default implementations return `false` (safe for in-memory stores that don't support
18+
/// these checks). Database-backed implementations override with actual checks.
1019
pub trait ListMetadataReader: Send + Sync {
1120
/// Get the metadata list for a filter key.
1221
///
1322
/// Returns `None` if no metadata has been stored for this key.
1423
fn get(&self, key: &str) -> Option<Vec<EntityMetadata>>;
24+
25+
/// Get the list_metadata_id (database rowid) for a given key.
26+
///
27+
/// Returns `None` if no list exists for this key yet, or if this is an
28+
/// in-memory implementation that doesn't support this operation.
29+
///
30+
/// Used by collections to cache the ID for efficient state update matching.
31+
fn get_list_metadata_id(&self, _key: &str) -> Option<i64> {
32+
None
33+
}
34+
35+
/// Check if a list_metadata_items row belongs to a specific key.
36+
///
37+
/// Given a rowid from the list_metadata_items table (from an UpdateHook),
38+
/// returns true if that item row belongs to the given key.
39+
///
40+
/// Default implementation returns `false` (in-memory stores don't track row IDs).
41+
fn is_item_row_for_key(&self, _item_row_id: i64, _key: &str) -> bool {
42+
false
43+
}
44+
45+
/// Check if a list_metadata_state row belongs to a specific list_metadata_id.
46+
///
47+
/// Given a rowid from the list_metadata_state table (from an UpdateHook),
48+
/// returns true if that state row belongs to the given list_metadata_id.
49+
///
50+
/// Default implementation returns `false` (in-memory stores don't track row IDs).
51+
fn is_state_row_for_list(&self, _state_row_id: i64, _list_metadata_id: i64) -> bool {
52+
false
53+
}
54+
55+
/// Get the current sync state for a list.
56+
///
57+
/// Returns the current `ListState` (Idle, FetchingFirstPage, FetchingNextPage, Error).
58+
/// Used by UI to show loading indicators or error states.
59+
///
60+
/// Default implementation returns `Idle` (in-memory stores don't track state).
61+
fn get_sync_state(&self, _key: &str) -> wp_mobile_cache::list_metadata::ListState {
62+
wp_mobile_cache::list_metadata::ListState::Idle
63+
}
1564
}
1665

1766
/// Store for list metadata (entity IDs + modified timestamps per filter).

wp_mobile/src/sync/metadata_collection.rs

Lines changed: 95 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::sync::{Arc, RwLock};
22

3-
use wp_mobile_cache::UpdateHook;
3+
use wp_mobile_cache::{DbTable, UpdateHook};
44

55
use crate::collection::FetchError;
66

@@ -14,6 +14,13 @@ struct PaginationState {
1414
per_page: u32,
1515
}
1616

17+
/// Cached state for relevance checking.
18+
#[derive(Debug, Default)]
19+
struct RelevanceCache {
20+
/// Cached list_metadata_id (populated lazily on first state relevance check)
21+
list_metadata_id: Option<i64>,
22+
}
23+
1724
/// Collection that uses metadata-first fetching strategy.
1825
///
1926
/// This collection type:
@@ -72,11 +79,14 @@ where
7279
/// Fetcher for metadata and full entities
7380
fetcher: F,
7481

75-
/// Tables to monitor for relevant updates
76-
relevant_tables: Vec<wp_mobile_cache::DbTable>,
82+
/// Tables to monitor for data updates (entity tables like PostsEditContext)
83+
relevant_data_tables: Vec<DbTable>,
7784

7885
/// Pagination state (uses interior mutability for UniFFI compatibility)
7986
pagination: RwLock<PaginationState>,
87+
88+
/// Cached state for relevance checking
89+
relevance_cache: RwLock<RelevanceCache>,
8090
}
8191

8292
impl<F> MetadataCollection<F>
@@ -90,25 +100,26 @@ where
90100
/// * `metadata_reader` - Read-only access to list metadata store
91101
/// * `state_reader` - Read-only access to entity state store
92102
/// * `fetcher` - Implementation for fetching metadata and entities
93-
/// * `relevant_tables` - DB tables to monitor for updates
103+
/// * `relevant_data_tables` - DB tables to monitor for data updates (entity tables)
94104
pub fn new(
95105
kv_key: String,
96106
metadata_reader: Arc<dyn ListMetadataReader>,
97107
state_reader: Arc<dyn EntityStateReader>,
98108
fetcher: F,
99-
relevant_tables: Vec<wp_mobile_cache::DbTable>,
109+
relevant_data_tables: Vec<DbTable>,
100110
) -> Self {
101111
Self {
102112
kv_key,
103113
metadata_reader,
104114
state_reader,
105115
fetcher,
106-
relevant_tables,
116+
relevant_data_tables,
107117
pagination: RwLock::new(PaginationState {
108118
current_page: 0,
109119
total_pages: None,
110120
per_page: 20,
111121
}),
122+
relevance_cache: RwLock::new(RelevanceCache::default()),
112123
}
113124
}
114125

@@ -135,12 +146,86 @@ where
135146
.collect()
136147
}
137148

138-
/// Check if a database update is relevant to this collection.
149+
/// Get the current sync state for this collection.
150+
///
151+
/// Returns the current `ListState`:
152+
/// - `Idle` - No sync in progress
153+
/// - `FetchingFirstPage` - Refresh in progress
154+
/// - `FetchingNextPage` - Load more in progress
155+
/// - `Error` - Last sync failed
156+
///
157+
/// Use this to show loading indicators in the UI.
158+
pub fn sync_state(&self) -> wp_mobile_cache::list_metadata::ListState {
159+
self.metadata_reader.get_sync_state(&self.kv_key)
160+
}
161+
162+
/// Check if a database update is relevant to this collection (either data or state).
139163
///
140-
/// Returns `true` if the update is to a table this collection monitors.
141-
/// Platform layers use this to determine when to notify observers.
164+
/// Returns `true` if the update affects either data or state.
165+
/// For more granular control, use `is_relevant_data_update` or `is_relevant_state_update`.
142166
pub fn is_relevant_update(&self, hook: &UpdateHook) -> bool {
143-
self.relevant_tables.contains(&hook.table)
167+
self.is_relevant_data_update(hook) || self.is_relevant_state_update(hook)
168+
}
169+
170+
/// Check if a database update affects this collection's data.
171+
///
172+
/// Returns `true` if the update is to:
173+
/// - An entity table this collection monitors (e.g., PostsEditContext, TermRelationships)
174+
/// - The ListMetadataItems table for this collection's key
175+
///
176+
/// Use this for data observers that should refresh list contents.
177+
pub fn is_relevant_data_update(&self, hook: &UpdateHook) -> bool {
178+
// Check entity tables
179+
if self.relevant_data_tables.contains(&hook.table) {
180+
return true;
181+
}
182+
183+
// Check ListMetadataItems for this specific key
184+
if hook.table == DbTable::ListMetadataItems {
185+
return self
186+
.metadata_reader
187+
.is_item_row_for_key(hook.row_id, &self.kv_key);
188+
}
189+
190+
false
191+
}
192+
193+
/// Check if a database update affects this collection's sync state.
194+
///
195+
/// Returns `true` if the update is to the ListMetadataState table
196+
/// for this collection's specific list.
197+
///
198+
/// Use this for state observers that should update loading indicators.
199+
pub fn is_relevant_state_update(&self, hook: &UpdateHook) -> bool {
200+
if hook.table != DbTable::ListMetadataState {
201+
return false;
202+
}
203+
204+
// Get or cache the list_metadata_id
205+
let list_metadata_id = {
206+
let cache = self.relevance_cache.read().unwrap();
207+
cache.list_metadata_id
208+
};
209+
210+
let list_metadata_id = match list_metadata_id {
211+
Some(id) => id,
212+
None => {
213+
// Try to get from database
214+
let id = self.metadata_reader.get_list_metadata_id(&self.kv_key);
215+
if let Some(id) = id {
216+
// Cache for next time
217+
self.relevance_cache.write().unwrap().list_metadata_id = Some(id);
218+
id
219+
} else {
220+
// List doesn't exist yet, so this state update isn't for us
221+
return false;
222+
}
223+
}
224+
};
225+
226+
// Check if the state row belongs to our list
227+
self.metadata_reader
228+
.is_state_row_for_list(hook.row_id, list_metadata_id)
144229
}
145230

146231
/// Refresh the collection (fetch page 1, replace metadata).

0 commit comments

Comments
 (0)