offheap 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,499 @@
1
+ use std::collections::HashMap;
2
+ use super::{CacheImpl, CacheEntry, CacheStats};
3
+
4
+ // Count-Min Sketch with 4-bit counters and aging.
5
+ // Sized dynamically relative to cache capacity.
6
+ struct FrequencySketch {
7
+ table: Vec<u8>,
8
+ mask: usize,
9
+ sample_size: usize,
10
+ additions: usize,
11
+ }
12
+
13
+ impl FrequencySketch {
14
+ fn new(capacity: usize) -> Self {
15
+ // Number of counters: next power of 2 of capacity * 4, min 128
16
+ let counters = (capacity * 4).next_power_of_two().max(128);
17
+ let bytes = counters / 2;
18
+ Self {
19
+ table: vec![0; bytes],
20
+ mask: counters - 1,
21
+ sample_size: (capacity * 10).max(1024),
22
+ additions: 0,
23
+ }
24
+ }
25
+
26
+ fn increment(&mut self, key: &str) {
27
+ let hash = seahash::hash(key.as_bytes());
28
+ let h1 = (hash & 0xFFFFFFFF) as usize;
29
+ let h2 = (hash >> 32) as usize;
30
+
31
+ let mut added = false;
32
+ for i in 0..4 {
33
+ let idx = (h1.wrapping_add(i * h2)) & self.mask;
34
+ if self.increment_at(idx) {
35
+ added = true;
36
+ }
37
+ }
38
+
39
+ if added {
40
+ self.additions += 1;
41
+ if self.additions >= self.sample_size {
42
+ self.decay();
43
+ }
44
+ }
45
+ }
46
+
47
+ fn increment_at(&mut self, idx: usize) -> bool {
48
+ let byte_idx = idx / 2;
49
+ let is_high = idx % 2 == 1;
50
+ let val = self.table[byte_idx];
51
+
52
+ if is_high {
53
+ let count = val >> 4;
54
+ if count < 15 {
55
+ self.table[byte_idx] = (val & 0x0F) | ((count + 1) << 4);
56
+ return true;
57
+ }
58
+ } else {
59
+ let count = val & 0x0F;
60
+ if count < 15 {
61
+ self.table[byte_idx] = (val & 0xF0) | (count + 1);
62
+ return true;
63
+ }
64
+ }
65
+ false
66
+ }
67
+
68
+ fn estimate(&self, key: &str) -> u8 {
69
+ let hash = seahash::hash(key.as_bytes());
70
+ let h1 = (hash & 0xFFFFFFFF) as usize;
71
+ let h2 = (hash >> 32) as usize;
72
+
73
+ let mut min_val = 15;
74
+ for i in 0..4 {
75
+ let idx = (h1.wrapping_add(i * h2)) & self.mask;
76
+ let val = self.counter_at(idx);
77
+ if val < min_val {
78
+ min_val = val;
79
+ }
80
+ }
81
+ min_val
82
+ }
83
+
84
+ fn counter_at(&self, idx: usize) -> u8 {
85
+ let byte_idx = idx / 2;
86
+ let is_high = idx % 2 == 1;
87
+ let val = self.table[byte_idx];
88
+ if is_high {
89
+ val >> 4
90
+ } else {
91
+ val & 0x0F
92
+ }
93
+ }
94
+
95
+ fn decay(&mut self) {
96
+ for val in self.table.iter_mut() {
97
+ let high = (*val >> 4) >> 1;
98
+ let low = (*val & 0x0F) >> 1;
99
+ *val = (high << 4) | low;
100
+ }
101
+ self.additions = 0;
102
+ }
103
+ }
104
+
105
+ #[derive(Copy, Clone, PartialEq, Debug)]
106
+ enum TinyLfuList {
107
+ Window,
108
+ Probation,
109
+ Protected,
110
+ }
111
+
112
+ struct TinyLfuNode {
113
+ key: String,
114
+ entry: CacheEntry,
115
+ list: TinyLfuList,
116
+ prev: Option<usize>,
117
+ next: Option<usize>,
118
+ }
119
+
120
+ struct TinyLfuListMetadata {
121
+ head: Option<usize>,
122
+ tail: Option<usize>,
123
+ len: usize,
124
+ capacity: usize,
125
+ }
126
+
127
+ impl TinyLfuListMetadata {
128
+ fn new(capacity: usize) -> Self {
129
+ Self {
130
+ head: None,
131
+ tail: None,
132
+ len: 0,
133
+ capacity,
134
+ }
135
+ }
136
+ }
137
+
138
+ pub struct TinyLfuCache {
139
+ capacity: usize,
140
+ map: HashMap<String, usize>,
141
+ nodes: Vec<TinyLfuNode>,
142
+ free_nodes: Vec<usize>,
143
+
144
+ window: TinyLfuListMetadata,
145
+ probation: TinyLfuListMetadata,
146
+ protected: TinyLfuListMetadata,
147
+
148
+ sketch: FrequencySketch,
149
+ hits: u64,
150
+ misses: u64,
151
+ }
152
+
153
+ impl TinyLfuCache {
154
+ pub fn new(capacity: usize) -> Self {
155
+ // Segment capacities:
156
+ // Window: 1% (min 1)
157
+ // Main: 99%
158
+ // Probation: 20% of Main (min 1 if Main capacity > 0)
159
+ // Protected: 80% of Main
160
+ let window_cap = (capacity / 100).max(1);
161
+ let main_cap = capacity.saturating_sub(window_cap);
162
+
163
+ let (probation_cap, protected_cap) = if main_cap > 0 {
164
+ let prob = (main_cap * 20 / 100).max(1);
165
+ let prot = main_cap.saturating_sub(prob);
166
+ (prob, prot)
167
+ } else {
168
+ (0, 0)
169
+ };
170
+
171
+ Self {
172
+ capacity,
173
+ map: HashMap::with_capacity(capacity),
174
+ nodes: Vec::with_capacity(capacity),
175
+ free_nodes: Vec::new(),
176
+ window: TinyLfuListMetadata::new(window_cap),
177
+ probation: TinyLfuListMetadata::new(probation_cap),
178
+ protected: TinyLfuListMetadata::new(protected_cap),
179
+ sketch: FrequencySketch::new(capacity),
180
+ hits: 0,
181
+ misses: 0,
182
+ }
183
+ }
184
+
185
+ fn detach(&mut self, idx: usize) {
186
+ let prev = self.nodes[idx].prev;
187
+ let next = self.nodes[idx].next;
188
+ let list = self.nodes[idx].list;
189
+
190
+ let meta = match list {
191
+ TinyLfuList::Window => &mut self.window,
192
+ TinyLfuList::Probation => &mut self.probation,
193
+ TinyLfuList::Protected => &mut self.protected,
194
+ };
195
+
196
+ if let Some(p) = prev {
197
+ self.nodes[p].next = next;
198
+ } else {
199
+ meta.head = next;
200
+ }
201
+
202
+ if let Some(n) = next {
203
+ self.nodes[n].prev = prev;
204
+ } else {
205
+ meta.tail = prev;
206
+ }
207
+
208
+ meta.len -= 1;
209
+ self.nodes[idx].prev = None;
210
+ self.nodes[idx].next = None;
211
+ }
212
+
213
+ fn attach_to_head(&mut self, idx: usize, list: TinyLfuList) {
214
+ self.nodes[idx].list = list;
215
+ let meta = match list {
216
+ TinyLfuList::Window => &mut self.window,
217
+ TinyLfuList::Probation => &mut self.probation,
218
+ TinyLfuList::Protected => &mut self.protected,
219
+ };
220
+
221
+ self.nodes[idx].prev = None;
222
+ self.nodes[idx].next = meta.head;
223
+
224
+ if let Some(h) = meta.head {
225
+ self.nodes[h].prev = Some(idx);
226
+ } else {
227
+ meta.tail = Some(idx);
228
+ }
229
+
230
+ meta.head = Some(idx);
231
+ meta.len += 1;
232
+ }
233
+
234
+ fn remove_completely(&mut self, idx: usize) -> String {
235
+ self.detach(idx);
236
+ self.free_nodes.push(idx);
237
+ let key = std::mem::take(&mut self.nodes[idx].key);
238
+ self.nodes[idx].entry = CacheEntry::new(Vec::new(), None);
239
+ key
240
+ }
241
+ }
242
+
243
+ impl CacheImpl for TinyLfuCache {
244
+ fn get(&mut self, key: &str) -> Option<Vec<u8>> {
245
+ self.sketch.increment(key);
246
+
247
+ if let Some(&idx) = self.map.get(key) {
248
+ if self.nodes[idx].entry.is_expired() {
249
+ self.remove_completely(idx);
250
+ self.map.remove(key);
251
+ self.misses += 1;
252
+ None
253
+ } else {
254
+ let list = self.nodes[idx].list;
255
+ match list {
256
+ TinyLfuList::Window => {
257
+ self.detach(idx);
258
+ self.attach_to_head(idx, TinyLfuList::Window);
259
+ }
260
+ TinyLfuList::Probation => {
261
+ // Promote to Protected segment
262
+ self.detach(idx);
263
+ self.attach_to_head(idx, TinyLfuList::Protected);
264
+
265
+ // Handle potential overflow of Protected segment
266
+ if self.protected.len > self.protected.capacity {
267
+ if let Some(prot_tail) = self.protected.tail {
268
+ self.detach(prot_tail);
269
+ self.attach_to_head(prot_tail, TinyLfuList::Probation);
270
+ }
271
+ }
272
+ }
273
+ TinyLfuList::Protected => {
274
+ self.detach(idx);
275
+ self.attach_to_head(idx, TinyLfuList::Protected);
276
+ }
277
+ }
278
+ self.hits += 1;
279
+ Some(self.nodes[idx].entry.value.clone())
280
+ }
281
+ } else {
282
+ self.misses += 1;
283
+ None
284
+ }
285
+ }
286
+
287
+ fn set(&mut self, key: &str, value: Vec<u8>, ttl_ms: Option<u64>) -> Option<Vec<u8>> {
288
+ self.sketch.increment(key);
289
+
290
+ if let Some(&idx) = self.map.get(key) {
291
+ // Key exists. Update value.
292
+ let old_value = std::mem::replace(
293
+ &mut self.nodes[idx].entry,
294
+ CacheEntry::new(value, ttl_ms),
295
+ ).value;
296
+
297
+ // Perform promotion/MRU updates identical to `get`
298
+ let list = self.nodes[idx].list;
299
+ match list {
300
+ TinyLfuList::Window => {
301
+ self.detach(idx);
302
+ self.attach_to_head(idx, TinyLfuList::Window);
303
+ }
304
+ TinyLfuList::Probation => {
305
+ self.detach(idx);
306
+ self.attach_to_head(idx, TinyLfuList::Protected);
307
+
308
+ if self.protected.len > self.protected.capacity {
309
+ if let Some(prot_tail) = self.protected.tail {
310
+ self.detach(prot_tail);
311
+ self.attach_to_head(prot_tail, TinyLfuList::Probation);
312
+ }
313
+ }
314
+ }
315
+ TinyLfuList::Protected => {
316
+ self.detach(idx);
317
+ self.attach_to_head(idx, TinyLfuList::Protected);
318
+ }
319
+ }
320
+ Some(old_value)
321
+ } else {
322
+ // Check if we have capacity at all
323
+ if self.capacity == 0 {
324
+ return None;
325
+ }
326
+
327
+ // Create new node inside Window segment
328
+ let new_idx = if let Some(free_idx) = self.free_nodes.pop() {
329
+ self.nodes[free_idx].key = key.to_string();
330
+ self.nodes[free_idx].entry = CacheEntry::new(value, ttl_ms);
331
+ free_idx
332
+ } else {
333
+ let free_idx = self.nodes.len();
334
+ self.nodes.push(TinyLfuNode {
335
+ key: key.to_string(),
336
+ entry: CacheEntry::new(value, ttl_ms),
337
+ list: TinyLfuList::Window,
338
+ prev: None,
339
+ next: None,
340
+ });
341
+ free_idx
342
+ };
343
+
344
+ self.attach_to_head(new_idx, TinyLfuList::Window);
345
+ self.map.insert(key.to_string(), new_idx);
346
+
347
+ // If Window segment overflows
348
+ if self.window.len > self.window.capacity {
349
+ if let Some(win_tail) = self.window.tail {
350
+ // Candidate to move to Main cache (Probation segment)
351
+ let candidate_idx = win_tail;
352
+ self.detach(candidate_idx);
353
+
354
+ let main_has_capacity = self.probation.capacity > 0;
355
+ if main_has_capacity {
356
+ let total_size = self.window.len + self.probation.len + self.protected.len;
357
+ if total_size + 1 <= self.capacity {
358
+ // Cache is not full yet. Admit candidate directly.
359
+ self.attach_to_head(candidate_idx, TinyLfuList::Probation);
360
+ } else {
361
+ // Cache is full. Compare candidate and victim.
362
+ if let Some(prob_tail) = self.probation.tail {
363
+ let victim_idx = prob_tail;
364
+
365
+ let candidate_key = &self.nodes[candidate_idx].key;
366
+ let victim_key = &self.nodes[victim_idx].key;
367
+
368
+ let candidate_freq = self.sketch.estimate(candidate_key);
369
+ let victim_freq = self.sketch.estimate(victim_key);
370
+
371
+ if candidate_freq > victim_freq {
372
+ // Candidate wins! Evict victim, admit candidate.
373
+ let evicted_key = self.remove_completely(victim_idx);
374
+ self.map.remove(&evicted_key);
375
+ self.attach_to_head(candidate_idx, TinyLfuList::Probation);
376
+ } else {
377
+ // Candidate loses! Evict candidate, keep victim.
378
+ let evicted_key = self.remove_completely(candidate_idx);
379
+ self.map.remove(&evicted_key);
380
+ }
381
+ } else {
382
+ // Fallback if no victim (should not happen if len == capacity > 0)
383
+ self.attach_to_head(candidate_idx, TinyLfuList::Probation);
384
+ }
385
+ }
386
+ } else {
387
+ // Main Cache capacity is 0 (capacity == 1 edge case).
388
+ // Candidate is evicted directly.
389
+ let evicted_key = self.remove_completely(candidate_idx);
390
+ self.map.remove(&evicted_key);
391
+ }
392
+ }
393
+ }
394
+ None
395
+ }
396
+ }
397
+
398
+ fn delete(&mut self, key: &str) -> bool {
399
+ if let Some(&idx) = self.map.get(key) {
400
+ self.remove_completely(idx);
401
+ self.map.remove(key);
402
+ true
403
+ } else {
404
+ false
405
+ }
406
+ }
407
+
408
+ fn clear(&mut self) {
409
+ self.map.clear();
410
+ self.nodes.clear();
411
+ self.free_nodes.clear();
412
+ self.window.head = None;
413
+ self.window.tail = None;
414
+ self.window.len = 0;
415
+ self.probation.head = None;
416
+ self.probation.tail = None;
417
+ self.probation.len = 0;
418
+ self.protected.head = None;
419
+ self.protected.tail = None;
420
+ self.protected.len = 0;
421
+ self.hits = 0;
422
+ self.misses = 0;
423
+ // FrequencySketch is not cleared completely, but can be decay'd or just left as is.
424
+ // Actually, clearing table is cleaner:
425
+ self.sketch.table.fill(0);
426
+ self.sketch.additions = 0;
427
+ }
428
+
429
+ fn stats(&self) -> CacheStats {
430
+ CacheStats {
431
+ hits: self.hits,
432
+ misses: self.misses,
433
+ capacity: self.capacity,
434
+ size: self.window.len + self.probation.len + self.protected.len,
435
+ }
436
+ }
437
+
438
+ fn keys(&self) -> Vec<String> {
439
+ let mut result = Vec::new();
440
+ // Return window keys
441
+ let mut curr = self.window.head;
442
+ while let Some(idx) = curr {
443
+ let node = &self.nodes[idx];
444
+ if !node.entry.is_expired() {
445
+ result.push(node.key.clone());
446
+ }
447
+ curr = node.next;
448
+ }
449
+ // Return probation keys
450
+ let mut curr = self.probation.head;
451
+ while let Some(idx) = curr {
452
+ let node = &self.nodes[idx];
453
+ if !node.entry.is_expired() {
454
+ result.push(node.key.clone());
455
+ }
456
+ curr = node.next;
457
+ }
458
+ // Return protected keys
459
+ let mut curr = self.protected.head;
460
+ while let Some(idx) = curr {
461
+ let node = &self.nodes[idx];
462
+ if !node.entry.is_expired() {
463
+ result.push(node.key.clone());
464
+ }
465
+ curr = node.next;
466
+ }
467
+ result
468
+ }
469
+ }
470
+
471
+ #[cfg(test)]
472
+ mod tests {
473
+ use super::*;
474
+
475
+ #[test]
476
+ fn test_tinylfu_basic() {
477
+ let mut cache = TinyLfuCache::new(5);
478
+
479
+ // Fill cache
480
+ for i in 0..5 {
481
+ cache.set(&format!("key-{}", i), vec![i], None);
482
+ }
483
+ assert_eq!(cache.stats().size, 5);
484
+
485
+ // Increase frequency of key-0 and key-1
486
+ for _ in 0..3 {
487
+ cache.get("key-0");
488
+ cache.get("key-1");
489
+ }
490
+
491
+ // Add a new key. TinyLFU will compare frequency of candidate vs probation victim.
492
+ // It will evict a low-frequency key.
493
+ cache.set("key-5", vec![5], None);
494
+
495
+ // key-0 and key-1 must remain because of high frequency.
496
+ assert!(cache.get("key-0").is_some());
497
+ assert!(cache.get("key-1").is_some());
498
+ }
499
+ }
package/src/cache.rs ADDED
@@ -0,0 +1,172 @@
1
+ use std::sync::Arc;
2
+ use napi::{Env, JsUnknown, JsBuffer, JsString, Result, ValueType};
3
+ use super::algorithms::{CacheImpl, CacheStats, lru::LruCache, arc::ArcCache, tinylfu::TinyLfuCache};
4
+
5
+ #[napi(object)]
6
+ pub struct CacheConfig {
7
+ pub policy: String,
8
+ pub capacity: u32,
9
+ }
10
+
11
+ #[napi(object)]
12
+ pub struct CacheStatsJs {
13
+ pub hits: f64,
14
+ pub misses: f64,
15
+ pub capacity: f64,
16
+ pub size: f64,
17
+ }
18
+
19
+ #[napi]
20
+ pub struct Cache {
21
+ inner: Arc<parking_lot::Mutex<Box<dyn CacheImpl>>>,
22
+ }
23
+
24
+ impl Clone for Cache {
25
+ fn clone(&self) -> Self {
26
+ Self {
27
+ inner: Arc::clone(&self.inner),
28
+ }
29
+ }
30
+ }
31
+
32
+ impl Cache {
33
+ pub fn from_config(config: CacheConfig) -> Self {
34
+ let capacity = config.capacity as usize;
35
+ let policy_lower = config.policy.to_lowercase();
36
+ let inner: Box<dyn CacheImpl> = match policy_lower.as_str() {
37
+ "arc" => Box::new(ArcCache::new(capacity)),
38
+ "tinylfu" | "w-tinylfu" => Box::new(TinyLfuCache::new(capacity)),
39
+ _ => Box::new(LruCache::new(capacity)),
40
+ };
41
+ Self {
42
+ inner: Arc::new(parking_lot::Mutex::new(inner)),
43
+ }
44
+ }
45
+ }
46
+
47
+ #[napi]
48
+ impl Cache {
49
+ #[napi]
50
+ pub fn get(&self, env: Env, key: String) -> Result<JsUnknown> {
51
+ let mut lock = self.inner.lock();
52
+ if let Some(bytes) = lock.get(&key) {
53
+ if bytes.is_empty() {
54
+ return Ok(env.get_undefined()?.into_unknown());
55
+ }
56
+ let tag = bytes[0];
57
+ let payload = &bytes[1..];
58
+ match tag {
59
+ 1 => {
60
+ // Buffer
61
+ let js_buf = env.create_buffer_copy(payload)?;
62
+ Ok(js_buf.into_unknown())
63
+ }
64
+ 2 => {
65
+ // String
66
+ let s = std::str::from_utf8(payload)
67
+ .map_err(|e| napi::Error::new(napi::Status::StringExpected, e.to_string()))?;
68
+ let js_str = env.create_string(s)?;
69
+ Ok(js_str.into_unknown())
70
+ }
71
+ 3 => {
72
+ // JSON
73
+ let json_val: serde_json::Value = serde_json::from_slice(payload)
74
+ .map_err(|e| napi::Error::new(napi::Status::InvalidArg, e.to_string()))?;
75
+ let parsed = env.to_js_value(&json_val)?;
76
+ Ok(parsed)
77
+ }
78
+ _ => Err(napi::Error::new(napi::Status::InvalidArg, "Invalid data type tag in cache storage")),
79
+ }
80
+ } else {
81
+ Ok(env.get_undefined()?.into_unknown())
82
+ }
83
+ }
84
+
85
+ #[napi]
86
+ pub fn set(&self, env: Env, key: String, value: JsUnknown, ttl_ms: Option<f64>) -> Result<JsUnknown> {
87
+ let mut bytes = Vec::new();
88
+ let value_type = value.get_type()?;
89
+
90
+ if value.is_buffer()? {
91
+ let buf = JsBuffer::try_from(value)?;
92
+ let raw_bytes: Vec<u8> = buf.into_value()?.to_vec();
93
+ bytes.push(1); // Tag: Buffer
94
+ bytes.extend_from_slice(&raw_bytes);
95
+ } else if value_type == ValueType::String {
96
+ let js_str = JsString::try_from(value)?;
97
+ let utf8 = js_str.into_utf8()?;
98
+ bytes.push(2); // Tag: String
99
+ bytes.extend_from_slice(utf8.as_slice());
100
+ } else {
101
+ // Treat as JSON
102
+ let json_val: serde_json::Value = env.from_js_value(value)?;
103
+ let serialized = serde_json::to_vec(&json_val)
104
+ .map_err(|e| napi::Error::new(napi::Status::InvalidArg, e.to_string()))?;
105
+ bytes.push(3); // Tag: JSON
106
+ bytes.extend_from_slice(&serialized);
107
+ }
108
+
109
+ let ttl = ttl_ms.map(|ms| ms as u64);
110
+ let mut lock = self.inner.lock();
111
+ let old_bytes = lock.set(&key, bytes, ttl);
112
+
113
+ if let Some(bytes) = old_bytes {
114
+ if bytes.is_empty() {
115
+ return Ok(env.get_undefined()?.into_unknown());
116
+ }
117
+ let tag = bytes[0];
118
+ let payload = &bytes[1..];
119
+ match tag {
120
+ 1 => {
121
+ let js_buf = env.create_buffer_copy(payload)?;
122
+ Ok(js_buf.into_unknown())
123
+ }
124
+ 2 => {
125
+ let s = std::str::from_utf8(payload)
126
+ .map_err(|e| napi::Error::new(napi::Status::StringExpected, e.to_string()))?;
127
+ let js_str = env.create_string(s)?;
128
+ Ok(js_str.into_unknown())
129
+ }
130
+ 3 => {
131
+ let json_val: serde_json::Value = serde_json::from_slice(payload)
132
+ .map_err(|e| napi::Error::new(napi::Status::InvalidArg, e.to_string()))?;
133
+ let parsed = env.to_js_value(&json_val)?;
134
+ Ok(parsed)
135
+ }
136
+ _ => Err(napi::Error::new(napi::Status::InvalidArg, "Invalid data type tag in cache storage")),
137
+ }
138
+ } else {
139
+ Ok(env.get_undefined()?.into_unknown())
140
+ }
141
+ }
142
+
143
+ #[napi]
144
+ pub fn delete(&self, key: String) -> bool {
145
+ let mut lock = self.inner.lock();
146
+ lock.delete(&key)
147
+ }
148
+
149
+ #[napi]
150
+ pub fn clear(&self) {
151
+ let mut lock = self.inner.lock();
152
+ lock.clear();
153
+ }
154
+
155
+ #[napi]
156
+ pub fn stats(&self) -> CacheStatsJs {
157
+ let lock = self.inner.lock();
158
+ let stats = lock.stats();
159
+ CacheStatsJs {
160
+ hits: stats.hits as f64,
161
+ misses: stats.misses as f64,
162
+ capacity: stats.capacity as f64,
163
+ size: stats.size as f64,
164
+ }
165
+ }
166
+
167
+ #[napi]
168
+ pub fn keys(&self) -> Vec<String> {
169
+ let lock = self.inner.lock();
170
+ lock.keys()
171
+ }
172
+ }
package/src/lib.rs ADDED
@@ -0,0 +1,6 @@
1
+ #[macro_use]
2
+ extern crate napi_derive;
3
+
4
+ pub mod algorithms;
5
+ pub mod cache;
6
+ pub mod manager;