Skip to main content

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