query_flow/
asset.rs

1//! Asset types for external resources.
2//!
3//! Assets are external inputs (files, network resources, etc.) that:
4//! - Are always leaves in the dependency graph (no dependencies)
5//! - May need IO to load
6//! - Loading differs by platform (filesystem locally, network/memory in playground)
7//! - Can be depended upon by queries with proper dependency tracking
8
9use std::any::{Any, TypeId};
10use std::fmt::Debug;
11use std::sync::Arc;
12
13use crate::db::Db;
14use crate::error::QueryError;
15use crate::key::Key;
16
17/// Durability levels for dependency tracking optimization.
18///
19/// Higher values indicate the data changes less frequently.
20/// Durability is specified when resolving assets, not on the type itself.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
22#[repr(u8)]
23pub enum DurabilityLevel {
24    /// Changes frequently (user input, live feeds).
25    #[default]
26    Volatile = 0,
27    /// Changes occasionally (configuration, session data).
28    Transient = 1,
29    /// Changes rarely (external dependencies).
30    Stable = 2,
31    /// Fixed for this session (bundled assets, constants).
32    Static = 3,
33}
34
35impl DurabilityLevel {
36    /// Convert to u8 for whale integration.
37    pub fn as_u8(self) -> u8 {
38        self as u8
39    }
40}
41
42/// Trait for asset keys that map to loadable assets.
43///
44/// Asset keys identify external resources (files, URLs, etc.) and define
45/// the type of asset they load. Assets are leaf nodes in the dependency
46/// graph - they have no dependencies but can be depended upon by queries.
47///
48/// Durability is specified when calling `resolve_asset()`, not on the key type.
49///
50/// # Example
51///
52/// ```ignore
53/// use query_flow::{asset_key, AssetKey};
54///
55/// #[asset_key(asset = String)]
56/// pub struct ConfigFile(pub PathBuf);
57///
58/// // Or manually:
59/// pub struct TextureId(pub u32);
60///
61/// impl AssetKey for TextureId {
62///     type Asset = ImageData;
63///
64///     fn asset_eq(old: &Self::Asset, new: &Self::Asset) -> bool {
65///         old.bytes == new.bytes
66///     }
67/// }
68/// ```
69pub trait AssetKey: Key + 'static {
70    /// The asset type this key loads.
71    type Asset: Send + Sync + 'static;
72
73    /// Compare two asset values for equality (for early cutoff).
74    ///
75    /// When an asset is re-resolved with the same value, dependent queries
76    /// can skip recomputation (early cutoff).
77    fn asset_eq(old: &Self::Asset, new: &Self::Asset) -> bool;
78}
79
80/// Result of locating an asset.
81#[derive(Debug, Clone, PartialEq, Eq)]
82pub enum LocateResult<A> {
83    /// Asset is immediately available (e.g., from memory cache).
84    Ready {
85        /// The asset value.
86        value: A,
87        /// The durability level of this asset.
88        durability: DurabilityLevel,
89    },
90    /// Asset needs to be loaded asynchronously.
91    /// The runtime will track this as a pending request.
92    Pending,
93}
94
95/// Trait for locating and loading assets.
96///
97/// Implement this trait to define how assets are found for a given key type.
98/// Different locators can be registered for different platforms:
99/// - Filesystem locator for desktop
100/// - Network locator for web/playground
101/// - Memory locator for testing
102///
103/// # Database Access
104///
105/// The `locate` method receives a database handle, allowing locators to:
106/// - Query configuration to determine loading behavior
107/// - Access other assets as dependencies
108/// - Make dynamic decisions based on runtime state
109///
110/// Any queries or assets accessed during `locate()` are tracked as dependencies
111/// of the calling query.
112///
113/// # Example
114///
115/// ```ignore
116/// struct ConfigAwareLocator;
117///
118/// impl AssetLocator<FilePath> for ConfigAwareLocator {
119///     fn locate(&self, db: &impl Db, key: &FilePath) -> Result<LocateResult<String>, QueryError> {
120///         // Access config to check if path is allowed
121///         let config = db.query(GetConfig)?.clone();
122///         if !config.allowed_paths.contains(&key.0) {
123///             return Err(QueryError::MissingDependency {
124///                 description: format!("Path not allowed: {:?}", key.0),
125///             });
126///         }
127///
128///         // Return pending for async loading
129///         Ok(LocateResult::Pending)
130///     }
131/// }
132/// ```
133pub trait AssetLocator<K: AssetKey>: Send + Sync + 'static {
134    /// Attempt to locate an asset for the given key.
135    ///
136    /// # Arguments
137    /// * `db` - Database handle for accessing queries and other assets
138    /// * `key` - The asset key to locate
139    ///
140    /// # Returns
141    /// * `Ok(Ready { value, durability })` - Asset is immediately available
142    /// * `Ok(Pending)` - Asset needs async loading (will be added to pending list)
143    /// * `Err(QueryError)` - Location failed (will NOT be added to pending list)
144    ///
145    /// # Dependency Tracking
146    ///
147    /// Any `db.query()` or `db.asset()` calls made during this method
148    /// become dependencies of the query that requested this asset.
149    fn locate(&self, db: &impl Db, key: &K) -> Result<LocateResult<K::Asset>, QueryError>;
150}
151
152/// A pending asset request that needs to be resolved.
153#[derive(Clone)]
154pub struct PendingAsset {
155    /// Type-erased key for the asset (stored as Arc for efficient cloning)
156    key: Arc<dyn Any + Send + Sync>,
157    /// Type ID of the AssetKey type
158    key_type: TypeId,
159    /// Debug representation
160    debug_repr: String,
161}
162
163impl PendingAsset {
164    /// Create a new pending asset.
165    pub fn new<K: AssetKey>(key: K) -> Self {
166        Self {
167            debug_repr: format!("{:?}", key),
168            key_type: TypeId::of::<K>(),
169            key: Arc::new(key),
170        }
171    }
172
173    /// Create from pre-computed parts (used by PendingStorage).
174    pub(crate) fn new_from_parts(
175        key_type: TypeId,
176        debug_repr: &str,
177        key: Arc<dyn Any + Send + Sync>,
178    ) -> Self {
179        Self {
180            key_type,
181            debug_repr: debug_repr.to_string(),
182            key,
183        }
184    }
185
186    /// Downcast the key to its concrete type.
187    pub fn key<K: AssetKey>(&self) -> Option<&K> {
188        if self.key_type == TypeId::of::<K>() {
189            self.key.downcast_ref()
190        } else {
191            None
192        }
193    }
194
195    /// Check if this pending asset is for the given key type.
196    pub fn is<K: AssetKey>(&self) -> bool {
197        self.key_type == TypeId::of::<K>()
198    }
199
200    /// Get the TypeId of the key type.
201    pub fn key_type(&self) -> TypeId {
202        self.key_type
203    }
204
205    /// Get debug representation.
206    pub fn debug_repr(&self) -> &str {
207        &self.debug_repr
208    }
209}
210
211impl Debug for PendingAsset {
212    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213        write!(f, "PendingAsset({})", self.debug_repr)
214    }
215}
216
217/// Full cache key for assets (includes AssetKey type information).
218///
219/// This is similar to `FullCacheKey` for queries but marks the entry
220/// as an asset in the dependency graph.
221#[derive(Clone)]
222pub(crate) struct FullAssetKey {
223    /// Type ID of the AssetKey type
224    key_type: TypeId,
225    /// Hash of the key value
226    key_hash: u64,
227    /// Debug representation
228    debug_repr: Arc<str>,
229}
230
231impl FullAssetKey {
232    /// Create a new full asset key.
233    pub fn new<K: AssetKey>(key: &K) -> Self {
234        use std::hash::Hasher;
235        let mut hasher = ahash::AHasher::default();
236        key.hash(&mut hasher);
237        let key_hash = hasher.finish();
238
239        Self {
240            key_type: TypeId::of::<K>(),
241            key_hash,
242            debug_repr: Arc::from(format!("Asset:{}({:?})", std::any::type_name::<K>(), key)),
243        }
244    }
245
246    /// Get debug representation for error messages.
247    pub fn debug_repr(&self) -> &str {
248        &self.debug_repr
249    }
250
251    /// Get the key type.
252    pub fn key_type(&self) -> TypeId {
253        self.key_type
254    }
255
256    /// Get the key hash.
257    pub fn key_hash(&self) -> u64 {
258        self.key_hash
259    }
260}
261
262impl Debug for FullAssetKey {
263    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
264        write!(f, "{}", self.debug_repr)
265    }
266}
267
268impl std::hash::Hash for FullAssetKey {
269    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
270        self.key_type.hash(state);
271        self.key_hash.hash(state);
272    }
273}
274
275impl PartialEq for FullAssetKey {
276    fn eq(&self, other: &Self) -> bool {
277        self.key_type == other.key_type && self.key_hash == other.key_hash
278    }
279}
280
281impl Eq for FullAssetKey {}