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::key::Key;
14
15/// Named durability levels for assets and queries.
16///
17/// Higher values indicate the data changes less frequently.
18/// This is used for optimization in the dependency tracking layer.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
20#[repr(u8)]
21pub enum DurabilityLevel {
22 /// Changes frequently (live feeds, real-time data).
23 #[default]
24 Volatile = 0,
25 /// Stable within a session.
26 Session = 1,
27 /// Changes rarely.
28 Stable = 2,
29 /// Never changes (bundled assets, constants).
30 Constant = 3,
31}
32
33impl DurabilityLevel {
34 /// Convert to u8 for whale integration.
35 pub fn as_u8(self) -> u8 {
36 self as u8
37 }
38}
39
40/// Trait for asset keys that map to loadable assets.
41///
42/// Asset keys identify external resources (files, URLs, etc.) and define
43/// the type of asset they load. Assets are leaf nodes in the dependency
44/// graph - they have no dependencies but can be depended upon by queries.
45///
46/// # Example
47///
48/// ```ignore
49/// use query_flow::{asset_key, AssetKey, DurabilityLevel};
50///
51/// #[asset_key(asset = String)]
52/// pub struct ConfigFile(pub PathBuf);
53///
54/// #[asset_key(asset = String, durability = constant)]
55/// pub struct BundledFile(pub PathBuf);
56///
57/// // Or manually:
58/// pub struct TextureId(pub u32);
59///
60/// impl AssetKey for TextureId {
61/// type Asset = ImageData;
62///
63/// fn asset_eq(old: &Self::Asset, new: &Self::Asset) -> bool {
64/// old.bytes == new.bytes
65/// }
66///
67/// fn durability(&self) -> DurabilityLevel {
68/// DurabilityLevel::Constant
69/// }
70/// }
71/// ```
72pub trait AssetKey: Key + 'static {
73 /// The asset type this key loads.
74 type Asset: Send + Sync + 'static;
75
76 /// Compare two asset values for equality (for early cutoff).
77 ///
78 /// When an asset is re-resolved with the same value, dependent queries
79 /// can skip recomputation (early cutoff).
80 fn asset_eq(old: &Self::Asset, new: &Self::Asset) -> bool;
81
82 /// Durability level for this asset type.
83 ///
84 /// Higher values indicate the asset changes less frequently.
85 /// Default: `Volatile` (changes frequently).
86 fn durability(&self) -> DurabilityLevel {
87 DurabilityLevel::Volatile
88 }
89}
90
91/// Result of locating an asset.
92#[derive(Debug, Clone, PartialEq, Eq)]
93pub enum LocateResult<A> {
94 /// Asset is immediately available (e.g., from memory cache).
95 Ready(A),
96 /// Asset needs to be loaded asynchronously.
97 /// The runtime will track this as a pending request.
98 Pending,
99 /// Asset does not exist or cannot be located.
100 NotFound,
101}
102
103/// Trait for locating and loading assets.
104///
105/// Implement this trait to define how assets are found for a given key type.
106/// Different locators can be registered for different platforms:
107/// - Filesystem locator for desktop
108/// - Network locator for web/playground
109/// - Memory locator for testing
110///
111/// # Example
112///
113/// ```ignore
114/// struct FileSystemLocator {
115/// base_path: PathBuf,
116/// }
117///
118/// impl AssetLocator<FilePath> for FileSystemLocator {
119/// fn locate(&self, key: &FilePath) -> LocateResult<String> {
120/// // For sync IO, could read directly:
121/// // let path = self.base_path.join(&key.0);
122/// // match std::fs::read_to_string(&path) {
123/// // Ok(content) => LocateResult::Ready(content),
124/// // Err(_) => LocateResult::NotFound,
125/// // }
126///
127/// // For async IO, return Pending:
128/// 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 /// This method should be fast and non-blocking:
136 /// - Return `Ready(value)` if the asset is immediately available
137 /// - Return `Pending` if the asset needs async loading
138 /// - Return `NotFound` if the asset cannot be found
139 ///
140 /// For assets requiring IO, typically return `Pending` and let the user
141 /// fetch the asset externally, then call `runtime.resolve_asset()`.
142 fn locate(&self, key: &K) -> LocateResult<K::Asset>;
143}
144
145/// A pending asset request that needs to be resolved.
146pub struct PendingAsset {
147 /// Type-erased key for the asset (stored as Arc for efficient cloning)
148 key: Arc<dyn Any + Send + Sync>,
149 /// Type ID of the AssetKey type
150 key_type: TypeId,
151 /// Debug representation
152 debug_repr: String,
153}
154
155impl PendingAsset {
156 /// Create a new pending asset.
157 #[allow(dead_code)] // Reserved for direct construction if needed
158 pub(crate) fn new<K: AssetKey>(key: K) -> Self {
159 Self {
160 debug_repr: format!("{:?}", key),
161 key_type: TypeId::of::<K>(),
162 key: Arc::new(key),
163 }
164 }
165
166 /// Create from pre-computed parts (used by PendingStorage).
167 pub(crate) fn new_from_parts(
168 key_type: TypeId,
169 debug_repr: &str,
170 key: Arc<dyn Any + Send + Sync>,
171 ) -> Self {
172 Self {
173 key_type,
174 debug_repr: debug_repr.to_string(),
175 key,
176 }
177 }
178
179 /// Downcast the key to its concrete type.
180 pub fn key<K: AssetKey>(&self) -> Option<&K> {
181 if self.key_type == TypeId::of::<K>() {
182 self.key.downcast_ref()
183 } else {
184 None
185 }
186 }
187
188 /// Check if this pending asset is for the given key type.
189 pub fn is<K: AssetKey>(&self) -> bool {
190 self.key_type == TypeId::of::<K>()
191 }
192
193 /// Get the TypeId of the key type.
194 pub fn key_type(&self) -> TypeId {
195 self.key_type
196 }
197
198 /// Get debug representation.
199 pub fn debug_repr(&self) -> &str {
200 &self.debug_repr
201 }
202}
203
204impl Debug for PendingAsset {
205 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
206 write!(f, "PendingAsset({})", self.debug_repr)
207 }
208}
209
210/// Full cache key for assets (includes AssetKey type information).
211///
212/// This is similar to `FullCacheKey` for queries but marks the entry
213/// as an asset in the dependency graph.
214#[derive(Clone)]
215pub(crate) struct FullAssetKey {
216 /// Type ID of the AssetKey type
217 key_type: TypeId,
218 /// Hash of the key value
219 key_hash: u64,
220 /// Debug representation
221 debug_repr: Arc<str>,
222}
223
224impl FullAssetKey {
225 /// Create a new full asset key.
226 pub fn new<K: AssetKey>(key: &K) -> Self {
227 use std::hash::Hasher;
228 let mut hasher = ahash::AHasher::default();
229 key.hash(&mut hasher);
230 let key_hash = hasher.finish();
231
232 Self {
233 key_type: TypeId::of::<K>(),
234 key_hash,
235 debug_repr: Arc::from(format!("Asset:{}({:?})", std::any::type_name::<K>(), key)),
236 }
237 }
238
239 /// Get debug representation for error messages.
240 pub fn debug_repr(&self) -> &str {
241 &self.debug_repr
242 }
243
244 /// Get the key type.
245 pub fn key_type(&self) -> TypeId {
246 self.key_type
247 }
248
249 /// Get the key hash.
250 pub fn key_hash(&self) -> u64 {
251 self.key_hash
252 }
253}
254
255impl Debug for FullAssetKey {
256 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
257 write!(f, "{}", self.debug_repr)
258 }
259}
260
261impl std::hash::Hash for FullAssetKey {
262 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
263 self.key_type.hash(state);
264 self.key_hash.hash(state);
265 }
266}
267
268impl PartialEq for FullAssetKey {
269 fn eq(&self, other: &Self) -> bool {
270 self.key_type == other.key_type && self.key_hash == other.key_hash
271 }
272}
273
274impl Eq for FullAssetKey {}