arkhe_kernel/state/
ledger.rs1use std::collections::BTreeMap;
10
11use serde::{Deserialize, Serialize};
12
13use crate::abi::{EntityId, TypeCode};
14
15#[derive(Debug, Default, Clone, Serialize, Deserialize)]
16pub(crate) struct ResourceLedger {
17 entity_bytes: BTreeMap<EntityId, u64>,
18 type_counts: BTreeMap<TypeCode, u32>,
19 total_bytes: u64,
20 total_entities: u32,
21}
22
23impl ResourceLedger {
24 pub(crate) fn new() -> Self {
25 Self::default()
26 }
27
28 #[inline]
31 pub(crate) fn total_bytes(&self) -> u64 {
32 self.total_bytes
33 }
34
35 #[cfg_attr(not(test), allow(dead_code))]
39 #[inline]
40 pub(crate) fn total_entities(&self) -> u32 {
41 self.total_entities
42 }
43
44 #[cfg_attr(not(test), allow(dead_code))]
45 pub(crate) fn entity_bytes(&self, id: EntityId) -> u64 {
46 *self.entity_bytes.get(&id).unwrap_or(&0)
47 }
48
49 #[cfg_attr(not(test), allow(dead_code))]
50 pub(crate) fn type_count(&self, tc: TypeCode) -> u32 {
51 *self.type_counts.get(&tc).unwrap_or(&0)
52 }
53
54 pub(crate) fn add_entity(&mut self, id: EntityId) -> bool {
57 if self.entity_bytes.insert(id, 0).is_none() {
58 self.total_entities = self.total_entities.saturating_add(1);
59 true
60 } else {
61 false
62 }
63 }
64
65 pub(crate) fn remove_entity(&mut self, id: EntityId) -> u64 {
71 if let Some(bytes) = self.entity_bytes.remove(&id) {
72 self.total_bytes = self.total_bytes.saturating_sub(bytes);
73 self.total_entities = self.total_entities.saturating_sub(1);
74 bytes
75 } else {
76 0
77 }
78 }
79
80 pub(crate) fn add_component(&mut self, entity: EntityId, tc: TypeCode, size: u64) -> bool {
83 let Some(bytes) = self.entity_bytes.get_mut(&entity) else {
84 return false;
85 };
86 *bytes = bytes.saturating_add(size);
87 self.total_bytes = self.total_bytes.saturating_add(size);
88 *self.type_counts.entry(tc).or_insert(0) += 1;
89 true
90 }
91
92 pub(crate) fn remove_component(&mut self, entity: EntityId, tc: TypeCode, size: u64) -> bool {
95 let Some(bytes) = self.entity_bytes.get_mut(&entity) else {
96 return false;
97 };
98 *bytes = bytes.saturating_sub(size);
99 self.total_bytes = self.total_bytes.saturating_sub(size);
100 if let Some(c) = self.type_counts.get_mut(&tc) {
101 *c = c.saturating_sub(1);
102 if *c == 0 {
103 self.type_counts.remove(&tc);
104 }
105 }
106 true
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 fn e(n: u64) -> EntityId {
115 EntityId::new(n).unwrap()
116 }
117 fn t(n: u32) -> TypeCode {
118 TypeCode(n)
119 }
120
121 #[test]
122 fn empty_ledger_zeroes() {
123 let l = ResourceLedger::new();
124 assert_eq!(l.total_entities(), 0);
125 assert_eq!(l.total_bytes(), 0);
126 assert_eq!(l.entity_bytes(e(1)), 0);
127 assert_eq!(l.type_count(t(1)), 0);
128 }
129
130 #[test]
131 fn add_entity_increments_total() {
132 let mut l = ResourceLedger::new();
133 assert!(l.add_entity(e(1)));
134 assert!(l.add_entity(e(2)));
135 assert_eq!(l.total_entities(), 2);
136 }
137
138 #[test]
139 fn add_entity_idempotent() {
140 let mut l = ResourceLedger::new();
141 assert!(l.add_entity(e(1)));
142 assert!(!l.add_entity(e(1)));
143 assert_eq!(l.total_entities(), 1);
144 }
145
146 #[test]
147 fn add_component_updates_bytes_and_count() {
148 let mut l = ResourceLedger::new();
149 l.add_entity(e(1));
150 assert!(l.add_component(e(1), t(10), 100));
151 assert_eq!(l.entity_bytes(e(1)), 100);
152 assert_eq!(l.total_bytes(), 100);
153 assert_eq!(l.type_count(t(10)), 1);
154 }
155
156 #[test]
157 fn add_component_to_unknown_entity_is_noop() {
158 let mut l = ResourceLedger::new();
159 assert!(!l.add_component(e(1), t(10), 100));
160 assert_eq!(l.total_bytes(), 0);
161 assert_eq!(l.type_count(t(10)), 0);
162 }
163
164 #[test]
165 fn remove_component_balances_add() {
166 let mut l = ResourceLedger::new();
167 l.add_entity(e(1));
168 l.add_component(e(1), t(10), 100);
169 l.add_component(e(1), t(20), 50);
170 assert_eq!(l.total_bytes(), 150);
171 l.remove_component(e(1), t(10), 100);
172 assert_eq!(l.total_bytes(), 50);
173 assert_eq!(l.entity_bytes(e(1)), 50);
174 assert_eq!(l.type_count(t(10)), 0);
175 assert_eq!(l.type_count(t(20)), 1);
176 }
177
178 #[test]
179 fn remove_entity_returns_bytes_and_drops_total() {
180 let mut l = ResourceLedger::new();
181 l.add_entity(e(1));
182 l.add_component(e(1), t(10), 100);
183 l.add_component(e(1), t(20), 50);
184 let bytes = l.remove_entity(e(1));
185 assert_eq!(bytes, 150);
186 assert_eq!(l.total_entities(), 0);
187 assert_eq!(l.total_bytes(), 0);
188 assert_eq!(l.entity_bytes(e(1)), 0);
189 }
190
191 #[test]
192 fn remove_unknown_entity_is_noop() {
193 let mut l = ResourceLedger::new();
194 assert_eq!(l.remove_entity(e(999)), 0);
195 }
196
197 #[test]
198 fn add_remove_balanced_yields_empty() {
199 let mut l = ResourceLedger::new();
200 l.add_entity(e(1));
201 l.add_component(e(1), t(10), 100);
202 l.remove_component(e(1), t(10), 100);
203 l.remove_entity(e(1));
204 assert_eq!(l.total_bytes(), 0);
205 assert_eq!(l.total_entities(), 0);
206 }
207
208 #[test]
209 fn type_count_aggregates_across_entities() {
210 let mut l = ResourceLedger::new();
211 l.add_entity(e(1));
212 l.add_entity(e(2));
213 l.add_component(e(1), t(7), 10);
214 l.add_component(e(2), t(7), 20);
215 assert_eq!(l.type_count(t(7)), 2);
216 assert_eq!(l.total_bytes(), 30);
217 }
218
219 #[test]
220 fn type_count_underflow_is_saturating() {
221 let mut l = ResourceLedger::new();
223 l.add_entity(e(1));
224 l.remove_component(e(1), t(99), 50); assert_eq!(l.type_count(t(99)), 0);
226 }
227}