khora_data/assets/storage.rs
1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! A generic, type-safe storage for loaded asset handles.
16
17use khora_core::asset::{Asset, AssetHandle, AssetUUID};
18use std::collections::HashMap;
19
20/// A central, in-memory cache for a specific type of asset `A`.
21///
22/// This structure maps a unique `AssetUUID` to a shared `AssetHandle<A>`.
23/// This ensures that any given asset is loaded only once. Subsequent requests
24/// for the same asset will receive a clone of the cached handle.
25#[derive(Default)]
26pub struct Assets<A: Asset> {
27 storage: HashMap<AssetUUID, AssetHandle<A>>,
28}
29
30impl<A: Asset> Clone for Assets<A> {
31 fn clone(&self) -> Self {
32 Self {
33 storage: self.storage.clone(),
34 }
35 }
36}
37
38impl<A: Asset> Assets<A> {
39 /// Creates a new, empty asset storage.
40 pub fn new() -> Self {
41 Self {
42 storage: HashMap::new(),
43 }
44 }
45
46 /// Inserts an asset handle into the storage, associated with its UUID.
47 /// If an asset with the same UUID already exists, it will be replaced.
48 /// This operation is always successful.
49 ///
50 /// # Arguments
51 /// * `uuid` - The unique identifier for the asset.
52 /// * `handle` - The handle to the asset to be stored.
53 pub fn insert(&mut self, uuid: AssetUUID, handle: AssetHandle<A>) {
54 self.storage.insert(uuid, handle);
55 }
56
57 /// Retrieves a reference to the asset handle associated with the given UUID.
58 /// Returns `None` if no asset with the specified UUID is found.
59 pub fn get(&self, uuid: &AssetUUID) -> Option<&AssetHandle<A>> {
60 self.storage.get(uuid)
61 }
62
63 /// Checks if an asset with the specified UUID exists in the storage.
64 pub fn contains(&self, uuid: &AssetUUID) -> bool {
65 self.storage.contains_key(uuid)
66 }
67
68 /// Drops the cached handle for `uuid`, returning it if present.
69 ///
70 /// Used by `AssetService::invalidate` (in `khora-io`) when a hot-reload
71 /// event invalidates a cached asset. Note that any outstanding clones of
72 /// the returned handle keep the *old* asset alive until they themselves
73 /// drop — that's by design: in-flight readers don't see a half-loaded
74 /// replacement. The next `AssetService::load` for the same UUID re-runs
75 /// the IO + decoder pipeline and produces a fresh handle.
76 pub fn remove(&mut self, uuid: &AssetUUID) -> Option<AssetHandle<A>> {
77 self.storage.remove(uuid)
78 }
79
80 /// Returns an iterator over every `AssetUUID` currently cached.
81 ///
82 /// Used by the asset-eviction maintenance pass to diff the cached set
83 /// against the set of UUIDs still referenced by live entities.
84 pub fn keys(&self) -> impl Iterator<Item = AssetUUID> + '_ {
85 self.storage.keys().copied()
86 }
87
88 /// The number of assets currently cached.
89 pub fn len(&self) -> usize {
90 self.storage.len()
91 }
92
93 /// Returns `true` if no assets are cached.
94 pub fn is_empty(&self) -> bool {
95 self.storage.is_empty()
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102 use khora_core::asset::AssetUUID;
103
104 /// Local newtype wrapper so the orphan rule lets us `impl Asset` for it.
105 /// `Asset` is just a marker trait (`Send + Sync + 'static`).
106 #[derive(Debug, PartialEq)]
107 struct TestAsset(String);
108 impl Asset for TestAsset {}
109
110 #[test]
111 fn remove_drops_cached_handle() {
112 let mut assets = Assets::<TestAsset>::new();
113 let uuid = AssetUUID::new_v5("textures/foo.png");
114 assets.insert(uuid, AssetHandle::new(TestAsset("hello".into())));
115
116 assert!(assets.contains(&uuid));
117 let popped = assets.remove(&uuid);
118 assert!(popped.is_some());
119 assert!(!assets.contains(&uuid));
120 // Removing again returns None (idempotent).
121 assert!(assets.remove(&uuid).is_none());
122 }
123}