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::CacheKey;
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/// #[derive(Clone, Debug, Hash, PartialEq, Eq)]
60/// pub struct TextureId(pub u32);
61///
62/// impl AssetKey for TextureId {
63///     type Asset = ImageData;
64///
65///     fn asset_eq(old: &Self::Asset, new: &Self::Asset) -> bool {
66///         old.bytes == new.bytes
67///     }
68/// }
69/// ```
70pub trait AssetKey: CacheKey + Clone + 'static {
71    /// The asset type this key loads.
72    type Asset: Send + Sync + 'static;
73
74    /// Compare two asset values for equality (for early cutoff).
75    ///
76    /// When an asset is re-resolved with the same value, dependent queries
77    /// can skip recomputation (early cutoff).
78    fn asset_eq(old: &Self::Asset, new: &Self::Asset) -> bool;
79}
80
81/// Result of locating an asset.
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub enum LocateResult<A> {
84    /// Asset is immediately available (e.g., from memory cache).
85    Ready {
86        /// The asset value.
87        value: A,
88        /// The durability level of this asset.
89        durability: DurabilityLevel,
90    },
91    /// Asset needs to be loaded asynchronously.
92    /// The runtime will track this as a pending request.
93    Pending,
94}
95
96/// Trait for locating and loading assets.
97///
98/// Implement this trait to define how assets are found for a given key type.
99/// Different locators can be registered for different platforms:
100/// - Filesystem locator for desktop
101/// - Network locator for web/playground
102/// - Memory locator for testing
103///
104/// # Database Access
105///
106/// The `locate` method receives a database handle, allowing locators to:
107/// - Query configuration to determine loading behavior
108/// - Access other assets as dependencies
109/// - Make dynamic decisions based on runtime state
110///
111/// Any queries or assets accessed during `locate()` are tracked as dependencies
112/// of the calling query.
113///
114/// # Example
115///
116/// ```ignore
117/// struct ConfigAwareLocator;
118///
119/// impl AssetLocator<FilePath> for ConfigAwareLocator {
120///     fn locate(&self, db: &impl Db, key: &FilePath) -> Result<LocateResult<String>, QueryError> {
121///         // Access config to check if path is allowed
122///         let config = db.query(GetConfig)?.clone();
123///         if !config.allowed_paths.contains(&key.0) {
124///             return Err(anyhow::anyhow!("Path not allowed: {:?}", key.0).into());
125///         }
126///
127///         // Return pending for async loading
128///         Ok(LocateResult::Pending)
129///     }
130/// }
131/// ```
132pub trait AssetLocator<K: AssetKey>: Send + Sync + 'static {
133    /// Attempt to locate an asset for the given key.
134    ///
135    /// # Arguments
136    /// * `db` - Database handle for accessing queries and other assets
137    /// * `key` - The asset key to locate
138    ///
139    /// # Returns
140    /// * `Ok(Ready { value, durability })` - Asset is immediately available
141    /// * `Ok(Pending)` - Asset needs async loading (will be added to pending list)
142    /// * `Err(QueryError)` - Location failed (will NOT be added to pending list)
143    ///
144    /// # Dependency Tracking
145    ///
146    /// Any `db.query()` or `db.asset()` calls made during this method
147    /// become dependencies of the query that requested this asset.
148    fn locate(&self, db: &impl Db, key: &K) -> Result<LocateResult<K::Asset>, QueryError>;
149}
150
151/// A pending asset request that needs to be resolved.
152#[derive(Clone)]
153pub struct PendingAsset {
154    /// Type-erased key for the asset (stored as Arc for efficient cloning)
155    key: Arc<dyn Any + Send + Sync>,
156    /// Type ID of the AssetKey type
157    key_type: TypeId,
158    /// Debug representation
159    debug_repr: String,
160}
161
162impl PendingAsset {
163    /// Create a new pending asset.
164    pub fn new<K: AssetKey>(key: K) -> Self {
165        Self {
166            debug_repr: format!("{:?}", key),
167            key_type: TypeId::of::<K>(),
168            key: Arc::new(key),
169        }
170    }
171
172    /// Create from pre-computed parts (used by PendingStorage).
173    pub(crate) fn new_from_parts(
174        key_type: TypeId,
175        debug_repr: &str,
176        key: Arc<dyn Any + Send + Sync>,
177    ) -> Self {
178        Self {
179            key_type,
180            debug_repr: debug_repr.to_string(),
181            key,
182        }
183    }
184
185    /// Downcast the key to its concrete type.
186    pub fn key<K: AssetKey>(&self) -> Option<&K> {
187        if self.key_type == TypeId::of::<K>() {
188            self.key.downcast_ref()
189        } else {
190            None
191        }
192    }
193
194    /// Check if this pending asset is for the given key type.
195    pub fn is<K: AssetKey>(&self) -> bool {
196        self.key_type == TypeId::of::<K>()
197    }
198
199    /// Get the TypeId of the key type.
200    pub fn key_type(&self) -> TypeId {
201        self.key_type
202    }
203
204    /// Get debug representation.
205    pub fn debug_repr(&self) -> &str {
206        &self.debug_repr
207    }
208}
209
210impl Debug for PendingAsset {
211    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
212        write!(f, "PendingAsset({})", self.debug_repr)
213    }
214}