diff --git a/.gitignore b/.gitignore
index f8f635b22..86bf4e458 100644
@@ -11,5 +11,9 @@ __pycache__
node_modules/
.envrc
package-lock.json
-
bun.lock
+uv.lock
+.gemini/
+.claude/
+.codex/
+.mcp.json
diff --git a/attic/kv/Cargo.toml b/attic/kv/Cargo.toml
deleted file mode 100644
index 38f449847..000000000
@@ -1,50 +0,0 @@
-[package]
-name = "tetra-kv"
-version.workspace = true
-edition.workspace = true
-authors.workspace = true
-homepage.workspace = true
-description = "Tetra key-value: pluggable storage backend"
-repository.workspace = true
-
-[lints]
-workspace = true
-
-[features]
-default = ["rocksdb", "heed"]
-rocksdb = ["dep:rocksdb", "dep:librocksdb-sys"]
-heed = ["dep:heed"]
-
-[dependencies]
-tetra-core.workspace = true
-rand.workspace = true
-tempfile.workspace = true
-parking_lot.workspace = true
-postcard.workspace = true
-serde.workspace = true
-bytes.workspace = true
-smallvec.workspace = true
-derive_more.workspace = true
-derive-where.workspace = true
-thiserror.workspace = true
-smol_str.workspace = true
-serde_cbor.workspace = true
-triomphe.workspace = true
-futures.workspace = true
-tokio.workspace = true
-blake3.workspace = true
-anyhow.workspace = true
-heapless.workspace = true
-ahash.workspace = true
-bon.workspace = true
-heed = { workspace = true, optional = true }
-rocksdb = { workspace = true, optional = true }
-librocksdb-sys = { workspace = true, optional = true }
-
-[dev-dependencies]
-criterion.workspace = true
-rayon.workspace = true
-
-[[bench]]
-name = "kvops"
-harness = false
diff --git a/attic/kv/README.md b/attic/kv/README.md
deleted file mode 100644
index cbd8b43de..000000000
@@ -1,4 +0,0 @@
-# tetra-kv
-
-Pluggable key–value storage for Tetra with both in‑memory and RocksDB
-implementations.
diff --git a/attic/kv/benches/kvops.rs b/attic/kv/benches/kvops.rs
deleted file mode 100644
index 2f3170662..000000000
@@ -1,371 +0,0 @@
-use std::{sync::Arc, time::Duration};
-
-use bytes::Bytes;
-use criterion::{Bencher, BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
-use rand::{Rng, RngCore, SeedableRng, rngs::StdRng};
-use rayon::prelude::*;
-use tetra_kv::{Store, Write, WriteBatch, ops};
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq, derive_more::From, derive_more::Into)]
-pub struct Lcg64(pub u64);
-
-impl rand::RngCore for Lcg64 {
- #[inline]
- fn next_u32(&mut self) -> u32 {
- self.next_u64() as u32
- }
-
- #[inline]
- fn next_u64(&mut self) -> u64 {
- self.0 = self
- .0
- .wrapping_mul(6364136223846793005)
- .wrapping_add(1442695040888963407);
- self.0
- }
-
- #[inline]
- fn fill_bytes(&mut self, dest: &mut [u8]) {
- let (qwords, rest) = dest.as_chunks_mut();
- for q in qwords {
- *q = self.next_u64().to_le_bytes();
- }
- if !rest.is_empty() {
- let n = rest.len();
- let buf = self.next_u64().to_le_bytes();
- dest[..n].copy_from_slice(&buf[..n]);
- }
- }
-}
-
-// A generic benchmark configuration
-struct BenchConfig {
- // Number of operations to perform
- num_ops: usize,
- // Size of values in bytes
- value_size: usize,
- // Number of threads to use (1 for single-threaded)
- threads: usize,
- // Batch size
- batch_size: usize,
- // (Read, Write, Update + name of the flavor) distributions
- rwu: (usize, usize, usize, &'static str),
- // Whether to prepare data before benchmarking
- prepare_data: bool,
-}
-
-impl BenchConfig {
- fn step<'a, S: Store>(
- &self,
- store: &'a S,
- key: &[u8],
- data: &[u8],
- batch: Option<&mut S::WriteBatch<'a>>,
- rng: &mut Lcg64,
- ) {
- let (pct_r, pct_w, pct_u, _) = self.rwu;
- let pct_sum = pct_r + pct_w + pct_u;
-
- let mut val = rng.random_range(0..pct_sum);
-
- val = val.saturating_sub(pct_r);
- if val == 0 {
- // Read operation
- let _ = store.get(key).unwrap();
- return;
- }
- val = val.saturating_sub(pct_w);
- if val == 0 {
- // Write operation
- if let Some(batch) = batch {
- batch.put(key, data);
- } else {
- store.put(key, data).unwrap();
- }
- return;
- }
-
- // Update operation
- if let Some(batch) = batch {
- batch.update(key, &ops::Append(data));
- } else {
- store.update(key, &ops::Append(data)).unwrap();
- }
- }
-
- fn step_n(
- &self,
- num_ops: usize,
- store: &impl Store,
- keys: &[Bytes],
- dataz: &[Bytes],
- rng: &mut Lcg64,
- ) {
- if self.batch_size == 1 {
- for _ in 0..num_ops {
- let key = &keys[rng.next_u64() as usize % keys.len()];
- let data = &dataz[rng.next_u64() as usize % dataz.len()];
- self.step(store, key, data, None, rng);
- }
- } else {
- let mut batch = store.batch();
-
- let mut at = 0;
- while at < num_ops {
- for _ in at..std::cmp::min(at + self.batch_size, num_ops) {
- let key = &keys[rng.next_u64() as usize % keys.len()];
- let data = &dataz[rng.next_u64() as usize % dataz.len()];
- self.step(store, key, data, Some(&mut batch), rng);
- at += 1;
- }
- batch.commit().unwrap();
- batch.reset();
- }
- }
- }
-}
-
-// Generate random data
-fn generate_data(size: usize) -> Bytes {
- let mut rng = StdRng::seed_from_u64(42);
- let mut data = vec![0u8; size];
- rng.fill(&mut data[..]);
- data.into()
-}
-
-// Generate a key for the given index
-fn generate_key(index: usize) -> Bytes {
- format!("key_{index:010}").into()
-}
-
-// Pre-generate a list of keys
-fn generate_keys(count: usize) -> Arc<[Bytes]> {
- (0..count).map(generate_key).collect()
-}
-
-trait BenchableStore {
- fn bench(&self, bencher: &mut Bencher, config: &BenchConfig);
-}
-
-struct StoreBench<S: Store> {
- store: S,
-}
-
-impl<S: Store> BenchableStore for StoreBench<S> {
- fn bench(&self, bencher: &mut Bencher, config: &BenchConfig) {
- bencher.iter_with_setup(
- move || {
- // Pre-generate all keys we'll use (1/3 the ops, allow some collision to
- // simulate hot keys)
- let keys = generate_keys(config.num_ops / 3);
-
- // Pre-generate data to write
- let data = (0..1000)
- .map(|_| generate_data(config.value_size))
- .collect::<Arc<[Bytes]>>();
-
- // Prepare data if needed
- if config.prepare_data {
- let mut rng = Lcg64(42);
- let wb = self.store.batch();
- for key in &keys[..(keys.len() * 3 / 4)] {
- let rand_data = &data[rng.next_u64() as usize % data.len()];
- wb.put(key, rand_data.as_ref());
- }
- wb.commit().unwrap();
- }
-
- // Return the prepared store and pre-generated keys/data
- (keys, data)
- },
- |(keys, data)| {
- // Actual benchmark
- if config.threads == 1 {
- // Single-threaded benchmark
- let mut rng = Lcg64(42);
- config.step_n(config.num_ops, &self.store, &keys[..], &data[..], &mut rng);
- } else {
- (0..config.threads).into_par_iter().for_each(|tid| {
- let num_ops = config.num_ops;
- let mut rng = Lcg64(42);
-
- let ops_per_thread = num_ops / config.threads;
- let thread_start = tid * ops_per_thread;
- let thread_end = std::cmp::min(thread_start + ops_per_thread, num_ops);
- let thread_cnt = thread_end - thread_start;
- config.step_n(thread_cnt, &self.store, &keys[..], &data[..], &mut rng);
- });
- }
- },
- );
- }
-}
-
-// Benchmark any Store implementation
-fn bench_kv(c: &mut Criterion, stores: &[(&str, Box<dyn BenchableStore>)]) {
- const C: usize = 24;
-
- // Single-threaded read benchmark
- const CONFIGS: &[BenchConfig] = &[
- // Small values, single-threaded reads
- BenchConfig {
- num_ops: 100_000,
- value_size: 64,
- threads: 1,
- batch_size: 1,
- rwu: (100, 0, 0, "read"),
- prepare_data: true,
- },
- // Small values, single-threaded writes
- BenchConfig {
- num_ops: 100_000,
- value_size: 64,
- threads: 1,
- batch_size: 1,
- rwu: (0, 100, 0, "write"),
- prepare_data: false,
- },
- // Small values, single-threaded writes, with batching
- BenchConfig {
- num_ops: 100_000,
- value_size: 64,
- threads: 1,
- batch_size: 128,
- rwu: (0, 100, 0, "write_batch"),
- prepare_data: false,
- },
- // Small values, single-threaded mixed (75% reads, 25% writes)
- BenchConfig {
- num_ops: 100_000,
- value_size: 64,
- threads: 1,
- batch_size: 1,
- rwu: (75, 25, 0, "mixed"),
- prepare_data: true,
- },
- // Small values, single-threaded update/reads
- BenchConfig {
- num_ops: 100_000,
- value_size: 64,
- threads: 1,
- batch_size: 1,
- rwu: (25, 0, 75, "update"),
- prepare_data: true,
- },
- // Small values, multi-threaded reads
- BenchConfig {
- num_ops: 100_000,
- value_size: 64,
- threads: C,
- batch_size: 1,
- rwu: (100, 0, 0, "read"),
- prepare_data: true,
- },
- // Small values, multi-threaded writes
- BenchConfig {
- num_ops: 100_000,
- value_size: 64,
- threads: C,
- batch_size: 1,
- rwu: (0, 100, 0, "write"),
- prepare_data: false,
- },
- // Small values, multi-threaded mixed (75% reads, 25% writes)
- BenchConfig {
- num_ops: 100_000,
- value_size: 64,
- threads: C,
- batch_size: 1,
- rwu: (75, 25, 0, "mixed"),
- prepare_data: true,
- },
- // Larger values, single-threaded reads
- BenchConfig {
- num_ops: 5_000,
- value_size: 1024,
- threads: 1,
- batch_size: 1,
- rwu: (100, 0, 0, "read"),
- prepare_data: true,
- },
- // Larger values, multi-threaded reads
- BenchConfig {
- num_ops: 50_000,
- value_size: 1024,
- threads: C,
- batch_size: 1,
- rwu: (100, 0, 0, "read"),
- prepare_data: true,
- },
- ];
-
- for config in CONFIGS {
- let operation_type = config.rwu.3;
- let thread_type = if config.threads == 1 {
- "single"
- } else {
- "multi"
- };
- let mut group = c.benchmark_group(format!(
- "{}_{}_{}",
- operation_type, thread_type, config.value_size
- ));
- group.throughput(Throughput::Elements(config.num_ops as u64));
-
- for (name, store) in stores {
- let benchmark_id = BenchmarkId::new((*name).to_string(), config.num_ops);
- group.bench_with_input(benchmark_id, config, |b, config| {
- store.bench(b, config);
- });
- }
- group.finish();
- }
-}
-
-fn criterion_benchmark(c: &mut Criterion) {
- let mut stores = Vec::<(&str, Box<dyn BenchableStore>)>::new();
-
- #[cfg(feature = "rocksdb")]
- {
- stores.push((
- "rocksdb",
- Box::new(StoreBench {
- store: tetra_kv::RocksDb::temp().expect("failed to create rocksdb"),
- }),
- ));
- }
- {
- stores.push((
- "memory",
- Box::new(StoreBench {
- store: tetra_kv::MemoryDb::new(),
- }),
- ));
- }
- {
- stores.push((
- "memory_erased",
- Box::new(StoreBench {
- store: tetra_kv::AnyDb::new(tetra_kv::MemoryDb::new()),
- }),
- ));
- }
- #[cfg(feature = "heed")]
- {
- stores.push((
- "lmdb",
- Box::new(StoreBench {
- store: tetra_kv::Lmdb::temp().expect("failed to create lmdb"),
- }),
- ));
- }
-
- bench_kv(c, &stores);
-}
-
-criterion_group!(
- name = benches;
- config = Criterion::default().sample_size(10).measurement_time(Duration::from_secs(5));
- targets = criterion_benchmark
-);
-criterion_main!(benches);
diff --git a/attic/kv/src/erased.rs b/attic/kv/src/erased.rs
deleted file mode 100644
index a39a0bc82..000000000
@@ -1,353 +0,0 @@
-use std::{fmt, iter};
-
-use bytes::Bytes;
-use smol_str::SmolStr;
-use tetra_core::containers::smol_box::SmolBox;
-
-use crate::{
- IVec, IVecs, Operator, Options,
- error::Result,
- traits::{self, IterMode},
-};
-
-type Unique<T> = SmolBox<T>;
-
-/// Holds a dynamic, boxed [`traits::WriteBatch`].
-pub struct AnyWriteBatch<'a>(pub Unique<dyn traits::WriteBatch<'a>>);
-
-impl<'a> fmt::Debug for AnyWriteBatch<'a> {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.debug_tuple("AnyWriteBatch").field(&self.0).finish()
- }
-}
-
-impl<'a> traits::Write for AnyWriteBatch<'a> {
- type Result = ();
- fn delete(&self, key: &[u8]) -> Self::Result {
- self.0.delete(key);
- }
- fn delete_range(&self, from: &[u8], to: &[u8]) -> Self::Result {
- self.0.delete_range(from, to);
- }
- fn put(&self, key: &[u8], value: &[u8]) -> Self::Result {
- self.0.put(key, value);
- }
- fn update(&self, key: &[u8], op: &dyn Operator) -> Self::Result {
- self.0.update(key, op);
- }
-}
-
-impl<'a> traits::WriteBatch<'a> for AnyWriteBatch<'a> {
- fn reset(&mut self) {
- self.0.reset();
- }
- fn commit(&self) -> Result<()> {
- self.0.commit()
- }
-}
-
-pub trait KeyIterator {
- fn next_key(&mut self) -> Option<&[u8]>;
-}
-
-pub(crate) fn adapt_key_iter<I: KeyIterator + ?Sized>(
- it: &mut I,
-) -> impl IntoIterator<Item: AsRef<[u8]>> {
- iter::from_fn(|| {
- if let Some(key) = it.next_key() {
- // TODO: Fix the lifetime here, although for now this is safe as long as the
- // iterator is not used after the lifetime of the view which always holds.
- let slice: &[u8] = key;
- let static_slice: &'static [u8] = unsafe { &*(slice as *const [u8]) };
- Some(static_slice)
- } else {
- None
- }
- })
-}
-
-struct KeyIteratorAdapter<I: Iterator<Item: AsRef<[u8]>>> {
- iter: I,
- peek: Option<I::Item>,
-}
-
-impl<I: Iterator<Item: AsRef<[u8]>>> KeyIterator for KeyIteratorAdapter<I> {
- fn next_key(&mut self) -> Option<&[u8]> {
- self.peek = self.iter.next();
- self.peek.as_ref().map(|item| item.as_ref())
- }
-}
-
-/// Object safe wrapper around [`traits::View`].
-pub trait DynView: fmt::Debug {
- /// Gets the value for the given key.
- fn get(&self, key: &[u8]) -> Result<Option<IVec<'_>>>;
- /// Returns true if the key exists in the store.
- fn contains(&self, key: &[u8]) -> Result<bool>;
- /// Iterates over key-value pairs.
- fn iter<'a>(
- &'a self,
- mode: IterMode<'_>,
- ) -> Unique<dyn Iterator<Item = Result<(Bytes, Bytes)>> + 'a>;
- /// Scans all keys with the given prefix.
- fn prefix_scan<'a>(
- &'a self,
- prefix: &'a [u8],
- mode: IterMode<'_>,
- ) -> Unique<dyn Iterator<Item = Result<(Bytes, Bytes)>> + 'a>;
-
- /// Gets the values for multiple keys at once.
- fn get_many(&self, keys: &mut dyn KeyIterator) -> Result<IVecs<'_>>;
-}
-
-pub trait DynViewable: fmt::Debug {
- fn viewable(&self) -> &(dyn DynView + '_);
-}
-
-pub struct AnyView<'a>(pub Unique<dyn DynView + 'a>);
-
-impl<'a> AnyView<'a> {
- pub fn new(view: impl traits::View + 'a) -> Self {
- AnyView(Unique::new(view))
- }
-}
-
-impl fmt::Debug for AnyView<'_> {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.debug_tuple("AnyView").field(&self.0).finish()
- }
-}
-
-impl<'a> DynViewable for AnyView<'a> {
- fn viewable(&self) -> &(dyn DynView + '_) {
- self.0.as_ref()
- }
-}
-
-impl<Q: DynViewable + ?Sized> traits::View for Q {
- type Iter<'a>
- = Unique<dyn Iterator<Item = Result<(Bytes, Bytes)>> + 'a>
- where
- Self: 'a;
-
- fn contains(&self, key: &[u8]) -> Result<bool> {
- DynView::contains(self.viewable(), key)
- }
- fn get(&self, key: &[u8]) -> Result<Option<IVec<'_>>> {
- DynView::get(self.viewable(), key)
- }
- fn get_many(&self, keys: impl IntoIterator<Item: AsRef<[u8]>>) -> Result<IVecs<'_>> {
- DynView::get_many(self.viewable(), &mut KeyIteratorAdapter {
- iter: keys.into_iter(),
- peek: None,
- })
- }
- fn iter<'a>(&'a self, mode: IterMode<'_>) -> Self::Iter<'a> {
- DynView::iter(self.viewable(), mode)
- }
-}
-
-impl<T: traits::View> DynView for T {
- fn get(&self, key: &[u8]) -> Result<Option<IVec<'_>>> {
- traits::View::get(self, key)
- }
-
- fn contains(&self, key: &[u8]) -> Result<bool> {
- traits::View::contains(self, key)
- }
-
- fn iter<'a>(
- &'a self,
- mode: IterMode<'_>,
- ) -> Unique<dyn Iterator<Item = Result<(Bytes, Bytes)>> + 'a> {
- Unique::new(traits::View::iter(self, mode))
- }
-
- fn prefix_scan<'a>(
- &'a self,
- prefix: &'a [u8],
- mode: IterMode<'_>,
- ) -> Unique<dyn Iterator<Item = Result<(Bytes, Bytes)>> + 'a> {
- Unique::new(traits::View::prefix_scan(self, prefix, mode))
- }
-
- fn get_many(&self, keys: &mut dyn KeyIterator) -> Result<IVecs<'_>> {
- traits::View::get_many(self, adapt_key_iter(keys))
- }
-}
-
-/// Object safe wrapper around [`traits::Store`].
-pub trait DynStore: DynView + Send + Sync {
- /// Creates a new write batch.
- fn batch(&self) -> AnyWriteBatch<'_>;
- /// Returns the namespace of this store.
- fn namespace(&self) -> &str;
- /// Creates a child store.
- fn child(&self, name: &str, opts: Options) -> Result<AnyDb>;
- /// Returns an iterator over the child stores.
- fn children(&self) -> Vec<SmolStr>;
- /// Sets the value for the given key.
- fn put(&self, key: &[u8], value: &[u8]) -> Result<()>;
- /// Deletes the value for the given key.
- fn delete(&self, key: &[u8]) -> Result<()>;
- /// Deletes all keys in the range [from, to)
- fn delete_range(&self, from: &[u8], to: &[u8]) -> Result<()>;
- /// Clears all data in the store.
- fn clear(&self) -> Result<()>;
- /// Flushes all data in the store to the underlying storage.
- fn flush(&self) -> Result<()>;
- /// Creates a snapshot of the store.
- fn snapshot(&self) -> AnyView<'_>;
- /// Estimate the size of the store.
- fn estimate_size(&self) -> Option<u64>;
- /// Returns the options for this store.
- fn options(&self) -> &Options;
- /// Merge the value for the given key with the given operation.
- fn update(&self, key: &[u8], op: &dyn Operator) -> Result<()>;
-}
-
-impl<T: traits::Store + Clone + Send + Sync + 'static> DynStore for T {
- fn batch(&self) -> AnyWriteBatch<'_> {
- AnyWriteBatch(Unique::new(traits::Store::batch(self)))
- }
-
- fn namespace(&self) -> &str {
- traits::Store::namespace(self)
- }
-
- fn child(&self, name: &str, opts: Options) -> Result<AnyDb> {
- Ok(AnyDb::new(traits::Store::child(self, name, opts)?))
- }
-
- fn children(&self) -> Vec<SmolStr> {
- traits::Store::children(self)
- }
-
- fn put(&self, key: &[u8], value: &[u8]) -> Result<()> {
- traits::Write::put(self, key, value)
- }
-
- fn delete(&self, key: &[u8]) -> Result<()> {
- traits::Write::delete(self, key)
- }
-
- fn update(&self, key: &[u8], op: &dyn Operator) -> Result<()> {
- traits::Write::update(self, key, op)
- }
-
- fn delete_range(&self, from: &[u8], to: &[u8]) -> Result<()> {
- traits::Write::delete_range(self, from, to)
- }
-
- fn clear(&self) -> Result<()> {
- traits::Store::clear(self)
- }
-
- fn flush(&self) -> Result<()> {
- traits::Store::flush(self)
- }
-
- fn snapshot(&self) -> AnyView<'_> {
- AnyView(Unique::new(traits::Store::snapshot(self)))
- }
-
- fn estimate_size(&self) -> Option<u64> {
- traits::Store::estimate_size(self)
- }
-
- fn options(&self) -> &Options {
- traits::Store::options(self)
- }
-}
-
-pub struct AnyDb(pub triomphe::Arc<dyn DynStore>);
-
-impl fmt::Debug for AnyDb {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.debug_tuple("AnyDb").field(&self.0).finish()
- }
-}
-
-impl AnyDb {
- /// Create a new [`AnyDb`] from a [`Store`].
- pub fn new(store: impl traits::Store + 'static) -> Self {
- let arc = triomphe::Arc::new(store);
- let ptr = triomphe::Arc::into_raw(arc);
- AnyDb(unsafe { triomphe::Arc::from_raw(ptr as *const _) })
- }
-}
-
-impl Clone for AnyDb {
- fn clone(&self) -> Self {
- AnyDb(triomphe::Arc::clone(&self.0))
- }
-}
-
-impl DynViewable for AnyDb {
- fn viewable(&self) -> &(dyn DynView + '_) {
- self.0.as_ref()
- }
-}
-
-impl traits::Write for AnyDb {
- type Result = Result<()>;
- fn delete(&self, key: &[u8]) -> Self::Result {
- DynStore::delete(self.0.as_ref(), key)
- }
- fn delete_range(&self, from: &[u8], to: &[u8]) -> Self::Result {
- DynStore::delete_range(self.0.as_ref(), from, to)
- }
- fn put(&self, key: &[u8], value: &[u8]) -> Self::Result {
- DynStore::put(self.0.as_ref(), key, value)
- }
- fn update(&self, key: &[u8], op: &dyn Operator) -> Self::Result {
- DynStore::update(self.0.as_ref(), key, op)
- }
-}
-
-impl traits::Store for AnyDb {
- type WriteBatch<'a>
- = AnyWriteBatch<'a>
- where
- Self: 'a;
- type Snapshot<'a>
- = AnyView<'a>
- where
- Self: 'a;
-
- fn batch(&self) -> Self::WriteBatch<'_> {
- DynStore::batch(self.0.as_ref())
- }
-
- fn namespace(&self) -> &str {
- DynStore::namespace(self.0.as_ref())
- }
-
- fn child(&self, name: &str, opts: Options) -> Result<Self> {
- DynStore::child(self.0.as_ref(), name, opts)
- }
-
- fn children(&self) -> Vec<SmolStr> {
- DynStore::children(self.0.as_ref())
- }
-
- fn flush(&self) -> Result<()> {
- DynStore::flush(self.0.as_ref())
- }
-
- fn clear(&self) -> Result<()> {
- DynStore::clear(self.0.as_ref())
- }
-
- fn snapshot<'b>(&'b self) -> Self::Snapshot<'b> {
- DynStore::snapshot(self.0.as_ref())
- }
-
- fn estimate_size(&self) -> Option<u64> {
- DynStore::estimate_size(self.0.as_ref())
- }
-
- fn options(&self) -> &Options {
- DynStore::options(self.0.as_ref())
- }
-}
diff --git a/attic/kv/src/error.rs b/attic/kv/src/error.rs
deleted file mode 100644
index 25d866ee8..000000000
@@ -1,39 +0,0 @@
-use smol_str::SmolStr;
-use thiserror::Error;
-
-#[derive(Error, Debug)]
-pub enum Error {
- #[error("Postcard serialization error: {0}")]
- PostcardSerialization(#[from] postcard::Error),
-
- #[error("CBOR serialization error: {0}")]
- CborSerialization(#[from] serde_cbor::Error),
-
- #[cfg(feature = "rocksdb")]
- #[error("RocksDB error: {0}")]
- RocksDb(#[from] rocksdb::Error),
-
- #[cfg(feature = "heed")]
- #[error("Heed error: {0}")]
- Heed(#[from] heed::Error),
-
- #[error("Column family not found: {0}")]
- ColumnFamilyNotFound(SmolStr),
-
- #[error("I/O error: {0}")]
- Io(#[from] std::io::Error),
-
- #[error("Invalid operation: {0}")]
- InvalidOperation(String),
-
- #[error("Database corrupted: {0}")]
- Corrupted(String),
-
- #[error("Other error: {0}")]
- Other(String),
-
- #[error("Invalid UTF-8 sequence: {0}")]
- InvalidUtf8(#[from] std::str::Utf8Error),
-}
-
-pub type Result<T, E = Error> = std::result::Result<T, E>;
diff --git a/attic/kv/src/ivec.rs b/attic/kv/src/ivec.rs
deleted file mode 100644
index 69b045ebf..000000000
@@ -1,1014 +0,0 @@
-//! A flexible, zero‑cost abstraction for owning or borrowing byte buffers.
-//!
-//! `IVec` can store a wide range of backing types (`&[u8]`, `Vec<u8>`, `Bytes`,
-//! etc.) without additional allocations. For short slices (≤ 23 bytes on
-//! 64‑bit) the bytes are stored **inline**; otherwise the backing value is kept
-//! on the heap and referenced through a custom vtable. Conversions back to the
-//! original container are zero‑cost as well.
-//!
-//! The implementation makes heavy use of `unsafe` to achieve this. Every
-//! `unsafe` block is documented with a **SAFETY** comment that explains the
-//! invariants the block relies on.
-
-use std::{
- fmt, iter,
- marker::PhantomData,
- mem::{self, ManuallyDrop, MaybeUninit},
- ops::Deref,
- ptr, slice,
-};
-
-use crate::{Error, Result};
-
-/// Trait implemented by every container that can back an [`IVec`].
-///
-/// The trait is intentionally minimal: each method either
-/// *returns* a borrowed slice, or *consumes* `self` and returns an owned
-/// buffer. Implementors must ensure that the memory pointed to by `as_slice`
-/// outlives `self`.
-pub trait IVecRef: Sized {
- /// Borrow the contents as `&[u8]`.
- fn as_slice(&self) -> &[u8];
- /// Consume `self` and return a `Vec<u8>` with identical bytes.
- fn into_vec(self) -> Vec<u8>;
- /// Consume `self` and return a [`bytes::Bytes`] with identical bytes.
- fn into_bytes(self) -> bytes::Bytes;
-}
-
-/// A wrapper for static slices that avoids allocations.
-#[derive(Clone, Copy)]
-struct StaticIVec(&'static [u8]);
-
-impl IVecRef for StaticIVec {
- fn as_slice(&self) -> &[u8] {
- self.0
- }
- fn into_vec(self) -> Vec<u8> {
- self.0.into()
- }
- fn into_bytes(self) -> bytes::Bytes {
- bytes::Bytes::from_static(self.0)
- }
-}
-
-// Blanket implementations for standard byte containers
-impl IVecRef for &[u8] {
- fn as_slice(&self) -> &[u8] {
- self
- }
- fn into_vec(self) -> Vec<u8> {
- self.into()
- }
- fn into_bytes(self) -> bytes::Bytes {
- bytes::Bytes::copy_from_slice(self)
- }
-}
-
-impl IVecRef for Box<[u8]> {
- fn as_slice(&self) -> &[u8] {
- self.as_ref()
- }
- fn into_vec(self) -> Vec<u8> {
- self.into()
- }
- fn into_bytes(self) -> bytes::Bytes {
- bytes::Bytes::from_owner(self)
- }
-}
-
-impl IVecRef for bytes::Bytes {
- fn as_slice(&self) -> &[u8] {
- self.as_ref()
- }
- fn into_vec(self) -> Vec<u8> {
- self.into()
- }
- fn into_bytes(self) -> bytes::Bytes {
- self
- }
-}
-
-impl IVecRef for String {
- fn as_slice(&self) -> &[u8] {
- self.as_ref()
- }
- fn into_vec(self) -> Vec<u8> {
- String::into_bytes(self)
- }
- fn into_bytes(self) -> bytes::Bytes {
- bytes::Bytes::from_owner(self.into_bytes())
- }
-}
-
-impl IVecRef for &str {
- fn as_slice(&self) -> &[u8] {
- self.as_ref()
- }
- fn into_vec(self) -> Vec<u8> {
- self.as_bytes().into()
- }
- fn into_bytes(self) -> bytes::Bytes {
- bytes::Bytes::copy_from_slice(self.as_ref())
- }
-}
-
-impl IVecRef for Box<str> {
- fn as_slice(&self) -> &[u8] {
- str::as_bytes(self)
- }
- fn into_vec(self) -> Vec<u8> {
- self.into_boxed_bytes().into()
- }
- fn into_bytes(self) -> bytes::Bytes {
- bytes::Bytes::from_owner(self.into_boxed_bytes())
- }
-}
-
-impl IVecRef for Vec<u8> {
- fn as_slice(&self) -> &[u8] {
- self.as_ref()
- }
- fn into_vec(self) -> Vec<u8> {
- self
- }
- fn into_bytes(self) -> bytes::Bytes {
- bytes::Bytes::from_owner(self)
- }
-}
-
-const MAX_INLINE: usize = 8 * 3;
-
-/// A tiny, inline buffer used by [`IVec`] for very small slices.
-///
-/// The buffer reserves space for `MAX_INLINE - 1` bytes so that the entire
-/// structure fits exactly into one [`IVecRepr`].
-#[derive(Debug, Clone, Copy)]
-#[repr(C, packed)]
-struct SmallIVec {
- /// Uninitialised storage – only the first `len` bytes hold valid data.
- data: MaybeUninit<[u8; MAX_INLINE - 1]>,
- /// Number of valid bytes inside `data` (0‥=23).
- len: u8,
-}
-
-const _: () = assert!(IVecVtable::is_inline::<SmallIVec>());
-
-impl SmallIVec {
- /// Try to build an inline buffer from `slice`.
- /// Returns `None` if `slice` is too large.
- pub const fn try_from(slice: &[u8]) -> Option<Self> {
- if slice.len() < MAX_INLINE {
- let mut data = MaybeUninit::uninit();
- // SAFETY: we just checked that `slice.len()` ≤ 23 ≤ capacity of
- // `data`, and both pointers are non‑overlapping. `data` is still
- // uninitialised, so writing arbitrary bytes is allowed.
- unsafe {
- ptr::copy_nonoverlapping(slice.as_ptr(), data.as_mut_ptr() as *mut u8, slice.len());
- }
- Some(Self {
- data,
- len: slice.len() as u8,
- })
- } else {
- None
- }
- }
-}
-
-impl IVecRef for SmallIVec {
- fn as_slice(&self) -> &[u8] {
- self.as_ref()
- }
- fn into_vec(self) -> Vec<u8> {
- self.as_ref().into()
- }
- fn into_bytes(self) -> bytes::Bytes {
- bytes::Bytes::from_owner(self)
- }
-}
-
-impl AsRef<[u8]> for SmallIVec {
- fn as_ref(&self) -> &[u8] {
- // SAFETY: `len` never exceeds the capacity of `data` (MAX_INLINE - 1) thanks to
- // the check in `try_from`. `data` was fully initialised up to `len`
- // bytes when the value was constructed and neither `data` nor `len` are
- // ever mutated afterwards, so the bytes remain valid for the lifetime of
- // `self`.
- unsafe { slice::from_raw_parts(self.data.as_ptr() as *const u8, self.len as usize) }
- }
-}
-
-/// `MAX_INLINE`‑byte storage that can either hold any container implementing
-/// [`IVecRef`], either inline (in `inl`) or a raw pointer to a boxed heap
-/// allocation (in `ext`).
-#[derive(Clone, Copy)]
-#[repr(align(8))]
-union IVecRepr {
- inl: MaybeUninit<[u8; MAX_INLINE]>,
- ext: *mut (),
-}
-
-/// Methods needed to convert/destroy an erased backing value.
-struct IVecVtable {
- into_vec: unsafe fn(&mut IVecRepr) -> Vec<u8>,
- into_bytes: unsafe fn(&mut IVecRepr) -> bytes::Bytes,
- drop: unsafe fn(&mut IVecRepr),
- as_ref: unsafe fn(&IVecRepr) -> *const [u8],
-}
-
-impl IVecVtable {
- /// Does `T` fit inside the inline storage? (Compile‑time check.)
- #[inline]
- const fn is_inline<T: IVecRef>() -> bool {
- mem::size_of::<T>() <= mem::size_of::<IVecRepr>()
- && mem::align_of::<T>() <= mem::align_of::<IVecRepr>()
- }
-
- /// Re‑interpret `storage` as `&T`.
- ///
- /// # Safety
- /// The caller must ensure that `storage` actually contains a valid `T`,
- /// either inline or via a valid pointer.
- #[inline]
- const fn as_ref<T: IVecRef>(storage: &IVecRepr) -> &T {
- unsafe {
- &*if Self::is_inline::<T>() {
- storage.inl.as_ptr().cast::<T>()
- } else {
- storage.ext as *const T
- }
- }
- }
-
- /// Consume and return the stored value as `T`.
- #[inline]
- fn into_inner<T: IVecRef>(storage: &mut IVecRepr) -> T {
- unsafe {
- if Self::is_inline::<T>() {
- // SAFETY: when the value was packed inline we wrote a valid `T`
- // into `storage.inl`; reading it back is safe and leaves the
- // memory uninitialised again.
- storage.inl.as_mut_ptr().cast::<T>().read()
- } else {
- // SAFETY: `storage.ext` originated from `Box::into_raw`, so it
- // is a valid pointer obtained by `Box::into_raw` for T.
- *Box::from_raw(storage.ext as *mut T)
- }
- }
- }
-
- /// Store `value` in an `IVecRepr` and build the corresponding vtable.
- #[inline]
- fn pack<T: IVecRef>(value: T) -> (IVecRepr, &'static IVecVtable) {
- let repr = if Self::is_inline::<T>() {
- let mut storage = IVecRepr {
- inl: MaybeUninit::uninit(),
- };
- // SAFETY: The inline storage is large & aligned enough by
- // `is_inline` check. We immediately overwrite it with a valid `T`.
- unsafe {
- ptr::write(storage.inl.as_mut_ptr().cast::<T>(), value);
- storage
- }
- } else {
- let boxed: Box<T> = Box::new(value);
- IVecRepr {
- ext: Box::into_raw(boxed) as *mut (),
- }
- };
- // The vtable is `const`, hence it lives for the entire program run.
- let vtable = &const { Self::new::<T>() };
- (repr, vtable)
- }
-
- /// Build a vtable specialised for `T`.
- #[inline]
- const fn new<T: IVecRef>() -> Self {
- Self {
- into_vec: |x| Self::into_inner::<T>(x).into_vec(),
- into_bytes: |x| Self::into_inner::<T>(x).into_bytes(),
- drop: |x| drop(Self::into_inner::<T>(x)),
- as_ref: |x| Self::as_ref::<T>(x).as_slice() as *const [u8],
- }
- }
-}
-
-#[derive(derive_more::Debug)]
-#[debug("IVec({:?})", self.as_ref())]
-pub struct IVec<'a> {
- /// Inline storage or pointer to box.
- storage: IVecRepr,
- /// Associated vtable (missing only after `into_vec`/`into_bytes` or `drop`).
- vtable: Option<&'static IVecVtable>,
- /// Ties the lifetime parameter to the contained reference variants.
- _marker: PhantomData<&'a ()>,
-}
-
-impl<'a, V: IVecRef + 'a> From<V> for IVec<'a> {
- fn from(value: V) -> Self {
- Self::new(value)
- }
-}
-
-pub(crate) struct IVecRaw {
- storage: IVecRepr,
- vtable: &'static IVecVtable,
-}
-
-impl<'a> IVec<'a> {
- pub(crate) fn into_raw(mut self) -> IVecRaw {
- IVecRaw {
- storage: self.storage,
- vtable: self.vtable.take().unwrap(),
- }
- }
-
- pub(crate) fn from_raw(raw: IVecRaw) -> Self {
- Self {
- storage: raw.storage,
- vtable: Some(raw.vtable),
- _marker: PhantomData,
- }
- }
-
- /// Create a new `IVec` from `value`.
- ///
- /// Small inputs are stored inline, larger ones on the heap.
- pub fn new<T: IVecRef + 'a>(value: T) -> Self {
- let (storage, vtable) = if let Some(svec) = SmallIVec::try_from(value.as_slice()) {
- IVecVtable::pack(svec)
- } else {
- IVecVtable::pack(value)
- };
- Self {
- storage,
- vtable: Some(vtable),
- _marker: PhantomData,
- }
- }
-
- /// Same as [`IVec::new`] but takes `&T` and therefore *clones* the value
- /// when needed. Convenience helper for ergonomic usage.
- pub fn from_ref<T: IVecRef + Clone + 'a>(value: &T) -> Self {
- let (storage, vtable) = if let Some(svec) = SmallIVec::try_from(value.as_slice()) {
- IVecVtable::pack(svec)
- } else {
- IVecVtable::pack(value.clone())
- };
- Self {
- storage,
- vtable: Some(vtable),
- _marker: PhantomData,
- }
- }
-
- /// Create an `IVec` from a static slice. This is optimized for static data
- /// that doesn't need to be copied or cloned.
- pub fn from_static(slice: &'static [u8]) -> Self {
- let (storage, vtable) = IVecVtable::pack(StaticIVec(slice));
- Self {
- storage,
- vtable: Some(vtable),
- _marker: PhantomData,
- }
- }
-}
-
-// Safety: `IVec` is Send+Sync as long as the backing types are Send+Sync. We
-// only ever refer to them through immutable references or by *moving* the value
-// out (which transfers ownership to the caller). There are no aliasing issues.
-unsafe impl<'a> Send for IVec<'a> {}
-unsafe impl<'a> Sync for IVec<'a> {}
-
-impl<'a> AsRef<[u8]> for IVec<'a> {
- #[inline]
- fn as_ref(&self) -> &[u8] {
- // SAFETY: The vtable *must* be present unless the value has already been
- // moved out via `From<IVec>` impls. In that case callers are not
- // allowed to use `self` anymore (it would be after‑move use) – Rust’s
- // move semantics guarantee this. Therefore the `unwrap` is safe.
- unsafe { &*(self.vtable.unwrap().as_ref)(&self.storage) }
- }
-}
-
-impl<'a> Deref for IVec<'a> {
- type Target = [u8];
- fn deref(&self) -> &Self::Target {
- self.as_ref()
- }
-}
-
-impl<'a> From<IVec<'a>> for Vec<u8> {
- fn from(mut val: IVec<'a>) -> Self {
- // Take ownership of the vtable so we can no longer be used afterwards.
- let vft = val.vtable.take().unwrap();
- // SAFETY: `vft.into_vec` was constructed to accept exactly our storage.
- unsafe { (vft.into_vec)(&mut val.storage) }
- }
-}
-
-impl<'a> From<IVec<'a>> for bytes::Bytes {
- fn from(mut val: IVec<'a>) -> Self {
- // Take ownership of the vtable so we can no longer be used afterwards.
- let vft = val.vtable.take().unwrap();
- // SAFETY: `vft.into_bytes` was constructed to accept exactly our storage.
- unsafe { (vft.into_bytes)(&mut val.storage) }
- }
-}
-
-impl<'a> Drop for IVec<'a> {
- fn drop(&mut self) {
- if let Some(vft) = self.vtable.take() {
- // SAFETY: `vft.drop` was constructed to accept exactly our storage.
- unsafe { (vft.drop)(&mut self.storage) }
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn ivec_from_small_slice() {
- let data = b"hello world";
- let ivec = IVec::new(&data[..]);
- assert_eq!(ivec.as_ref(), data);
- let vec: Vec<u8> = ivec.into();
- assert_eq!(vec, data);
- }
-
- #[test]
- fn ivec_from_large_vec() {
- let data = vec![42u8; 100];
- let ivec = IVec::from(data.clone());
- assert_eq!(ivec.as_ref(), &data[..]);
- let bytes: bytes::Bytes = ivec.into();
- assert_eq!(bytes.as_ref(), &data[..]);
- }
-
- #[test]
- fn ivec_from_string() {
- let s = "a somewhat longer string".to_owned();
- let ivec = IVec::from(s.clone());
- assert_eq!(ivec.as_ref(), s.as_bytes());
- }
-
- #[test]
- fn ivec_from_bytes() {
- let data = bytes::Bytes::from_static(b"bytes data");
- let ivec = IVec::from(data.clone());
- assert_eq!(ivec.as_ref(), data.as_ref());
- }
-
- #[test]
- fn ivec_zero_len() {
- let data: &[u8] = b"";
- let ivec = IVec::new(data);
- assert!(ivec.as_ref().is_empty());
- let vec: Vec<u8> = ivec.into();
- assert!(vec.is_empty());
- }
-
- #[test]
- fn ivec_from_static_slice() {
- // Test with small static slice
- const SMALL_STATIC: &[u8] = b"small data";
- let ivec_small = IVec::from_static(SMALL_STATIC);
- assert_eq!(ivec_small.as_ref(), SMALL_STATIC);
- let vec_small: Vec<u8> = ivec_small.into();
- assert_eq!(vec_small, SMALL_STATIC);
-
- // Test with large static slice
- const LARGE_STATIC: &[u8] =
- b"this is a much longer static slice that exceeds the inline capacity";
- let ivec_large = IVec::from_static(LARGE_STATIC);
- assert_eq!(ivec_large.as_ref(), LARGE_STATIC);
-
- // Convert to Bytes - this should use Bytes::from_static internally (no
- // allocation)
- let bytes_large: bytes::Bytes = ivec_large.into();
- assert_eq!(bytes_large.as_ref(), LARGE_STATIC);
-
- // Verify pointer equality - Bytes::from_static should preserve the pointer
- assert_eq!(bytes_large.as_ptr(), LARGE_STATIC.as_ptr());
-
- // Test that regular from with non-static slice allocates (different behavior)
- let non_static_slice: &[u8] = &LARGE_STATIC[..10];
- let ivec_regular = IVec::from(non_static_slice);
- assert_eq!(ivec_regular.as_ref(), &LARGE_STATIC[..10]);
- }
-}
-
-// Maximum number of IVec items that can be stored inline without heap
-// allocation
-const MAX_INLINE_VECS: usize = 6;
-// Configuration flag for whether to attempt inlining when converting from Box
-const TRY_INLINE_WITH_BOX: bool = false;
-
-/// Union type that can either store IVec items inline or as a pointer to heap
-/// storage. This is the core storage mechanism for IVecs, similar to IVecRepr
-/// but for collections.
-#[repr(C)]
-union IVecsRepr {
- // Inline storage using heapless::Vec for small collections (up to MAX_INLINE_VECS items)
- // Each item is Result<Option<IVecRaw>, Error> to handle errors and missing values
- inl: ManuallyDrop<heapless::Vec<Result<Option<IVecRaw>, Error>, MAX_INLINE_VECS>>,
- // External storage as a raw pointer and length for larger collections
- ext: (*mut (), usize),
-}
-
-impl IVecsRepr {
- /// Collect an iterator of IVecsResult items into an IVecsRepr with
- /// appropriate vtable. Intelligently chooses between inline and heap
- /// storage based on size hints.
- pub fn collect<T: IVecsResult>(iter: impl Iterator<Item = T>) -> (Self, &'static IVecsVtable) {
- let (low, high) = iter.size_hint();
- // If we have a reliable upper bound that fits inline, use bounded collection
- if high.is_some_and(|x| x <= MAX_INLINE_VECS || x == low) {
- Self::collect_bounded(iter, high.unwrap_or(low))
- } else {
- // Otherwise use unbounded collection that may spill to heap
- Self::collect_unbounded(iter)
- }
- }
-
- /// Handle collection when size is unknown or potentially large.
- /// Starts with inline storage and spills to heap if needed.
- #[cold]
- fn collect_unbounded<T: IVecsResult>(
- mut iter: impl Iterator<Item = T>,
- ) -> (Self, &'static IVecsVtable) {
- // Try to collect into inline buffer first
- let mut buffer = heapless::Vec::<T, MAX_INLINE_VECS>::new();
- for item in iter.by_ref() {
- // Still have space in the inline buffer
- if let Err(e) = buffer.push(item) {
- // Buffer full - switch to heap allocation
- return Self::from_boxed(
- buffer
- .into_iter()
- .chain(iter::once(e))
- .chain(iter.by_ref())
- .collect::<Box<[T]>>(),
- );
- }
- }
-
- // All items fit inline - transform to internal representation
- let mut transformed = heapless::Vec::<Result<Option<IVecRaw>, Error>, MAX_INLINE_VECS>::new();
- for mut item in buffer {
- transformed
- .push(item.take().map(|x| x.map(|x| x.into_raw())))
- .map_err(|_| ())
- .expect("inline overflow");
- }
-
- (
- Self {
- inl: ManuallyDrop::new(transformed),
- },
- &IVecsVtable::INLINE_VTABLE,
- )
- }
-
- /// Collect when we know the upper bound of items.
- /// Directly chooses inline or heap storage based on size.
- fn collect_bounded<T: IVecsResult>(
- iter: impl Iterator<Item = T>,
- higher_bound: usize,
- ) -> (Self, &'static IVecsVtable) {
- if higher_bound <= MAX_INLINE_VECS {
- // Fits inline - collect directly into inline storage
- let mut inline = heapless::Vec::new();
- for mut item in iter {
- inline
- .push(item.take().map(|x| x.map(|x| x.into_raw())))
- .map_err(|_| ())
- .expect("inline overflow");
- }
- (
- Self {
- inl: ManuallyDrop::new(inline),
- },
- &IVecsVtable::INLINE_VTABLE,
- )
- } else {
- // Too large for inline - allocate on heap
- let vec = iter.collect::<Vec<_>>();
- let count = vec.len();
- let boxed = Box::into_raw(vec.into_boxed_slice());
- (
- Self {
- ext: (boxed.as_mut_ptr() as *mut (), count),
- },
- // Create type-specific vtable for external storage
- &const { IVecsVtable::new_external::<T>() },
- )
- }
- }
-
- /// Convert a boxed slice into IVecsRepr.
- /// May attempt to inline if TRY_INLINE_WITH_BOX is enabled and size permits.
- pub fn from_boxed<T: IVecsResult>(boxd: Box<[T]>) -> (Self, &'static IVecsVtable) {
- let len = boxd.len();
- if TRY_INLINE_WITH_BOX && len <= MAX_INLINE_VECS {
- // Try to inline the boxed content
- let mut inline = heapless::Vec::new();
- for mut item in boxd {
- inline
- .push(item.take().map(|x| x.map(|x| x.into_raw())))
- .map_err(|_| ())
- .expect("inline overflow");
- }
- (
- Self {
- inl: ManuallyDrop::new(inline),
- },
- &IVecsVtable::INLINE_VTABLE,
- )
- } else {
- // Keep as external storage
- let boxd = Box::into_raw(boxd);
- (
- Self {
- ext: (boxd.as_mut_ptr() as *mut (), len),
- },
- &const { IVecsVtable::new_external::<T>() },
- )
- }
- }
-}
-
-/// Virtual function table for IVecs operations.
-/// Provides polymorphic behavior for inline vs external storage.
-struct IVecsVtable {
- // Get a reference to the item at index without consuming it
- get: unsafe fn(&IVecsRepr, usize) -> Result<Option<*const [u8]>>,
- // Take ownership of the item at index, leaving None in its place
- take: unsafe fn(&mut IVecsRepr, usize) -> Result<Option<IVecRaw>>,
- // Drop the entire collection
- drop: unsafe fn(&mut IVecsRepr),
- // Flag indicating if storage is inline
- is_inline: bool,
-}
-
-/// A collection of IVec values with efficient storage.
-/// Like IVec, but for multiple values with support for None and Error variants.
-pub struct IVecs<'a> {
- // Storage representation (inline or external)
- repr: IVecsRepr,
- // Virtual function table for operations
- vtable: &'static IVecsVtable,
- // Lifetime marker for borrowed data
- _lifetime: PhantomData<&'a ()>,
-}
-
-impl<'a> fmt::Debug for IVecs<'a> {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- let mut list = f.debug_list();
- // Debug print each item in the collection
- for i in 0..self.len() {
- list.entry(&self.get(i));
- }
- list.finish()
- }
-}
-
-/// Trait for types that can be stored in IVecs.
-/// Supports both getting references and taking ownership.
-pub trait IVecsResult {
- // Get a reference to the underlying byte slice
- fn get(&self) -> Result<Option<&[u8]>>;
- // Take ownership and convert to IVec
- fn take(&mut self) -> Result<Option<IVec<'_>>>;
-}
-
-// Implementation for Result<Option<T>, E> types
-impl<T: IVecRef, E: Into<Error> + std::error::Error> IVecsResult for Result<Option<T>, E> {
- fn get(&self) -> Result<Option<&[u8]>> {
- match self {
- Ok(Some(x)) => Ok(Some(x.as_slice())),
- Ok(None) => Ok(None),
- Err(e) => Err(Error::Other(format!("IVecsResult::get: {e:?}"))),
- }
- }
- fn take(&mut self) -> Result<Option<IVec<'_>>> {
- match mem::replace(self, Ok(None)) {
- Ok(Some(x)) => Ok(Some(IVec::from(x))),
- Ok(None) => Ok(None),
- Err(e) => Err(e.into()),
- }
- }
-}
-
-// Implementation for Option<T> types (simpler case without errors)
-impl<T: IVecRef> IVecsResult for Option<T> {
- fn get(&self) -> Result<Option<&[u8]>> {
- match self {
- Some(x) => Ok(Some(x.as_slice())),
- None => Ok(None),
- }
- }
- fn take(&mut self) -> Result<Option<IVec<'_>>> {
- match self.take() {
- Some(x) => Ok(Some(IVec::from(x))),
- None => Ok(None),
- }
- }
-}
-
-// Conversion implementations for common collection types
-impl<'a, T: IVecsResult> From<Vec<T>> for IVecs<'a> {
- fn from(value: Vec<T>) -> Self {
- Self::from_vec(value)
- }
-}
-
-impl<'a, T: IVecsResult + Clone> From<&'a [T]> for IVecs<'a> {
- fn from(value: &'a [T]) -> Self {
- Self::from_iter(value.iter().cloned())
- }
-}
-
-impl<'a, const N: usize, T: IVecsResult + Clone> From<[T; N]> for IVecs<'a> {
- fn from(value: [T; N]) -> Self {
- Self::from_iter(value)
- }
-}
-
-impl<'a, T: IVecsResult> FromIterator<T> for IVecs<'a> {
- fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
- let (repr, vtable) = IVecsRepr::collect(iter.into_iter());
- Self {
- repr,
- vtable,
- _lifetime: PhantomData,
- }
- }
-}
-
-impl<'a> IVecs<'a> {
- /// Create IVecs from a Vec, converting to boxed slice for efficiency
- pub fn from_vec<T: IVecsResult>(vec: Vec<T>) -> Self {
- let (repr, vtable) = IVecsRepr::from_boxed(vec.into_boxed_slice());
- Self {
- repr,
- vtable,
- _lifetime: PhantomData,
- }
- }
-
- /// Get the number of items in the collection
- #[inline]
- pub fn len(&self) -> usize {
- if self.vtable.is_inline {
- // For inline storage, get length from heapless::Vec
- unsafe { self.repr.inl.len() }
- } else {
- // For external storage, length is stored in the second field
- unsafe { self.repr.ext.1 }
- }
- }
-
- /// Check if the collection is empty
- #[inline]
- pub fn is_empty(&self) -> bool {
- self.len() == 0
- }
-
- /// Get a reference to the item at the given index
- #[inline]
- pub fn get(&self, idx: usize) -> Result<Option<&[u8]>> {
- if idx >= self.len() {
- return Ok(None);
- }
- // Call the appropriate vtable function and convert pointer to reference
- unsafe { (self.vtable.get)(&self.repr, idx).map(|x| x.map(|x| &*x)) }
- }
-
- /// Take ownership of the item at the given index
- #[inline]
- pub fn take(&mut self, idx: usize) -> Result<Option<IVec<'_>>> {
- if idx >= self.len() {
- return Ok(None);
- }
- // Call the appropriate vtable function and convert raw to IVec
- unsafe { (self.vtable.take)(&mut self.repr, idx).map(|x| x.map(IVec::from_raw)) }
- }
-}
-
-impl<'a> Drop for IVecs<'a> {
- fn drop(&mut self) {
- // Use vtable to properly drop the storage
- unsafe { (self.vtable.drop)(&mut self.repr) }
- }
-}
-
-/// Iterator over IVecs that consumes the collection
-#[derive(Debug)]
-pub struct IVecsIter<'a> {
- // The IVecs being iterated (ownership transferred)
- inner: IVecs<'a>,
- // Current index in the iteration
- idx: usize,
-}
-
-impl<'a> IntoIterator for IVecs<'a> {
- type Item = Result<Option<IVec<'a>>>;
- type IntoIter = IVecsIter<'a>;
-
- fn into_iter(self) -> Self::IntoIter {
- IVecsIter {
- inner: self,
- idx: 0,
- }
- }
-}
-
-impl<'a> Iterator for IVecsIter<'a> {
- type Item = Result<Option<IVec<'a>>>;
-
- fn next(&mut self) -> Option<Self::Item> {
- if self.idx == self.inner.len() {
- return None;
- }
- let i = self.idx;
- self.idx += 1;
- // Take the item at current index and transmute lifetime
- let ivec: IVec<'_> = match self.inner.take(i) {
- Ok(Some(ivec)) => ivec,
- Ok(None) => return Some(Ok(None)),
- Err(e) => return Some(Err(e)),
- };
-
- // SAFETY: The lifetime is valid because we own the IVecs
- Some(Ok(Some(unsafe {
- mem::transmute::<IVec<'_>, IVec<'a>>(ivec)
- })))
- }
-
- fn size_hint(&self) -> (usize, Option<usize>) {
- let n = self.inner.len() - self.idx;
- (n, Some(n))
- }
-}
-impl<'a> ExactSizeIterator for IVecsIter<'a> {}
-
-impl IVecsVtable {
- /// Create a vtable for external (heap) storage of type T
- const fn new_external<T: IVecsResult>() -> Self {
- Self {
- get: |repr, idx| {
- // Cast raw pointer back to T and call get()
- let t: &T = unsafe { &*(repr.ext.0 as *const T).add(idx) };
- t.get().map(|x| x.map(|x| x as *const [u8]))
- },
- take: |repr, idx| {
- // Cast raw pointer back to mutable T and call take()
- let t: &mut T = unsafe { &mut *(repr.ext.0 as *mut T).add(idx) };
- t.take().map(|x| x.map(|x| x.into_raw()))
- },
- drop: |repr| unsafe {
- // Reconstruct the Box and drop it properly
- let t: &mut [T] = slice::from_raw_parts_mut(repr.ext.0 as *mut T, repr.ext.1);
- drop(Box::<[T]>::from_raw(t as *mut [T]));
- },
- is_inline: false,
- }
- }
-
- /// Static vtable for inline storage
- const INLINE_VTABLE: Self = Self {
- get: |repr, idx| unsafe {
- // Access inline heapless::Vec and get item
- let r: &heapless::Vec<_, MAX_INLINE_VECS> = &repr.inl;
- match &r[idx] {
- Ok(Some(x)) => Ok(Some((x.vtable.as_ref)(&x.storage))),
- Ok(None) => Ok(None),
- Err(e) => Err(Error::Other(format!("IVecsResult::get: {e:?}"))),
- }
- },
- take: |repr, idx| unsafe {
- // Access inline heapless::Vec and take item
- let m: &mut heapless::Vec<_, MAX_INLINE_VECS> = &mut repr.inl;
- mem::replace(&mut m[idx], Ok(None))
- },
- drop: |repr| unsafe { ManuallyDrop::drop(&mut repr.inl) },
- is_inline: true,
- };
-}
-
-#[cfg(test)]
-mod ivecs_tests {
- use super::*;
-
- #[test]
- fn test_basic_functionality() {
- let data = vec![
- Some(vec![1u8, 2, 3]),
- Some(vec![4, 5, 6]),
- None,
- Some(vec![7, 8, 9]),
- ];
-
- let ivecs = IVecs::from_vec(data);
- assert_eq!(ivecs.len(), 4);
- assert!(!ivecs.is_empty());
-
- // Test get
- assert_eq!(ivecs.get(0).unwrap(), Some(&[1, 2, 3][..]));
- assert_eq!(ivecs.get(1).unwrap(), Some(&[4, 5, 6][..]));
- assert_eq!(ivecs.get(2).unwrap(), None);
- assert_eq!(ivecs.get(3).unwrap(), Some(&[7, 8, 9][..]));
- assert_eq!(ivecs.get(4).unwrap(), None); // Out of bounds
- }
-
- #[test]
- fn test_take() {
- let data = vec![Some(vec![1u8, 2, 3]), Some(vec![4, 5, 6])];
-
- let mut ivecs = IVecs::from_vec(data);
-
- // Take first element
- let taken = ivecs.take(0).unwrap().unwrap();
- assert_eq!(taken.as_ref(), &[1, 2, 3]);
- drop(taken);
-
- // Try to take again - should be None
- assert!(ivecs.take(0).unwrap().is_none());
-
- // Take second element
- let taken = ivecs.take(1).unwrap().unwrap();
- assert_eq!(taken.as_ref(), &[4, 5, 6]);
- }
-
- #[test]
- fn test_iterator() {
- let data = vec![Some(vec![1u8, 2, 3]), None, Some(vec![4, 5, 6])];
-
- let ivecs = IVecs::from_vec(data);
- let mut iter = ivecs.into_iter();
-
- let first = iter.next().unwrap().unwrap().unwrap();
- assert_eq!(first.as_ref(), &[1, 2, 3]);
-
- let second = iter.next().unwrap().unwrap();
- assert!(second.is_none());
-
- let third = iter.next().unwrap().unwrap().unwrap();
- assert_eq!(third.as_ref(), &[4, 5, 6]);
-
- assert!(iter.next().is_none());
- }
-
- #[test]
- fn test_from_slice() {
- let data = [Some(vec![1u8, 2, 3]), Some(vec![4, 5, 6])];
-
- let ivecs = IVecs::from(data.as_ref());
- assert_eq!(ivecs.len(), 2);
- assert_eq!(ivecs.get(0).unwrap(), Some(&[1, 2, 3][..]));
- assert_eq!(ivecs.get(1).unwrap(), Some(&[4, 5, 6][..]));
- }
-
- #[test]
- fn test_result_type() {
- let data: Vec<Result<Option<Vec<u8>>, std::io::Error>> = vec![
- Ok(Some(vec![1, 2, 3])),
- Err(std::io::Error::other("test error")),
- Ok(None),
- ];
-
- let ivecs = IVecs::from_vec(data);
- assert_eq!(ivecs.len(), 3);
-
- assert_eq!(ivecs.get(0).unwrap(), Some(&[1, 2, 3][..]));
- assert!(ivecs.get(1).is_err());
- assert_eq!(ivecs.get(2).unwrap(), None);
- }
-
- #[test]
- fn test_empty_ivecs() {
- let data: Vec<Option<Vec<u8>>> = vec![];
- let ivecs = IVecs::from_vec(data);
-
- assert_eq!(ivecs.len(), 0);
- assert!(ivecs.is_empty());
- assert_eq!(ivecs.get(0).unwrap(), None);
-
- let mut iter = ivecs.into_iter();
- assert!(iter.next().is_none());
- }
-
- #[test]
- fn test_debug_impl() {
- let data = vec![Some(vec![1u8, 2, 3]), None, Some(vec![4, 5, 6])];
-
- let ivecs = IVecs::from_vec(data);
- let debug_str = format!("{ivecs:?}");
- assert!(debug_str.contains("Ok(Some([1, 2, 3]))"));
- assert!(debug_str.contains("Ok(None)"));
- assert!(debug_str.contains("Ok(Some([4, 5, 6]))"));
- }
-}
diff --git a/attic/kv/src/lib.rs b/attic/kv/src/lib.rs
deleted file mode 100644
index c5d7b19b4..000000000
@@ -1,46 +0,0 @@
-#![feature(btree_cursors)]
-#![feature(slice_ptr_get)]
-#![feature(bigint_helper_methods)]
-#![feature(array_windows)]
-
-#[cfg(feature = "rocksdb")]
-pub mod rocksdb;
-#[cfg(feature = "rocksdb")]
-pub use rocksdb::RocksDb;
-
-pub mod memorydb;
-pub use memorydb::MemoryDb;
-
-#[cfg(feature = "heed")]
-pub mod lmdb;
-#[cfg(feature = "heed")]
-pub use lmdb::{Lmdb, LmdbOptions};
-
-pub mod traits;
-pub use traits::*;
-
-pub mod typed;
-pub use typed::*;
-
-pub mod erased;
-pub use erased::AnyDb;
-
-pub mod ttl;
-pub use ttl::{TtlDb, TtlExt};
-
-pub mod prefixed;
-pub use prefixed::{PrefixExt, Prefixed};
-
-pub mod error;
-pub use error::{Error, Result};
-
-pub mod rmw;
-pub use rmw::*;
-
-pub mod ivec;
-pub use ivec::*;
-
-pub mod pfx_iter;
-
-#[cfg(test)]
-mod tests;
diff --git a/attic/kv/src/lmdb.rs b/attic/kv/src/lmdb.rs
deleted file mode 100644
index 47aa7774d..000000000
@@ -1,723 +0,0 @@
-//! LMDB implementation of the [`crate::traits::Store`] API using the heed
-//! crate.
-//!
-//! This implementation provides:
-//! - Efficient key-value storage using LMDB
-//! - Support for namespaces via LMDB databases
-//! - Atomic batch operations via LMDB transactions
-//! - Consistent snapshots via read transactions
-//! - Manual merge operator implementation for update operations
-
-use std::{
- borrow::Cow,
- cell::UnsafeCell,
- fmt, fs,
- ops::Bound,
- path::{Path, PathBuf},
- sync::{Arc, RwLock},
-};
-
-use ahash::AHashMap;
-use bytes::Bytes;
-use heed::{Database, Env, EnvFlags, EnvOpenOptions, RoTxn, RwTxn, WithoutTls};
-use smol_str::{SmolStr, ToSmolStr};
-use tempfile::TempDir;
-
-use super::{error::Result, traits};
-use crate::{IVec, IVecRef, IVecs, Operator, Options, Store};
-
-/// LMDB environment configuration
-const DEFAULT_MAP_SIZE: usize = 10 * 1024 * 1024 * 1024; // 10GB
-const MAX_DBS: u32 = 1000;
-
-/// LMDB-specific options
-#[derive(Debug, Clone, Copy)]
-pub struct LmdbOptions {
- /// Map size in bytes (virtual address space reserved)
- pub map_size: usize,
- /// Maximum number of named databases
- pub max_dbs: u32,
-}
-
-impl Default for LmdbOptions {
- fn default() -> Self {
- Self {
- map_size: DEFAULT_MAP_SIZE,
- max_dbs: MAX_DBS,
- }
- }
-}
-
-/// Type alias for the LMDB database
-type LmdbDatabase = Database<heed::types::Bytes, heed::types::Bytes>;
-
-/// Shared state for the LMDB environment
-struct Core {
- /// The LMDB environment
- env: Env<WithoutTls>,
- /// Map of database names to their handles
- dbs: RwLock<AHashMap<SmolStr, LmdbDatabase>>,
- /// Optional temporary directory for cleanup
- _tempdir: Option<TempDir>,
- /// Root path of the database
- path: PathBuf,
-}
-
-impl Core {
- /// Opens or creates a database with the given name
- fn open_db(&self, name: &str) -> Result<LmdbDatabase> {
- // Fast path: check if database already exists
- let dbs = self.dbs.read().unwrap();
- if let Some(db) = dbs.get(name) {
- return Ok(*db);
- }
- drop(dbs);
-
- // Slow path: need to create the database
- let mut dbs = self.dbs.write().unwrap();
- // Double-check after acquiring write lock
- if let Some(db) = dbs.get(name) {
- return Ok(*db);
- }
-
- // Try to open existing database first
- let rtxn = self.env.read_txn()?;
- match self.env.open_database(&rtxn, Some(name)) {
- Ok(Some(db)) => {
- drop(rtxn);
- dbs.insert(SmolStr::from(name), db);
- Ok(db)
- },
- Ok(None) => {
- // Database doesn't exist, need to create it
- drop(rtxn);
- let mut wtxn = self.env.write_txn()?;
- let db = self.env.create_database(&mut wtxn, Some(name))?;
- wtxn.commit()?;
- dbs.insert(SmolStr::from(name), db);
- Ok(db)
- },
- Err(e) => Err(e.into()),
- }
- }
-}
-
-/// A namespace in the LMDB hierarchy
-struct Namespace {
- /// Full name of this namespace
- full_name: SmolStr,
- /// Offset to child name
- name_offset: usize,
- /// The LMDB database handle
- db: LmdbDatabase,
- /// Child namespaces
- children: RwLock<AHashMap<SmolStr, Arc<Namespace>>>,
- /// Shared core state
- core: Arc<Core>,
- /// Store options
- opts: Options,
-}
-
-impl Namespace {
- #[inline]
- fn is_root(&self) -> bool {
- self.name_offset == 0
- }
-
- fn format_child_name(&self, name: &str) -> SmolStr {
- if self.is_root() {
- name.to_smolstr()
- } else {
- format!("{}.{}", self.full_name, name).to_smolstr()
- }
- }
-}
-
-/// LMDB-backed key-value store
-#[derive(Clone)]
-pub struct Lmdb {
- inner: Arc<Namespace>,
-}
-
-impl fmt::Debug for Lmdb {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.debug_struct("Lmdb")
- .field("namespace", &self.namespace())
- .field("path", &self.inner.core.path)
- .finish()
- }
-}
-
-impl Lmdb {
- /// Creates a new LMDB store with the given path, options, and LMDB-specific
- /// options
- pub fn with_lmdb_opts(
- path: Option<&Path>,
- opts: Options,
- lmdb_opts: LmdbOptions,
- ) -> Result<Self> {
- let mut temp_dir = None;
- let path = if let Some(path) = path {
- path.to_path_buf()
- } else {
- temp_dir = Some(TempDir::with_prefix("tetra-lmdb-")?);
- temp_dir.as_ref().unwrap().path().to_path_buf()
- };
-
- // Ensure directory exists
- fs::create_dir_all(&path)?;
-
- // Open LMDB environment with performance optimizations
- let mut env_builder = EnvOpenOptions::new();
- env_builder.map_size(lmdb_opts.map_size);
- env_builder.max_dbs(lmdb_opts.max_dbs);
-
- let env = unsafe {
- let mut flags = if opts.sync {
- EnvFlags::empty()
- } else {
- EnvFlags::NO_SYNC | EnvFlags::MAP_ASYNC
- };
- flags |= EnvFlags::WRITE_MAP; // Use memory-mapped writes
- flags |= EnvFlags::NO_META_SYNC; // Don't sync metadata
- flags |= EnvFlags::NO_MEM_INIT; // Don't initialize memory
- env_builder.flags(flags);
- env_builder.read_txn_without_tls().open(&path)?
- };
-
- let core = Arc::new(Core {
- env,
- dbs: RwLock::new(AHashMap::new()),
- _tempdir: temp_dir,
- path,
- });
-
- // Open root database with empty name to match RocksDB convention
- let root_name = SmolStr::new_static("");
- let db = core.open_db("$")?; // Use internal name for LMDB since it doesn't support empty names
-
- Ok(Self {
- inner: Arc::new(Namespace {
- full_name: root_name,
- name_offset: 0,
- db,
- children: RwLock::new(AHashMap::new()),
- core,
- opts,
- }),
- })
- }
-
- /// Creates a new LMDB store with the given path and options
- pub fn with_opts(path: Option<&Path>, opts: Options) -> Result<Self> {
- Self::with_lmdb_opts(path, opts, LmdbOptions::default())
- }
-
- /// Opens an LMDB store at the given path
- pub fn open(path: impl AsRef<Path>) -> Result<Self> {
- Self::with_opts(Some(path.as_ref()), Options::default())
- }
-
- /// Creates a temporary LMDB store
- pub fn temp() -> Result<Self> {
- Self::with_opts(None, Options::default())
- }
-
- /// Returns a reference to the underlying LMDB environment
- #[inline]
- pub fn env(&self) -> &Env<WithoutTls> {
- &self.inner.core.env
- }
-
- /// Opens a child namespace
- fn open_child(&self, name: &str, opts: Options) -> Result<Self> {
- // Fast path: check if child already exists
- {
- let children = self.inner.children.read().unwrap();
- if let Some(child) = children.get(name) {
- return Ok(Self {
- inner: child.clone(),
- });
- }
- }
-
- // Slow path: need to create the child
- // Acquire write lock on children first to establish consistent lock ordering
- let mut children = self.inner.children.write().unwrap();
-
- // Double-check after acquiring write lock
- if let Some(child) = children.get(name) {
- return Ok(Self {
- inner: child.clone(),
- });
- }
-
- // Now open/create the database (this will acquire dbs lock internally)
- let full_name = self.inner.format_child_name(name);
- let db = self.inner.core.open_db(&full_name)?;
-
- let child = Arc::new(Namespace {
- name_offset: full_name.len() - name.len(),
- full_name: full_name.clone(),
- db,
- children: RwLock::new(AHashMap::new()),
- core: self.inner.core.clone(),
- opts,
- });
-
- children.insert(name.to_smolstr(), child.clone());
- Ok(Self { inner: child })
- }
-}
-
-/// LMDB snapshot using a read transaction
-///
-/// Note: LMDB snapshots have limitations compared to other backends:
-/// - They hold a read transaction which can become invalid if too many write
-/// transactions occur (LMDB has a limited number of reader slots)
-/// - Long-lived snapshots can prevent LMDB from reclaiming space
-/// - The snapshot may fail with BadRslot error if the environment is heavily
-/// modified
-pub struct Snapshot<'a> {
- txn: RoTxn<'a, WithoutTls>,
- store: &'a Lmdb,
-}
-
-impl<'a> fmt::Debug for Snapshot<'a> {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.debug_struct("Snapshot")
- .field("namespace", &self.store.namespace())
- .finish()
- }
-}
-
-/// Inner iterator state
-enum IterVariant<'a> {
- Error(Option<heed::Error>),
- Forward(heed::RoIter<'a, heed::types::Bytes, heed::types::Bytes>),
- Reverse(heed::RoRevIter<'a, heed::types::Bytes, heed::types::Bytes>),
- ForwardRange(heed::RoRange<'a, heed::types::Bytes, heed::types::Bytes>),
- ReverseRange(heed::RoRevRange<'a, heed::types::Bytes, heed::types::Bytes>),
-}
-
-pub struct PrefixIter<'a, T> {
- _txn: T,
- inner: Option<heed::Result<heed::RoPrefix<'a, heed::types::Bytes, heed::types::Bytes>>>,
-}
-
-impl<'a> PrefixIter<'a, ()> {
- fn new(db: &'a Lmdb, txn: &'_ RoTxn<'a, WithoutTls>, prefix: &[u8]) -> Self {
- let txn = unsafe { &*(txn as *const _ as *const RoTxn<'a, WithoutTls>) };
- Self {
- _txn: (),
- inner: Some(db.inner.db.prefix_iter(txn, prefix)),
- }
- }
-}
-
-impl<'a> PrefixIter<'a, RoTxn<'a, WithoutTls>> {
- fn new(db: &'a Lmdb, prefix: &[u8]) -> Self {
- let txn = db.env().read_txn().unwrap();
- Self {
- inner: PrefixIter::<()>::new(db, &txn, prefix).inner,
- _txn: txn,
- }
- }
-}
-
-impl<'a, T> Iterator for PrefixIter<'a, T> {
- type Item = Result<(Bytes, Bytes)>;
-
- fn next(&mut self) -> Option<Self::Item> {
- match &mut self.inner {
- Some(Ok(it)) => it.next().map(|x| {
- x.map(|(k, v)| (Bytes::copy_from_slice(k), Bytes::copy_from_slice(v)))
- .map_err(Into::into)
- }),
- x @ Some(Err(_)) => Some(Err(x.take().unwrap().unwrap_err().into())),
- None => None,
- }
- }
-}
-
-/// Iterator wrapper for LMDB
-pub struct Iter<'a, T> {
- // Keeps the read-txn alive for store-level iterators.
- _txn: T,
- inner: IterVariant<'a>,
-}
-
-impl<'a> Iter<'a, ()> {
- fn new(db: &'a Lmdb, txn: &'_ RoTxn<'a, WithoutTls>, mode: traits::IterMode<'_>) -> Self {
- let txn = unsafe { &*(txn as *const _ as *const RoTxn<'a, WithoutTls>) };
- use traits::IterMode::*;
- let inner = match mode {
- Forward(start) => {
- if let Some(start_key) = start {
- db.inner
- .db
- .range(txn, &(Bound::Included(start_key), Bound::Unbounded))
- .map(IterVariant::ForwardRange)
- .unwrap_or_else(|e| IterVariant::Error(Some(e)))
- } else {
- db.inner
- .db
- .iter(txn)
- .map(IterVariant::Forward)
- .unwrap_or_else(|e| IterVariant::Error(Some(e)))
- }
- },
- Reverse(start) => {
- if let Some(start_key) = start {
- db.inner
- .db
- .rev_range(txn, &(Bound::Unbounded, Bound::Included(start_key)))
- .map(IterVariant::ReverseRange)
- .unwrap_or_else(|e| IterVariant::Error(Some(e)))
- } else {
- db.inner
- .db
- .rev_iter(txn)
- .map(IterVariant::Reverse)
- .unwrap_or_else(|e| IterVariant::Error(Some(e)))
- }
- },
- };
-
- Self { _txn: (), inner }
- }
-}
-
-impl<'a> Iter<'a, RoTxn<'a, WithoutTls>> {
- fn new(db: &'a Lmdb, mode: traits::IterMode<'_>) -> Self {
- let txn = db.env().read_txn().unwrap();
- Self {
- inner: Iter::<()>::new(db, &txn, mode).inner,
- _txn: txn,
- }
- }
-}
-
-impl<'a, T> Iterator for Iter<'a, T> {
- type Item = Result<(Bytes, Bytes)>;
-
- #[inline]
- fn next(&mut self) -> Option<Self::Item> {
- let map = |r: heed::Result<(&[u8], &[u8])>| -> Result<_> {
- r.map(|(k, v)| (Bytes::copy_from_slice(k), Bytes::copy_from_slice(v)))
- .map_err(Into::into)
- };
- match &mut self.inner {
- IterVariant::Error(e) => e.take().map(|e| Err(e.into())),
- IterVariant::Forward(it) => it.next().map(map),
- IterVariant::Reverse(it) => it.next().map(map),
- IterVariant::ForwardRange(it) => it.next().map(map),
- IterVariant::ReverseRange(it) => it.next().map(map),
- }
- }
-}
-
-impl<'a> traits::View for Snapshot<'a> {
- type Iter<'b>
- = Iter<'b, ()>
- where
- Self: 'b;
-
- #[inline]
- fn get(&self, key: &[u8]) -> Result<Option<IVec<'_>>> {
- Ok(self.store.inner.db.get(&self.txn, key)?.map(IVec::from))
- }
-
- #[inline]
- fn contains(&self, key: &[u8]) -> Result<bool> {
- Ok(self.store.inner.db.get(&self.txn, key)?.is_some())
- }
-
- fn get_many(&self, keys: impl IntoIterator<Item: AsRef<[u8]>>) -> Result<IVecs<'_>> {
- Ok(IVecs::from_iter(keys.into_iter().map(
- |k| -> Result<Option<&[u8]>> {
- self
- .store
- .inner
- .db
- .get(&self.txn, k.as_ref())
- .map_err(Into::into)
- },
- )))
- }
-
- fn iter<'b>(&'b self, mode: traits::IterMode<'_>) -> Self::Iter<'b> {
- Self::Iter::new(self.store, &self.txn, mode)
- }
-
- fn scan<'b>(
- &'b self,
- prefix: &[u8],
- ) -> impl Iterator<Item = Result<(Bytes, Bytes)>> + use<'b, 'a> {
- PrefixIter::<()>::new(self.store, &self.txn, prefix)
- }
-}
-
-/// Write batch for LMDB
-pub struct WriteBatch<'a> {
- parent: &'a Lmdb,
- wtxn: UnsafeCell<Option<RwTxn<'a>>>,
-}
-
-impl<'a> WriteBatch<'a> {
- fn new(parent: &'a Lmdb) -> Self {
- Self {
- parent,
- wtxn: UnsafeCell::new(None),
- }
- }
-
- fn wtxn(&self) -> &mut RwTxn<'a> {
- let opt = unsafe { &mut *self.wtxn.get() };
- if let Some(txn) = opt {
- return txn;
- };
-
- let txn = self.parent.env().write_txn().unwrap();
- *opt = Some(txn);
-
- let Some(txn) = opt.as_mut() else {
- panic!("Failed to get write transaction");
- };
- txn
- }
-}
-
-impl<'a> fmt::Debug for WriteBatch<'a> {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.debug_struct("WriteBatch")
- .field("namespace", &self.parent.namespace())
- .finish()
- }
-}
-
-impl<'a> traits::Write for WriteBatch<'a> {
- type Result = ();
-
- fn put(&self, key: &[u8], value: &[u8]) -> Self::Result {
- self.parent.inner.db.put(self.wtxn(), key, value).unwrap();
- }
-
- fn delete(&self, key: &[u8]) -> Self::Result {
- self.parent.inner.db.delete(self.wtxn(), key).unwrap();
- }
-
- fn delete_range(&self, from: &[u8], to: &[u8]) -> Self::Result {
- let range = (Bound::Included(from), Bound::Excluded(to));
- self
- .parent
- .inner
- .db
- .delete_range(self.wtxn(), &range)
- .unwrap();
- }
-
- fn update(&self, key: &[u8], op: &dyn Operator) -> Self::Result {
- // Get current value
- let prev_data = self.parent.inner.db.get(self.wtxn(), key).unwrap();
- let result = op.apply(key, prev_data.map(Cow::Borrowed));
- self.parent.inner.db.put(self.wtxn(), key, &result).unwrap();
- }
-}
-
-impl<'a> traits::WriteBatch<'a> for WriteBatch<'a> {
- fn commit(&self) -> Result<()> {
- let opt = unsafe { &mut *self.wtxn.get() };
- if let Some(txn) = opt.take() {
- txn.commit()?;
- }
- Ok(())
- }
-
- fn reset(&mut self) {
- let opt = unsafe { &mut *self.wtxn.get() };
- if let Some(txn) = opt.take() {
- txn.abort();
- }
- }
-}
-
-// ============================== View ==================================== //
-
-struct SliceWithOwner<T> {
- _rtxn: T,
- slice: *const [u8],
-}
-
-impl<T> IVecRef for SliceWithOwner<T> {
- fn as_slice(&self) -> &[u8] {
- unsafe { &*self.slice }
- }
- fn into_vec(self) -> Vec<u8> {
- self.as_slice().to_vec()
- }
- fn into_bytes(self) -> bytes::Bytes {
- bytes::Bytes::copy_from_slice(self.as_slice())
- }
-}
-
-impl<'a, T> SliceWithOwner<T> {
- fn new(rtxn: T, vec: *const [u8]) -> Self {
- Self {
- _rtxn: rtxn,
- slice: vec,
- }
- }
-}
-
-impl traits::View for Lmdb {
- type Iter<'a>
- = Iter<'a, RoTxn<'a, WithoutTls>>
- where
- Self: 'a;
-
- fn get(&self, key: &[u8]) -> Result<Option<IVec<'_>>> {
- let rtxn = self.env().read_txn()?;
- let value = self.inner.db.get(&rtxn, key)?;
-
- match value.map(|x| x as *const [u8]) {
- Some(v) => Ok(Some(IVec::from(SliceWithOwner::new(rtxn, v)))),
- None => Ok(None),
- }
- }
-
- fn contains(&self, key: &[u8]) -> Result<bool> {
- let rtxn = self.env().read_txn()?;
- Ok(self
- .inner
- .db
- .lazily_decode_data()
- .get(&rtxn, key)?
- .is_some())
- }
-
- fn get_many(&self, keys: impl IntoIterator<Item: AsRef<[u8]>>) -> Result<IVecs<'_>> {
- // Reuse a single transaction for all reads
- let rtxn = Arc::new(self.env().read_txn()?);
- Ok(IVecs::from_iter(keys.into_iter().map(
- |k| -> Result<Option<SliceWithOwner<_>>> {
- match self.inner.db.get(&rtxn, k.as_ref()) {
- Ok(Some(v)) => Ok(Some(SliceWithOwner::new(Arc::clone(&rtxn), v))),
- Ok(None) => Ok(None),
- Err(e) => Err(e.into()),
- }
- },
- )))
- }
-
- fn iter<'a>(&'a self, mode: traits::IterMode<'_>) -> Self::Iter<'a> {
- Self::Iter::new(self, mode)
- }
-
- fn scan<'b>(&'b self, prefix: &[u8]) -> impl Iterator<Item = Result<(Bytes, Bytes)>> + use<'b> {
- PrefixIter::<RoTxn<'b, WithoutTls>>::new(self, prefix)
- }
-}
-
-// ============================== Write =================================== //
-
-impl traits::Write for Lmdb {
- type Result = Result<()>;
-
- fn put(&self, key: &[u8], value: &[u8]) -> Self::Result {
- let mut wtxn = self.env().write_txn()?;
- self.inner.db.put(&mut wtxn, key, value)?;
- wtxn.commit()?;
- Ok(())
- }
-
- fn delete(&self, key: &[u8]) -> Self::Result {
- let mut wtxn = self.env().write_txn()?;
- self.inner.db.delete(&mut wtxn, key)?;
- wtxn.commit()?;
- Ok(())
- }
-
- fn delete_range(&self, from: &[u8], to: &[u8]) -> Self::Result {
- let mut wtxn = self.env().write_txn()?;
-
- let range = (Bound::Included(from), Bound::Excluded(to));
- self.inner.db.delete_range(&mut wtxn, &range)?;
- wtxn.commit()?;
- Ok(())
- }
-
- fn update(&self, key: &[u8], op: &dyn Operator) -> Self::Result {
- let mut wtxn = self.env().write_txn()?;
- let prev_data = self
- .inner
- .db
- .get(unsafe { &mut *(&mut wtxn as *mut RwTxn<'_>) }, key)?;
- let result = op.apply(key, prev_data.map(Cow::Borrowed));
- self.inner.db.put(&mut wtxn, key, &result)?;
- wtxn.commit()?;
- Ok(())
- }
-}
-
-// ============================== Store =================================== //
-
-impl traits::Store for Lmdb {
- type WriteBatch<'a> = WriteBatch<'a>;
- type Snapshot<'a> = Snapshot<'a>;
-
- fn batch(&self) -> Self::WriteBatch<'_> {
- WriteBatch::new(self)
- }
-
- fn namespace(&self) -> &str {
- &self.inner.full_name[self.inner.name_offset..]
- }
-
- fn child(&self, name: &str, opts: Options) -> Result<Self> {
- self.open_child(name, opts)
- }
-
- fn children(&self) -> Vec<SmolStr> {
- self
- .inner
- .children
- .read()
- .unwrap()
- .keys()
- .cloned()
- .collect()
- }
-
- fn clear(&self) -> Result<()> {
- let mut wtxn = self.env().write_txn()?;
- self.inner.db.clear(&mut wtxn)?;
- wtxn.commit()?;
- Ok(())
- }
-
- fn flush(&self) -> Result<()> {
- if self.inner.opts.sync {
- self.env().force_sync()?;
- }
- Ok(())
- }
-
- fn snapshot<'b>(&'b self) -> Self::Snapshot<'b> {
- // Note: snapshot() doesn't return Result, so we must panic on error
- // This is a design limitation of the trait
- let txn = self
- .env()
- .read_txn()
- .expect("Failed to create read transaction for snapshot");
- Snapshot { txn, store: self }
- }
-
- fn options(&self) -> &Options {
- &self.inner.opts
- }
-
- fn estimate_size(&self) -> Option<u64> {
- self.env().non_free_pages_size().ok()
- }
-}
diff --git a/attic/kv/src/memorydb.rs b/attic/kv/src/memorydb.rs
deleted file mode 100644
index 400ab3c12..000000000
@@ -1,688 +0,0 @@
-//! In-memory implementation of the [`crate::traits::Store`] API that supports
-//! cheap, copy-on-write snapshots. The design is centred around the
-//! [`Transaction`] structure which represents *one* version chain of a logical
-//! namespace. Each call to [`Store::snapshot`] freezes the current state by
-//! creating a new empty *live* layer and returning an `Arc` to the previous
-//! chain head. Any further writes are appended to the live layer. When the
-//! last `Arc` that still references the frozen head gets dropped we try to
-//! merge ("compact") the live overlay back into the previous layer so that the
-//! total memory usage stays bounded.
-//!
-//! The implementation is *optimised for simplicity* rather than ultimate
-//! performance because it is mainly used in unit tests where correctness and
-//! determinism matter more than speed.
-
-use std::{
- borrow::Cow,
- cell::RefCell,
- cmp::Ordering,
- collections::{btree_map, BTreeMap},
- fmt,
- iter::FusedIterator,
- marker::PhantomData,
- mem,
- ops::{Bound, Deref, Range},
- sync::LazyLock,
-};
-
-use ahash::AHashMap;
-use bytes::{Bytes, BytesMut};
-use parking_lot::RwLock;
-use smallvec::SmallVec;
-use smol_str::{SmolStr, ToSmolStr};
-use tetra_core::alloc::quickbuf;
-use traits::IterMode;
-use triomphe::Arc;
-
-use super::{error::Result, traits};
-use crate::{IVec, IVecs, Operator, OperatorRepr, Options, Store, WriteBatch as _};
-
-// ======================= Underlying map utils ============================ //
-
-/// Overlay map that distinguishes between *set* (Some) and *delete* (None).
-type Map = BTreeMap<Bytes, Option<Bytes>>;
-
-#[derive(Clone)]
-struct MapIter<'a> {
- cursor: btree_map::Cursor<'a, Bytes, Option<Bytes>>,
- rev: bool,
-}
-
-impl<'a> fmt::Debug for MapIter<'a> {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.debug_struct("MapIter")
- .field(
- "cursor",
- &format_args!(
- "<prev: {:?}, next: {:?}>",
- self.cursor.peek_prev(),
- self.cursor.peek_next()
- ),
- )
- .field("rev", &self.rev)
- .finish()
- }
-}
-
-impl<'a> MapIter<'a> {
- fn new<'b>(vns: &'a Map, at: Option<&'b [u8]>, rev: bool) -> Self {
- let bound = at.map_or(Bound::Unbounded, Bound::Included);
- let cursor = if rev {
- vns.upper_bound(bound)
- } else {
- vns.lower_bound(bound)
- };
- Self { cursor, rev }
- }
-
- fn with_mode(vns: &'a Map, mode: IterMode<'_>) -> Self {
- Self::new(vns, mode.at(), mode.is_reverse())
- }
-
- fn peek(&mut self) -> Option<(&'a Bytes, &'a Option<Bytes>)> {
- if self.rev {
- self.cursor.peek_prev()
- } else {
- self.cursor.peek_next()
- }
- }
-}
-
-impl<'a> Iterator for MapIter<'a> {
- type Item = (&'a Bytes, &'a Option<Bytes>);
-
- fn next(&mut self) -> Option<Self::Item> {
- if self.rev {
- self.cursor.prev()
- } else {
- self.cursor.next()
- }
- }
-}
-
-impl<'a> FusedIterator for MapIter<'a> {}
-
-// ======================= Versioned namespace ============================ //
-
-/// One *version* of a namespace. `live` keeps the local changes while `prev`
-/// points to the (immutable) previous state.
-#[derive(Default)]
-struct Transaction {
- /// Previous, immutable state (if any).
- prev: Option<Arc<Transaction>>, // None means this is the root.
- /// The overlay with changes that happened *after* `prev` was frozen.
- /// `None` means the key was deleted.
- live: Map,
-}
-
-impl fmt::Debug for Transaction {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.debug_struct("Transaction")
- .field("prev", &self.prev)
- .field("live", &self.live)
- .finish()
- }
-}
-
-static EMPTY_TXN: LazyLock<Arc<Transaction>> = LazyLock::new(|| Arc::new(Transaction::default()));
-
-fn kv_to_bytes(key: &[u8], value: &[u8]) -> (Bytes, Bytes) {
- let mut bmut = BytesMut::with_capacity(key.len() + value.len());
- bmut.extend_from_slice(key);
- bmut.extend_from_slice(value);
- let mut value = bmut.freeze();
- let key = value.split_to(key.len());
- (key, value)
-}
-
-impl Transaction {
- const TXN_INTERN_THRESHOLD: usize = 32;
-
- #[inline]
- fn find<'a, R>(&'a self, f: impl Fn(&'a Transaction) -> Option<R>) -> Option<R> {
- let mut at: Option<&'a Transaction> = Some(self);
- while let Some(txn) = at {
- if let Some(r) = f(txn) {
- return Some(r);
- }
- at = txn.prev.as_deref();
- }
- None
- }
-
- /// Recursively look up `key` starting from the current version.
- fn get(&self, key: &[u8]) -> Option<&Bytes> {
- self
- .find(|tx| tx.live.get(key).map(|v| v.as_ref()))
- .flatten()
- }
-
- /// Apply a *put* operation to the live layer.
- fn put(&mut self, key: &[u8], value: &[u8]) {
- let (key, value) = kv_to_bytes(key, value);
- self.live.insert(key, Some(value));
- }
-
- /// Apply a *delete* operation to the live layer.
- fn delete(&mut self, key: &[u8]) {
- if let Some((key, Some(_))) = self.find(|tx| tx.live.get_key_value(key)) {
- self.live.insert(key.clone(), None);
- }
- // Otherwise we don't care as either:
- // - None: value does not exist
- // - Some(K, None): value exists but is in tombstone state
- }
-
- fn update<T: Operator + ?Sized>(&mut self, key: &[u8], op: &T) -> Result<()> {
- let prev = self
- .live
- .get(key)
- .and_then(|x| x.as_ref().map(|v| v.as_ref()));
-
- let result = op.apply(key, prev.map(Cow::Borrowed));
- let (key_bytes, value_bytes) = kv_to_bytes(key, &result);
- self.live.insert(key_bytes, Some(value_bytes));
- Ok(())
- }
-
- /// Freeze the current version and return an `Arc` to it, creating a fresh
- /// empty *live* layer so that new writes do not mutate the snapshot.
- fn snapshot(&mut self) -> Arc<Transaction> {
- // If there are *no* local changes we can just reuse the previous head.
- if self.live.is_empty() {
- return self.prev.clone().unwrap_or_else(|| EMPTY_TXN.clone());
- }
-
- let mut frozen = mem::take(self);
-
- if Self::TXN_INTERN_THRESHOLD > 0 {
- while let Some(prev) = frozen.prev.as_deref() {
- if prev.live.len() < Self::TXN_INTERN_THRESHOLD {
- for (k, v_opt) in prev.live.iter() {
- frozen.live.entry(k.clone()).or_insert(v_opt.clone());
- }
- frozen.prev = prev.prev.clone();
- } else {
- break;
- }
- }
- }
-
- let frozen = Arc::new(frozen);
- *self = Transaction {
- prev: Some(frozen.clone()),
- live: Map::new(),
- };
- frozen
- }
-
- /// Try to merge (`compact`) the *live* changes back into the previous layer
- /// if it is uniquely owned (i.e. no snapshot still references it).
- fn compact(&mut self) {
- let Some(prev_arc) = &mut self.prev else {
- return;
- }; // no previous layer
- if let Some(prev_mut) = Arc::get_mut(prev_arc) {
- // Safe to mutate.
- for (k, v_opt) in mem::take(&mut self.live) {
- match v_opt {
- Some(v) => {
- prev_mut.live.insert(k, Some(v));
- },
- None => {
- prev_mut.live.insert(k, None);
- },
- }
- }
- // After merging we can drop the prev arc and point to its prev to
- // shorten the chain the next time.
- self.prev = prev_mut.prev.clone();
- }
- }
-}
-
-#[derive(Clone, derive_more::Debug)]
-enum TransactionRef<'a> {
- #[debug("Owned({:?})", self.as_ref())]
- Owned(Arc<Transaction>),
- #[debug("Borrowed({:?})", self.as_ref())]
- Borrowed(&'a Transaction),
-}
-
-impl<'a> From<&'a Transaction> for TransactionRef<'a> {
- fn from(vns: &'a Transaction) -> Self {
- Self::Borrowed(vns)
- }
-}
-
-impl<'a> From<Arc<Transaction>> for TransactionRef<'a> {
- fn from(vns: Arc<Transaction>) -> Self {
- Self::Owned(vns)
- }
-}
-
-impl<'a> AsRef<Transaction> for TransactionRef<'a> {
- fn as_ref(&self) -> &Transaction {
- match self {
- Self::Owned(vns) => vns.as_ref(),
- Self::Borrowed(vns) => vns,
- }
- }
-}
-
-impl<'a> Deref for TransactionRef<'a> {
- type Target = Transaction;
- fn deref(&self) -> &Self::Target {
- self.as_ref()
- }
-}
-
-impl<'a> TransactionRef<'a> {
- /// Returns a reference to the underlying `Transaction`.
- ///
- /// This method escapes the lifetime constraint only when holding an
- /// `Arc<Transaction>`. In the `Owned` variant, it converts the reference
- /// with lifetime tied to the `Arc` into a reference with lifetime 'a.
- ///
- /// # Safety
- ///
- /// This is safe as long as iterators or other values using the returned
- /// reference do not outlive the lifetime 'a. The `Arc` ensures the
- /// `Transaction` remains valid for the `Owned` variant, while the
- /// `Borrowed` variant directly respects the 'a lifetime.
- unsafe fn as_ref_ext(&self) -> &'a Transaction {
- match self {
- Self::Owned(vns) => {
- let r: &'_ Transaction = vns.as_ref();
- // SAFETY: `r` is a valid reference to the `Transaction` for as long as the Arc
- // exists, which is at least as long as 'self' exists
- unsafe { &*(r as *const Transaction) }
- },
- Self::Borrowed(vns) => vns,
- }
- }
-}
-
-#[derive(derive_more::Debug)]
-#[debug("TransactionIter {{ iters: {:?} }}", iters)]
-pub struct TransactionIter<'a> {
- _refn: TransactionRef<'a>,
- iters: SmallVec<[(MapIter<'a>, bool); 8]>,
-}
-
-impl<'a> TransactionIter<'a> {
- fn new(txn: impl Into<TransactionRef<'a>>, mode: IterMode<'_>) -> Self {
- let mut iters = SmallVec::new();
- let txref: TransactionRef<'a> = txn.into();
-
- let mut vref: &'a Transaction = unsafe { txref.as_ref_ext() };
- loop {
- iters.push((MapIter::with_mode(&vref.live, mode), false));
- if let Some(prev) = &vref.prev {
- vref = prev.as_ref();
- } else {
- break;
- }
- }
- Self {
- iters,
- _refn: txref,
- }
- }
-}
-
-impl<'a> Iterator for TransactionIter<'a> {
- type Item = Result<(Bytes, Bytes)>;
-
- fn next(&mut self) -> Option<Self::Item> {
- let mut yielded = None;
- while yielded.is_none() {
- let mut min_key: Option<Bytes> = None;
- let mut last_val: Option<&Bytes> = None;
-
- let mut skiptail = 0;
- for (i, (iter, skip)) in self.iters.iter_mut().enumerate().rev() {
- *skip = false;
- if let Some((key, value)) = iter.peek() {
- let value: Option<&Bytes> = value.as_ref();
- let cmp = min_key
- .as_ref()
- .map(|k| k.as_ref().cmp(key.as_ref()))
- .unwrap_or(Ordering::Less);
- if cmp == Ordering::Greater {
- continue;
- }
- if cmp == Ordering::Less {
- min_key = Some(key.clone());
- skiptail = i + 1;
- }
- last_val = value;
- *skip = true;
- }
- }
-
- match (&min_key, last_val) {
- // If we have no min key, we are done as all iterators are exhausted
- (None, _) => return None,
- // If last value is not a tombstone, we can yield the min key and value
- (Some(k), Some(v)) => yielded = Some(Ok((k.clone(), v.clone()))),
- // Otherwise we will skip to the next key, but we still have to forward the
- // iterators.
- _ => {},
- }
-
- // Skip the iterators that were at this key
- for (iter, skip) in &mut self.iters[..skiptail] {
- if *skip {
- let at = iter.next();
- debug_assert_eq!(at.unwrap().0, min_key.as_ref().unwrap());
- }
- }
- }
- yielded
- }
-}
-
-// =========================== Namespace ================================== //
-
-#[derive(Debug)]
-struct Namespace {
- name: SmolStr,
- opts: Options,
- txn: RwLock<Transaction>,
- children: RwLock<AHashMap<SmolStr, Arc<Namespace>>>,
-}
-
-impl Namespace {
- fn new(name: SmolStr, opts: Options) -> Self {
- Self {
- name,
- opts,
- txn: RwLock::new(Transaction::default()),
- children: RwLock::new(AHashMap::new()),
- }
- }
-
- fn get(&self, key: &[u8]) -> Option<Bytes> {
- self.txn.read().get(key).cloned()
- }
-
- fn put(&self, key: &[u8], value: &[u8]) {
- self.txn.write().put(key, value);
- }
-
- fn delete(&self, key: &[u8]) {
- self.txn.write().delete(key);
- }
-
- fn snapshot(&self) -> Arc<Transaction> {
- self.txn.write().snapshot()
- }
-
- fn update<T: Operator + ?Sized>(&self, key: &[u8], op: &T) -> Result<()> {
- self.txn.write().update(key, op)
- }
-
- fn try_compact(&self) {
- self.txn.write().compact();
- }
-
- fn emplace_child(&self, name: &str, opts: Options) -> Arc<Namespace> {
- let mut guard = self.children.upgradable_read();
- if let Some(child) = guard.get(name) {
- return child.clone();
- }
- guard.with_upgraded(|children| {
- use std::collections::hash_map::Entry;
- match children.entry(name.to_smolstr()) {
- Entry::Occupied(child) => child.get().clone(),
- Entry::Vacant(entry) => {
- let child = Arc::new(Namespace::new(name.to_smolstr(), opts));
- entry.insert(child.clone());
- child
- },
- }
- })
- }
-
- fn children(&self) -> Vec<SmolStr> {
- self.children.read().keys().cloned().collect()
- }
-}
-
-// ============================ MemoryDb ================================== //
-
-#[derive(Clone, Debug)]
-pub struct MemoryDb {
- ns: Arc<Namespace>,
-}
-
-impl Default for MemoryDb {
- fn default() -> Self {
- Self::new()
- }
-}
-
-impl MemoryDb {
- pub fn new() -> Self {
- Self {
- ns: Arc::new(Namespace::new(SmolStr::new_static("$"), Options::default())),
- }
- }
-
- fn namespace(&self) -> &Arc<Namespace> {
- &self.ns
- }
-}
-
-#[derive(Debug)]
-pub struct Snapshot<'a> {
- vns: Arc<Transaction>,
- ns: Arc<Namespace>,
- _phantom: PhantomData<&'a ()>,
-}
-impl<'a> Drop for Snapshot<'a> {
- fn drop(&mut self) {
- if Arc::strong_count(&self.vns) == 1 {
- self.ns.try_compact();
- }
- }
-}
-
-impl<'a> traits::View for Snapshot<'a> {
- type Iter<'b>
- = TransactionIter<'b>
- where
- Self: 'b;
-
- fn get(&self, key: &[u8]) -> Result<Option<IVec<'a>>> {
- Ok(self.vns.get(key).map(IVec::from_ref))
- }
- fn contains(&self, key: &[u8]) -> Result<bool> {
- Ok(self.vns.get(key).is_some())
- }
- fn get_many(&self, keys: impl IntoIterator<Item: AsRef<[u8]>>) -> Result<IVecs<'_>> {
- Ok(keys
- .into_iter()
- .map(|k| self.vns.get(k.as_ref()))
- .map(|x| x.cloned())
- .collect())
- }
- fn iter(&self, mode: IterMode) -> Self::Iter<'_> {
- TransactionIter::new(self.vns.clone(), mode)
- }
-}
-
-#[derive(Debug, Default)]
-struct WriteBatchInner {
- ops: SmallVec<[Op; 8]>,
- pool: quickbuf::StackBuffer,
-}
-
-impl WriteBatchInner {
- fn stash_range(&mut self, data: &[u8]) -> Range<usize> {
- let beg = self.pool.len();
- self.pool.extend_from_slice(data);
- let end = self.pool.len();
- beg..end
- }
-}
-
-#[derive(Debug)]
-pub struct WriteBatch<'a> {
- inner: RefCell<WriteBatchInner>,
- parent: &'a MemoryDb,
-}
-
-#[derive(Debug)]
-enum Op {
- Put(Bytes, Bytes),
- Del(Range<usize>),
- Rmw(Range<usize>, Range<usize>),
-}
-
-impl<'a> traits::Write for WriteBatch<'a> {
- type Result = ();
- fn put(&self, k: &[u8], v: &[u8]) -> Self::Result {
- let (key, value) = kv_to_bytes(k, v);
- self.inner.borrow_mut().ops.push(Op::Put(key, value));
- }
- fn delete(&self, k: &[u8]) -> Self::Result {
- let mut inner = self.inner.borrow_mut();
- let rng = inner.stash_range(k);
- inner.ops.push(Op::Del(rng));
- }
- fn delete_range(&self, from: &[u8], to: &[u8]) -> Self::Result {
- use crate::traits::{IterMode, View};
-
- // Collect keys in the range
- for (k, _) in self.parent.iter(IterMode::Forward(Some(from))).flatten() {
- if k.as_ref() >= to {
- break;
- }
- self.delete(k.as_ref());
- }
- }
- fn update(&self, key: &[u8], op: &dyn Operator) -> Self::Result {
- let mut inner = self.inner.borrow_mut();
- let k = inner.stash_range(key);
- let v = op.write_to(&mut inner.pool);
- inner.ops.push(Op::Rmw(k, v));
- }
-}
-
-impl<'a> traits::WriteBatch<'a> for WriteBatch<'a> {
- fn commit(&self) -> Result<()> {
- let ns = self.parent.namespace();
- let mut inner = self.inner.borrow_mut();
- let WriteBatchInner { ops, pool } = &mut *inner;
- for op in ops.drain(..) {
- match op {
- Op::Put(k, v) => ns.put(&k, &v),
- Op::Del(rng) => ns.delete(&pool[rng]),
- Op::Rmw(k, v) => {
- let k = &pool[k];
- let op = OperatorRepr::from_bytes(&pool[v]);
- ns.update(k, &op)?;
- },
- }
- }
- Ok(())
- }
- fn reset(&mut self) {
- let inner = self.inner.get_mut();
- inner.ops.clear();
- inner.pool.clear();
- }
-}
-
-// ============================== View ==================================== //
-impl traits::View for MemoryDb {
- type Iter<'b>
- = TransactionIter<'b>
- where
- Self: 'b;
- fn get(&self, key: &[u8]) -> Result<Option<IVec<'_>>> {
- Ok(self.namespace().get(key).map(IVec::new))
- }
- fn contains(&self, key: &[u8]) -> Result<bool> {
- Ok(self.namespace().get(key).is_some())
- }
- fn get_many(&self, keys: impl IntoIterator<Item: AsRef<[u8]>>) -> Result<IVecs<'_>> {
- Ok(keys
- .into_iter()
- .map(|k| self.namespace().get(k.as_ref()))
- .collect())
- }
- fn iter(&self, mode: IterMode) -> Self::Iter<'_> {
- TransactionIter::new(self.namespace().snapshot(), mode)
- }
-}
-
-// ============================== Store =================================== //
-
-impl traits::Write for MemoryDb {
- type Result = crate::Result<()>;
-
- fn put(&self, key: &[u8], value: &[u8]) -> Result<()> {
- self.namespace().put(key, value);
- Ok(())
- }
- fn delete(&self, key: &[u8]) -> Result<()> {
- self.namespace().delete(key);
- Ok(())
- }
- fn update(&self, key: &[u8], op: &dyn Operator) -> Result<()> {
- self.namespace().update(key, op)
- }
- fn delete_range(&self, from: &[u8], to: &[u8]) -> Result<()> {
- let wb = self.batch();
- wb.delete_range(from, to);
- wb.commit()
- }
-}
-
-impl traits::Store for MemoryDb {
- type WriteBatch<'a> = WriteBatch<'a>;
- type Snapshot<'a> = Snapshot<'a>;
-
- fn batch(&self) -> Self::WriteBatch<'_> {
- WriteBatch {
- inner: Default::default(),
- parent: self,
- }
- }
- fn options(&self) -> &Options {
- &self.ns.opts
- }
- fn namespace(&self) -> &str {
- &self.ns.name
- }
- fn clear(&self) -> Result<()> {
- let ns = self.namespace();
- {
- let mut lock = ns.txn.write();
- *lock = Transaction::default();
- }
- Ok(())
- }
- fn child(&self, name: &str, opts: Options) -> Result<Self> {
- Ok(Self {
- ns: self.ns.emplace_child(name, opts),
- })
- }
- fn children(&self) -> Vec<SmolStr> {
- self.ns.children()
- }
- fn flush(&self) -> Result<()> {
- Ok(())
- }
- fn snapshot<'b>(&'b self) -> Self::Snapshot<'b> {
- let snap = self.namespace().snapshot();
- Snapshot {
- vns: snap,
- ns: self.ns.clone(),
- _phantom: PhantomData,
- }
- }
-}
diff --git a/attic/kv/src/pfx_iter.rs b/attic/kv/src/pfx_iter.rs
deleted file mode 100644
index b3b4453c4..000000000
@@ -1,754 +0,0 @@
-use std::{cmp, fmt, iter::Peekable};
-
-use bytes::{Buf, Bytes};
-use tetra_core::alloc::quickbuf;
-
-use super::{
- error::Result,
- traits::{IterMode, View},
-};
-
-/// A helper struct for efficiently checking key prefixes.
-///
-/// This stores a prefix with a hybrid strategy:
-/// - For prefixes ≤ [`INLINE`] bytes: store inline in the buffer
-/// - For prefixes > [`INLINE`] bytes: store first 8 bytes, last 8 bytes, and
-/// hash the middle (16 bytes total)
-pub struct CompactPrefix {
- /// Length of the original prefix
- len: usize,
- /// Fixed-size buffer for prefix storage:
- /// - For prefixes ≤ [`INLINE`] bytes: stores the entire prefix
- /// - For prefixes > [`INLINE`] bytes: stores
- /// [first_8_bytes][last_8_bytes][16_byte_hash]
- key: [u8; Self::INLINE],
-}
-
-impl CompactPrefix {
- /// Maximum size for inline prefix storage
- const INLINE: usize = 64;
- /// Length of the hash
- const HASH_LEN: usize = 32;
-
- /// Hash function.
- fn hash(src: &[u8]) -> [u8; Self::HASH_LEN] {
- *blake3::hash(src).as_bytes()
- }
-
- /// Creates a new [`CompactPrefix`] from the given source prefix.
- ///
- /// For prefixes ≤ [`INLINE`] bytes, stores the entire prefix inline.
- /// For prefixes > [`INLINE`] bytes, stores the first and last parts, and
- /// a hash of the middle portion.
- pub fn new(src: &[u8]) -> Self {
- let len = src.len();
- let mut key = [0u8; Self::INLINE];
-
- if len <= Self::INLINE {
- // Store the entire prefix inline
- key[..len].copy_from_slice(src);
- Self { len, key }
- } else {
- let mut result = Self {
- len,
- key: [0u8; Self::INLINE],
- };
-
- let (parts, hash) = result.key.split_last_chunk_mut().unwrap();
- let (front, back) = parts.split_at_mut(parts.len() / 2);
- front.copy_from_slice(&src[..front.len()]);
- back.copy_from_slice(&src[len - back.len()..]);
-
- // Hash the middle portion and store as HASH_SIZE bytes
- *hash = Self::hash(&src[front.len()..len - back.len()]);
-
- result
- }
- }
-
- /// Checks if the given key matches the stored prefix.
- ///
- /// # Returns
- /// `true` if `key` starts with the prefix stored in this
- /// [`CompactPrefix`], `false` otherwise.
- pub fn matches(&self, key: &[u8]) -> bool {
- if self.len <= Self::INLINE {
- key.starts_with(&self.key[..self.len])
- } else {
- let Some(key) = key.get(..self.len) else {
- return false;
- };
-
- let (parts, hash) = self.key.split_last_chunk().unwrap();
- let (front, back) = parts.split_at(parts.len() / 2);
-
- let Some(rem) = key.strip_prefix(front) else {
- return false;
- };
- let Some(rem) = rem.strip_suffix(back) else {
- return false;
- };
- Self::hash(rem) == *hash
- }
- }
-}
-
-impl fmt::Debug for CompactPrefix {
- /// Formats the [`CompactPrefix`] for debugging.
- ///
- /// Displays the length of the stored prefix and the stored key portions.
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- let mut debug = f.debug_struct("CompactPrefix");
-
- debug.field("len", &self.len);
-
- if self.len <= Self::INLINE {
- // Show the entire inline prefix
- debug.field("key", &format_args!("{:x?}", &self.key[..self.len]));
- } else {
- let (parts, hash) = self.key.split_last_chunk::<{ Self::HASH_LEN }>().unwrap();
- let (front, back) = parts.split_at(parts.len() / 2);
- debug.field("key", &format_args!("{front:x?}..{back:x?}"));
- debug.field("hash", &format_args!("{hash:x?}"));
- }
-
- debug.finish()
- }
-}
-
-#[derive(Debug)]
-pub enum PrefixIter<I: Iterator<Item = Result<(Bytes, Bytes)>>> {
- Alive {
- it: Peekable<I>,
- prefix: CompactPrefix,
- strip: usize,
- },
- Ended,
-}
-
-struct PrefixCmp<'a>(&'a [u8], u8); // prefix + filler
-
-impl<'a> Eq for PrefixCmp<'a> {}
-
-impl<'a> PartialEq for PrefixCmp<'a> {
- fn eq(&self, other: &Self) -> bool {
- self.0 == other.0 && self.1 == other.1
- }
-}
-
-impl<'a> PartialOrd for PrefixCmp<'a> {
- fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
- Some(self.cmp(other))
- }
-}
-
-impl<'a> Ord for PrefixCmp<'a> {
- fn cmp(&self, other: &Self) -> cmp::Ordering {
- let n = self.0.len().max(other.0.len());
- for i in 0..n {
- let a = self.0.get(i).unwrap_or(&self.1);
- let b = other.0.get(i).unwrap_or(&other.1);
- if a != b {
- return a.cmp(b);
- }
- }
- cmp::Ordering::Equal
- }
-}
-
-impl<I: Iterator<Item = Result<(Bytes, Bytes)>>> PrefixIter<I> {
- fn new_forward<'a, V: View<Iter<'a> = I> + ?Sized + 'a>(
- view: &'a V,
- pfx: impl AsRef<[u8]>,
- seek: Option<&[u8]>,
- ) -> Peekable<V::Iter<'a>> {
- let pfx = pfx.as_ref();
- match seek {
- Some(seek) if PrefixCmp(seek, 0) >= PrefixCmp(pfx, 0) => {
- view.iter(IterMode::Forward(Some(seek))).peekable()
- },
- _ => view.iter(IterMode::Forward(Some(pfx))).peekable(),
- }
- }
-
- fn new_backward<'a, V: View<Iter<'a> = I> + ?Sized + 'a>(
- view: &'a V,
- pfx: impl AsRef<[u8]>,
- seek: Option<&[u8]>,
- ) -> Peekable<V::Iter<'a>> {
- let pfx = pfx.as_ref();
- match seek {
- Some(seek) if PrefixCmp(seek, 0xff) < PrefixCmp(pfx, 0xff) => {
- view.iter(IterMode::Reverse(Some(seek))).peekable()
- },
- _ => quickbuf::with(|buf| {
- let n = pfx.len();
- buf.extend_from_slice(pfx);
- let mut end = None;
- for i in (0..n).rev() {
- if let Some(x) = buf[i].checked_add(1) {
- buf[i] = x;
- end = Some(i + 1);
- break;
- }
- }
- let pos = if let Some(i) = end {
- &buf[0..i]
- } else {
- buf.extend_from_slice(&[0xFF; 1]);
- &buf[0..(n + 1)]
- };
-
- let seek = IterMode::Reverse(Some(pos));
- let mut iter = view.iter(seek).peekable();
- while let Some(Ok((key, _))) = iter.peek() {
- if key.as_ref() < pfx {
- break;
- }
- if key.starts_with(pfx) {
- break;
- }
- iter.next();
- }
- iter
- }),
- }
- }
-
- pub fn new<'a, V: View<Iter<'a> = I> + ?Sized + 'a>(
- view: &'a V,
- prefix: impl AsRef<[u8]>,
- mode: IterMode<'_>,
- ) -> Self {
- let pfx = prefix.as_ref();
- let it = match mode {
- IterMode::Forward(seek) => Self::new_forward(view, pfx, seek),
- IterMode::Reverse(seek) => Self::new_backward(view, pfx, seek),
- };
- Self::Alive {
- it,
- prefix: CompactPrefix::new(pfx),
- strip: 0,
- }
- }
-
- pub(crate) fn with_strip(self, strip: usize) -> Self {
- if let Self::Alive { it, prefix, .. } = self {
- Self::Alive {
- it,
- strip: strip.min(prefix.len),
- prefix,
- }
- } else {
- self
- }
- }
-}
-
-impl<I: Iterator<Item = Result<(Bytes, Bytes)>>> Iterator for PrefixIter<I> {
- type Item = Result<(Bytes, Bytes)>;
-
- fn next(&mut self) -> Option<Self::Item> {
- let Self::Alive { it, prefix, strip } = self else {
- return None;
- };
-
- let result = it.next()?;
- match result {
- Ok((mut key, value)) => {
- if !prefix.matches(&key) {
- *self = Self::Ended;
- return None;
- }
- if *strip > 0 {
- key.advance(*strip);
- }
- Some(Ok((key, value)))
- },
- Err(err) => {
- *self = Self::Ended;
- Some(Err(err))
- },
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- fn long_prefix_part_size() -> usize {
- (CompactPrefix::INLINE - CompactPrefix::HASH_LEN) / 2
- }
-
- #[test]
- fn test_empty_prefix() {
- let prefix = CompactPrefix::new(&[]);
-
- // Empty prefix should match everything (everything starts with nothing)
- assert!(prefix.matches(&[]));
- assert!(prefix.matches(&[1]));
- assert!(prefix.matches(&[1, 2, 3]));
- assert!(prefix.matches(b"hello"));
- assert!(prefix.matches(&[0u8; 100]));
- }
-
- #[test]
- fn test_single_byte_prefix() {
- let prefix = CompactPrefix::new(&[42]);
-
- // Should match keys that start with 42
- assert!(prefix.matches(&[42]));
- assert!(prefix.matches(&[42, 0]));
- assert!(prefix.matches(&[42, 1, 2, 3]));
-
- // Should not match keys that don't start with 42
- assert!(!prefix.matches(&[]));
- assert!(!prefix.matches(&[41]));
- assert!(!prefix.matches(&[43]));
- assert!(!prefix.matches(&[0, 42]));
- }
-
- #[test]
- fn test_short_prefix_exact_match() {
- let prefix = CompactPrefix::new(b"hello");
-
- // Exact match
- assert!(prefix.matches(b"hello"));
-
- // Longer keys with same prefix
- assert!(prefix.matches(b"hello world"));
- assert!(prefix.matches(b"hello!"));
- assert!(prefix.matches(b"hello, how are you?"));
- }
-
- #[test]
- fn test_short_prefix_no_match() {
- let prefix = CompactPrefix::new(b"hello");
-
- // Too short
- assert!(!prefix.matches(b""));
- assert!(!prefix.matches(b"h"));
- assert!(!prefix.matches(b"hell"));
-
- // Wrong prefix
- assert!(!prefix.matches(b"world"));
- assert!(!prefix.matches(b"HELLO")); // case sensitive
- assert!(!prefix.matches(b"hallo"));
- assert!(!prefix.matches(b"help"));
- }
-
- #[test]
- fn test_boundary_just_under_inline() {
- let key_len = CompactPrefix::INLINE - 1;
- let key = vec![0xAA; key_len];
- let prefix = CompactPrefix::new(&key);
-
- assert_eq!(prefix.len, key_len);
-
- // Exact match
- assert!(prefix.matches(&key));
-
- // Longer keys with same prefix
- let mut longer = key.clone();
- longer.push(0xBB);
- assert!(prefix.matches(&longer));
-
- // Different prefix
- let mut different = key.clone();
- different[key_len - 1] = 0xBB;
- assert!(!prefix.matches(&different));
-
- // Too short
- assert!(!prefix.matches(&key[..key_len - 1]));
- }
-
- #[test]
- fn test_boundary_exactly_inline() {
- let key_len = CompactPrefix::INLINE;
- let key = vec![0xCC; key_len];
- let prefix = CompactPrefix::new(&key);
-
- assert_eq!(prefix.len, key_len);
-
- // Exact match
- assert!(prefix.matches(&key));
-
- // Longer keys with same prefix
- let mut longer = key.clone();
- longer.extend_from_slice(b"extra");
- assert!(prefix.matches(&longer));
-
- // Different at last byte
- let mut different = key.clone();
- different[key_len - 1] = 0xDD;
- assert!(!prefix.matches(&different));
-
- // Too short
- assert!(!prefix.matches(&key[..key_len - 1]));
- }
-
- #[test]
- fn test_boundary_just_over_inline() {
- let key_len = CompactPrefix::INLINE + 1;
- let key = vec![0xEE; key_len];
- let prefix = CompactPrefix::new(&key);
-
- assert_eq!(prefix.len, key_len);
-
- // Exact match
- assert!(prefix.matches(&key));
-
- // Longer keys with same prefix
- let mut longer = key.clone();
- longer.push(0xFF);
- assert!(prefix.matches(&longer));
-
- let part_size = long_prefix_part_size();
-
- // Different in first part
- let mut different = key.clone();
- different[0] = 0xFF;
- assert!(!prefix.matches(&different));
- different = key.clone();
- different[part_size - 1] = 0xFF;
- assert!(!prefix.matches(&different));
-
- // Different in last part
- let mut different = key.clone();
- different[key_len - 1] = 0xFF;
- assert!(!prefix.matches(&different));
- different = key.clone();
- different[key_len - part_size] = 0xFF;
- assert!(!prefix.matches(&different));
-
- // Different in middle (should fail hash check)
- let mut different = key.clone();
- different[key_len / 2] = 0xFF;
- assert!(!prefix.matches(&different));
- }
-
- #[test]
- fn test_long_prefix_comprehensive() {
- let part_size = long_prefix_part_size();
- let key_len = CompactPrefix::INLINE * 3; // Sufficiently long
-
- // Create a key with recognizable pattern
- let mut key = Vec::new();
- key.extend_from_slice(&vec![0x11; part_size]); // First part
- key.extend_from_slice(&vec![0x22; key_len - 2 * part_size]); // Middle
- key.extend_from_slice(&vec![0x33; part_size]); // Last part
- assert_eq!(key.len(), key_len);
-
- let prefix = CompactPrefix::new(&key);
-
- // Exact match
- assert!(prefix.matches(&key));
-
- // Extended key
- let mut extended = key.clone();
- extended.extend_from_slice(b"_suffix");
- assert!(prefix.matches(&extended));
-
- // Test each component that's checked:
-
- // 1. Wrong first part
- for i in 0..part_size {
- let mut wrong = key.clone();
- wrong[i] = 0xFF;
- assert!(
- !prefix.matches(&wrong),
- "Failed to detect change in first section at position {i}"
- );
- }
-
- // 2. Wrong last part
- for i in (key_len - part_size)..key_len {
- let mut wrong = key.clone();
- wrong[i] = 0xFF;
- assert!(
- !prefix.matches(&wrong),
- "Failed to detect change in last section at position {i}"
- );
- }
-
- // 3. Wrong middle (hash should catch this)
- let mut wrong_middle = key.clone();
- wrong_middle[key_len / 2] = 0xFF;
- assert!(!prefix.matches(&wrong_middle));
-
- // 4. Too short
- assert!(!prefix.matches(&key[..key_len - 1]));
- }
-
- #[test]
- fn test_hash_sensitivity() {
- // Test that even small changes in the middle are detected
- let key_len = CompactPrefix::INLINE * 2;
- let base = vec![0u8; key_len];
- let prefix = CompactPrefix::new(&base);
-
- let part_size = long_prefix_part_size();
-
- // Test changing each byte in the middle section
- for i in part_size..(key_len - part_size) {
- let mut modified = base.clone();
- modified[i] = 1;
- assert!(
- !prefix.matches(&modified),
- "Failed to detect change at position {i}"
- );
- }
- }
-
- #[test]
- fn test_various_lengths() {
- // Test various lengths around boundaries
- let inline_size = CompactPrefix::INLINE;
- let lengths = vec![
- 0,
- 1,
- 8,
- 16,
- inline_size / 4,
- inline_size / 2,
- inline_size * 3 / 4,
- inline_size - 1,
- inline_size,
- inline_size + 1,
- inline_size + 8,
- inline_size * 2,
- inline_size * 4,
- inline_size * 16,
- ];
-
- for len in lengths {
- let key = vec![0xAA; len];
- let prefix = CompactPrefix::new(&key);
-
- // Should match itself
- assert!(
- prefix.matches(&key),
- "Prefix of length {len} should match itself"
- );
-
- // Should match extended version
- let mut extended = key.clone();
- extended.push(0xBB);
- assert!(
- prefix.matches(&extended),
- "Prefix of length {len} should match extended version"
- );
-
- // Should not match if too short (unless empty)
- if len > 0 {
- assert!(
- !prefix.matches(&key[..len - 1]),
- "Prefix of length {len} should not match shorter key"
- );
- }
- }
- }
-
- #[test]
- fn test_unicode_strings() {
- // Test with UTF-8 strings
- let inline_size = CompactPrefix::INLINE;
- let test_cases = vec![
- "hello".to_string(),
- "a".repeat(inline_size - 10),
- "b".repeat(inline_size + 20),
- ];
-
- for s in test_cases {
- let prefix = CompactPrefix::new(s.as_bytes());
-
- // Should match itself
- assert!(prefix.matches(s.as_bytes()));
-
- // Should match with suffix
- let with_suffix = format!("{s}_suffix");
- assert!(prefix.matches(with_suffix.as_bytes()));
-
- // Should not match different string
- assert!(!prefix.matches(b"different"));
- }
- }
-
- #[test]
- fn test_binary_data() {
- // Test with non-UTF8 binary data
- let binary_data: Vec<u8> = (0..=255).collect();
- let inline_size = CompactPrefix::INLINE;
-
- // Test prefix within INLINE threshold
- let prefix_short = CompactPrefix::new(&binary_data[..inline_size / 2]);
- assert!(prefix_short.matches(&binary_data[..inline_size / 2]));
- assert!(prefix_short.matches(&binary_data)); // Full data includes prefix
- assert!(!prefix_short.matches(&binary_data[..inline_size / 2 - 1])); // Too short
-
- // Test prefix exceeding INLINE threshold
- let prefix_long = CompactPrefix::new(&binary_data[..inline_size + 20]);
- assert!(prefix_long.matches(&binary_data[..inline_size + 20]));
- assert!(prefix_long.matches(&binary_data));
- assert!(!prefix_long.matches(&binary_data[..inline_size + 19]));
-
- // Modified version
- let mut modified = binary_data.clone();
- modified[inline_size / 2] = 255 - modified[inline_size / 2];
- assert!(!prefix_long.matches(&modified[..inline_size + 20]));
- }
-
- #[test]
- fn test_consistency() {
- // Same input should always produce same CompactPrefix
- let key = b"this is a test key that is longer than the inline threshold which triggers the hash-based storage strategy for efficient prefix checking";
-
- let prefix1 = CompactPrefix::new(key);
- let prefix2 = CompactPrefix::new(key);
-
- // Internal representation should be identical
- assert_eq!(prefix1.len, prefix2.len);
- assert_eq!(prefix1.key, prefix2.key);
-
- // Both should match the same keys
- assert_eq!(prefix1.matches(key), prefix2.matches(key));
- assert_eq!(prefix1.matches(b"different"), prefix2.matches(b"different"));
- }
-
- #[test]
- fn test_edge_case_exactly_part_size() {
- // Special case: exactly the size of one part in long prefix
- let part_size = long_prefix_part_size();
- let key = vec![0x42; part_size];
- let prefix = CompactPrefix::new(&key);
-
- assert!(prefix.matches(&key));
- assert!(prefix.matches(&[&key[..], b"_more"].concat()));
- assert!(!prefix.matches(&key[..part_size - 1]));
- }
-
- #[test]
- fn test_pathological_cases() {
- let key_len = CompactPrefix::INLINE * 3;
-
- // All zeros
- let zeros = vec![0u8; key_len];
- let prefix_zeros = CompactPrefix::new(&zeros);
- assert!(prefix_zeros.matches(&zeros));
-
- // All ones
- let ones = vec![0xFFu8; key_len];
- let prefix_ones = CompactPrefix::new(&ones);
- assert!(prefix_ones.matches(&ones));
-
- // They should not match each other
- assert!(!prefix_zeros.matches(&ones));
- assert!(!prefix_ones.matches(&zeros));
- }
-
- #[test]
- fn test_realistic_key_patterns() {
- // Test with realistic key patterns (like database keys)
-
- // Path-like keys
- let prefix1 = CompactPrefix::new(b"/users/alice/documents/projects/2024/quarterly-reports/");
- assert!(prefix1.matches(b"/users/alice/documents/projects/2024/quarterly-reports/"));
- assert!(prefix1.matches(b"/users/alice/documents/projects/2024/quarterly-reports/Q1.pdf"));
- assert!(!prefix1.matches(b"/users/alice/documents/projects/2024/annual-reports/"));
-
- // Long UUID-like keys that exceed INLINE
- let uuid_base = b"550e8400-e29b-41d4-a716-446655440000-";
- let mut long_uuid = uuid_base.to_vec();
- while long_uuid.len() <= CompactPrefix::INLINE {
- long_uuid.extend_from_slice(uuid_base);
- }
- long_uuid.extend_from_slice(b"-extra-data");
-
- let prefix_len = CompactPrefix::INLINE + 10;
- let prefix2 = CompactPrefix::new(&long_uuid[..prefix_len]);
- assert!(prefix2.matches(&long_uuid));
-
- // Different UUID
- let mut different_uuid = long_uuid.clone();
- different_uuid[10] = b'X';
- assert!(!prefix2.matches(&different_uuid));
-
- // Versioned keys with metadata
- let versioned = b"data:v2:user:123456789:metadata:created=2024-01-01T00:00:00Z:updated=2024-12-01T00:00:00Z";
- let prefix3 = CompactPrefix::new(&versioned[..CompactPrefix::INLINE / 2]);
- assert!(prefix3.matches(versioned));
- assert!(!prefix3.matches(b"data:v2:user:987654321:metadata:created=2024-01-01T00:00:00Z"));
- }
-
- #[test]
- fn test_prefix_matching_semantics() {
- // Ensure prefix matching works correctly at all boundaries
- let max_len = CompactPrefix::INLINE * 2;
- let base = b"a".repeat(max_len);
-
- // Test prefixes of various lengths
- let prefix_lengths = vec![
- 1,
- long_prefix_part_size(),
- CompactPrefix::INLINE / 2,
- CompactPrefix::INLINE - 1,
- CompactPrefix::INLINE,
- CompactPrefix::INLINE + 1,
- CompactPrefix::INLINE + long_prefix_part_size(),
- max_len,
- ];
-
- for prefix_len in prefix_lengths {
- let prefix = CompactPrefix::new(&base[..prefix_len]);
-
- // Should match any key that starts with this prefix
- for key_len in prefix_len..=(max_len + 20) {
- let key = b"a".repeat(key_len);
- assert!(
- prefix.matches(&key),
- "Prefix len {prefix_len} should match key len {key_len}"
- );
- }
-
- // Should not match shorter keys
- if prefix_len > 0 {
- let short_key = b"a".repeat(prefix_len - 1);
- assert!(
- !prefix.matches(&short_key),
- "Prefix len {} should not match key len {}",
- prefix_len,
- prefix_len - 1
- );
- }
- }
- }
-
- #[test]
- fn test_hash_distribution() {
- // Test that similar keys with small differences are distinguished
- let key_len = CompactPrefix::INLINE * 2;
- let base = vec![0x42; key_len];
- let part_size = long_prefix_part_size();
-
- // Create variations with single bit flips in the middle section
- for i in part_size..(key_len - part_size) {
- for bit in 0..8 {
- let mut variant = base.clone();
- variant[i] ^= 1 << bit; // Flip one bit
-
- let prefix_base = CompactPrefix::new(&base);
- let prefix_variant = CompactPrefix::new(&variant);
-
- assert!(
- !prefix_base.matches(&variant),
- "Should detect bit flip at byte {i} bit {bit}"
- );
- assert!(
- !prefix_variant.matches(&base),
- "Should detect bit flip at byte {i} bit {bit} (reverse)"
- );
- }
- }
- }
-}
diff --git a/attic/kv/src/prefixed.rs b/attic/kv/src/prefixed.rs
deleted file mode 100644
index 917afabf2..000000000
@@ -1,285 +0,0 @@
-use std::fmt;
-
-use bytes::Bytes;
-use tetra_core::alloc::quickbuf;
-
-use super::traits;
-use crate::{pfx_iter::PrefixIter, IVec, IVecs, Operator, Options, Result};
-
-#[derive(Clone, Copy, Debug)]
-pub struct Prefixed<V, P: AsRef<[u8]>>(pub V, pub P);
-impl<V, P: AsRef<[u8]>> Prefixed<V, P> {
- pub fn new(inner: V, prefix: impl Into<P>) -> Self {
- Self(inner, prefix.into())
- }
- pub fn inner(&self) -> &V {
- &self.0
- }
- pub fn inner_mut(&mut self) -> &mut V {
- &mut self.0
- }
- pub fn prefix(&self) -> &[u8] {
- self.1.as_ref()
- }
- fn with_key<'a, R>(&'a self, key: &[u8], f: impl FnOnce(&'a V, &[u8]) -> R) -> R {
- quickbuf::with(|buf| {
- let Self(inner, pfx) = self;
- buf.extend_from_slice(pfx.as_ref());
- buf.extend_from_slice(key);
- f(inner, buf.as_ref())
- })
- }
-}
-
-pub type PfxStore<S> = Prefixed<S, Box<[u8]>>;
-pub type PfxWriteBatch<'a, W> = Prefixed<W, &'a [u8]>;
-pub type PfxSnapshot<'a, S> = Prefixed<S, &'a [u8]>;
-
-impl<'a, W: traits::WriteBatch<'a>> traits::Write for PfxWriteBatch<'a, W> {
- type Result = ();
-
- fn put(&self, key: &[u8], value: &[u8]) -> Self::Result {
- self.with_key(key, |db, k| db.put(k, value));
- }
- fn delete(&self, key: &[u8]) -> Self::Result {
- self.with_key(key, |db, k| db.delete(k));
- }
- fn delete_range(&self, from: &[u8], to: &[u8]) -> Self::Result {
- quickbuf::with(|buf| {
- let pfx = self.prefix();
-
- // Create prefixed from key
- let from_start = buf.len();
- buf.extend_from_slice(pfx);
- buf.extend_from_slice(from);
- let from_end = buf.len();
-
- // Create prefixed to key
- let to_start = buf.len();
- buf.extend_from_slice(pfx);
- buf.extend_from_slice(to);
- let to_end = buf.len();
-
- self
- .inner()
- .delete_range(&buf[from_start..from_end], &buf[to_start..to_end]);
- });
- }
- fn update(&self, key: &[u8], op: &dyn Operator) -> Self::Result {
- self.with_key(key, |db, k| db.update(k, op));
- }
-}
-
-impl<'a, W: traits::WriteBatch<'a>> traits::WriteBatch<'a> for PfxWriteBatch<'a, W> {
- fn commit(&self) -> Result<()> {
- self.inner().commit()
- }
- fn reset(&mut self) {
- self.inner_mut().reset();
- }
-}
-
-impl<V: traits::View, P: AsRef<[u8]> + Send + Sync + fmt::Debug> traits::View for Prefixed<V, P> {
- type Iter<'b>
- = PrefixIter<V::Iter<'b>>
- where
- V: 'b,
- Self: 'b;
-
- fn get(&self, key: &[u8]) -> Result<Option<IVec<'_>>> {
- self.with_key(key, |db, k| db.get(k))
- }
-
- fn get_many(&self, keys: impl IntoIterator<Item: AsRef<[u8]>>) -> Result<IVecs<'_>> {
- struct KeyIteratorAdapter<'a, I: Iterator<Item: AsRef<[u8]>>> {
- iter: I,
- prefix: usize,
- buf: &'a mut quickbuf::StackBuffer,
- }
- impl<'a, I: Iterator<Item: AsRef<[u8]>>> crate::erased::KeyIterator for KeyIteratorAdapter<'a, I> {
- fn next_key(&mut self) -> Option<&[u8]> {
- let key = self.iter.next()?;
- self.buf.truncate(self.prefix);
- self.buf.extend_from_slice(key.as_ref());
- Some(self.buf.as_ref())
- }
- }
-
- quickbuf::with(|buf| {
- buf.extend_from_slice(self.prefix());
- let mut iter = KeyIteratorAdapter {
- iter: keys.into_iter(),
- prefix: buf.len(),
- buf,
- };
- self
- .inner()
- .get_many(crate::erased::adapt_key_iter(&mut iter))
- })
- }
-
- fn contains(&self, key: &[u8]) -> Result<bool> {
- self.with_key(key, |db, k| db.contains(k))
- }
-
- fn iter<'b>(&'b self, mode: traits::IterMode<'_>) -> Self::Iter<'b> {
- quickbuf::with(|buf| {
- let prefix = self.prefix();
-
- buf.extend_from_slice(prefix);
- if let Some(seek) = mode.inner() {
- buf.extend_from_slice(seek);
- }
-
- let mode = mode.replace_bound(Some(buf.as_ref()));
- PrefixIter::new(self.inner(), prefix, mode).with_strip(prefix.len())
- })
- }
-
- fn prefix_scan<'a>(
- &'a self,
- prefix: &[u8],
- mode: crate::IterMode<'_>,
- ) -> impl Iterator<Item = Result<(Bytes, Bytes)>> + use<'a, V, P> {
- quickbuf::with(|buf| {
- let root: &[u8] = self.prefix();
- let user: &[u8] = prefix;
-
- buf.extend_from_slice(root);
- buf.extend_from_slice(user);
- let pfx = buf.len();
-
- let seek = if let Some(seek) = mode.inner() {
- if seek.starts_with(user) {
- buf.extend_from_slice(&seek[user.len()..]);
- Some(0..buf.len())
- } else {
- buf.extend_from_slice(root);
- buf.extend_from_slice(seek);
- Some(pfx..buf.len())
- }
- } else {
- None
- };
-
- let mode = mode.replace_bound(seek.map(|r| &buf[r]));
- PrefixIter::new(self.inner(), &buf[0..pfx], mode).with_strip(root.len())
- })
- }
-}
-
-impl<S: traits::Store> traits::Write for PfxStore<S> {
- type Result = crate::Result<()>;
-
- fn delete(&self, key: &[u8]) -> Self::Result {
- self.with_key(key, |db, k| db.delete(k))
- }
- fn delete_range(&self, from: &[u8], to: &[u8]) -> Self::Result {
- quickbuf::with(|buf| {
- let Self(inner, pfx) = self;
- buf.extend_from_slice(pfx.as_ref());
- buf.extend_from_slice(from);
- let from_p = buf.len();
- buf.extend_from_slice(pfx.as_ref());
- buf.extend_from_slice(to);
-
- let (from, to) = buf.as_ref().split_at(from_p);
- inner.delete_range(from, to)
- })
- }
- fn put(&self, key: &[u8], value: &[u8]) -> Self::Result {
- self.with_key(key, |db, k| db.put(k, value))
- }
- fn update(&self, key: &[u8], op: &dyn Operator) -> Self::Result {
- self.with_key(key, |db, k| db.update(k, op))
- }
-}
-
-impl<S: traits::Store> traits::Store for PfxStore<S> {
- type WriteBatch<'a>
- = PfxWriteBatch<'a, S::WriteBatch<'a>>
- where
- S: 'a;
- type Snapshot<'a>
- = PfxSnapshot<'a, S::Snapshot<'a>>
- where
- S: 'a;
-
- fn batch(&self) -> Self::WriteBatch<'_> {
- PfxWriteBatch::new(self.inner().batch(), self.prefix())
- }
-
- fn namespace(&self) -> &str {
- self.inner().namespace()
- }
-
- fn child(&self, name: &str, opts: Options) -> Result<Self> {
- Ok(Self(self.inner().child(name, opts)?, self.1.clone()))
- }
-
- fn children(&self) -> Vec<smol_str::SmolStr> {
- self.inner().children()
- }
-
- fn clear(&self) -> Result<()> {
- quickbuf::with(|buf| {
- let prefix = self.prefix();
-
- // Create range start (prefix)
- let from_start = buf.len();
- buf.extend_from_slice(prefix);
- let from_end = buf.len();
-
- // Create range end (prefix + 1)
- let to_start = buf.len();
- buf.extend_from_slice(prefix);
-
- // Find the first byte we can increment
- let mut found = false;
- for i in (0..prefix.len()).rev() {
- if let Some(incremented) = buf[to_start + i].checked_add(1) {
- buf[to_start + i] = incremented;
- found = true;
- break;
- }
- // If byte is 0xFF, it will overflow, so continue to previous
- // byte
- }
-
- if found {
- // We successfully incremented a byte in the prefix
- let to_end = to_start + prefix.len();
- self
- .inner()
- .delete_range(&buf[from_start..from_end], &buf[to_start..to_end])
- } else {
- // All bytes were 0xFF, so we need to delete everything from prefix onwards
- // Use a very high byte sequence as the upper bound
- buf.extend_from_slice(&[0xFF; 8]); // Add some 0xFF bytes
- let to_end = buf.len();
- self
- .inner()
- .delete_range(&buf[from_start..from_end], &buf[to_start..to_end])
- }
- })
- }
-
- fn flush(&self) -> Result<()> {
- self.inner().flush()
- }
-
- fn snapshot<'b>(&'b self) -> Self::Snapshot<'b> {
- PfxSnapshot::new(self.inner().snapshot(), self.prefix())
- }
-
- fn options(&self) -> &Options {
- self.inner().options()
- }
-}
-
-pub trait PrefixExt: traits::Store + Sized {
- fn with_prefix(self, prefix: impl AsRef<[u8]>) -> PfxStore<Self> {
- PfxStore::new(self, prefix.as_ref().to_vec())
- }
-}
-impl<T: traits::Store> PrefixExt for T {}
diff --git a/attic/kv/src/rmw.rs b/attic/kv/src/rmw.rs
deleted file mode 100644
index bb188f16a..000000000
@@ -1,423 +0,0 @@
-//! Read-modify-write operator abstraction for the KV store.
-//!
-//! This module provides a small, self-contained framework that allows the
-//! key-value engine to attach *read-modify-write operators* to write requests.
-//! A read-modify-write operator receives the key being written together with
-//! the current value (if any) and must compute the new value that will be
-//! stored. The framework is purposely designed to work with zero-copy
-//! deserialization via [`postcard`](https://docs.rs/postcard) which keeps allocations to a minimum
-//! and allows read-modify-write operators to borrow their parameters directly
-//! from a transient buffer.
-//!
-//! In order to make read-modify-write operators transferable over the network
-//! (or across thread boundaries) we distinguish between two layers:
-//!
-//! * [`OperatorDefn`] – the *definition* of an operator. Because definitions
-//! may borrow data, the trait has an explicit lifetime parameter. Definitions
-//! are (de)serializable via *postcard*.
-//! * [`Operator`] – an object-safe, type-erased wrapper around a definition
-//! that hides the lifetime and can therefore be stored behind a trait object.
-//!
-//! A globally shared registry (see [`registry::GLOBAL`]) maps each concrete
-//! operator type to a function pointer that can *apply* the operator without
-//! knowing its concrete type. This indirection makes it possible to
-//! deserialize an [`OperatorRepr`] coming from the wire and invoke it
-//! immediately without pulling it through a full boxing step first.
-//!
-//! The bottom of this file contains a set of small but useful
-//! *built-in* operators – feel free to use them as inspiration for your own.
-
-use std::{any::TypeId, borrow::Cow, fmt, marker::PhantomData, sync::LazyLock};
-
-use parking_lot::RwLock;
-use serde::{de::Deserialize, Serialize};
-use tetra_core::{alloc::quickbuf, containers::append_vec::AppendVec};
-
-/// Unique identifier for an operator type in the registry.
-///
-/// This is used to look up the appropriate implementation in the
-/// [`registry::GLOBAL`] when deserializing an operator from the wire.
-pub type OperatorId = u8;
-
-/// Definition of a read-modify-write operator with potentially borrowed data.
-///
-///
-/// This trait represents the core logic of a read-modify-write operator.
-/// Implementations define how to combine an existing value with new data when
-/// writing to a key. Because definitions may borrow data, the trait has an
-/// explicit lifetime parameter. Definitions are serializable/deserializable
-/// via [`postcard`](https://docs.rs/postcard).
-pub trait OperatorDefn<'a>: Serialize + Deserialize<'a> + fmt::Debug + 'a {
- /// Version of `Self` where all borrows have been rebound to the lifetime
- /// `'b`.
- ///
- /// This associated type is crucial for deserialization: when we read a
- /// value from a byte slice we obviously cannot give it the original
- /// lifetime `'a`; instead we need to *re-borrow* every reference from the
- /// freshly read slice. The compiler takes care of the heavy lifting here –
- /// all you have to do is write `type Extend<'b> = Self` for operator types
- /// that do not actually borrow anything.
- type Extend<'b>: OperatorDefn<'b>;
-
- /// Apply the operator to `value` which is currently stored under `key`.
- ///
- /// * `key` – the full database key (namespaces included). The operator may
- /// inspect the key but must **never modify it**.
- /// * `value` – the value currently stored for `key`. `None` signifies that
- /// the key is absent in the database.
- ///
- /// The function must return the new value that should be written. It is
- /// perfectly fine to return a borrowed slice (`Cow::Borrowed`) if the data
- /// lives long enough.
- fn apply<'d>(&self, key: &[u8], value: Option<Cow<'d, [u8]>>) -> Cow<'d, [u8]>
- where
- Self: 'd;
-}
-
-/// Object-safe, type-erased wrapper around an [`OperatorDefn`].
-///
-/// This trait hides the lifetime parameter of [`OperatorDefn`], allowing
-/// operators to be stored behind trait objects and passed across thread
-/// boundaries. It provides methods to apply the operator and serialize it
-/// for network transmission.
-pub trait Operator: fmt::Debug {
- /// Returns the *operator id* – an index into the global
- /// [`registry::GLOBAL`].
- fn opid(&self) -> OperatorId;
- /// Applies the operator (see [`OperatorDefn::apply`]).
- fn apply<'d>(&self, key: &[u8], value: Option<Cow<'d, [u8]>>) -> Cow<'d, [u8]>
- where
- Self: 'd;
-
- /// Serialises the operator into a [`OperatorRepr`] that can be sent over
- /// the network.
- ///
- /// Returns the range in the buffer where the serialized operator is stored.
- fn write_to(&self, pool: &mut quickbuf::StackBuffer) -> std::ops::Range<usize>;
-}
-
-/// Blanket implementation of [`Operator`] for any type implementing
-/// [`OperatorDefn`].
-///
-/// This automatically provides the type-erased interface for all concrete
-/// operator definitions, handling the registration with the global registry and
-/// serialization.
-impl<'a, T: OperatorDefn<'a>> Operator for T {
- fn opid(&self) -> OperatorId {
- registry::GLOBAL.emplace_if::<T::Extend<'static>>()
- }
-
- fn apply<'d>(&self, key: &[u8], value: Option<Cow<'d, [u8]>>) -> Cow<'d, [u8]>
- where
- Self: 'd,
- {
- self.apply(key, value)
- }
-
- fn write_to(&self, pool: &mut quickbuf::StackBuffer) -> std::ops::Range<usize> {
- let beg = pool.len();
- pool.extend_from_slice(&self.opid().to_le_bytes());
- postcard::to_extend(self, &mut *pool).unwrap();
- beg..pool.len()
- }
-}
-
-/// Compact on-wire representation of an [`Operator`].
-///
-/// The structure contains nothing more than the operator id and the raw
-/// *postcard* payload that represents the original [`OperatorDefn`].
-#[derive(Debug, Copy, Clone)]
-pub struct OperatorRepr<'a> {
- opid: OperatorId,
- rest: &'a [u8],
-}
-
-impl<'a> OperatorRepr<'a> {
- /// Returns the operator ID for this representation.
- pub fn opid(&self) -> OperatorId {
- self.opid
- }
-
- /// Creates a new [`OperatorRepr`] from a byte slice.
- ///
- /// The byte slice should contain a serialized operator, with the first 8
- /// bytes being the operator ID in little-endian format, followed by the
- /// serialized operator data.
- ///
- /// # Panics
- ///
- /// Panics if the input slice is too small to contain an operator ID.
- pub fn from_bytes(bytes: &'a [u8]) -> Self {
- let (opid, rest) = bytes.split_first_chunk().unwrap();
- Self {
- opid: OperatorId::from_le_bytes(*opid),
- rest,
- }
- }
-
- /// Applies this operator to the given key and value.
- ///
- /// This method consumes the operator representation and applies it to the
- /// provided key and optional value, returning the resulting value.
- ///
- /// The implementation looks up the appropriate operator implementation from
- /// the global registry using the stored operator ID, then delegates to that
- /// implementation.
- pub fn apply<'d>(self, key: &[u8], value: Option<Cow<'d, [u8]>>) -> Cow<'d, [u8]>
- where
- Self: 'd,
- {
- let applier = registry::GLOBAL.get(self.opid());
- applier.apply(self.rest, key, value)
- }
-}
-
-impl<'a> Operator for OperatorRepr<'a> {
- fn opid(&self) -> OperatorId {
- self.opid
- }
-
- fn apply<'d>(&self, key: &[u8], value: Option<Cow<'d, [u8]>>) -> Cow<'d, [u8]>
- where
- Self: 'd,
- {
- OperatorRepr::apply(*self, key, value)
- }
-
- fn write_to(&self, pool: &mut quickbuf::StackBuffer) -> std::ops::Range<usize> {
- let beg = pool.len();
- pool.extend_from_slice(&self.opid().to_le_bytes());
- pool.extend_from_slice(self.rest);
- beg..pool.len()
- }
-}
-
-pub mod ops {
- use derive_more::Constructor;
- use serde::{Deserialize, Serialize};
-
- use super::*;
-
- /// Append read-modify-write operator.
- ///
- /// This operator appends the provided bytes to the existing value. If no
- /// value exists, it treats the existing value as empty and simply returns
- /// the bytes to append.
- #[derive(Debug, Clone, Copy, Serialize, Deserialize, Constructor)]
- pub struct Append<'a>(pub &'a [u8]);
-
- impl<'a> OperatorDefn<'a> for Append<'a> {
- type Extend<'b> = Append<'b>;
-
- fn apply<'d>(&self, _key: &[u8], value: Option<Cow<'d, [u8]>>) -> Cow<'d, [u8]>
- where
- 'a: 'd,
- {
- let mut v = value.unwrap_or_default().into_owned();
- v.extend_from_slice(self.0);
- Cow::Owned(v)
- }
- }
-
- /// Compare-and-exchange read-modify-write operator.
- ///
- /// If the current value equals [`expected`], it is **replaced** by
- /// [`desired`]. Otherwise the value remains unchanged.
- #[derive(Debug, Clone, Copy, Serialize, Deserialize, Constructor)]
- pub struct CompareExchange<'a> {
- pub expected: Option<&'a [u8]>,
- pub desired: &'a [u8],
- }
-
- impl<'a> OperatorDefn<'a> for CompareExchange<'a> {
- type Extend<'b> = CompareExchange<'b>;
-
- fn apply<'d>(&self, _key: &[u8], value: Option<Cow<'d, [u8]>>) -> Cow<'d, [u8]>
- where
- 'a: 'd,
- {
- if self.expected == value.as_deref() {
- Cow::Borrowed(self.desired)
- } else {
- value.unwrap_or_default()
- }
- }
- }
-
- /// Increment read-modify-write operator.
- ///
- /// This operator treats the value as a little-endian encoded
- /// multi-precision integer and increments it by the specified amount.
- /// If the value is empty, it's treated as zero.
- ///
- /// # Implementation Details
- ///
- /// The value is processed as a sequence of 8-byte chunks in little-endian
- /// order. The increment is applied to the first chunk, with any overflow
- /// carrying to subsequent chunks. If all chunks overflow, a new chunk is
- /// appended to handle the remaining carry.
- #[derive(Debug, Clone, Copy, Serialize, Deserialize, Constructor)]
- #[serde(transparent)]
- pub struct Inc(pub u64);
-
- impl<'a> OperatorDefn<'a> for Inc {
- type Extend<'b> = Inc;
-
- fn apply<'d>(&self, _key: &[u8], value: Option<Cow<'d, [u8]>>) -> Cow<'d, [u8]>
- where
- Self: 'd,
- {
- let mut v = value.unwrap_or_default().into_owned();
- let qword_count = v.len().div_ceil(8);
- if v.len() != qword_count * 8 {
- v.resize(qword_count * 8, 0);
- }
-
- let mut inc = self.0;
- let (chunks, _) = v.as_chunks_mut::<8>();
- for chunk in chunks {
- let current = u64::from_le_bytes(*chunk);
- let (current, of) = current.overflowing_add(inc);
- *chunk = current.to_le_bytes();
- inc = of as u64;
- if !of {
- break;
- }
- }
- if inc != 0 {
- v.extend_from_slice(&inc.to_le_bytes());
- }
- Cow::Owned(v)
- }
- }
-
- /// Saturating decrement read-modify-write operator.
- ///
- /// This operator treats the value as a little-endian encoded
- /// multi-precision integer and decrements it by the specified amount.
- /// If the result would be negative, the value is set to zero
- /// (saturating behavior).
- ///
- /// # Implementation Details
- ///
- /// The value is processed as a sequence of 8-byte chunks in little-endian
- /// order. The decrement is applied to the first chunk, with any underflow
- /// borrowing from subsequent chunks. If the entire value would underflow
- /// (result would be negative), the value is set to zero.
- #[derive(Debug, Clone, Copy, Serialize, Deserialize, Constructor)]
- #[serde(transparent)]
- pub struct SaturatingDec(pub u64);
-
- impl<'a> OperatorDefn<'a> for SaturatingDec {
- type Extend<'b> = SaturatingDec;
-
- fn apply<'d>(&self, _key: &[u8], value: Option<Cow<'d, [u8]>>) -> Cow<'d, [u8]>
- where
- Self: 'd,
- {
- let mut v = value.unwrap_or_default().into_owned();
- let qword_count = v.len().div_ceil(8);
- if v.len() != qword_count * 8 {
- v.resize(qword_count * 8, 0);
- }
-
- let mut dec = self.0;
- let (chunks, _) = v.as_chunks_mut::<8>();
- for chunk in chunks {
- let current = u64::from_le_bytes(*chunk);
- let (current, of) = current.overflowing_sub(dec);
- *chunk = current.to_le_bytes();
- dec = of as u64;
- if !of {
- break;
- }
- }
- if dec != 0 {
- v.clear();
- v.resize(8, 0);
- }
- Cow::Owned(v)
- }
- }
-}
-
-mod registry {
- use ahash::AHashMap;
-
- use super::*;
-
- #[derive(Default)]
- pub struct Registry {
- values: AppendVec<&'static dyn OperatorVmt>,
- types: RwLock<LazyLock<AHashMap<TypeId, usize>>>,
- }
-
- pub trait OperatorVmt: 'static + Send + Sync {
- fn apply<'a, 'd>(
- &self,
- this: &'a [u8],
- key: &[u8],
- value: Option<Cow<'d, [u8]>>,
- ) -> Cow<'d, [u8]>
- where
- 'a: 'd;
- }
-
- struct DefnVmt<T: OperatorDefn<'static>>(PhantomData<fn() -> T>);
- impl<T: OperatorDefn<'static>> DefnVmt<T> {
- const DEFN: Self = Self(PhantomData);
- }
-
- impl<T: OperatorDefn<'static>> OperatorVmt for DefnVmt<T> {
- fn apply<'a, 'd>(
- &self,
- this: &'a [u8],
- key: &[u8],
- value: Option<Cow<'d, [u8]>>,
- ) -> Cow<'d, [u8]>
- where
- 'a: 'd,
- {
- let this: T::Extend<'a> = postcard::from_bytes(this).unwrap();
- <T::Extend<'a> as OperatorDefn<'a>>::apply(&this, key, value)
- }
- }
-
- impl Registry {
- pub const fn new() -> Self {
- Self {
- values: AppendVec::new(),
- types: RwLock::new(LazyLock::new(AHashMap::new)),
- }
- }
-
- pub fn emplace_if<T: OperatorDefn<'static>>(&self) -> OperatorId {
- let mut guard = self.types.upgradable_read();
- let type_id = TypeId::of::<T>();
- guard
- .get(&type_id)
- .copied()
- .unwrap_or_else(
- #[cold]
- || {
- guard.with_upgraded(|map| {
- if let Some(idx) = map.get(&type_id) {
- return *idx;
- }
- let idx = self.values.push(&DefnVmt::<T>::DEFN);
- map.insert(type_id, idx);
- idx
- })
- },
- )
- .try_into()
- .expect("invalid operator id")
- }
-
- pub fn get(&self, opid: OperatorId) -> &'static dyn OperatorVmt {
- *self.values.get(opid as usize).expect("invalid operator id")
- }
- }
-
- pub static GLOBAL: Registry = Registry::new();
-}
diff --git a/attic/kv/src/rocksdb.rs b/attic/kv/src/rocksdb.rs
deleted file mode 100644
index 91008c369..000000000
@@ -1,729 +0,0 @@
-use std::{borrow::Cow, cell::RefCell, fmt, mem, path::Path, sync::Arc};
-
-use ahash::{AHashMap, AHashSet};
-use bytes::Bytes;
-pub use librocksdb_sys as rocks_sys;
-use parking_lot::RwLock;
-pub use rocksdb as rocks_api;
-use rocksdb::{BlockBasedOptions, Cache};
-use smol_str::{format_smolstr, SmolStr, ToSmolStr};
-use tetra_core::alloc::quickbuf;
-use traits::IterMode;
-
-use super::{
- error::{Error, Result},
- traits,
-};
-use crate::{IVec, IVecRef, IVecs, Operator, OperatorRepr, Options, Store};
-
-type Db = rocksdb::DBWithThreadMode<rocksdb::MultiThreaded>;
-
-/// A column family in RocksDB with a static lifetime.
-struct InternedColumnFamily {
- /// The RocksDB column family handle with a static lifetime.
- inner: Arc<rocksdb::BoundColumnFamily<'static>>,
-}
-
-impl InternedColumnFamily {
- /// Creates a new interned column family, creating it if it doesn't exist.
- ///
- /// # Arguments
- ///
- /// * `db` - The RocksDB database instance.
- /// * `opts` - The options to use when creating the column family.
- /// * `name` - The name of the column family.
- ///
- /// # Returns
- ///
- /// A new [`InternedColumnFamily`] instance.
- ///
- /// # Errors
- ///
- /// Returns an error if the column family cannot be created.
- fn new(db: &Db, opts: &rocksdb::Options, name: &SmolStr) -> Result<Self> {
- let handle = match db.cf_handle(name) {
- Some(handle) => handle,
- None => {
- db.create_cf(name, opts)?;
- db.cf_handle(name)
- .ok_or_else(|| Error::ColumnFamilyNotFound(name.clone()))?
- },
- };
-
- let cf = unsafe {
- mem::transmute::<
- Arc<rocksdb::BoundColumnFamily<'_>>,
- Arc<rocksdb::BoundColumnFamily<'static>>,
- >(handle)
- };
- Ok(Self { inner: cf })
- }
-}
-
-/// The core shared state of a RocksDB instance.
-struct Core {
- /// The RocksDB database instance.
- db: Db,
- /// The options used for creating column families.
- db_opts: rocksdb::Options,
- /// Optional temporary directory that will be cleaned up when the Core is
- /// dropped.
- _tempdir: Option<tempfile::TempDir>,
- /// Map of column family names to their handles.
- cfs: RwLock<AHashMap<SmolStr, InternedColumnFamily>>,
-}
-
-impl Core {
- fn open_cf(
- &self,
- name: &SmolStr,
- opts: &rocksdb::Options,
- ) -> Result<*mut librocksdb_sys::rocksdb_column_family_handle_t> {
- let mut cfs = self.cfs.upgradable_read();
- if let Some(i) = cfs.get(name) {
- return Ok(rocksdb::AsColumnFamilyRef::inner(&i.inner));
- }
- cfs.with_upgraded(|cfs| {
- if let Some(i) = cfs.get(name) {
- return Ok(rocksdb::AsColumnFamilyRef::inner(&i.inner));
- }
- let cf = InternedColumnFamily::new(&self.db, opts, name)?;
- let handle = rocksdb::AsColumnFamilyRef::inner(&cf.inner);
- cfs.insert(name.clone(), cf);
- Ok(handle)
- })
- }
-}
-
-impl Drop for Core {
- fn drop(&mut self) {
- self.cfs.write().clear();
- }
-}
-
-/// A column family in the RocksDB hierarchy.
-struct ColumnFamily {
- /// The full name of this column family.
- full_name: SmolStr,
- /// Offset to child name.
- name_offset: usize,
- /// The RocksDB column family handle
- handle: *mut librocksdb_sys::rocksdb_column_family_handle_t,
- /// Child column families.
- children: RwLock<AHashMap<SmolStr, Arc<ColumnFamily>>>,
- /// The shared core state.
- core: Arc<Core>,
- /// Store options we're using within this space.
- opts: Options,
- /// The default read options.
- ro: rocksdb::ReadOptions,
- /// The default write options.
- wo: rocksdb::WriteOptions,
-}
-
-impl ColumnFamily {
- fn is_root(&self) -> bool {
- self.name_offset == 0
- }
- fn format_child_name(&self, name: &str) -> SmolStr {
- if self.is_root() && name != ROOT_CF {
- name.to_smolstr() // If child doesn't conflict with the root, we can
- // just use it as is
- } else {
- format_smolstr!("{}.{}", self.full_name, name) // Otherwise, let's use
- // a seperator
- }
- }
-}
-
-unsafe impl Send for ColumnFamily {}
-unsafe impl Sync for ColumnFamily {}
-
-impl rocksdb::AsColumnFamilyRef for ColumnFamily {
- fn inner(&self) -> *mut librocksdb_sys::rocksdb_column_family_handle_t {
- self.handle
- }
-}
-
-#[derive(Clone)]
-pub struct RocksDb {
- /// The current column family.
- inner: Arc<ColumnFamily>,
-}
-
-impl fmt::Debug for RocksDb {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.debug_struct("RocksDb")
- .field("cf", &self.namespace())
- .field("db", &self.inner.core.db)
- .finish()
- }
-}
-
-const ROOT_CF: &str = "default";
-
-impl RocksDb {
- /// Returns the default options and column families for opening a RocksDB
- /// instance.
- ///
- /// # Arguments
- ///
- /// * `path` - Optional path to the database. If provided, existing column
- /// families will be listed.
- ///
- /// # Returns
- ///
- /// A tuple containing the options and a list of column family names.
- fn default_open_options(path: Option<&Path>) -> (rocksdb::Options, AHashSet<SmolStr>) {
- const KB: usize = 1024;
- const MB: usize = 1024 * KB;
- const GB: usize = 1024 * MB;
- let mut opts = rocksdb::Options::default();
- opts.create_if_missing(true);
- opts.set_write_buffer_size(64 * MB);
- opts.set_max_write_buffer_number(8);
- opts.set_min_write_buffer_number_to_merge(2);
- opts.set_compression_type(rocksdb::DBCompressionType::Lz4);
-
- // Enable blob storage for large values
- opts.set_enable_blob_files(true);
- opts.set_min_blob_size(8 * KB as u64); // Store values >= 8KB in blob files
- opts.set_blob_file_size(256 * MB as u64); // 256MB blob files
- opts.set_blob_compression_type(rocksdb::DBCompressionType::Lz4);
- opts.set_enable_blob_gc(true);
- opts.set_blob_gc_age_cutoff(0.25); // GC oldest 25% of blob files
- opts.set_blob_gc_force_threshold(1.0);
-
- // Use bloom filters
- let mut block_opts = BlockBasedOptions::default();
- block_opts.set_bloom_filter(10.0, false);
- block_opts.set_block_cache(&Cache::new_lru_cache(GB)); // 1GB cache
- opts.set_block_based_table_factory(&block_opts);
-
- // Optimize for sequential writes
- opts.set_use_direct_io_for_flush_and_compaction(true);
- opts.set_compaction_style(rocksdb::DBCompactionStyle::Level);
-
- opts.set_merge_operator(
- "merge",
- |key, val, ops| {
- let mut val = val.map(Cow::Borrowed);
- for op in ops.iter() {
- let op = OperatorRepr::from_bytes(op);
- val = Some(op.apply(key, val));
- }
- val.map(|v| v.into_owned())
- },
- |_, _, _| None,
- );
- opts.create_missing_column_families(true);
- opts.optimize_level_style_compaction(1024 * 1024 * 1024);
- opts.increase_parallelism(
- std::thread::available_parallelism()
- .map(|p| p.get().min(32) as i32)
- .unwrap_or_default()
- >> 2,
- );
-
- // https://github.com/facebook/rocksdb/wiki/Setup-Options-and-Basic-Tuning#other-general-options
- opts.set_level_compaction_dynamic_level_bytes(true);
- //pts.set_bytes_per_sync(1048576);
- opts.set_compaction_pri(rocksdb::CompactionPri::MinOverlappingRatio);
-
- let mut cfs = path.map_or(AHashSet::new(), |path| {
- Db::list_cf(&opts, path)
- .unwrap_or_default()
- .into_iter()
- .map(|x| x.to_smolstr())
- .collect()
- });
- cfs.insert(SmolStr::new_static(ROOT_CF));
- (opts, cfs)
- }
-
- /// Creates a new RocksDB instance with the given database and options.
- ///
- /// # Arguments
- ///
- /// * `db` - The RocksDB database instance.
- /// * `opts` - The options used for creating column families.
- /// * `cfs` - The list of column family names.
- /// * `tempdir` - Optional temporary directory.
- ///
- /// # Returns
- ///
- /// A new [`RocksDb`] instance.
- ///
- /// # Errors
- ///
- /// Returns an error if any column family cannot be created.
- fn new(
- db: Db,
- db_opts: rocksdb::Options,
- cfs: AHashSet<SmolStr>,
- tempdir: Option<tempfile::TempDir>,
- opts: Options,
- ) -> Result<Self> {
- let core = Arc::new(Core {
- _tempdir: tempdir,
- cfs: RwLock::new(AHashMap::new()),
- db,
- db_opts,
- });
-
- // Initialize all column families
- for name in &cfs {
- let interned_cf = InternedColumnFamily::new(&core.db, &core.db_opts, name)?;
- core.cfs.write().insert(name.clone(), interned_cf);
- }
-
- // Get the root column family
- let root_name = SmolStr::new_static(ROOT_CF);
- let handle = core.open_cf(&root_name, &core.db_opts)?;
-
- Ok(Self {
- inner: Arc::new(ColumnFamily {
- full_name: root_name,
- name_offset: 0,
- handle,
- children: RwLock::new(AHashMap::new()),
- core,
- opts,
- ro: Self::new_read_options(&opts),
- wo: Self::new_write_options(&opts),
- }),
- })
- }
-
- /// Opens a RocksDB instance at the given path with the given options.
- ///
- /// # Arguments
- ///
- /// * `path` - The path to the database, if `None` is provided, a temporary
- /// directory will be used.
- /// * `opts` - The options to use for the database.
- ///
- /// # Returns
- ///
- /// A new [`RocksDb`] instance.
- ///
- /// # Errors
- ///
- /// Returns an error if the database cannot be opened.
- pub fn with_opts(path: Option<&Path>, opts: Options) -> Result<Self> {
- let (db_opts, cfs) = Self::default_open_options(path);
- let mut temp_dir = None;
- let path = if let Some(path) = path {
- path
- } else {
- temp_dir = Some(tempfile::TempDir::with_prefix("tetra-")?);
- temp_dir.as_ref().unwrap().path()
- };
- let db = Db::open_cf_descriptors(
- &db_opts,
- path,
- cfs.iter()
- .map(|x| rocksdb::ColumnFamilyDescriptor::new(x.to_string(), db_opts.clone())),
- )?;
- Self::new(db, db_opts, cfs, temp_dir, opts)
- }
-
- /// Opens a RocksDB instance at the given path with the default options.
- ///
- /// # Arguments
- ///
- /// * `path` - The path to the database.
- ///
- /// # Returns
- ///
- /// A new [`RocksDb`] instance.
- ///
- /// # Errors
- ///
- /// Returns an error if the database cannot be opened.
- pub fn open(path: impl AsRef<Path>) -> Result<Self> {
- Self::with_opts(Some(path.as_ref()), Options::DEF)
- }
-
- /// Creates a temporary RocksDB instance.
- ///
- /// # Returns
- ///
- /// A new [`RocksDb`] instance backed by a temporary directory.
- ///
- /// # Errors
- ///
- /// Returns an error if the temporary database cannot be created.
- pub fn temp() -> Result<Self> {
- Self::with_opts(None, Options::DEF)
- }
-
- /// Returns a reference to the underlying RocksDB database.
- pub fn db(&self) -> &Db {
- &self.inner.core.db
- }
-
- /// Returns the shared default instance of the [`rocksdb::ReadOptions`]
- /// struct.
- pub fn read_options(&self) -> &rocksdb::ReadOptions {
- &self.inner.ro
- }
-
- /// Returns a new instance of the [`rocksdb::ReadOptions`] struct given the
- /// options set on the store.
- pub fn new_read_options(_no: &Options) -> rocksdb::ReadOptions {
- rocksdb::ReadOptions::default()
- }
-
- /// Returns the shared default instance of the [`rocksdb::WriteOptions`]
- /// struct.
- pub fn write_options(&self) -> &rocksdb::WriteOptions {
- &self.inner.wo
- }
-
- /// Returns a new instance of the [`rocksdb::WriteOptions`] struct given the
- /// options set on the store.
- pub fn new_write_options(no: &Options) -> rocksdb::WriteOptions {
- let mut opts = rocksdb::WriteOptions::default();
- opts.disable_wal(!no.wal);
- opts.set_sync(no.sync);
- opts
- }
-
- /// Returns a reference to the current column family handle.
- pub fn handle(&self) -> &impl rocksdb::AsColumnFamilyRef {
- self.inner.as_ref()
- }
-
- /// Opens a new child column family with the given name and options.
- ///
- /// # Arguments
- ///
- /// * `name` - The name of the child column family.
- /// * `opts` - The options to use for the child column family.
- /// * `fndb` - The function callback to mutate the db options.
- pub fn open_child(
- &self,
- name: &str,
- opts: Options,
- fndb: impl Fn(&rocksdb::Options) -> Cow<rocksdb::Options>,
- ) -> Result<Self> {
- let mut cfs = self.inner.children.upgradable_read();
- if let Some(inst) = cfs.get(name) {
- return Ok(Self {
- inner: inst.clone(),
- });
- }
-
- cfs.with_upgraded(|cfs| {
- use std::collections::hash_map::Entry;
- let core: &Core = &self.inner.core;
- let entry = cfs.entry(name.to_smolstr());
- let inst = match entry {
- Entry::Vacant(e) => {
- let full_name = self.inner.format_child_name(name);
- let db_opts = fndb(&core.db_opts);
- let handle = core.open_cf(&full_name, db_opts.as_ref())?;
- e.insert(Arc::new(ColumnFamily {
- name_offset: full_name.len() - name.len(),
- full_name,
- handle,
- children: RwLock::new(AHashMap::new()),
- core: self.inner.core.clone(),
- opts,
- ro: Self::new_read_options(&opts),
- wo: Self::new_write_options(&opts),
- }))
- .clone()
- },
- Entry::Occupied(e) => e.get().clone(),
- };
- Ok(Self { inner: inst })
- })
- }
-}
-
-fn to_iter<R>(mode: IterMode, iterator_cf: impl Fn(rocksdb::IteratorMode) -> R) -> R {
- iterator_cf(match mode {
- IterMode::Forward(Some(&[])) | IterMode::Forward(None) => rocksdb::IteratorMode::Start,
- IterMode::Reverse(None) => rocksdb::IteratorMode::End,
- IterMode::Forward(Some(seek)) => {
- rocksdb::IteratorMode::From(seek, rocksdb::Direction::Forward)
- },
- IterMode::Reverse(Some(seek)) => {
- rocksdb::IteratorMode::From(seek, rocksdb::Direction::Reverse)
- },
- })
-}
-
-#[derive(derive_more::Debug)]
-pub struct Snapshot<'a> {
- #[debug("<rocksdb::Snapshot>")]
- snapshot: rocksdb::SnapshotWithThreadMode<'a, Db>,
- db: &'a RocksDb,
- #[debug(skip)]
- ro: rocksdb::ReadOptions,
-}
-
-impl<'a> IVecRef for rocksdb::DBPinnableSlice<'a> {
- fn into_bytes(self) -> bytes::Bytes {
- bytes::Bytes::from_owner(self.to_vec())
- }
- fn as_slice(&self) -> &[u8] {
- self.as_ref()
- }
- fn into_vec(self) -> Vec<u8> {
- self.to_vec()
- }
-}
-
-#[derive(derive_more::From, derive_more::Debug)]
-pub struct Iter<'a> {
- #[debug("<rocksdb::DBIterator>")]
- iter: rocksdb::DBIteratorWithThreadMode<'a, Db>,
-}
-
-impl<'a> Iterator for Iter<'a> {
- type Item = Result<(Bytes, Bytes)>;
- fn next(&mut self) -> Option<Self::Item> {
- self.iter.by_ref().next().map(|r| -> Result<_> {
- let (k, v) = r?;
- Ok((Bytes::from_owner(k), Bytes::from_owner(v)))
- })
- }
-}
-
-impl<'a> traits::View for Snapshot<'a> {
- type Iter<'b>
- = Iter<'b>
- where
- Self: 'b;
-
- fn get(&self, key: &[u8]) -> Result<Option<IVec<'_>>> {
- Ok(self
- .db
- .db()
- .get_pinned_cf_opt(self.db.handle(), key, &self.ro)?
- .map(IVec::new))
- }
-
- fn contains(&self, key: &[u8]) -> Result<bool> {
- let db = self.db.db();
- if db.key_may_exist_cf_opt(self.db.handle(), key, &self.ro) {
- Ok(db
- .get_pinned_cf_opt(self.db.handle(), key, &self.ro)?
- .is_some())
- } else {
- Ok(false)
- }
- }
-
- fn get_many(&self, keys: impl IntoIterator<Item: AsRef<[u8]>>) -> Result<IVecs<'_>> {
- // TODO: Remove the collect, currently needed to AsRef<[u8]> -> &'a AsRef<[u8]>
- let vec = keys.into_iter().collect::<Vec<_>>();
- Ok(self
- .db
- .db()
- .batched_multi_get_cf(self.db.handle(), &vec, false)
- .into())
- }
-
- fn iter(&self, mode: IterMode) -> Self::Iter<'_> {
- Iter::from(to_iter(mode, |mode| {
- self.snapshot.iterator_cf(self.db.handle(), mode)
- }))
- }
-}
-
-#[derive(derive_more::Debug)]
-pub struct WriteBatch<'a> {
- #[debug("<rocksdb::WriteBatch>")]
- inner: RefCell<rocksdb::WriteBatch>,
- parent: &'a RocksDb,
-}
-
-impl<'a> traits::Write for WriteBatch<'a> {
- type Result = ();
- fn put(&self, key: &[u8], value: &[u8]) -> Self::Result {
- self
- .inner
- .borrow_mut()
- .put_cf(self.parent.handle(), key, value);
- }
-
- fn delete(&self, key: &[u8]) -> Self::Result {
- self.inner.borrow_mut().delete_cf(self.parent.handle(), key);
- }
-
- fn delete_range(&self, from: &[u8], to: &[u8]) -> Self::Result {
- self
- .inner
- .borrow_mut()
- .delete_range_cf(self.parent.handle(), from, to);
- }
- fn update(&self, key: &[u8], op: &dyn Operator) -> Self::Result {
- quickbuf::with(|buf| {
- let rng = op.write_to(buf);
- self
- .inner
- .borrow_mut()
- .merge_cf(self.parent.handle(), key, &buf[rng]);
- });
- }
-}
-
-impl<'a> traits::WriteBatch<'a> for WriteBatch<'a> {
- fn commit(&self) -> Result<()> {
- Ok(self
- .parent
- .db()
- .write_opt(self.inner.take(), self.parent.write_options())?)
- }
-
- fn reset(&mut self) {
- self.inner.get_mut().clear();
- }
-}
-
-impl traits::View for RocksDb {
- type Iter<'a>
- = Iter<'a>
- where
- Self: 'a;
-
- fn get(&self, key: &[u8]) -> Result<Option<IVec<'_>>> {
- Ok(self
- .db()
- .get_pinned_cf_opt(self.handle(), key, self.read_options())?
- .map(IVec::new))
- }
-
- fn contains(&self, key: &[u8]) -> Result<bool> {
- let db = self.db();
- if db.key_may_exist_cf_opt(self.handle(), key, self.read_options()) {
- Ok(db
- .get_pinned_cf_opt(self.handle(), key, self.read_options())?
- .is_some())
- } else {
- Ok(false)
- }
- }
-
- fn get_many(&self, keys: impl IntoIterator<Item: AsRef<[u8]>>) -> Result<IVecs<'_>> {
- // TODO: Remove the collect, currently needed to AsRef<[u8]> -> &'a AsRef<[u8]>
- let vec = keys.into_iter().collect::<Vec<_>>();
- Ok(self
- .db()
- .batched_multi_get_cf(self.handle(), &vec, false)
- .into())
- }
-
- fn iter(&self, mode: IterMode) -> Self::Iter<'_> {
- Iter::from(to_iter(mode, |mode| {
- self.db().iterator_cf(self.handle(), mode)
- }))
- }
-}
-
-impl traits::Write for RocksDb {
- type Result = Result<()>;
-
- fn update(&self, key: &[u8], op: &dyn Operator) -> Self::Result {
- quickbuf::with(|buf| {
- let rng = op.write_to(buf);
- let op = &buf[rng];
- Ok(self
- .db()
- .merge_cf_opt(self.handle(), key, op, self.write_options())?)
- })
- }
-
- fn put(&self, key: &[u8], value: &[u8]) -> Self::Result {
- Ok(self
- .db()
- .put_cf_opt(self.handle(), key, value, self.write_options())?)
- }
-
- fn delete(&self, key: &[u8]) -> Self::Result {
- Ok(self
- .db()
- .delete_cf_opt(self.handle(), key, self.write_options())?)
- }
-
- fn delete_range(&self, from: &[u8], to: &[u8]) -> Self::Result {
- Ok(self
- .db()
- .delete_range_cf_opt(self.handle(), from, to, self.write_options())?)
- }
-}
-
-impl traits::Store for RocksDb {
- type WriteBatch<'a> = WriteBatch<'a>;
- type Snapshot<'a> = Snapshot<'a>;
-
- fn batch(&self) -> Self::WriteBatch<'_> {
- WriteBatch {
- inner: Default::default(),
- parent: self,
- }
- }
-
- fn options(&self) -> &Options {
- &self.inner.opts
- }
-
- fn namespace(&self) -> &str {
- &self.inner.full_name[self.inner.name_offset..]
- }
-
- fn flush(&self) -> Result<()> {
- Ok(self.db().flush_cf(self.handle())?)
- }
-
- fn clear(&self) -> Result<()> {
- let max_key = self
- .db()
- .iterator_cf(self.handle(), rocksdb::IteratorMode::End)
- .next()
- .map(|x| x.map(|(k, _)| k));
-
- if let Some(Ok(max_key)) = max_key {
- let empty: &[u8] = &[];
- self
- .db()
- .delete_range_cf_opt(self.handle(), empty, &max_key, self.write_options())?;
- self
- .db()
- .delete_cf_opt(self.handle(), &max_key, self.write_options())?;
- }
-
- Ok(())
- }
-
- fn child(&self, name: &str, opts: Options) -> Result<Self> {
- self.open_child(name, opts, |x| Cow::Borrowed(x))
- }
-
- fn children(&self) -> Vec<SmolStr> {
- self.inner.children.read().keys().cloned().collect()
- }
-
- fn snapshot<'b>(&'b self) -> Self::Snapshot<'b> {
- let snap = self.db().snapshot();
- let mut ro = Self::new_read_options(&self.inner.opts);
- ro.set_snapshot(&snap);
- Snapshot {
- snapshot: snap,
- db: self,
- ro,
- }
- }
-
- fn estimate_size(&self) -> Option<u64> {
- self
- .db()
- .property_int_value_cf(self.handle(), rocksdb::properties::TOTAL_SST_FILES_SIZE)
- .ok()
- .flatten()
- }
-}
diff --git a/attic/kv/src/tests.rs b/attic/kv/src/tests.rs
deleted file mode 100644
index 54ad3f33c..000000000
@@ -1,603 +0,0 @@
-use ahash::AHashMap;
-
-use crate::{
- TypedStore, TypedView, TypedWriteBatch, Write,
- error::Result,
- ops,
- traits::{IterMode, Options, Store, View, WriteBatch},
- typed::ser::IntoTypedStore,
-};
-
-/// Test basic operations of a key-value store implementation
-fn test_basic_kv_ops<S: Store>(store: &S) -> Result<()> {
- // Test set and get
- store.put(b"key1", b"value1")?;
- let val = store.get(b"key1")?;
- assert_eq!(val.as_deref(), Some(b"value1".as_ref()));
-
- // Test contains
- assert!(store.contains(b"key1")?);
- assert!(!store.contains(b"nonexistent")?);
-
- // Test delete
- store.delete(b"key1")?;
- assert!(!store.contains(b"key1")?);
-
- // Test batch operations
- let batch = store.batch();
- batch.put(b"batch1", b"batch_value1");
- batch.put(b"batch2", b"batch_value2");
- batch.commit()?;
-
- assert_eq!(
- store.get(b"batch1")?.as_deref(),
- Some(b"batch_value1".as_ref())
- );
- assert_eq!(
- store.get(b"batch2")?.as_deref(),
- Some(b"batch_value2".as_ref())
- );
-
- // Test iteration
- let mut iter_pairs = Vec::new();
- for result in store.iter(IterMode::forward()) {
- let (k, v) = result?;
- iter_pairs.push((k.to_vec(), v.to_vec()));
- }
-
- assert_eq!(iter_pairs.len(), 2);
- assert!(iter_pairs.contains(&(b"batch1".to_vec(), b"batch_value1".to_vec())));
- assert!(iter_pairs.contains(&(b"batch2".to_vec(), b"batch_value2".to_vec())));
-
- // Test backward iteration
- let mut backward_pairs = Vec::new();
- for result in store.iter(IterMode::reverse()) {
- let (k, v) = result?;
- backward_pairs.push((k.to_vec(), v.to_vec()));
- }
-
- assert_eq!(backward_pairs.len(), 2);
- // Should be in reverse lexicographical order
- assert_eq!(backward_pairs[0].0, b"batch2");
- assert_eq!(backward_pairs[1].0, b"batch1");
-
- // Test get_many
- let values = store.get_many([
- b"batch1".as_ref(),
- b"batch2".as_ref(),
- b"nonexistent".as_ref(),
- ])?;
- assert_eq!(values.len(), 3);
- assert_eq!(values.get(0)?, Some(b"batch_value1".as_ref()));
- assert_eq!(values.get(1)?, Some(b"batch_value2".as_ref()));
- assert_eq!(values.get(2)?, None);
-
- // Test seeking
- store.put(b"seek1", b"seek_value1")?;
- store.put(b"seek2", b"seek_value2")?;
-
- let mut seek_pairs = Vec::new();
- for result in store.iter(IterMode::lower_bound(b"seek")) {
- let (k, v) = result?;
- seek_pairs.push((k.to_vec(), v.to_vec()));
- }
-
- assert_eq!(seek_pairs.len(), 2);
- assert!(seek_pairs.contains(&(b"seek1".to_vec(), b"seek_value1".to_vec())));
- assert!(seek_pairs.contains(&(b"seek2".to_vec(), b"seek_value2".to_vec())));
-
- Ok(())
-}
-
-/// Test snapshot functionality
-fn test_snapshot<S: Store>(store: &S) -> Result<()> {
- // Setup initial data
- store.put(b"snap1", b"snap_value1")?;
- store.put(b"snap2", b"snap_value2")?;
-
- // Create snapshot
- let snapshot = store.snapshot();
-
- // Modify data after snapshot
- store.put(b"snap1", b"modified_value1")?;
- store.put(b"snap3", b"snap_value3")?;
-
- // Verify snapshot has original values
- assert_eq!(
- snapshot.get(b"snap1")?.as_deref(),
- Some(b"snap_value1".as_ref())
- );
- assert_eq!(
- snapshot.get(b"snap2")?.as_deref(),
- Some(b"snap_value2".as_ref())
- );
- assert_eq!(snapshot.get(b"snap3")?.as_deref(), None);
-
- // Verify store has new values
- assert_eq!(
- store.get(b"snap1")?.as_deref(),
- Some(b"modified_value1".as_ref())
- );
- assert_eq!(
- store.get(b"snap3")?.as_deref(),
- Some(b"snap_value3".as_ref())
- );
-
- Ok(())
-}
-
-/// Test prefix scanning functionality
-fn test_prefix_scan<S: Store>(store: &S) -> Result<()> {
- // Clear and setup test data with specific prefixes
- let batch = store.batch();
- batch.put(b"prefix1_a", b"value1a");
- batch.put(b"prefix1_b", b"value1b");
- batch.put(b"prefix2_a", b"value2a");
- batch.put(b"prefix2_b", b"value2b");
- batch.commit()?;
-
- // Scan with prefix1
- let mut prefix1_results = Vec::new();
- for result in store.prefix_scan(b"prefix1_", IterMode::forward()) {
- let (k, v) = result?;
- prefix1_results.push((k.to_vec(), v.to_vec()));
- }
-
- assert_eq!(prefix1_results.len(), 2);
- println!("prefix1_results: {prefix1_results:?}");
- assert!(prefix1_results.contains(&(b"prefix1_a".to_vec(), b"value1a".to_vec())));
- assert!(prefix1_results.contains(&(b"prefix1_b".to_vec(), b"value1b".to_vec())));
-
- // Scan with prefix2
- let mut prefix2_results = Vec::new();
- for result in store.prefix_scan(b"prefix2_", IterMode::forward()) {
- let (k, v) = result?;
- prefix2_results.push((k.to_vec(), v.to_vec()));
- }
-
- assert_eq!(prefix2_results.len(), 2);
- assert!(prefix2_results.contains(&(b"prefix2_a".to_vec(), b"value2a".to_vec())));
- assert!(prefix2_results.contains(&(b"prefix2_b".to_vec(), b"value2b".to_vec())));
-
- // Backward scan
- let mut backward_results = Vec::new();
- for result in store.prefix_scan(b"prefix1_", IterMode::reverse()) {
- let (k, v) = result?;
- backward_results.push((k.to_vec(), v.to_vec()));
- }
-
- assert_eq!(backward_results.len(), 2);
- assert_eq!(backward_results[0].0, b"prefix1_b");
- assert_eq!(backward_results[1].0, b"prefix1_a");
-
- Ok(())
-}
-
-/// Test child namespace functionality
-fn test_child_namespaces<S: Store>(store: &S) -> Result<()> {
- // Set value in parent namespace
- store.put(b"parent_key", b"parent_value")?;
-
- // Create child namespace
- let child1 = store.child("child1", Options::DEF)?;
-
- // Set values in child namespace
- child1.put(b"child1_key", b"child1_value")?;
-
- // Create another child
- let child2 = store.child("child2", Options::DEF)?;
- child2.put(b"child2_key", b"child2_value")?;
-
- // Values in each namespace should be isolated
- assert_eq!(
- store.get(b"parent_key")?.as_deref(),
- Some(b"parent_value".as_ref())
- );
- assert_eq!(
- child1.get(b"child1_key")?.as_deref(),
- Some(b"child1_value".as_ref())
- );
- assert_eq!(
- child2.get(b"child2_key")?.as_deref(),
- Some(b"child2_value".as_ref())
- );
-
- // Child shouldn't see parent's keys
- assert_eq!(child1.get(b"parent_key")?.as_deref(), None);
-
- // Parent shouldn't see child's keys
- assert_eq!(store.get(b"child1_key")?.as_deref(), None);
-
- // Test clearing child namespace
- child1.clear()?;
- assert_eq!(child1.get(b"child1_key")?.as_deref(), None);
-
- // But other namespaces should be unaffected
- assert_eq!(
- store.get(b"parent_key")?.as_deref(),
- Some(b"parent_value".as_ref())
- );
- assert_eq!(
- child2.get(b"child2_key")?.as_deref(),
- Some(b"child2_value".as_ref())
- );
-
- // Test listing children
- let children = store.children();
- assert_eq!(children.len(), 2);
- assert!(children.contains(&"child1".into()));
- assert!(children.contains(&"child2".into()));
-
- Ok(())
-}
-
-/// Test typed key-value store functionality
-fn test_typed_kv<S: Store + Clone>(store: S) -> Result<()> {
- // Create typed store for storing strings
- let typed_store = store.clone().into_typed::<str, str>();
-
- // Test basic operations
- typed_store.put("typed_key1", "typed_value1")?;
- typed_store.put("typed_key2", "typed_value2")?;
-
- assert_eq!(
- typed_store.get("typed_key1")?,
- Some("typed_value1".to_string())
- );
- assert_eq!(
- typed_store.get("typed_key2")?,
- Some("typed_value2".to_string())
- );
-
- // Test batch operations
- let mut batch = typed_store.batch();
- batch.put("batch_key1", "batch_value1");
- batch.put("batch_key2", "batch_value2");
- batch.write()?;
-
- assert_eq!(
- typed_store.get("batch_key1")?,
- Some("batch_value1".to_string())
- );
-
- // Test get_many
- let values = typed_store.get_many(&["typed_key1", "typed_key2", "nonexistent"])?;
- assert_eq!(values.len(), 3);
- assert_eq!(values.get(0)?, Some("typed_value1".to_string()));
- assert_eq!(values.get(1)?, Some("typed_value2".to_string()));
- assert_eq!(values.get(2)?, None);
-
- // Test iteration
- let mut iter_pairs = Vec::new();
- for result in typed_store.iter(IterMode::forward()) {
- let (k, v) = result?;
- iter_pairs.push((k, v));
- }
-
- assert!(iter_pairs.contains(&("typed_key1".to_string(), "typed_value1".to_string())));
- assert!(iter_pairs.contains(&("typed_key2".to_string(), "typed_value2".to_string())));
-
- // Test with complex types
- let complex_store = store.into_typed::<u32, AHashMap<String, Vec<i32>>>();
-
- let mut map1 = AHashMap::new();
- map1.insert("nums".to_string(), vec![1, 2, 3]);
-
- let mut map2 = AHashMap::new();
- map2.insert("chars".to_string(), vec![65, 66, 67]);
-
- complex_store.put(&100, &map1)?;
- complex_store.put(&200, &map2)?;
-
- assert_eq!(complex_store.get(&100)?, Some(map1));
- assert_eq!(complex_store.get(&200)?, Some(map2));
-
- Ok(())
-}
-
-/// Test iterator seek edge cases to ensure correct inclusiveness
-///
-/// Both forward and backward iteration should include the seek key.
-fn test_iterator_one_offs<S: Store>(store: &S) -> Result<()> {
- // Insert ordered keys
- store.put(b"alpha", b"val_alpha")?;
- store.put(b"beta", b"val_beta")?;
- store.put(b"gamma", b"val_gamma")?;
-
- // ------------------------------------------------------------
- // Backward iteration starting AT the seek key must *include* the key
- // ------------------------------------------------------------
- let mut bw_iter = store.iter(IterMode::upper_bound(b"beta"));
-
- // The first element yielded must be the seek key itself
- let (first_k, _first_v) = bw_iter
- .next()
- .expect("iterator should yield at least one element")?;
- assert_eq!(first_k.as_ref(), b"beta");
-
- // The second element should be the previous key
- let (second_k, _second_v) = bw_iter
- .next()
- .expect("iterator should yield at least two elements")?;
- assert_eq!(second_k.as_ref(), b"alpha");
-
- // ------------------------------------------------------------
- // Forward iteration starting AT the seek key must *include* the key
- // ------------------------------------------------------------
- let mut fw_iter = store.iter(IterMode::lower_bound(b"beta"));
- let (first_k_fw, _first_v_fw) = fw_iter
- .next()
- .expect("iterator should yield at least one element")?;
- assert_eq!(first_k_fw.as_ref(), b"beta");
-
- // The second element should be the next key
- let (second_k_fw, _second_v_fw) = fw_iter
- .next()
- .expect("iterator should yield at least two elements")?;
- assert_eq!(second_k_fw.as_ref(), b"gamma");
-
- Ok(())
-}
-
-/// Test read-modify-write operator functionality (Append and CompareExchange)
-fn test_rmw<S: Store>(store: &S) -> Result<()> {
- // ----- Append semantics -----
- // Appending to a nonexistent key should create it.
- store.update(b"append_key", &ops::Append(b"abc"))?;
- assert_eq!(store.get(b"append_key")?.as_deref(), Some(b"abc".as_ref()));
-
- // Subsequent appends should concatenate at the end.
- store.update(b"append_key", &ops::Append(b"def"))?;
- assert_eq!(
- store.get(b"append_key")?.as_deref(),
- Some(b"abcdef".as_ref())
- );
-
- // ----- CompareExchange semantics -----
- // Successful CAS should update the value.
- store.put(b"cas_key", b"old")?;
- store.update(b"cas_key", &ops::CompareExchange {
- expected: Some(b"old"),
- desired: b"new",
- })?;
- assert_eq!(store.get(b"cas_key")?.as_deref(), Some(b"new".as_ref()));
-
- // Failed CAS (mismatched expected) should leave the value unchanged.
- store.update(b"cas_key", &ops::CompareExchange {
- expected: Some(b"old"),
- desired: b"should_not_apply",
- })?;
- assert_eq!(store.get(b"cas_key")?.as_deref(), Some(b"new".as_ref()));
-
- // CAS from None to Some should succeed when key does not exist.
- store.update(b"new_key", &ops::CompareExchange {
- expected: None,
- desired: b"value",
- })?;
- assert_eq!(store.get(b"new_key")?.as_deref(), Some(b"value".as_ref()));
-
- // CAS expecting Some should fail when key is absent.
- store.update(b"absent_key", &ops::CompareExchange {
- expected: Some(b"whatever"),
- desired: b"value",
- })?;
- assert_eq!(
- store.get(b"absent_key")?.as_deref().unwrap_or_default(),
- b""
- );
-
- // ----- Increment semantics -----
- // Test basic increment operation
- store.put(b"counter", &0u64.to_le_bytes())?;
- store.update(b"counter", &ops::Inc(1))?;
- assert_eq!(
- u64::from_le_bytes(store.get(b"counter")?.unwrap().as_ref().try_into().unwrap()),
- 1
- );
-
- // Test multiple increments
- store.update(b"counter", &ops::Inc(5))?;
- assert_eq!(
- u64::from_le_bytes(store.get(b"counter")?.unwrap().as_ref().try_into().unwrap()),
- 6
- );
-
- // Test increment on non-existent key (should create and initialize to the
- // increment value)
- store.update(b"new_counter", &ops::Inc(10))?;
- assert_eq!(
- u64::from_le_bytes(
- store
- .get(b"new_counter")?
- .unwrap()
- .as_ref()
- .try_into()
- .unwrap()
- ),
- 10
- );
-
- // Test overflow behavior
- store.put(b"max_counter", &u64::MAX.to_le_bytes())?;
- store.update(b"max_counter", &ops::Inc(1))?;
- let result = store.get(b"max_counter")?.unwrap();
- assert_eq!(result.len(), 16); // Should now be 16 bytes (two u64s)
- assert_eq!(u64::from_le_bytes(result[0..8].try_into().unwrap()), 0);
- assert_eq!(u64::from_le_bytes(result[8..16].try_into().unwrap()), 1);
-
- // ----- Decrement semantics -----
- // Test basic decrement operation
- store.put(b"counter", &10u64.to_le_bytes())?;
- store.update(b"counter", &ops::SaturatingDec(1))?;
- assert_eq!(
- u64::from_le_bytes(store.get(b"counter")?.unwrap().as_ref().try_into().unwrap()),
- 9
- );
-
- // Test decrement on non-existent key (should create and initialize to 0)
- store.update(b"new_counter", &ops::SaturatingDec(10))?;
- assert_eq!(
- u64::from_le_bytes(
- store
- .get(b"new_counter")?
- .unwrap()
- .as_ref()
- .try_into()
- .unwrap()
- ),
- 0
- );
-
- // Test decrement saturation
- store.put(b"saturate_counter", &1u64.to_le_bytes())?;
- store.update(b"saturate_counter", &ops::SaturatingDec(2))?;
- assert_eq!(
- u64::from_le_bytes(
- store
- .get(b"saturate_counter")?
- .unwrap()
- .as_ref()
- .try_into()
- .unwrap()
- ),
- 0
- );
-
- Ok(())
-}
-
-/// Test delete_range functionality
-fn test_delete_range<S: Store>(store: &S) -> Result<()> {
- // Setup test data
- store.put(b"a", b"value_a")?;
- store.put(b"b", b"value_b")?;
- store.put(b"c", b"value_c")?;
- store.put(b"d", b"value_d")?;
- store.put(b"e", b"value_e")?;
-
- // Delete range [b, d) - should delete b and c but not a, d, or e
- store.delete_range(b"b", b"d")?;
-
- // Verify results
- assert_eq!(store.get(b"a")?.as_deref(), Some(b"value_a".as_ref()));
- assert_eq!(store.get(b"b")?.as_deref(), None);
- assert_eq!(store.get(b"c")?.as_deref(), None);
- assert_eq!(store.get(b"d")?.as_deref(), Some(b"value_d".as_ref()));
- assert_eq!(store.get(b"e")?.as_deref(), Some(b"value_e".as_ref()));
-
- // Test with batch
- let batch = store.batch();
- batch.put(b"f", b"value_f");
- batch.put(b"g", b"value_g");
- batch.put(b"h", b"value_h");
- batch.commit()?;
-
- // Delete range using batch
- let batch = store.batch();
- batch.delete_range(b"f", b"h");
- batch.commit()?;
-
- assert_eq!(store.get(b"f")?.as_deref(), None);
- assert_eq!(store.get(b"g")?.as_deref(), None);
- assert_eq!(store.get(b"h")?.as_deref(), Some(b"value_h".as_ref()));
-
- // Test edge cases
- // Empty range
- store.delete_range(b"z", b"z")?;
-
- // Range with no keys
- store.delete_range(b"y", b"z")?;
-
- Ok(())
-}
-
-macro_rules! run_test_suite_over {
- ($ctor:expr) => {
- #[test]
- fn test_db_basic_ops() {
- let db = $ctor;
- super::test_basic_kv_ops(&db).expect("basic ops failed");
- }
-
- #[test]
- fn test_db_snapshot() {
- let db = $ctor;
- super::test_snapshot(&db).expect("snapshot failed");
- }
-
- #[test]
- fn test_db_prefix_scan() {
- let db = $ctor;
- super::test_prefix_scan(&db).expect("prefix scan failed");
- }
-
- #[test]
- fn test_db_child_namespaces() {
- let db = $ctor;
- super::test_child_namespaces(&db).expect("child namespaces failed");
- }
-
- #[test]
- fn test_db_typed_kv() {
- let db = $ctor;
- super::test_typed_kv(db).expect("typed kv failed");
- }
-
- #[test]
- fn test_db_iterator_one_offs() {
- let db = $ctor;
- super::test_iterator_one_offs(&db).expect("iterator one offs failed");
- }
-
- #[test]
- fn test_db_rmw() {
- let db = $ctor;
- super::test_rmw(&db).expect("rmw failed");
- }
-
- #[test]
- fn test_db_delete_range() {
- let db = $ctor;
- super::test_delete_range(&db).expect("delete range failed");
- }
- };
-}
-
-#[cfg(test)]
-#[cfg(feature = "rocksdb")]
-mod rocksdb_tests {
- run_test_suite_over! {
- crate::rocksdb::RocksDb::temp().expect("failed to create rocksdb")
- }
-}
-
-#[cfg(test)]
-mod memorydb_tests {
- run_test_suite_over! {
- crate::memorydb::MemoryDb::new()
- }
-}
-
-#[cfg(test)]
-mod erased_tests {
- run_test_suite_over! {
- crate::erased::AnyDb::new(crate::memorydb::MemoryDb::new())
- }
-}
-
-#[cfg(test)]
-mod prefix_tests {
- use crate::prefixed::PrefixExt;
-
- run_test_suite_over! {
- crate::memorydb::MemoryDb::new().with_prefix(b"pfx_")
- }
-}
-
-#[cfg(feature = "heed")]
-#[cfg(test)]
-mod heed_tests {
- run_test_suite_over! {
- crate::lmdb::Lmdb::temp().expect("failed to create heed db")
- }
-}
diff --git a/attic/kv/src/traits.rs b/attic/kv/src/traits.rs
deleted file mode 100644
index e88f90092..000000000
@@ -1,292 +0,0 @@
-use std::fmt;
-
-use bon::Builder;
-use bytes::Bytes;
-use smol_str::SmolStr;
-use tetra_core::alloc::quickbuf::{self, FmtTuple};
-
-use super::error::Result;
-use crate::{pfx_iter::PrefixIter, IVec, IVecs, Operator};
-
-#[derive(Copy, Clone, Debug, Builder)]
-pub struct Options {
- /// Whether WAL is used for this namespace (if disabled, writes may not
- /// persist across crashes)
- #[builder(default = Options::DEF.wal)]
- pub wal: bool,
-
- /// Whether to sync writes to the database (unlesss enabled, writes may be
- /// lost when the system crashes [not the process!])
- #[builder(default = Options::DEF.sync)]
- pub sync: bool,
-}
-
-impl Default for Options {
- fn default() -> Self {
- Self::DEF
- }
-}
-
-impl Options {
- pub const DEF: Self = Self {
- wal: false,
- sync: false,
- };
-}
-
-/// Direction for database iteration.
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub enum IterMode<'a> {
- /// Iterate in forward direction (ascending keys), starting from the given
- /// key.
- Forward(Option<&'a [u8]>),
- /// Iterate in backward direction (descending keys), starting from the given
- /// key.
- Reverse(Option<&'a [u8]>),
-}
-
-impl<'a> Default for IterMode<'a> {
- fn default() -> Self {
- Self::Forward(None)
- }
-}
-
-impl<'a> IterMode<'a> {
- /// Returns an iterator mode that iterates forward from the beginning
- pub fn forward() -> Self {
- Self::Forward(None)
- }
-
- /// Returns an iterator mode that iterates backward from the end
- pub fn reverse() -> Self {
- Self::Reverse(None)
- }
-
- /// Returns an iterator mode that iterates forward starting at >= key
- pub fn lower_bound(key: &'a [u8]) -> Self {
- Self::Forward(Some(key))
- }
-
- /// Returns an iterator mode that iterates backward starting at < key
- pub fn upper_bound(key: &'a [u8]) -> Self {
- Self::Reverse(Some(key))
- }
-
- /// Replaces the inner bound into another
- pub fn replace_bound<'b>(self, other: Option<&'b [u8]>) -> IterMode<'b> {
- match self {
- Self::Forward(_key) => IterMode::Forward(other),
- Self::Reverse(_key) => IterMode::Reverse(other),
- }
- }
-
- /// Gets the inner bound
- pub fn inner(&self) -> Option<&'a [u8]> {
- match self {
- Self::Forward(key) => *key,
- Self::Reverse(key) => *key,
- }
- }
-
- /// Returns true if the iterator mode is reverse
- pub fn is_reverse(&self) -> bool {
- matches!(self, Self::Reverse(_))
- }
-
- /// Returns the initial position for the iterator, if any
- pub fn at(&self) -> Option<&'a [u8]> {
- match self {
- Self::Forward(key) => *key,
- Self::Reverse(key) => *key,
- }
- }
-}
-
-pub trait Write {
- type Result;
-
- /// Sets the value for the given key
- fn put(&self, key: &[u8], value: &[u8]) -> Self::Result;
-
- /// Deletes the value for the given key
- fn delete(&self, key: &[u8]) -> Self::Result;
-
- /// Applies the given atomic merge operation to the key
- fn update(&self, key: &[u8], op: &dyn Operator) -> Self::Result;
-
- /// Deletes all keys in the range [from, to)
- /// # Arguments
- /// * `from` - The inclusive start of the range
- /// * `to` - The exclusive end of the range
- ///
- /// # Implementation Note
- /// Each implementation must provide its own delete_range logic. For
- /// databases without native range deletion support, implementations
- /// should iterate through the range and delete keys individually.
- fn delete_range(&self, from: &[u8], to: &[u8]) -> Self::Result;
-}
-
-pub trait Writef: Write {
- /// Sets the value for the given key using a displayable key
- fn putf(&self, key: impl fmt::Display, value: &[u8]) -> Self::Result {
- with_fmtbuf((&key,), |[key]| self.put(key, value))
- }
-
- /// Deletes the value for the given key using a displayable key
- fn deletef(&self, key: impl fmt::Display) -> Self::Result {
- with_fmtbuf((&key,), |[key]| self.delete(key))
- }
-
- /// Applies the given atomic merge operation to the key using a displayable
- /// key
- fn updatef(&self, key: impl fmt::Display, op: &dyn Operator) -> Self::Result {
- with_fmtbuf((&key,), |[key]| self.update(key, op))
- }
-
- /// Deletes all keys in the range [from, to) using displayable keys
- fn delete_rangef(&self, from: impl fmt::Display, to: impl fmt::Display) -> Self::Result {
- with_fmtbuf((&from, &to), |[from, to]| self.delete_range(from, to))
- }
-}
-impl<T: Write> Writef for T {}
-
-/// Trait for write batches that can be used to perform multiple operations
-/// atomically
-pub trait WriteBatch<'a>: Write<Result = ()> + Send + fmt::Debug + 'a {
- /// Writes all batched operations to the database
- fn commit(&self) -> Result<()>;
-
- /// Clear the operations in this batch
- fn reset(&mut self);
-}
-
-/// Trait for mutable key-value store operations
-pub trait Store:
- Write<Result = Result<()>> + View + Clone + Send + Sync + fmt::Debug + 'static
-{
- /// The type of write batch used by this key-value store
- type WriteBatch<'a>: WriteBatch<'a>;
-
- /// The type of snapshot used by this key-value store
- type Snapshot<'a>: View + 'a;
- /// Creates a new write batch for this key-value store
- fn batch(&self) -> Self::WriteBatch<'_>;
-
- /// Returns the namespace of this key-value store
- fn namespace(&self) -> &str;
-
- /// Creates a child key-value store with the given name
- fn child(&self, name: &str, opts: Options) -> Result<Self>;
-
- /// Returns an iterator over each child key-value store
- fn children(&self) -> Vec<SmolStr>;
-
- /// Clears all data in this key-value store
- fn clear(&self) -> Result<()>;
-
- /// Flushes all data in this key-value store to the underlying storage
- fn flush(&self) -> Result<()>;
-
- /// Creates a snapshot of the key-value store at the current point in time
- fn snapshot<'b>(&'b self) -> Self::Snapshot<'b>;
-
- /// Returns the options for this key-value store
- fn options(&self) -> &Options;
-
- /// Estimate the size of the key-value store
- fn estimate_size(&self) -> Option<u64> {
- None
- }
-}
-
-/// Trait for read-only key-value store operations
-pub trait View: fmt::Debug {
- /// The type of iterator returned by this key-value store
- type Iter<'a>: Iterator<Item = Result<(Bytes, Bytes)>> + 'a
- where
- Self: 'a;
-
- /// Gets the value for the given key
- fn get(&self, key: &[u8]) -> Result<Option<IVec<'_>>>;
-
- /// Gets the value for the given key using a displayable key
- fn getf(&self, key: impl fmt::Display) -> Result<Option<IVec<'_>>> {
- with_fmtbuf((&key,), |[key]| self.get(key))
- }
-
- /// Gets the values for multiple keys at once
- fn get_many(&self, keys: impl IntoIterator<Item: AsRef<[u8]>>) -> Result<IVecs<'_>>;
-
- /// Returns true if the key exists in the key-value store
- fn contains(&self, key: &[u8]) -> Result<bool>;
-
- /// Returns true if the key exists in the key-value store using a displayable
- /// key
- fn containsf(&self, key: impl fmt::Display) -> Result<bool> {
- with_fmtbuf((&key,), |[key]| self.contains(key))
- }
-
- /// Iterates over key-value pairs in the key-value store
- fn iter<'a>(&'a self, mode: IterMode<'_>) -> Self::Iter<'a>;
-
- /// Scans over a prefix
- fn scan<'a>(
- &'a self,
- prefix: &[u8],
- ) -> impl Iterator<Item = Result<(Bytes, Bytes)>> + use<'a, Self> {
- self.prefix_scan(prefix, IterMode::forward())
- }
-
- /// Scans over a prefix using a displayable key
- fn scanf<'a>(
- &'a self,
- prefix: &dyn fmt::Display,
- ) -> impl Iterator<Item = Result<(Bytes, Bytes)>> + use<'a, Self> {
- with_fmtbuf((&prefix,), |[prefix]| {
- self.prefix_scan(prefix, IterMode::forward())
- })
- }
-
- /// Scans all keys with the given prefix.
- ///
- /// This is a convenience method that uses `iter` to scan all keys with the
- /// given prefix.
- fn prefix_scan<'a>(
- &'a self,
- prefix: &[u8],
- mode: IterMode<'_>,
- ) -> impl Iterator<Item = Result<(Bytes, Bytes)>> + use<'a, Self> {
- PrefixIter::new(self, prefix, mode)
- }
-}
-
-/// Formats displayable values into byte slices using a type-safe tuple
-/// abstraction.
-///
-/// This function provides a zero-cost abstraction for formatting one or more
-/// `Display` values into byte slices, with compile-time optimization of buffer
-/// sizes. It uses the `FmtTuple` trait to handle different arities (1-tuple,
-/// 2-tuple, etc.) in a type-safe manner.
-///
-/// # Type Parameters
-/// - `R`: The return type of the callback function
-/// - `F`: A type implementing `FmtTuple` (typically a tuple of references to
-/// Display types)
-///
-/// # Arguments
-/// - `key`: A tuple of references to `Display` objects (e.g., `(&key,)` or
-/// `(&from, &to)`)
-/// - `f`: A callback that receives the formatted byte slices as a tuple
-///
-/// # Design
-///
-/// The function leverages Rust's type system to:
-/// 1. Automatically determine the appropriate buffer size via `F::StackHint`
-/// 2. Return the correct tuple type via `F::Output<'_>`
-/// 3. Ensure type safety without runtime overhead
-fn with_fmtbuf<R, F: FmtTuple>(key: F, f: impl FnOnce(F::Output<'_>) -> R) -> R {
- // Create a stack buffer with size hint from the FmtTuple implementation
- let mut buf = quickbuf::StackBufferN::<F::SizeHint>::new();
- // Format the keys and pass the resulting byte slices to the callback
- f(buf.fmt_within(key))
-}
diff --git a/attic/kv/src/ttl.rs b/attic/kv/src/ttl.rs
deleted file mode 100644
index 4e03ad5ed..000000000
@@ -1,459 +0,0 @@
-use std::{
- collections::BTreeMap,
- ops::DerefMut,
- sync::{
- Arc,
- atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering},
- },
- time::{Duration, SystemTime, UNIX_EPOCH},
-};
-
-use bytes::Bytes;
-use futures::{FutureExt, future::BoxFuture};
-use parking_lot::Mutex;
-use smol_str::SmolStr;
-use tokio::sync::Notify;
-
-use super::{Operator, Options, error::Result, traits};
-use crate::{IVec, IVecs, Write, WriteBatch as _};
-
-/// Represents a key in the expiry database.
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Default)]
-#[repr(C, packed)]
-struct ExpiryKey {
- time: u64,
- seq: u32,
-}
-
-impl ExpiryKey {
- const LEN: usize = 12;
-
- #[inline]
- const fn new(time: u64, seq: u32) -> Self {
- Self { time, seq }
- }
-
- #[inline]
- fn to_bytes(self) -> [u8; Self::LEN] {
- let mut out = [0u8; Self::LEN];
- out[..8].copy_from_slice(&self.time.to_be_bytes());
- out[8..].copy_from_slice(&self.seq.to_be_bytes());
- out
- }
-
- #[inline]
- fn from_bytes(b: &[u8]) -> Option<Self> {
- (b.len() == Self::LEN).then(|| Self {
- time: u64::from_be_bytes(*b.first_chunk().unwrap()),
- seq: u32::from_be_bytes(*b.last_chunk().unwrap()),
- })
- }
-}
-
-struct GcTimer {
- /// Timestamp (in millis since Unix epoch) for the next scheduled GC run.
- time: AtomicU64,
- /// Notifies the background GC task that the schedule has been updated and
- /// it may need to wake up sooner than previously planned.
- signal: Notify,
-}
-
-impl GcTimer {
- fn new() -> Arc<Self> {
- Arc::new(Self {
- time: AtomicU64::new(u64::MAX),
- signal: Notify::new(),
- })
- }
-}
-
-/// Internal structure managing TTL metadata.
-struct ExpiryDb<S: traits::Store> {
- ttl_store: S,
- dst_store: S,
- sequence: AtomicU32,
- mem_index: Arc<parking_lot::RwLock<BTreeMap<Bytes, ExpiryKey>>>,
- timer: Arc<GcTimer>,
- gc_started: AtomicBool,
- gc_future: Mutex<Option<BoxFuture<'static, ()>>>,
-}
-
-impl<S: traits::Store> ExpiryDb<S> {
- const MAX_GC_IDLE: Duration = Duration::from_secs(30);
-
- fn new(ttl_store: S, dst_store: S) -> Arc<Self> {
- let timer = GcTimer::new();
- let mem_index = Arc::new(parking_lot::RwLock::new(BTreeMap::new()));
- let now = Self::now();
- {
- let mut to_remove = Vec::new();
- for item in ttl_store.iter(traits::IterMode::forward()) {
- let (ttl_key, key) = item.expect("invalid expiry entry");
- let exp = ExpiryKey::from_bytes(ttl_key.as_ref()).expect("invalid expiry key");
- if exp.time < now {
- to_remove.push((ttl_key, key));
- } else {
- mem_index.write().insert(key, exp);
- }
- }
- for (ttl, key) in to_remove {
- let _ = ttl_store.delete(&ttl);
- let _ = dst_store.delete(&key);
- }
- }
- let me = Arc::new(Self {
- ttl_store,
- dst_store,
- sequence: AtomicU32::new(0),
- timer,
- mem_index,
- gc_started: AtomicBool::new(false),
- gc_future: Mutex::new(None),
- });
-
- *me.gc_future.lock() = Some({
- let weak = Arc::downgrade(&me);
- let notify = me.timer.clone();
- async move {
- loop {
- tokio::task::yield_now().await;
-
- // Wait until either the next GC deadline is reached or we are
- // explicitly woken up because the deadline was moved closer.
- loop {
- let now = Self::now();
- let t = notify.time.load(Ordering::Relaxed);
-
- // Compute how long we should sleep (never exceeding the
- // maximum idle window).
- let mut idle = Duration::from_millis(t.saturating_sub(now));
- if idle > Self::MAX_GC_IDLE {
- idle = Self::MAX_GC_IDLE;
- }
-
- _ = tokio::time::timeout(idle, notify.signal.notified()).await;
-
- // If the scheduled time has elapsed break so GC is run.
- if notify.time.load(Ordering::Relaxed) <= Self::now() {
- break;
- }
- }
-
- let Some(db) = weak.upgrade() else { break };
- let _ = db.gc();
- }
- }
- .boxed()
- });
-
- me
- }
-
- fn gc(&self) -> Result<usize> {
- let now = Self::now();
- let mut next = now + Self::MAX_GC_IDLE.as_millis() as u64;
- let mut expired = Vec::new();
- for item in self.ttl_store.iter(traits::IterMode::forward()) {
- let (ttl_key, key): (Bytes, Bytes) = item?;
- let exp = ExpiryKey::from_bytes(ttl_key.as_ref()).expect("invalid expiry key");
- if exp.time > now {
- next = if next < exp.time { next } else { exp.time };
- break;
- }
- expired.push((ttl_key, key, exp));
- }
-
- let mut removed = 0;
- {
- let mut mem_index = self.mem_index.write();
- for (ttl_key, key, exp) in &expired {
- if let Some(entry) = mem_index.get(key) {
- if *entry == *exp {
- mem_index.remove(key);
- } else {
- continue;
- }
- }
- self.dst_store.delete(key)?;
- self.ttl_store.delete(ttl_key)?;
- removed += 1;
- }
- }
-
- let prev = self.timer.time.swap(next, Ordering::AcqRel);
- if next < prev {
- self.timer.signal.notify_one();
- }
- Ok(removed)
- }
-
- fn now() -> u64 {
- SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .unwrap_or(Duration::ZERO)
- .as_millis() as u64
- }
-
- fn remove(&self, key: &[u8]) -> Result<()> {
- if let Some(exp) = self.mem_index.write().remove(key) {
- self.ttl_store.delete(&exp.to_bytes())?;
- }
- Ok(())
- }
-
- fn remove_range(&self, from: &[u8], to: &[u8]) -> Result<()> {
- let mut keys = Vec::new();
- {
- let idx = self.mem_index.read();
- let mut from = idx.lower_bound(std::ops::Bound::Included(from));
- while let Some((k, exp)) = from.next() {
- if k.as_ref() >= to {
- break;
- }
- keys.push((k.clone(), *exp));
- }
- }
-
- let mut idx = self.mem_index.write();
- let batch = self.ttl_store.batch();
- for (k, exp) in keys {
- batch.delete(&exp.to_bytes());
- idx.remove(k.as_ref());
- }
-
- batch.commit()?;
-
- Ok(())
- }
-
- fn insert(&self, key: Bytes, ttl: u64, while_locked: impl FnOnce() -> Result<()>) -> Result<()> {
- if !self.gc_started.load(Ordering::SeqCst) {
- self.start_gc();
- }
- let exp_key = ExpiryKey::new(ttl, self.sequence.fetch_add(1, Ordering::SeqCst));
- self.ttl_store.put(&exp_key.to_bytes(), &key)?;
- while_locked()?;
- self.mem_index.write().insert(key, exp_key);
-
- let prev = self.timer.time.fetch_min(ttl, Ordering::AcqRel);
- if ttl < prev {
- self.timer.signal.notify_one();
- }
- Ok(())
- }
-
- #[cold]
- fn start_gc(&self) {
- if !self.gc_started.swap(true, Ordering::SeqCst) {
- let mut lock = self.gc_future.lock();
- let opt: &mut Option<_> = lock.deref_mut();
- if let Some(task) = opt.take() {
- tokio::spawn(task);
- }
- }
- }
-
- fn clear(&self) -> Result<()> {
- self.ttl_store.clear()?;
- self.mem_index.write().clear();
- self.timer.time.store(u64::MAX, Ordering::Release);
- self.timer.signal.notify_one();
- Ok(())
- }
-}
-
-/// TTL-enabled key-value store wrapper.
-#[derive(Clone, derive_more::Debug)]
-pub struct TtlDb<S: traits::Store> {
- inner: S,
- #[debug(skip)]
- ttl: Arc<ExpiryDb<S>>,
-}
-
-impl<S: traits::Store> TtlDb<S> {
- pub fn new(inner: S) -> Result<Self> {
- let ttl_store = inner.child("$ttl", *inner.options())?;
- let ttl = ExpiryDb::new(ttl_store, inner.clone());
- Ok(Self { inner, ttl })
- }
-
- fn check_expiry(&self, key: &[u8]) -> Result<()> {
- if let Some(exp) = self.ttl.mem_index.read().get(key).cloned()
- && exp.time <= ExpiryDb::<S>::now()
- {
- self.inner.delete(key)?;
- self.ttl.remove(key)?;
- }
- Ok(())
- }
-
- pub fn put_ttl(&self, key: &[u8], value: &[u8], ttl: Duration) -> Result<()> {
- let expire = ExpiryDb::<S>::now() + ttl.as_millis() as u64;
- self.ttl.insert(Bytes::copy_from_slice(key), expire, || {
- self.inner.put(key, value)
- })
- }
-
- pub fn purge_expired(&self) -> Result<usize> {
- self.ttl.gc()
- }
-}
-
-impl<S: traits::Store> traits::View for TtlDb<S> {
- type Iter<'a>
- = TtlIter<'a, S>
- where
- S: 'a;
-
- fn get(&self, key: &[u8]) -> Result<Option<IVec<'_>>> {
- self.check_expiry(key)?;
- self.inner.get(key)
- }
-
- fn contains(&self, key: &[u8]) -> Result<bool> {
- self.check_expiry(key)?;
- self.inner.contains(key)
- }
-
- fn get_many(&self, keys: impl IntoIterator<Item: AsRef<[u8]>>) -> Result<IVecs<'_>> {
- self.inner.get_many(keys)
- }
-
- fn iter<'a>(&'a self, mode: traits::IterMode<'_>) -> Self::Iter<'a> {
- TtlIter {
- inner: self.inner.iter(mode),
- parent: self,
- }
- }
-}
-
-pub struct TtlIter<'a, S: traits::Store> {
- inner: S::Iter<'a>,
- parent: &'a TtlDb<S>,
-}
-
-impl<'a, S: traits::Store> Iterator for TtlIter<'a, S> {
- type Item = Result<(Bytes, Bytes)>;
- fn next(&mut self) -> Option<Self::Item> {
- for item in self.inner.by_ref() {
- match item {
- Ok((k, v)) => {
- if let Err(e) = self.parent.check_expiry(k.as_ref()) {
- return Some(Err(e));
- }
- if let Some(exp) = self.parent.ttl.mem_index.read().get(k.as_ref()).cloned()
- && exp.time <= ExpiryDb::<S>::now()
- {
- continue;
- }
- return Some(Ok((k, v)));
- },
- Err(e) => return Some(Err(e)),
- }
- }
- None
- }
-}
-
-impl<S: traits::Store> traits::Write for TtlDb<S> {
- type Result = Result<()>;
-
- fn put(&self, key: &[u8], value: &[u8]) -> Self::Result {
- self.ttl.remove(key)?;
- self.inner.put(key, value)
- }
-
- fn delete(&self, key: &[u8]) -> Self::Result {
- self.ttl.remove(key)?;
- self.inner.delete(key)
- }
- fn update(&self, key: &[u8], op: &dyn Operator) -> Self::Result {
- self.inner.update(key, op)
- }
- fn delete_range(&self, from: &[u8], to: &[u8]) -> Self::Result {
- let batch = self.inner.batch();
- batch.delete_range(from, to);
- self.ttl.remove_range(from, to)?;
- batch.commit()?;
- Ok(())
- }
-}
-
-impl<S: traits::Store> traits::Store for TtlDb<S> {
- type WriteBatch<'a>
- = S::WriteBatch<'a>
- where
- S: 'a;
- type Snapshot<'a>
- = S::Snapshot<'a>
- where
- S: 'a;
-
- fn batch(&self) -> Self::WriteBatch<'_> {
- self.inner.batch()
- }
-
- fn namespace(&self) -> &str {
- self.inner.namespace()
- }
-
- fn child(&self, name: &str, opts: traits::Options) -> Result<Self> {
- let child = self.inner.child(name, opts)?;
- TtlDb::new(child)
- }
-
- fn children(&self) -> Vec<SmolStr> {
- self.inner.children()
- }
-
- fn clear(&self) -> Result<()> {
- self.ttl.clear()?;
- self.inner.clear()
- }
-
- fn flush(&self) -> Result<()> {
- self.inner.flush()
- }
-
- fn snapshot<'b>(&'b self) -> Self::Snapshot<'b> {
- self.inner.snapshot()
- }
-
- fn estimate_size(&self) -> Option<u64> {
- self.inner.estimate_size()
- }
-
- fn options(&self) -> &Options {
- self.inner.options()
- }
-}
-
-/// Extension trait to add TTL functionality to any store.
-pub trait TtlExt: traits::Store + Sized {
- fn with_ttl(self) -> Result<TtlDb<Self>> {
- TtlDb::new(self)
- }
-}
-
-impl<T: traits::Store> TtlExt for T {}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{Store, View, memorydb::MemoryDb};
-
- fn test_ttl<S: Store>(store: &TtlDb<S>) -> Result<()> {
- store.put_ttl(b"ephemeral", b"val", std::time::Duration::from_millis(10))?;
- assert!(store.contains(b"ephemeral")?);
- std::thread::sleep(std::time::Duration::from_millis(20));
- store.purge_expired()?;
- assert!(!store.contains(b"ephemeral")?);
- Ok(())
- }
-
- #[tokio::test]
- async fn test_ttl_memorydb() {
- let store = MemoryDb::new().with_ttl().unwrap();
- test_ttl(&store).unwrap();
- }
-}
diff --git a/attic/kv/src/typed.rs b/attic/kv/src/typed.rs
deleted file mode 100644
index a499ef870..000000000
@@ -1,746 +0,0 @@
-use std::borrow::Borrow;
-
-use smol_str::SmolStr;
-
-use super::{
- error::{Error, Result},
- traits, IterMode,
-};
-use crate::Options;
-
-pub trait TypedEntity {
- /// The type of the underlying implementation
- type Entity;
-
- /// Converts to the underlying implementation
- fn into_inner(self) -> Self::Entity;
-
- /// Returns a reference to the underlying implementation
- fn inner(&self) -> &Self::Entity;
-}
-
-pub trait TypedWriteBatch<'a, K: ?Sized, V: ?Sized>:
- TypedEntity<Entity: traits::WriteBatch<'a>>
-{
- /// Sets the value for the given key
- fn put(&mut self, key: impl Borrow<K>, value: impl Borrow<V>) -> &mut Self;
-
- /// Deletes the value for the given key
- fn delete(&mut self, key: impl Borrow<K>) -> &mut Self;
-
- /// Writes all batched operations to the database
- fn write(&mut self) -> Result<()>;
-}
-
-pub trait TypedView<K: ToOwned + ?Sized, V: ToOwned + ?Sized>:
- TypedEntity<Entity: traits::View>
-{
- /// Gets the value for the given key
- fn get(&self, key: impl Borrow<K>) -> Result<Option<V::Owned>>;
-
- /// Gets the values for multiple keys at once
- fn get_many<'a>(
- &'a self,
- keys: impl IntoIterator<Item: AsRef<K>>,
- ) -> Result<ser::TypedIVecs<'a, V::Owned>>;
-
- /// Returns true if the key exists in the key-value store
- fn contains(&self, key: impl Borrow<K>) -> Result<bool>;
-
- /// Iterates over key-value pairs in the key-value store
- fn iter<'a>(&'a self, mode: IterMode)
- -> impl Iterator<Item = Result<(K::Owned, V::Owned)>> + 'a;
-
- /// Scans all keys with the given prefix.
- ///
- /// This is a convenience method that uses `iter` to scan all keys with the
- /// given prefix.
- fn prefix_scan<'a>(
- &'a self,
- prefix: impl Borrow<K>,
- mode: IterMode<'a>,
- ) -> impl Iterator<Item = Result<(K::Owned, V::Owned)>> + 'a;
-}
-
-pub trait TypedStore<K: ToOwned + ?Sized, V: ToOwned + ?Sized>:
- TypedView<K, V> + Clone + Send + TypedEntity<Entity: traits::Store>
-{
- /// The type of write batch used by this key-value store
- type WriteBatch<'a>: TypedWriteBatch<'a, K, V>
- where
- Self: 'a,
- K: 'a,
- V: 'a;
-
- /// The type of snapshot used by this key-value store
- type Snapshot<'a>: TypedView<K, V> + 'a
- where
- Self: 'a,
- K: 'a,
- V: 'a;
-
- /// Creates a new write batch for this key-value store
- fn batch<'a>(&'a self) -> Self::WriteBatch<'a>;
-
- /// Returns the namespace of this key-value store
- fn namespace(&self) -> &str;
-
- /// Creates a child key-value store with the given name
- fn child(&self, name: &str, opts: Options) -> Result<Self>;
-
- /// Returns an iterator over each child key-value store
- fn children(&self) -> Vec<SmolStr>;
-
- /// Sets the value for the given key
- fn put(&self, key: impl Borrow<K>, value: impl Borrow<V>) -> Result<()>;
-
- /// Deletes the value for the given key
- fn delete(&self, key: impl Borrow<K>) -> Result<()>;
-
- /// Clears all data in this key-value store
- fn clear(&mut self) -> Result<()>;
-
- /// Creates a snapshot of the key-value store at the current point in time
- fn snapshot<'b>(&'b self) -> Self::Snapshot<'b>;
-}
-
-pub mod ser {
- //! Module ser implements typed store interfaces using serde for
- //! serialization and deserialization.
-
- use std::{borrow::Borrow, fmt, marker::PhantomData};
-
- use derive_where::derive_where;
- use smol_str::SmolStr;
- use tetra_core::alloc::{quickbuf, ranges::SmallRangeList};
-
- use super::*;
- use crate::{IVecs, IVecsIter, Options};
-
- const SELF_DESCRIBING: bool = false;
-
- pub fn encode<T: ?Sized + serde::Serialize>(
- sbuf: &mut quickbuf::StackBuffer,
- value: &T,
- ) -> Result<std::ops::Range<usize>, Error> {
- let beg = sbuf.len();
- if SELF_DESCRIBING {
- serde_cbor::to_writer(&mut *sbuf, &value)?;
- } else {
- postcard::to_extend(value, &mut *sbuf)?;
- }
- Ok(beg..sbuf.len())
- }
-
- pub fn decode<'de, T: serde::de::Deserialize<'de>>(data: &'de [u8]) -> Result<T, Error> {
- if SELF_DESCRIBING {
- Ok(serde_cbor::from_slice(data)?)
- } else {
- Ok(postcard::from_bytes(data)?)
- }
- }
-
- // --- Into Traits ---
-
- pub trait IntoTypedWriteBatch {
- type Output<K, V>
- where
- K: serde::Serialize,
- V: serde::Serialize;
- fn into_typed<K: serde::Serialize, V: serde::Serialize>(self) -> Self::Output<K, V>;
- }
-
- impl<'a, T: traits::WriteBatch<'a>> IntoTypedWriteBatch for T {
- type Output<K, V>
- = WriteBatch<K, V, T>
- where
- K: serde::Serialize,
- V: serde::Serialize;
-
- fn into_typed<K: serde::Serialize, V: serde::Serialize>(self) -> Self::Output<K, V> {
- WriteBatch {
- inner: self,
- err: None,
- _marker: PhantomData,
- }
- }
- }
-
- pub trait IntoTypedView {
- type Output<K, V>
- where
- K: serde::Serialize,
- V: serde::Serialize;
- fn into_typed<K: serde::Serialize, V: serde::Serialize>(self) -> Self::Output<K, V>;
- }
-
- impl<T: traits::View> IntoTypedView for T {
- type Output<K, V>
- = View<K, V, T>
- where
- K: serde::Serialize,
- V: serde::Serialize;
-
- fn into_typed<K: serde::Serialize, V: serde::Serialize>(self) -> Self::Output<K, V> {
- View {
- inner: self,
- _marker: PhantomData,
- }
- }
- }
-
- pub trait IntoTypedStore {
- type Output<K, V>
- where
- K: serde::Serialize + ToOwned<Owned: serde::de::DeserializeOwned> + ?Sized,
- V: serde::Serialize + ToOwned<Owned: serde::de::DeserializeOwned> + ?Sized;
- fn into_typed<
- K: serde::Serialize + ToOwned<Owned: serde::de::DeserializeOwned> + ?Sized,
- V: serde::Serialize + ToOwned<Owned: serde::de::DeserializeOwned> + ?Sized,
- >(
- self,
- ) -> Self::Output<K, V>;
- }
-
- impl<T: traits::Store> IntoTypedStore for T {
- type Output<K, V>
- = Store<K, V, T>
- where
- K: serde::Serialize + ToOwned<Owned: serde::de::DeserializeOwned> + ?Sized,
- V: serde::Serialize + ToOwned<Owned: serde::de::DeserializeOwned> + ?Sized;
-
- fn into_typed<
- K: serde::Serialize + ToOwned<Owned: serde::de::DeserializeOwned> + ?Sized,
- V: serde::Serialize + ToOwned<Owned: serde::de::DeserializeOwned> + ?Sized,
- >(
- self,
- ) -> Self::Output<K, V> {
- Store {
- inner: self,
- _marker: PhantomData,
- }
- }
- }
-
- // --- Wrapper Structs Impls ---
-
- pub struct WriteBatch<K: ?Sized, V: ?Sized, I> {
- inner: I,
- err: Option<Error>,
- _marker: PhantomData<fn(K, V)>,
- }
-
- impl<K: ?Sized, V: ?Sized, I> From<I> for WriteBatch<K, V, I> {
- fn from(inner: I) -> Self {
- Self {
- inner,
- err: None,
- _marker: PhantomData,
- }
- }
- }
-
- impl<'a, K: ?Sized, V: ?Sized, I: traits::WriteBatch<'a> + fmt::Debug> fmt::Debug
- for WriteBatch<K, V, I>
- {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.debug_struct(
- format!(
- "WriteBatch<{}, {}>",
- std::any::type_name::<K>(),
- std::any::type_name::<V>()
- )
- .as_str(),
- )
- .field("inner", &self.inner)
- .finish()
- }
- }
-
- /// A typed value slice wrapper for a key-value store
- #[repr(transparent)]
- pub struct TypedIVecs<'a, V: ?Sized> {
- inner: IVecs<'a>,
- _marker: PhantomData<fn(&'a ()) -> V>,
- }
-
- impl<'a, V: ?Sized> From<IVecs<'a>> for TypedIVecs<'a, V> {
- fn from(inner: IVecs<'a>) -> Self {
- Self {
- inner,
- _marker: PhantomData,
- }
- }
- }
-
- impl<'a, V: ?Sized + fmt::Debug> fmt::Debug for TypedIVecs<'a, V> {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.debug_struct(format!("TypedIVecs<{}>", std::any::type_name::<V>()).as_str())
- .field("inner", &self.inner)
- .finish()
- }
- }
- impl<'a, V: serde::de::DeserializeOwned> TypedIVecs<'a, V> {
- pub fn get(&self, index: usize) -> Result<Option<V>> {
- match self.inner.get(index)? {
- Some(bytes) => match decode(bytes) {
- Ok(value) => Ok(Some(value)),
- Err(err) => Err(err),
- },
- None => Ok(None),
- }
- }
-
- pub fn len(&self) -> usize {
- self.inner.len()
- }
-
- pub fn is_empty(&self) -> bool {
- self.inner.is_empty()
- }
- }
-
- impl<'a, V: serde::de::DeserializeOwned> IntoIterator for TypedIVecs<'a, V> {
- type Item = Result<Option<V>>;
- type IntoIter = TypedIVecsIter<'a, V>;
-
- fn into_iter(self) -> Self::IntoIter {
- TypedIVecsIter {
- inner: self.inner.into_iter(),
- _marker: PhantomData,
- }
- }
- }
-
- /// Iterator for a typed value slice
- #[derive(derive_more::From)]
- #[repr(transparent)]
- pub struct TypedIVecsIter<'a, V: ?Sized> {
- inner: IVecsIter<'a>,
- #[from(ignore)]
- _marker: PhantomData<fn(&'a ()) -> V>,
- }
-
- impl<'a, V: ?Sized + fmt::Debug> fmt::Debug for TypedIVecsIter<'a, V> {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.debug_struct(format!("TypedIVecsIter<{}>", std::any::type_name::<V>()).as_str())
- .field("inner", &self.inner)
- .finish()
- }
- }
-
- impl<'a, V: serde::de::DeserializeOwned> Iterator for TypedIVecsIter<'a, V> {
- type Item = Result<Option<V>>;
-
- fn next(&mut self) -> Option<Self::Item> {
- let r = self.inner.next()?;
- let b = match r {
- Ok(Some(bytes)) => bytes,
- Ok(None) => return Some(Ok(None)),
- Err(e) => return Some(Err(e)),
- };
- let v = match decode(b.as_ref()) {
- Ok(value) => Ok(Some(value)),
- Err(err) => Err(err),
- };
- Some(v)
- }
- }
-
- /// A typed view wrapper for a key-value store
- #[derive_where(Clone; I: Clone)]
- #[repr(transparent)]
- pub struct View<K: ?Sized, V: ?Sized, I: traits::View> {
- inner: I,
- _marker: PhantomData<fn(K, V)>,
- }
-
- impl<K: ?Sized, V: ?Sized, I: traits::View> From<I> for View<K, V, I> {
- fn from(inner: I) -> Self {
- Self {
- inner,
- _marker: PhantomData,
- }
- }
- }
-
- impl<K: ?Sized, V: ?Sized, I: traits::View + fmt::Debug> fmt::Debug for View<K, V, I> {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.debug_struct(
- format!(
- "View<{}, {}>",
- std::any::type_name::<K>(),
- std::any::type_name::<V>()
- )
- .as_str(),
- )
- .field("inner", &self.inner)
- .finish()
- }
- }
-
- #[derive_where(Clone; I: Clone)]
- #[repr(transparent)]
- pub struct Store<K: ?Sized, V: ?Sized, I: traits::Store> {
- inner: I,
- _marker: PhantomData<fn(K, V)>,
- }
-
- impl<K: ?Sized, V: ?Sized, I: traits::Store> From<I> for Store<K, V, I> {
- fn from(inner: I) -> Self {
- Self {
- inner,
- _marker: PhantomData,
- }
- }
- }
-
- impl<K: ?Sized, V: ?Sized, I: traits::Store + fmt::Debug> fmt::Debug for Store<K, V, I> {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.debug_struct(
- format!(
- "Store<{}, {}>",
- std::any::type_name::<K>(),
- std::any::type_name::<V>()
- )
- .as_str(),
- )
- .field("inner", &self.inner)
- .finish()
- }
- }
-
- // --- Actual Trait Implementations ---
-
- impl<'a, K: ?Sized, V: ?Sized, I: traits::WriteBatch<'a>> TypedEntity for WriteBatch<K, V, I> {
- type Entity = I;
- fn into_inner(self) -> Self::Entity {
- self.inner
- }
- fn inner(&self) -> &Self::Entity {
- &self.inner
- }
- }
- impl<K: ?Sized, V: ?Sized, I: traits::View> TypedEntity for View<K, V, I> {
- type Entity = I;
- fn into_inner(self) -> Self::Entity {
- self.inner
- }
- fn inner(&self) -> &Self::Entity {
- &self.inner
- }
- }
- impl<K: ?Sized, V: ?Sized, I: traits::Store> TypedEntity for Store<K, V, I> {
- type Entity = I;
- fn into_inner(self) -> Self::Entity {
- self.inner
- }
- fn inner(&self) -> &Self::Entity {
- &self.inner
- }
- }
-
- impl<
- 'a,
- K: serde::Serialize + ?Sized,
- V: serde::Serialize + ?Sized,
- I: traits::WriteBatch<'a>,
- > TypedWriteBatch<'a, K, V> for WriteBatch<K, V, I>
- {
- fn put(&mut self, key: impl Borrow<K>, value: impl Borrow<V>) -> &mut Self {
- if self.err.is_some() {
- return self;
- }
- quickbuf::with(move |buf| {
- let kv = encode(buf, key.borrow())
- .and_then(|kr| encode(buf, value.borrow()).map(|vr| (kr, vr)));
- let (kr, vr) = match kv {
- Ok((kr, vr)) => (kr, vr),
- Err(e) => {
- self.err = Some(e);
- return self;
- },
- };
- self.inner.put(&buf[kr], &buf[vr]);
- self
- })
- }
- fn delete(&mut self, key: impl Borrow<K>) -> &mut Self {
- if self.err.is_some() {
- return self;
- }
- quickbuf::with(move |buf| {
- let kr = match encode(buf, key.borrow()) {
- Ok(kr) => kr,
- Err(e) => {
- self.err = Some(e);
- return self;
- },
- };
- self.inner.delete(&buf[kr]);
- self
- })
- }
- fn write(&mut self) -> Result<()> {
- if let Some(err) = self.err.take() {
- return Err(err);
- }
- self.inner.commit()
- }
- }
-
- impl<
- K: serde::Serialize + ToOwned<Owned: serde::de::DeserializeOwned> + ?Sized,
- V: serde::Serialize + ToOwned<Owned: serde::de::DeserializeOwned> + ?Sized,
- I: traits::View,
- > TypedView<K, V> for View<K, V, I>
- {
- fn get(&self, key: impl Borrow<K>) -> Result<Option<V::Owned>> {
- quickbuf::with(move |buf| -> Result<_> {
- let kr = encode(buf, key.borrow())?;
- self
- .inner
- .get(&buf[kr])?
- .map(|v| decode(v.as_ref()))
- .transpose()
- })
- }
-
- fn contains(&self, key: impl Borrow<K>) -> Result<bool> {
- quickbuf::with(move |buf| {
- let kr = encode(buf, key.borrow())?;
- self.inner.contains(&buf[kr])
- })
- }
-
- fn get_many<'a>(
- &'a self,
- keys: impl IntoIterator<Item: AsRef<K>>,
- ) -> Result<ser::TypedIVecs<'a, V::Owned>> {
- Ok(ser::TypedIVecs {
- _marker: PhantomData,
- inner: quickbuf::with(move |buf| -> Result<_> {
- self.inner.get_many(
- keys
- .into_iter()
- .map(|k| encode(buf, k.as_ref()))
- .collect::<Result<Vec<_>>>()?
- .into_iter()
- .map(|kr| &buf[kr]),
- )
- })?,
- })
- }
-
- fn iter<'a>(
- &'a self,
- mode: traits::IterMode,
- ) -> impl Iterator<Item = Result<(K::Owned, V::Owned)>> + 'a {
- self.inner.iter(mode).map(|result| {
- result.and_then(|(k, v)| {
- let key = decode(k.as_ref())?;
- let value = decode(v.as_ref())?;
- Ok((key, value))
- })
- })
- }
-
- fn prefix_scan<'a>(
- &'a self,
- prefix: impl Borrow<K>,
- mode: traits::IterMode<'a>,
- ) -> impl Iterator<Item = Result<(K::Owned, V::Owned)>> + 'a {
- enum MaybeErrIter<I> {
- Ok(I),
- Err(Option<Error>),
- }
- impl<I: Iterator<Item = Result<R>>, R> Iterator for MaybeErrIter<I> {
- type Item = Result<R>;
- fn next(&mut self) -> Option<Self::Item> {
- match self {
- Self::Ok(iter) => iter.next(),
- Self::Err(e) => e.take().map(|e| Err(e)),
- }
- }
- }
-
- let kbuf = quickbuf::with(|buf| -> Result<Box<[u8]>> {
- let kr = encode(buf, prefix.borrow())?;
- Ok(buf[kr].into())
- });
-
- match kbuf {
- Err(e) => MaybeErrIter::Err(Some(e)),
- Ok(v) => MaybeErrIter::Ok(self.inner.prefix_scan(v.as_ref(), mode).map(
- |r| -> Result<(K::Owned, V::Owned)> {
- let (k, v) = r?;
- let key = decode(k.as_ref())?;
- let value = decode(v.as_ref())?;
- Ok((key, value))
- },
- )),
- }
- }
- }
-
- impl<
- K: serde::Serialize + ToOwned<Owned: serde::de::DeserializeOwned> + ?Sized,
- V: serde::Serialize + ToOwned<Owned: serde::de::DeserializeOwned> + ?Sized,
- I: traits::Store,
- > TypedView<K, V> for Store<K, V, I>
- {
- fn get(&self, key: impl Borrow<K>) -> Result<Option<V::Owned>> {
- quickbuf::with(move |buf| {
- let kr = encode(buf, key.borrow())?;
- self
- .inner
- .get(&buf[kr])?
- .map(|v| decode(v.as_ref()))
- .transpose()
- })
- }
-
- fn contains(&self, key: impl Borrow<K>) -> Result<bool> {
- quickbuf::with(move |buf| {
- let kr = encode(buf, key.borrow())?;
- self.inner.contains(&buf[kr])
- })
- }
-
- fn get_many<'a>(
- &'a self,
- keys: impl IntoIterator<Item: AsRef<K>>,
- ) -> Result<ser::TypedIVecs<'a, V::Owned>> {
- Ok(ser::TypedIVecs {
- _marker: PhantomData,
- inner: quickbuf::with(move |buf| -> Result<_> {
- let mut ranges = SmallRangeList::<32>::new();
- for key in keys {
- let kr = encode(buf, key.as_ref())?;
- ranges.push_range(kr);
- }
- self.inner.get_many(ranges.into_ranges().deref(&buf[..]))
- })?,
- })
- }
-
- fn iter<'a>(
- &'a self,
- mode: traits::IterMode,
- ) -> impl Iterator<Item = Result<(K::Owned, V::Owned)>> + 'a {
- self.inner.iter(mode).map(|result| {
- result.and_then(|(k, v)| {
- let key = decode(k.as_ref())?;
- let value = decode(v.as_ref())?;
- Ok((key, value))
- })
- })
- }
-
- fn prefix_scan<'a>(
- &'a self,
- prefix: impl Borrow<K>,
- mode: traits::IterMode<'a>,
- ) -> impl Iterator<Item = Result<(K::Owned, V::Owned)>> + 'a {
- enum MaybeErrIter<I> {
- Ok(I),
- Err(Option<Error>),
- }
- impl<I: Iterator<Item = Result<R>>, R> Iterator for MaybeErrIter<I> {
- type Item = Result<R>;
- fn next(&mut self) -> Option<Self::Item> {
- match self {
- Self::Ok(iter) => iter.next(),
- Self::Err(e) => e.take().map(|e| Err(e)),
- }
- }
- }
-
- let kbuf = quickbuf::with(|buf| -> Result<Box<[u8]>> {
- let kr = encode(buf, prefix.borrow())?;
- Ok(buf[kr].into())
- });
-
- match kbuf {
- Err(e) => MaybeErrIter::Err(Some(e)),
- Ok(v) => MaybeErrIter::Ok(self.inner.prefix_scan(v.as_ref(), mode).map(
- |r| -> Result<(K::Owned, V::Owned)> {
- let (k, v) = r?;
- let key = decode(k.as_ref())?;
- let value = decode(v.as_ref())?;
- Ok((key, value))
- },
- )),
- }
- }
- }
-
- impl<
- K: serde::Serialize + ToOwned<Owned: serde::de::DeserializeOwned> + ?Sized,
- V: serde::Serialize + ToOwned<Owned: serde::de::DeserializeOwned> + ?Sized,
- I: traits::Store,
- > TypedStore<K, V> for Store<K, V, I>
- {
- type WriteBatch<'a>
- = WriteBatch<K, V, I::WriteBatch<'a>>
- where
- Self: 'a,
- K: 'a,
- V: 'a;
- type Snapshot<'a>
- = View<K, V, I::Snapshot<'a>>
- where
- Self: 'a,
- K: 'a,
- V: 'a;
-
- fn batch<'a>(&'a self) -> Self::WriteBatch<'a> {
- WriteBatch {
- inner: self.inner.batch(),
- err: None,
- _marker: PhantomData,
- }
- }
-
- fn namespace(&self) -> &str {
- self.inner.namespace()
- }
-
- fn child(&self, name: &str, opts: Options) -> Result<Self> {
- Ok(Store {
- inner: self.inner.child(name, opts)?,
- _marker: PhantomData,
- })
- }
-
- fn children(&self) -> Vec<SmolStr> {
- self.inner.children()
- }
-
- fn put(&self, key: impl Borrow<K>, value: impl Borrow<V>) -> Result<()> {
- quickbuf::with(move |buf| {
- let kr = encode(buf, key.borrow())?;
- let vr = encode(buf, value.borrow())?;
- self.inner.put(&buf[kr], &buf[vr])
- })
- }
-
- fn delete(&self, key: impl Borrow<K>) -> Result<()> {
- quickbuf::with(move |buf| {
- let kr = encode(buf, key.borrow())?;
- self.inner.delete(&buf[kr])
- })
- }
-
- fn clear(&mut self) -> Result<()> {
- self.inner.clear()
- }
-
- fn snapshot<'b>(&'b self) -> Self::Snapshot<'b> {
- View {
- inner: self.inner.snapshot(),
- _marker: PhantomData,
- }
- }
- }
-}
diff --git a/attic/net/http/client.rs b/attic/net/http/client.rs
deleted file mode 100644
index c114a9653..000000000
@@ -1,912 +0,0 @@
-use std::{
- fmt::Display,
- mem,
- sync::LazyLock,
- time::{Duration, Instant},
-};
-
-use http::{HeaderMap, HeaderName, HeaderValue, Version, header, header::CONTENT_TYPE};
-use reqwest::{IntoUrl, Method, Request, ResponseBuilderExt, multipart};
-use serde::Serialize;
-use smol_str::{SmolStr, ToSmolStr};
-use tower::ServiceExt;
-
-use super::{server::RouterTable, sse, upgrade, utils, ws};
-
-const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
-const MIN_TIMEOUT: Duration = Duration::from_millis(500);
-const MIN_RETRY_DELAY: Duration = Duration::from_millis(50);
-
-#[derive(Debug, thiserror::Error)]
-pub enum Error {
- /// Error when a request fails.
- #[error(transparent)]
- RequestFailed(#[from] reqwest::Error),
- /// Error when a request times out.
- #[error("Request timed out")]
- LocalTimeout,
- /// Error when the request built has already been constructed.
- #[error("Request has already been constructed")]
- RequestAlreadyBuilt,
- /// Error when the request body cannot be serialized into a postcard.
- #[error("Failed to serialize request body into postcard")]
- Postcard(#[from] postcard::Error),
- /// Error when the request body cannot be serialized into a JSON.
- #[error("Failed to serialize request body into JSON")]
- Json(#[from] serde_json::Error),
- /// Error when a WebSocket connection fails.
- #[error("WebSocket error: {0}")]
- WebSocket(#[from] ws::Error),
-}
-pub type Result<T, E = Error> = std::result::Result<T, E>;
-
-/// [`Client`] is a wrapper around [`reqwest::Client`] which runs middleware on
-/// every request.
-#[derive(Clone, Debug, derive_more::From, derive_more::Into, derive_more::Constructor)]
-pub struct Client<'a> {
- inner: reqwest::Client,
- router: &'a RouterTable,
-}
-
-/// Default implementation for [`Client`] that clones from the global client.
-impl Default for Client<'_> {
- fn default() -> Self {
- Client::global().clone()
- }
-}
-
-/// Static methods for accessing a global singleton [`Client`] instance.
-impl Client<'static> {
- /// Returns a reference to the global [`Client`] instance.
- ///
- /// The global client is lazily initialized with default settings
- /// and uses the global router table.
- pub fn global() -> &'static Self {
- static GLOBAL_CLIENT: LazyLock<Client<'static>> = LazyLock::new(|| Client {
- inner: reqwest::Client::builder()
- .build()
- .unwrap_or_else(|_| reqwest::Client::new()),
- router: RouterTable::global(),
- });
- &GLOBAL_CLIENT
- }
-}
-
-impl<'a> Client<'a> {
- /// Returns the inner [`reqwest::Client`].
- pub const fn inner(&self) -> &reqwest::Client {
- &self.inner
- }
-
- /// Replaces the inner [`reqwest::Client`] with a new one.
- pub fn with_inner(mut self, inner: reqwest::Client) -> Self {
- self.inner = inner;
- self
- }
-
- /// Returns the [`RouterTable`] used by the client.
- pub const fn router(&self) -> &RouterTable {
- self.router
- }
-
- /// Replaces the [`RouterTable`] with a new one.
- pub const fn with_router(mut self, router: &'a RouterTable) -> Self {
- self.router = router;
- self
- }
-
- /// Convenience method to make a [`Method::GET`] request to a URL.
- ///
- /// # Errors
- ///
- /// This method fails whenever the supplied [`reqwest::Url`] cannot be
- /// parsed.
- pub fn get<U: IntoUrl>(&self, url: U) -> RequestBuilder<'a> {
- self.request(Method::GET, url)
- }
-
- /// Convenience method to make a [`Method::POST`] request to a URL.
- ///
- /// # Errors
- ///
- /// This method fails whenever the supplied [`reqwest::Url`] cannot be
- /// parsed.
- pub fn post<U: IntoUrl>(&self, url: U) -> RequestBuilder<'a> {
- self.request(Method::POST, url)
- }
-
- /// Convenience method to make a [`Method::PUT`] request to a URL.
- ///
- /// # Errors
- ///
- /// This method fails whenever the supplied [`reqwest::Url`] cannot be
- /// parsed.
- pub fn put<U: IntoUrl>(&self, url: U) -> RequestBuilder<'a> {
- self.request(Method::PUT, url)
- }
-
- /// Convenience method to make a [`Method::PATCH`] request to a URL.
- ///
- /// # Errors
- ///
- /// This method fails whenever the supplied [`reqwest::Url`] cannot be
- /// parsed.
- pub fn patch<U: IntoUrl>(&self, url: U) -> RequestBuilder<'a> {
- self.request(Method::PATCH, url)
- }
-
- /// Convenience method to make a [`Method::DELETE`] request to a URL.
- ///
- /// # Errors
- ///
- /// This method fails whenever the supplied [`reqwest::Url`] cannot be
- /// parsed.
- pub fn delete<U: IntoUrl>(&self, url: U) -> RequestBuilder<'a> {
- self.request(Method::DELETE, url)
- }
-
- /// Convenience method to make a [`Method::HEAD`] request to a URL.
- ///
- /// # Errors
- ///
- /// This method fails whenever the supplied [`reqwest::Url`] cannot be
- /// parsed.
- pub fn head<U: IntoUrl>(&self, url: U) -> RequestBuilder<'a> {
- self.request(Method::HEAD, url)
- }
-
- /// Convenience method to make a WebSocket request to a URL.
- ///
- /// # Errors
- ///
- /// This method fails whenever the supplied [`reqwest::Url`] cannot be
- /// parsed.
- pub fn websocket<U: IntoUrl>(&self, url: U) -> WebsocketBuilder<'a> {
- self.request(Method::GET, url).into()
- }
-
- /// Convenience method to open a Server-Sent Events stream.
- ///
- /// # Errors
- ///
- /// This method fails whenever the supplied [`reqwest::Url`] cannot be
- /// parsed.
- pub fn sse<U: IntoUrl>(&self, url: U) -> SseBuilder<'a> {
- self.request(Method::GET, url).into()
- }
-
- /// Start building a [`Request`] with the [`Method`] and [`reqwest::Url`].
- ///
- /// Returns a [`RequestBuilder`], which will allow setting headers and
- /// the request body before sending.
- ///
- /// # Errors
- ///
- /// This method fails whenever the supplied [`reqwest::Url`] cannot be
- /// parsed.
- pub fn request<U: IntoUrl>(&self, method: Method, url: U) -> RequestBuilder<'a> {
- RequestBuilder {
- inner: Ok(self
- .inner
- .request(method, url)
- .timeout(Duration::from_secs(10))),
- extensions: http::Extensions::new(),
- router: self.router,
- }
- }
-
- /// Executes a [`Request`].
- ///
- /// A [`Request`] can be built manually with [`Request::new()`] or obtained
- /// from a [`RequestBuilder`] with [`RequestBuilder::build()`].
- ///
- /// You should prefer to use the [`RequestBuilder`] and
- /// [`RequestBuilder::send()`].
- ///
- /// # Errors
- ///
- /// This method fails if there was an error while sending request,
- /// redirect loop was detected or redirect limit was exhausted.
- pub async fn execute(&self, req: Request) -> Result<reqwest::Response> {
- let mut ext = http::Extensions::new();
- self.execute_with_extensions(req, &mut ext, false).await
- }
-
- /// Executes a [`Request`] with initial [`http::Extensions`].
- ///
- /// A [`Request`] can be built manually with [`Request::new()`] or obtained
- /// from a [`RequestBuilder`] with [`RequestBuilder::build()`].
- ///
- /// You should prefer to use the [`RequestBuilder`] and
- /// [`RequestBuilder::send()`].
- ///
- /// # Errors
- ///
- /// This method fails if there was an error while sending request,
- /// redirect loop was detected or redirect limit was exhausted.
- pub async fn execute_with_extensions(
- &self,
- mut req: Request,
- ext: &mut http::Extensions,
- is_streaming_response: bool,
- ) -> Result<reqwest::Response> {
- let timeout = *req.timeout().unwrap_or(&DEFAULT_TIMEOUT);
- let deadline = Instant::now() + timeout;
-
- // TODO: Implement P2P URL handling
- // Check if this is a P2P URL
- // if let Some(p2p) = &self.p2p {
- // if let Some(host) = req.url().host_str() {
- // if host.starts_with("p2p:") || iroh::NodeId::from_str(host).is_ok() {
- // // This is a P2P request
- // return self.execute_p2p(p2p, req, ext).await;
- // }
- // }
- // }
-
- let router = req
- .url()
- .host_str()
- .and_then(|host| self.router.lookup(host));
-
- if is_streaming_response {
- *req.timeout_mut() = None;
- }
-
- let base_req = req.try_clone();
- let base_ext = mem::take(ext);
-
- let mut req = req;
- loop {
- let now = Instant::now();
- let rem = deadline
- .checked_duration_since(now)
- .unwrap_or_default()
- .max(MIN_TIMEOUT);
-
- let mut cur_ext = base_ext.clone();
- let mut resp = if let Some(router) = router.clone() {
- passthrough_local(req, &mut cur_ext, router, rem).await?
- } else {
- if !is_streaming_response {
- *req.timeout_mut() = Some(rem);
- }
- tokio::time::timeout(rem, self.inner.execute(req))
- .await
- .map_err(|_| Error::LocalTimeout)??
- };
-
- let status = resp.status();
- if (status.is_client_error() || status.is_server_error())
- && let Some(dur) = utils::retry_after(resp.headers())
- {
- let dur = dur.max(MIN_RETRY_DELAY);
- if let Some(base) = &base_req
- && Instant::now() + dur < deadline
- && let Some(req_new) = base.try_clone()
- {
- req = req_new;
- tokio::time::sleep(dur).await;
- continue;
- }
- resp.extensions_mut().insert(utils::RetryAfter(dur));
- }
-
- *ext = cur_ext;
- return Ok(resp);
- }
- }
-}
-
-pub trait ReqwestBuilder: Sized {
- fn parts(&mut self) -> (&mut Result<reqwest::RequestBuilder>, &mut http::Extensions);
-
- /// Returns a mutable reference to the internal set of extensions for this
- /// request
- fn extensions(&mut self) -> &mut http::Extensions {
- let (_, ext) = self.parts();
- ext
- }
-
- /// Inserts the extension into this request builder
- fn extension<T: Send + Sync + Clone + 'static>(&mut self, extension: T) -> &mut Self {
- self.extensions().insert(extension);
- self
- }
-
- /// Updates the internal request builder with a new one
- fn update(
- &mut self,
- f: impl FnOnce(reqwest::RequestBuilder) -> reqwest::RequestBuilder,
- ) -> &mut Self {
- self.try_update(|b| Ok(f(b)))
- }
-
- /// Updates the internal request builder with a new one, returning an error
- /// if the update fails
- fn try_update<F>(&mut self, f: F) -> &mut Self
- where
- F: FnOnce(reqwest::RequestBuilder) -> Result<reqwest::RequestBuilder>,
- {
- let (builder, _) = self.parts();
- let inner = mem::replace(builder, Err(Error::RequestAlreadyBuilt));
- *builder = inner.and_then(f);
- self
- }
-
- /// Build a [`Request`], which can be inspected, modified and executed with
- /// [`Client::execute()`].
- fn build(&mut self) -> Result<Request> {
- let (builder, _) = self.parts();
- mem::replace(builder, Err(Error::RequestAlreadyBuilt))?
- .build()
- .map_err(Error::RequestFailed)
- }
-
- /// Add a header to this [`Request`].
- fn header<K, V>(&mut self, key: K, value: V) -> &mut Self
- where
- HeaderName: TryFrom<K>,
- <HeaderName as TryFrom<K>>::Error: Into<http::Error>,
- HeaderValue: TryFrom<V>,
- <HeaderValue as TryFrom<V>>::Error: Into<http::Error>,
- {
- self.update(|builder| builder.header(key, value))
- }
-
- /// Add a set of headers to the existing ones on this [`Request`].
- ///
- /// The headers will be merged in to any already set.
- fn headers(&mut self, headers: HeaderMap) -> &mut Self {
- self.update(|builder| builder.headers(headers))
- }
-
- /// Set the HTTP version of the request.
- fn version(&mut self, version: reqwest::Version) -> &mut Self {
- self.update(|builder| builder.version(version))
- }
-
- /// Enable HTTP basic authentication.
- fn basic_auth<U, P>(&mut self, username: U, password: Option<P>) -> &mut Self
- where
- U: Display,
- P: Display,
- {
- self.update(|builder| builder.basic_auth(username, password))
- }
-
- /// Enable HTTP bearer authentication.
- fn bearer_auth<T>(&mut self, token: T) -> &mut Self
- where
- T: Display,
- {
- self.update(|builder| builder.bearer_auth(token))
- }
-
- /// Set the request body.
- fn body<T: Into<reqwest::Body>>(&mut self, body: T) -> &mut Self {
- self.update(|builder| builder.body(body))
- }
-
- /// Enables a request timeout.
- ///
- /// The timeout is applied from when the request starts connecting until the
- /// response body has finished. It affects only this request and overrides
- /// the timeout configured using [`reqwest::ClientBuilder::timeout()`].
- #[cfg(not(target_arch = "wasm32"))]
- fn timeout(&mut self, timeout: Duration) -> &mut Self {
- self.update(|builder| builder.timeout(timeout))
- }
-
- /// Sends a multipart/form-data body.
- ///
- /// In additional the request's body, the Content-Type and Content-Length
- /// fields are appropriately set.
- fn multipart(&mut self, multipart: multipart::Form) -> &mut Self {
- self.update(|builder| builder.multipart(multipart))
- }
-
- /// Modify the query string of the URL.
- ///
- /// Modifies the URL of this request, adding the parameters provided.
- /// This method appends and does not overwrite. This means that it can
- /// be called multiple times and that existing query parameters are not
- /// overwritten if the same key is used. The key will simply show up
- /// twice in the query string.
- /// Calling `.query(&[("foo", "a"), ("foo", "b")])` gives `"foo=a&foo=b"`.
- ///
- /// # Note
- /// This method does not support serializing a single key-value
- /// pair. Instead of using `.query(("key", "val"))`, use a sequence, such
- /// as `.query(&[("key", "val")])`. It's also possible to serialize structs
- /// and maps into a key-value pair.
- ///
- /// # Errors
- /// This method will fail if the object you provide cannot be serialized
- /// into a query string.
- fn query<T: serde::Serialize + ?Sized>(&mut self, query: &T) -> &mut Self {
- self.update(|builder| builder.query(query))
- }
-
- /// Send a form body.
- ///
- /// Sets the body to the url encoded serialization of the passed value,
- /// and also sets the `Content-Type: application/x-www-form-urlencoded`
- /// header.
- ///
- /// # Errors
- ///
- /// This method fails if the passed value cannot be serialized into
- /// url encoded format
- fn form<T: Serialize + ?Sized>(&mut self, form: &T) -> &mut Self {
- self.update(|builder| builder.form(form))
- }
-
- /// Send a JSON body.
- ///
- /// # Optional
- ///
- /// This requires the optional `json` feature enabled.
- ///
- /// # Errors
- ///
- /// Serialization can fail if `T`'s implementation of [`Serialize`] decides
- /// to fail, or if `T` contains a map with non-string keys.
- fn json<T: serde::Serialize + ?Sized>(&mut self, json: &T) -> &mut Self {
- self.try_update(|builder| {
- let (client, req) = builder.build_split();
- let mut req = req?;
- if !req.headers().contains_key(CONTENT_TYPE) {
- req.headers_mut()
- .insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
- }
- *req.body_mut() = Some(serde_json::to_string(json).map_err(Error::Json)?.into());
- Ok(reqwest::RequestBuilder::from_parts(client, req))
- })
- }
-
- /// Send a Postcard body.
- ///
- /// # Errors
- ///
- /// This method fails if the passed value cannot be serialized into
- /// postcard format
- fn postcard<T: serde::Serialize + ?Sized>(&mut self, postcard: &T) -> &mut Self {
- self.try_update(|builder| {
- let (client, req) = builder.build_split();
- let mut req = req?;
- if !req.headers().contains_key(CONTENT_TYPE) {
- req.headers_mut().insert(
- CONTENT_TYPE,
- HeaderValue::from_static("application/x-postcard"),
- );
- }
-
- *req.body_mut() = Some(
- postcard::to_allocvec(postcard)
- .map_err(Error::Postcard)?
- .into(),
- );
- Ok(reqwest::RequestBuilder::from_parts(client, req))
- })
- }
-}
-/// Merges two [`http::Extensions`] instances.
-///
-/// If the source extensions are empty, nothing happens.
-/// If the destination extensions are empty, they are replaced with the source.
-/// Otherwise, the source extensions are added to the destination.
-fn merge_extensions(dst: &mut http::Extensions, src: http::Extensions) {
- if dst.is_empty() {
- *dst = src;
- } else {
- dst.extend(src);
- }
-}
-
-/// Handles a request locally using the provided router.
-///
-/// This function creates a local HTTP request from the given [`Request`] and
-/// processes it through the provided [`axum::Router`]. It handles WebSocket
-/// upgrades and properly manages extensions between the request and response.
-///
-/// # Arguments
-///
-/// * `req` - The request to be processed locally
-/// * `ext` - Extensions to be merged with the request
-/// * `router` - The Axum router that will handle the request
-/// * `timeout` - Maximum duration to wait for the router to process the request
-///
-/// # Returns
-///
-/// A [`reqwest::Response`] containing the response from the router
-///
-/// # Errors
-///
-/// Returns an error if:
-/// * The request conversion fails
-/// * The router times out while processing the request
-#[inline(never)]
-pub async fn passthrough_local(
- req: Request,
- ext: &mut http::Extensions,
- router: axum::Router,
- timeout: Duration,
-) -> Result<reqwest::Response> {
- let url = req.url().clone();
- let mut req: http::Request<_> = req.try_into()?;
- merge_extensions(req.extensions_mut(), mem::take(ext));
-
- let mut server_tx: Option<upgrade::Promise> = None;
- if req.headers().contains_key(header::UPGRADE) {
- server_tx = Some(upgrade::OnUpgrade::pending_at(req.extensions_mut()));
- }
-
- let (mut parts, body) = tokio::time::timeout(timeout, router.oneshot(req))
- .await
- .map_err(|_| Error::LocalTimeout)?
- .expect("infallible")
- .into_parts();
-
- let mut builder = http::response::Builder::new();
- {
- *builder.headers_mut().unwrap() = mem::take(&mut parts.headers);
-
- let ext = builder.extensions_mut().unwrap();
- merge_extensions(ext, mem::take(&mut parts.extensions));
-
- if let Some(server_tx) = server_tx {
- let cli_tx = upgrade::OnUpgrade::pending_at(ext);
- if parts.status == http::StatusCode::SWITCHING_PROTOCOLS {
- let (client_io, server_io) = tokio::io::duplex(1024);
- let _ = cli_tx.send(Ok(upgrade::Upgraded::custom(client_io)));
- let _ = server_tx.send(Ok(upgrade::Upgraded::custom(server_io)));
- } else {
- let _ = cli_tx.send(Err(upgrade::UpgradeError::Cancelled));
- let _ = server_tx.send(Err(upgrade::UpgradeError::Cancelled));
- }
- }
- }
-
- let response = builder
- .status(parts.status)
- .version(parts.version)
- .url(url)
- .body(reqwest::Body::wrap_stream(body.into_data_stream()))
- .unwrap();
- Ok(reqwest::Response::from(response))
-}
-
-/// This is a wrapper around [`reqwest::RequestBuilder`] exposing the same API.
-#[derive(Debug)]
-#[must_use = "RequestBuilder does nothing until you 'send' it"]
-pub struct RequestBuilder<'a> {
- inner: Result<reqwest::RequestBuilder>,
- extensions: http::Extensions,
- router: &'a RouterTable,
-}
-
-impl ReqwestBuilder for RequestBuilder<'_> {
- fn parts(&mut self) -> (&mut Result<reqwest::RequestBuilder>, &mut http::Extensions) {
- (&mut self.inner, &mut self.extensions)
- }
-}
-
-impl<'a> RequestBuilder<'a> {
- /// Build a [`Request`], which can be inspected, modified and executed with
- /// [`Client::execute()`].
- ///
- /// This is similar to [`RequestBuilder::build()`], but also returns the
- /// embedded [`Client`].
- pub fn build_split(&mut self) -> Result<(Client<'a>, Request)> {
- let Self { inner, router, .. } = self;
- let (inner, req) = mem::replace(inner, Err(Error::RequestAlreadyBuilt))?.build_split();
- let client = Client { inner, router };
- Ok((client, req?))
- }
-
- /// Constructs the Request and sends it to the target URL, returning a
- /// future Response.
- ///
- /// # Errors
- ///
- /// This method fails if there was an error while sending request,
- /// redirect loop was detected or redirect limit was exhausted.
- pub async fn send(&mut self) -> Result<reqwest::Response> {
- let (client, req) = self.build_split()?;
- client
- .execute_with_extensions(req, &mut self.extensions, false)
- .await
- }
-
- /// Attempt to clone the [`RequestBuilder`].
- ///
- /// [`None`] is returned if the [`RequestBuilder`] can not be cloned,
- /// i.e. if the request body is a stream.
- pub fn try_clone(&self) -> Option<Self> {
- self
- .inner
- .as_ref()
- .ok()
- .and_then(|i| i.try_clone())
- .map(|inner| Self {
- inner: Ok(inner),
- extensions: self.extensions.clone(),
- router: self.router,
- })
- }
-}
-
-/// This is a wrapper around [`reqwest::RequestBuilder`] building a
-/// [`Websocket`] instead.
-#[derive(Debug)]
-#[must_use = "WebsocketBuilder does nothing until you 'send' it"]
-pub struct WebsocketBuilder<'a> {
- inner: Result<reqwest::RequestBuilder>,
- extensions: http::Extensions,
- websocket_config: ws::WebSocketConfig,
- protocols: Vec<SmolStr>,
- router: &'a RouterTable,
-}
-
-/// Wrapper around [`reqwest::RequestBuilder`] building a Server-Sent Events
-/// stream.
-#[derive(Debug)]
-#[must_use = "SseBuilder does nothing until you 'send' it"]
-pub struct SseBuilder<'a> {
- inner: Result<reqwest::RequestBuilder>,
- extensions: http::Extensions,
- router: &'a RouterTable,
-}
-
-impl ReqwestBuilder for SseBuilder<'_> {
- fn parts(&mut self) -> (&mut Result<reqwest::RequestBuilder>, &mut http::Extensions) {
- (&mut self.inner, &mut self.extensions)
- }
-}
-
-impl<'a> From<RequestBuilder<'a>> for SseBuilder<'a> {
- fn from(req: RequestBuilder<'a>) -> Self {
- Self {
- inner: req.inner,
- extensions: req.extensions,
- router: req.router,
- }
- }
-}
-
-impl<'a> SseBuilder<'a> {
- pub fn build_split(&mut self) -> Result<(Client<'a>, Request)> {
- let Self { inner, router, .. } = self;
- let (inner, req) = mem::replace(inner, Err(Error::RequestAlreadyBuilt))?.build_split();
- let client = Client { inner, router };
- Ok((client, req?))
- }
-
- pub async fn send(&mut self) -> Result<sse::Response> {
- let (client, mut req) = self.build_split()?;
-
- req.headers_mut().insert(
- reqwest::header::ACCEPT,
- HeaderValue::from_static(sse::EventFormat::accepted_mimes()),
- );
-
- let response = client
- .execute_with_extensions(req, &mut self.extensions, true)
- .await?;
-
- Ok(sse::Response { response })
- }
-
- pub fn try_clone(&self) -> Option<Self> {
- self
- .inner
- .as_ref()
- .ok()
- .and_then(|i| i.try_clone())
- .map(|inner| Self {
- inner: Ok(inner),
- extensions: self.extensions.clone(),
- router: self.router,
- })
- }
-}
-
-impl ReqwestBuilder for WebsocketBuilder<'_> {
- fn parts(&mut self) -> (&mut Result<reqwest::RequestBuilder>, &mut http::Extensions) {
- (&mut self.inner, &mut self.extensions)
- }
-}
-
-impl<'a> From<RequestBuilder<'a>> for WebsocketBuilder<'a> {
- fn from(req: RequestBuilder<'a>) -> Self {
- Self {
- inner: req.inner,
- extensions: req.extensions,
- websocket_config: ws::WebSocketConfig::default(),
- protocols: Default::default(),
- router: req.router,
- }
- }
-}
-
-impl ws::ConfigBuilder for WebsocketBuilder<'_> {
- fn websocket_config(&mut self) -> &mut ws::WebSocketConfig {
- &mut self.websocket_config
- }
- fn protocols(&mut self, extend: impl IntoIterator<Item: ToSmolStr>) -> &mut Self {
- self
- .protocols
- .extend(extend.into_iter().map(|x| x.to_smolstr()));
- self
- }
-}
-
-impl<'a> WebsocketBuilder<'a> {
- /// Build a [`Request`], which can be inspected, modified and executed with
- /// [`Client::execute()`].
- ///
- /// This is similar to [`RequestBuilder::build()`], but also returns the
- /// embedded [`Client`].
- pub fn build_split(&mut self) -> Result<(Client<'a>, Request)> {
- let Self { inner, router, .. } = self;
- let (inner, req) = mem::replace(inner, Err(Error::RequestAlreadyBuilt))?.build_split();
- let client = Client { inner, router };
- Ok((client, req?))
- }
-
- /// Constructs the Request and sends it to the target URL, returning a
- /// future Response.
- ///
- /// # Errors
- ///
- /// This method fails if there was an error while sending request,
- /// redirect loop was detected or redirect limit was exhausted.
- pub async fn send(&mut self) -> Result<ws::Response> {
- let (client, mut req) = self.build_split()?;
-
- // change the scheme from wss? to https?
- let url = req.url_mut();
- match url.scheme() {
- "ws" => {
- url.set_scheme("http")
- .expect("url should accept http scheme");
- },
- "wss" => {
- url.set_scheme("https")
- .expect("url should accept https scheme");
- },
- _ => {},
- }
-
- let nonce = ws::Nonce::new();
- *req.version_mut() = Version::HTTP_11;
- let headers = req.headers_mut();
- headers.insert(
- reqwest::header::CONNECTION,
- HeaderValue::from_static("upgrade"),
- );
- headers.insert(
- reqwest::header::UPGRADE,
- HeaderValue::from_static("websocket"),
- );
- headers.insert(
- reqwest::header::SEC_WEBSOCKET_KEY,
- HeaderValue::from_str(&nonce.key).expect("nonce is a invalid header value"),
- );
- headers.insert(
- reqwest::header::SEC_WEBSOCKET_VERSION,
- HeaderValue::from_static("13"),
- );
- if !self.protocols.is_empty() {
- headers.insert(
- reqwest::header::SEC_WEBSOCKET_PROTOCOL,
- HeaderValue::from_str(&self.protocols.join(", "))
- .expect("protocols is an invalid header value"),
- );
- }
-
- let response = client
- .execute_with_extensions(req, &mut self.extensions, true)
- .await?;
-
- Ok(ws::Response {
- response,
- nonce,
- protocols: mem::take(&mut self.protocols),
- websocket_config: Some(self.websocket_config),
- })
- }
-
- /// Attempt to clone the [`RequestBuilder`].
- ///
- /// [`None`] is returned if the [`RequestBuilder`] can not be cloned,
- /// i.e. if the request body is a stream.
- pub fn try_clone(&self) -> Option<Self> {
- self
- .inner
- .as_ref()
- .ok()
- .and_then(|i| i.try_clone())
- .map(|inner| Self {
- inner: Ok(inner),
- extensions: self.extensions.clone(),
- websocket_config: self.websocket_config,
- protocols: self.protocols.clone(),
- router: self.router,
- })
- }
-}
-
-#[cfg(test)]
-mod tests {
- use std::{
- sync::atomic::{AtomicUsize, Ordering},
- time::Duration,
- };
-
- use axum::{Router, routing::get};
-
- use super::*;
- use crate::http::server::SharedRouter;
-
- #[tokio::test]
- async fn retry_after_extension_present() {
- let app = SharedRouter::new(Router::new().route(
- "/retry",
- get(|| async {
- axum::response::Response::builder()
- .status(axum::http::StatusCode::TOO_MANY_REQUESTS)
- .header("Retry-After", "3")
- .body(axum::body::Body::empty())
- .unwrap()
- }),
- ));
- app.bind(["retry.local"]);
-
- let resp = Client::global()
- .get("https://retry.local/retry")
- .send()
- .await
- .expect("request failed");
-
- let ext = resp.extensions().get::<utils::RetryAfter>().copied();
- assert_eq!(ext.map(|r| r.0), Some(Duration::from_secs(3)));
- }
-
- #[tokio::test]
- async fn retry_after_auto_retries() {
- static CALLS: AtomicUsize = AtomicUsize::new(0);
-
- let app = SharedRouter::new(Router::new().route(
- "/retry-auto",
- get(|| async {
- if CALLS.fetch_add(1, Ordering::SeqCst) == 0 {
- axum::response::Response::builder()
- .status(axum::http::StatusCode::SERVICE_UNAVAILABLE)
- .header("Retry-After", "0")
- .body(axum::body::Body::empty())
- .unwrap()
- } else {
- axum::response::Response::builder()
- .status(axum::http::StatusCode::OK)
- .body(axum::body::Body::from("ok"))
- .unwrap()
- }
- }),
- ));
- app.bind(["retry-auto.local"]);
-
- let body = Client::global()
- .get("https://retry-auto.local/retry-auto")
- .send()
- .await
- .expect("request failed")
- .text()
- .await
- .expect("read body");
-
- assert_eq!(body, "ok");
- assert!(CALLS.load(Ordering::SeqCst) >= 2);
- }
-}
diff --git a/attic/net/http/mod.rs b/attic/net/http/mod.rs
deleted file mode 100644
index d4491c9d8..000000000
@@ -1,6 +0,0 @@
-pub mod client;
-pub mod server;
-pub mod sse;
-pub mod upgrade;
-pub mod utils;
-pub mod ws;
diff --git a/attic/net/http/server.rs b/attic/net/http/server.rs
deleted file mode 100644
index 12b0c4e18..000000000
@@ -1,407 +0,0 @@
-use std::{
- borrow::{Borrow, ToOwned},
- fmt, hash,
- mem::MaybeUninit,
- ptr,
- sync::{Arc, LazyLock, Weak},
-};
-
-use ahash::{AHashMap, AHashSet};
-use axum::{
- Router,
- body::Body,
- extract::{FromRequestParts, Request},
- handler::{Handler, HandlerService, HandlerWithoutStateExt},
- response::IntoResponse,
- routing::IntoMakeService,
- serve::Serve,
-};
-use axum_extra::extract::Host;
-use bon::Builder;
-use futures::FutureExt;
-use parking_lot::RwLock;
-use smallvec::SmallVec;
-use smol_str::ToSmolStr;
-use tower::ServiceExt;
-
-use crate::replica::Hostname;
-
-/// A case-insensitive string slice, used for case-insensitive host name
-/// lookups. This is a transparent wrapper around [`str`] that implements
-/// case-insensitive hashing and equality.
-#[repr(transparent)]
-pub struct IStr(str);
-
-impl fmt::Debug for IStr {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.debug_tuple("IStr").field(&self.as_str()).finish()
- }
-}
-
-impl IStr {
- /// Converts a string slice to an [`IStr`] reference.
- ///
- /// This is a zero-cost conversion since [`IStr`] is `repr(transparent)`.
- #[inline]
- const fn from_str(s: &str) -> &Self {
- // SAFETY: IStr is repr(transparent) around str
- unsafe { &*(ptr::from_ref::<str>(s) as *const Self) }
- }
-
- /// Converts an [`IStr`] reference back to a string slice.
- ///
- /// This is a zero-cost conversion since [`IStr`] is `repr(transparent)`.
- #[inline]
- const fn as_str(&self) -> &str {
- // SAFETY: IStr is repr(transparent) around str
- unsafe { &*(ptr::from_ref::<Self>(self) as *const str) }
- }
-}
-
-impl hash::Hash for IStr {
- /// Implements case-insensitive hashing by converting all ASCII characters
- /// to lowercase during hashing.
- fn hash<H: hash::Hasher>(&self, state: &mut H) {
- let mut chunks = self.as_str().as_bytes();
- while let Some((qc, rem)) = chunks.split_first_chunk() {
- let q = u64::from_le_bytes(*qc);
- state.write_u64(q | 0x2020202020202020);
- chunks = rem;
- }
- for c in chunks {
- state.write_u8(*c | 0x20);
- }
- }
-}
-impl Eq for IStr {}
-impl PartialEq for IStr {
- /// Implements case-insensitive equality comparison.
- fn eq(&self, other: &Self) -> bool {
- self.as_str().eq_ignore_ascii_case(other.as_str())
- }
-}
-
-impl ToOwned for IStr {
- type Owned = IString;
- fn to_owned(&self) -> Self::Owned {
- IString(self.as_str().into())
- }
-}
-
-/// An owned case-insensitive string, used for case-insensitive host name
-/// storage. This is a wrapper around [`String`] that implements
-/// case-insensitive hashing and equality.
-#[derive(Clone, derive_more::Debug, derive_more::From, derive_more::Into)]
-pub struct IString(Hostname);
-
-impl IString {
- /// Returns a reference to the underlying [`IStr`].
- fn as_istr(&self) -> &IStr {
- IStr::from_str(&self.0)
- }
-}
-
-impl hash::Hash for IString {
- /// Delegates to the case-insensitive hashing implementation of [`IStr`].
- fn hash<H: hash::Hasher>(&self, state: &mut H) {
- self.as_istr().hash(state);
- }
-}
-
-impl Eq for IString {}
-impl PartialEq for IString {
- /// Implements case-insensitive equality comparison.
- fn eq(&self, other: &Self) -> bool {
- self.as_istr().eq(other.as_istr())
- }
-}
-
-impl Borrow<IStr> for IString {
- fn borrow(&self) -> &IStr {
- self.as_istr()
- }
-}
-
-/// A thread-safe reference-counted handle to an Axum [`Router`].
-///
-/// Internally the router is stored in an [`Arc`] wrapped [`RwLock`] so that it
-/// can be mutated in place while still being shared across threads. See the
-/// individual methods for the available operations.
-#[derive(Debug, Default, Clone)]
-#[repr(transparent)]
-pub struct SharedRouter(Arc<RwLock<Router>>);
-
-impl From<Router> for SharedRouter {
- fn from(router: Router) -> Self {
- Self::new(router)
- }
-}
-
-impl From<Arc<RwLock<Router>>> for SharedRouter {
- fn from(router: Arc<RwLock<Router>>) -> Self {
- Self(router)
- }
-}
-
-impl SharedRouter {
- /// Creates a new [`SharedRouter`] from the given [`Router`].
- ///
- /// The router is wrapped in an [`Arc`] + [`RwLock`], making the returned
- /// value cheap to clone and safe to use from multiple threads.
- pub fn new(router: Router) -> Self {
- Self(Arc::new(RwLock::new(router)))
- }
- /// Applies the provided closure to the inner [`Router`] while holding the
- /// write-side of the [`RwLock`].
- ///
- /// This allows updating the router in place (e.g. adding routes) without
- /// having to reconstruct the surrounding [`SharedRouter`].
- pub fn update(&self, f: impl FnOnce(Router) -> Router) {
- let mut write = self.0.write();
- write.update(f);
- }
- /// Downgrades this [`SharedRouter`] to a [`WeakRouter`].
- ///
- /// The weak reference does not contribute to the [`Arc`] strong count and
- /// can later be [`WeakRouter::upgrade`]d back to a strong reference.
- pub fn downgrade(&self) -> WeakRouter {
- WeakRouter(Arc::downgrade(&self.0))
- }
- /// Clones and returns the inner [`Router`].
- ///
- /// The read-side of the lock is taken to obtain a snapshot of the router.
- pub fn read(&self) -> Router {
- self.0.read().clone()
- }
-
- /// Registers the router for the given list of host names into the given
- /// router table.
- pub fn bind_in(&self, hosts: impl IntoIterator<Item: ToSmolStr>, table: &RouterTable) {
- table.bind(hosts, self);
- }
-
- /// Removes the router from the given router table
- pub fn unbind_in(&self, hosts: impl IntoIterator<Item: AsRef<str>>, table: &RouterTable) {
- table.retain(hosts, |_, r| !Self::ptr_eq(&r, self));
- }
-
- /// Registers the router for the given list of host names into the global
- /// router table.
- pub fn bind(&self, hosts: impl IntoIterator<Item: ToSmolStr>) {
- self.bind_in(hosts, RouterTable::global());
- }
-
- /// Removes the router from the global router table.
- pub fn unbind(&self, hosts: impl IntoIterator<Item: AsRef<str>>) {
- self.unbind_in(hosts, RouterTable::global());
- }
-
- /// Checks if two [`SharedRouter`] instances point to the same underlying
- /// [`Router`].
- #[inline]
- pub fn ptr_eq(a: &Self, b: &Self) -> bool {
- Arc::ptr_eq(&a.0, &b.0)
- }
-}
-
-/// A non-owning weak reference to a [`SharedRouter`].
-///
-/// The reference can be upgraded back to a strong [`SharedRouter`] using
-/// [`WeakRouter::upgrade`]. It becomes invalid once the last strong reference
-/// is dropped.
-#[derive(Debug, Default, Clone)]
-pub struct WeakRouter(Weak<RwLock<Router>>);
-
-impl WeakRouter {
- /// Attempts to upgrade the weak reference to a [`SharedRouter`].
- ///
- /// Returns `Some(shared)` if the router is still alive, or `None`
- /// otherwise.
- pub fn upgrade(&self) -> Option<SharedRouter> {
- self.0.upgrade().map(SharedRouter)
- }
-}
-
-/// A thread-safe table that maps host names to Axum routers.
-///
-/// This is used to route requests to the appropriate handler based on the host
-/// name. The host names are compared case-insensitively.
-#[derive(Debug, Default, Clone)]
-pub struct RouterTable {
- routers: Arc<RwLock<AHashMap<IString, WeakRouter>>>,
-}
-
-impl RouterTable {
- /// Returns the global router table singleton as an `Arc`.
- pub fn global() -> &'static Self {
- static GLOBAL_TABLE: LazyLock<RouterTable> = LazyLock::new(Default::default);
- &GLOBAL_TABLE
- }
-
- /// Binds a router for the given host.
- ///
- /// If a router was already registered for this host, it will be replaced.
- pub fn bind(&self, hosts: impl IntoIterator<Item: ToSmolStr>, router: &SharedRouter) {
- for host in hosts {
- self.routers.write().insert(
- IString(host.to_smolstr()),
- SharedRouter::downgrade(router),
- );
- }
- }
-
- /// Retains the router for the given host if the predicate returns `true`.
- ///
- /// If the predicate returns `false`, the router is removed from the table.
- pub fn retain(
- &self,
- hosts: impl IntoIterator<Item: AsRef<str>>,
- f: impl Fn(&str, SharedRouter) -> bool,
- ) {
- let mut write = self.routers.write();
- for host in hosts {
- let istr = IStr::from_str(host.as_ref());
- let entry = write.get(istr);
- if let Some(entry) = entry {
- let retain = entry.upgrade().is_some_and(|r| f(istr.as_str(), r));
- if !retain {
- write.remove(istr);
- }
- }
- }
- }
-
- /// Looks up a router by host name.
- ///
- /// The lookup is case-insensitive.
- pub fn lookup(&self, host: &str) -> Option<Router> {
- self
- .routers
- .read()
- .get(IStr::from_str(host))
- .and_then(|r| r.upgrade())
- .map(|r| r.read())
- }
-
- /// Serves the router table on the given listener.
- #[allow(clippy::type_complexity)]
- pub fn serve<L>(
- self,
- listener: L,
- ) -> Serve<L, IntoMakeService<HandlerService<Self, (), ()>>, HandlerService<Self, (), ()>>
- where
- L: axum::serve::Listener,
- {
- axum::serve(listener, self.into_make_service())
- }
-}
-
-/// Implements an axum handler for the router table.
-impl Handler<(), ()> for RouterTable {
- type Future = futures::future::BoxFuture<'static, http::Response<Body>>;
- fn call(self, req: Request, state: ()) -> Self::Future {
- async move {
- let (mut parts, body) = req.into_parts();
- let host = Host::from_request_parts(&mut parts, &state)
- .await
- .map(|h| h.0)
- .unwrap_or_default();
-
- // Split host and port if a port is specified
- let host = if let Some(idx) = host.find(':') {
- host.split_at(idx).0
- } else {
- host.as_str()
- };
-
- if let Some(router) = self.lookup(host) {
- let req = Request::from_parts(parts, body);
- router.oneshot(req).await.expect("infallible")
- } else {
- (http::StatusCode::NOT_FOUND, format!("Not found: {host}")).into_response()
- }
- }
- .boxed()
- }
-}
-
-/// Serves the global router table on the given listener.
-///
-/// This is a convenience function that delegates to
-/// `RouterTable::global().serve()`.
-#[allow(clippy::type_complexity)]
-pub fn serve<L>(
- listener: L,
-) -> Serve<
- L,
- IntoMakeService<HandlerService<RouterTable, (), ()>>,
- HandlerService<RouterTable, (), ()>,
->
-where
- L: axum::serve::Listener,
-{
- RouterTable::global().clone().serve(listener)
-}
-
-/// Convenience wrapper that owns a [`SharedRouter`] and manages its
-/// registration in one or more [`RouterTable`] instances.
-///
-/// The type tracks the set of host names the router should be reachable under
-/// and exposes ergonomic helper methods to mutate the router or extend the
-/// host list.
-#[derive(Debug, Clone, Builder)]
-pub struct Server {
- #[builder(default = AHashSet::default())]
- hosts: AHashSet<Hostname>,
- #[builder(default = SmallVec::from_iter([RouterTable::global().clone()]))]
- rtables: SmallVec<[RouterTable; 1]>,
- #[builder(default = SharedRouter::new(Router::new()))]
- router: SharedRouter,
-}
-
-impl Server {
- /// Returns a reference to the inner [`SharedRouter`].
- pub const fn router(&self) -> &SharedRouter {
- &self.router
- }
-
- /// Mutates the inner [`Router`] by applying the given closure.
- ///
- /// This is delegated to [`SharedRouter::update`].
- pub fn update(&mut self, f: impl FnOnce(Router) -> Router) {
- self.router.update(f);
- }
-
- /// Nests the provided `router` under the given `path` on the inner router.
- ///
- /// Internally this calls [`Router::nest`].
- pub fn nest(&mut self, path: &str, router: Router) {
- self.router.update(|r| r.nest(path, router));
- }
-
- /// Registers an additional `host` for this server.
- ///
- /// The new host name is propagated to all known [`RouterTable`]s so that
- /// the server becomes reachable under the added host name immediately.
- pub fn add_host<H: ToSmolStr + ?Sized>(&mut self, host: &H) {
- if self.hosts.insert(host.to_smolstr()) {
- for table in &mut self.rtables {
- table.bind(self.hosts.iter().cloned(), &self.router);
- }
- }
- }
-}
-
-trait RouterExt {
- fn update(&mut self, f: impl FnOnce(Self) -> Self) -> &mut Self
- where
- Self: Sized,
- {
- unsafe {
- let tmp = &mut *ptr::from_mut::<Self>(self).cast::<MaybeUninit<Self>>();
- tmp.write(f(tmp.assume_init_read()));
- }
- self
- }
-}
-impl RouterExt for Router {}
diff --git a/attic/net/http/sse/mod.rs b/attic/net/http/sse/mod.rs
deleted file mode 100644
index ca693fc88..000000000
@@ -1,12 +0,0 @@
-pub mod request;
-pub mod response;
-pub mod stream;
-pub mod types;
-
-pub use request::Request;
-pub use response::Response;
-pub use stream::{EventFormat, Incoming, Outgoing};
-pub use types::{Error, Event, Result, Value};
-
-#[cfg(test)]
-pub mod tests;
diff --git a/attic/net/http/sse/request.rs b/attic/net/http/sse/request.rs
deleted file mode 100644
index cd311b0f3..000000000
@@ -1,74 +0,0 @@
-use std::{
- borrow::Borrow,
- convert::Infallible,
- future::{self, Future},
- ops::Deref,
-};
-
-use futures::{Stream, StreamExt};
-use http::request::Parts;
-
-use super::Event;
-use crate::http::{sse, utils, utils::Result};
-
-#[derive(Debug)]
-pub struct Request {
- pub request: Option<Parts>,
-}
-
-impl Request {
- pub const fn new(request: Parts) -> Self {
- Self {
- request: Some(request),
- }
- }
-
- pub fn stream<S>(
- self,
- events: S,
- ) -> Result<
- sse::Outgoing<impl Stream<Item = Result<impl Borrow<Event>, anyhow::Error>> + Send + 'static>,
- >
- where
- S: Stream<Item: Borrow<Event>> + Send + 'static,
- {
- self.try_stream(events.map(anyhow::Ok))
- }
-
- pub fn try_stream<S, I, E>(mut self, events: S) -> Result<sse::Outgoing<S>>
- where
- S: Stream<Item = Result<I, E>> + Send + 'static,
- I: Borrow<Event>,
- E: Into<axum::BoxError>,
- {
- let parts = self.request.take().expect("request used after finalized");
- let format = utils::negotiate_content_type(&parts.headers, &[
- sse::EventFormat::NdJson,
- sse::EventFormat::Dom,
- ])?;
- Ok(sse::Outgoing::new(events).with_format(format))
- }
-
- pub fn into_inner(self) -> Parts {
- self.request.expect("request used after finalized")
- }
-}
-
-impl<S> axum::extract::FromRequestParts<S> for Request {
- type Rejection = Infallible;
-
- fn from_request_parts(
- parts: &mut Parts,
- _state: &S,
- ) -> impl Future<Output = Result<Self, Self::Rejection>> + Send {
- let result = Self::new(parts.clone());
- future::ready(Ok(result))
- }
-}
-
-impl Deref for Request {
- type Target = Parts;
- fn deref(&self) -> &Self::Target {
- self.request.as_ref().expect("request used after finalized")
- }
-}
diff --git a/attic/net/http/sse/response.rs b/attic/net/http/sse/response.rs
deleted file mode 100644
index e182610c3..000000000
@@ -1,31 +0,0 @@
-use http::StatusCode;
-
-use super::{Incoming, Result};
-use crate::http::{sse::EventFormat, utils};
-
-#[derive(Debug)]
-pub struct Response {
- pub response: reqwest::Response,
-}
-
-impl Response {
- pub async fn stream(
- self,
- ) -> Result<Incoming<impl futures::Stream<Item = reqwest::Result<bytes::Bytes>>>> {
- if self.response.status() != StatusCode::OK {
- return Err(utils::read_remote_error(self.response).await.into());
- }
-
- let format = utils::match_content_type(self.response.headers(), &[
- EventFormat::NdJson,
- EventFormat::Dom,
- ])?;
-
- let stream = self.response.bytes_stream();
- Ok(Incoming::new(stream).with_format(format))
- }
-
- pub fn into_inner(self) -> reqwest::Response {
- self.response
- }
-}
diff --git a/attic/net/http/sse/stream.rs b/attic/net/http/sse/stream.rs
deleted file mode 100644
index 42e3a2d33..000000000
@@ -1,252 +0,0 @@
-use std::{
- borrow::Borrow,
- pin::Pin,
- task::{Context, Poll},
- time::Duration,
-};
-
-use axum::response::IntoResponse;
-use bytes::{Bytes, BytesMut};
-use futures::Stream;
-use http_body::Frame;
-use pin_project_lite::pin_project;
-use tokio::time::{self, Instant};
-
-use super::{Error, Event, Result};
-use crate::http::utils::ToMime;
-
-#[derive(
- Debug, Clone, Copy, PartialEq, Eq, Hash, strum::EnumString, strum::IntoStaticStr, strum::EnumIter,
-)]
-pub enum EventFormat {
- #[strum(serialize = "text/event-stream")]
- Dom,
- #[strum(serialize = "application/x-ndjson")]
- NdJson,
-}
-
-impl EventFormat {
- pub const fn accepted_mimes() -> &'static str {
- "application/x-ndjson, text/event-stream"
- }
-}
-
-impl ToMime for EventFormat {
- fn to_mime(&self) -> &'static str {
- self.into()
- }
-}
-
-pin_project! {
- pub struct Incoming<S> {
- #[pin]
- inner: S,
- buffer: BytesMut,
- format: EventFormat,
- }
-}
-
-impl<S> Incoming<S> {
- pub fn new(stream: S) -> Self {
- Self {
- inner: stream,
- buffer: BytesMut::new(),
- format: EventFormat::Dom,
- }
- }
- pub const fn with_format(mut self, format: EventFormat) -> Self {
- self.format = format;
- self
- }
-}
-
-pin_project! {
- struct Keepalive {
- t0: Option<(Instant, Duration)>,
- #[pin]
- sleep: Option<tokio::time::Sleep>,
- }
-}
-
-impl From<Option<Duration>> for Keepalive {
- fn from(interval: Option<Duration>) -> Self {
- Self {
- t0: interval.map(|i| (Instant::now(), i)),
- sleep: None,
- }
- }
-}
-
-impl Default for Keepalive {
- fn default() -> Self {
- Self {
- t0: Some((Instant::now(), Self::DEFAULT_INTERVAL)),
- sleep: None,
- }
- }
-}
-
-impl Keepalive {
- const DEFAULT_INTERVAL: Duration = Duration::from_secs(30);
-
- pub const fn enabled(&self) -> bool {
- self.t0.is_some()
- }
-
- pub fn reset(self: Pin<&mut Self>) {
- let Some((_, interval)) = self.t0 else {
- return;
- };
-
- let mut this = self.project();
- let now = Instant::now();
- if this.sleep.is_none() {
- *this.t0 = Some((now, interval));
- this.sleep.set(Some(time::sleep_until(now + interval)));
- } else {
- this.sleep.as_pin_mut().unwrap().reset(now + interval);
- }
- }
-
- pub fn poll_tick(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
- let Some((t0, interval)) = self.t0 else {
- panic!("keepalive is disabled, should not have called poll_tick");
- };
-
- let mut this = self.project();
- let now = Instant::now();
- if this.sleep.is_none() {
- let elapsed = now.saturating_duration_since(t0);
- if let Some(rem) = interval.checked_sub(elapsed) {
- this.sleep.set(Some(time::sleep_until(now + rem)));
- return Poll::Pending;
- }
- this.sleep.set(Some(time::sleep_until(now + interval)));
- return Poll::Ready(());
- }
-
- let Some(mut sleep) = this.sleep.as_pin_mut() else {
- unreachable!("sleep is not None");
- };
- match sleep.as_mut().poll(cx) {
- Poll::Pending => Poll::Pending,
- Poll::Ready(()) => {
- sleep.reset(now + interval);
- Poll::Ready(())
- },
- }
- }
-}
-
-pin_project! {
- pub struct Outgoing<S> {
- #[pin]
- inner: S,
- format: EventFormat,
- #[pin]
- keepalive: Keepalive,
- }
-}
-
-impl<S> Outgoing<S> {
- pub fn new(stream: S) -> Self {
- Self {
- inner: stream,
- format: EventFormat::Dom,
- keepalive: Keepalive::default(),
- }
- }
- pub const fn with_format(mut self, format: EventFormat) -> Self {
- self.format = format;
- self
- }
- pub fn with_keepalive(mut self, interval: Option<Duration>) -> Self {
- self.keepalive = interval.into();
- self
- }
-}
-
-impl<S> IntoResponse for Outgoing<S>
-where
- Self: http_body::Body<Data = Bytes, Error: Into<axum::BoxError>>,
- S: Send + 'static,
-{
- fn into_response(self) -> axum::response::Response {
- (
- [
- (http::header::CONTENT_TYPE, self.format.to_mime()),
- (http::header::CACHE_CONTROL, "no-cache"),
- ],
- axum::body::Body::new(self),
- )
- .into_response()
- }
-}
-
-impl<S> Stream for Incoming<S>
-where
- S: Stream<Item = reqwest::Result<Bytes>>,
-{
- type Item = Result<Event>;
-
- fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
- let mut this = self.project();
- loop {
- match Event::parse_body(this.buffer, *this.format) {
- Ok(Some(event)) => return Poll::Ready(Some(Ok(event))),
- Err(e) => return Poll::Ready(Some(Err(e))),
- Ok(None) => {},
- }
- match futures::ready!(this.inner.as_mut().poll_next(cx)) {
- Some(Ok(chunk)) => this.buffer.extend_from_slice(&chunk),
- Some(Err(e)) => return Poll::Ready(Some(Err(Error::Reqwest(e)))),
- None => {
- if this.buffer.is_empty() {
- return Poll::Ready(None);
- }
- match Event::parse_body(this.buffer, *this.format) {
- Ok(Some(event)) => return Poll::Ready(Some(Ok(event))),
- Ok(None) => return Poll::Ready(None),
- Err(e) => return Poll::Ready(Some(Err(e))),
- }
- },
- }
- }
- }
-}
-
-impl<S, I, E> http_body::Body for Outgoing<S>
-where
- I: Borrow<Event>,
- S: Stream<Item = Result<I, E>>,
- E: Into<axum::BoxError>,
-{
- type Data = Bytes;
- type Error = E;
-
- fn poll_frame(
- self: Pin<&mut Self>,
- cx: &mut Context<'_>,
- ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
- let this = self.project();
-
- match this.inner.poll_next(cx) {
- Poll::Pending => {
- if this.keepalive.enabled() {
- this
- .keepalive
- .poll_tick(cx)
- .map(|()| Some(Ok(Frame::data(Event::EMPTY.to_body(*this.format)))))
- } else {
- Poll::Pending
- }
- },
- Poll::Ready(Some(Ok(e))) => {
- this.keepalive.reset();
- Poll::Ready(Some(Ok(Frame::data(e.borrow().to_body(*this.format)))))
- },
- Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(e))),
- Poll::Ready(None) => Poll::Ready(None),
- }
- }
-}
diff --git a/attic/net/http/sse/tests.rs b/attic/net/http/sse/tests.rs
deleted file mode 100644
index 603dcdfe5..000000000
@@ -1,119 +0,0 @@
-use std::{
- sync::atomic::{AtomicUsize, Ordering},
- time::Duration,
-};
-
-use axum::{Router, response::IntoResponse, routing::get};
-use futures::StreamExt;
-use tokio::time::timeout;
-
-use crate::http::{client, server::SharedRouter, sse, utils};
-
-#[tokio::test]
-async fn test_sse_communication() {
- let app = SharedRouter::new(Router::new().route("/sse", get(sse_handler)));
- app.bind(["sse-example.local"]);
-
- let mut stream = client::Client::global()
- .sse("https://sse-example.local/sse")
- .send()
- .await
- .expect("Failed to send SSE request")
- .stream()
- .await
- .expect("Failed to open SSE stream");
-
- let mut events = Vec::new();
- for _ in 0..2 {
- if let Ok(Some(evt)) = timeout(std::time::Duration::from_secs(1), stream.next()).await {
- let evt = evt.expect("Failed to read event");
- events.push(evt.text_data().unwrap_or_default().to_owned());
- } else {
- panic!("Timed out waiting for event");
- }
- }
-
- assert_eq!(events, ["one", "two"]);
-}
-
-async fn sse_handler(req: sse::Request) -> impl IntoResponse {
- let events = vec![
- sse::Event::new().with_raw_data("one"),
- sse::Event::new().with_raw_data("two"),
- ];
- let stream = tokio_stream::iter(events);
- req.stream(stream)
-}
-
-#[tokio::test]
-async fn test_sse_retry_after_error() {
- let app = SharedRouter::new(Router::new().route(
- "/sse",
- get(|| async {
- axum::response::Response::builder()
- .status(axum::http::StatusCode::SERVICE_UNAVAILABLE)
- .header("Retry-After", "5")
- .body(axum::body::Body::empty())
- .unwrap()
- }),
- ));
- app.bind(["sse-retry.local"]);
-
- let response = client::Client::global()
- .sse("https://sse-retry.local/sse")
- .send()
- .await
- .expect("send should succeed");
-
- let err = match response.stream().await {
- Ok(_) => panic!("expected failure"),
- Err(e) => e,
- };
-
- match err {
- sse::Error::Remote(utils::Error::ServerError { retry_after, .. }) => {
- assert_eq!(retry_after, Some(Duration::from_secs(5)));
- },
- other => panic!("unexpected error: {other:?}"),
- }
-}
-
-#[tokio::test]
-async fn test_sse_retry_automatically() {
- static CALLS: AtomicUsize = AtomicUsize::new(0);
-
- async fn handler(req: sse::Request) -> impl IntoResponse {
- if CALLS.fetch_add(1, Ordering::SeqCst) == 0 {
- axum::response::Response::builder()
- .status(axum::http::StatusCode::SERVICE_UNAVAILABLE)
- .header("Retry-After", "0")
- .body(axum::body::Body::empty())
- .unwrap()
- } else {
- let events = vec![sse::Event::new().with_raw_data("retry")];
- let stream = tokio_stream::iter(events);
- req.stream(stream).into_response()
- }
- }
-
- let app = SharedRouter::new(Router::new().route("/sse-auto", get(handler)));
- app.bind(["sse-auto.local"]);
-
- let mut stream = client::Client::global()
- .sse("https://sse-auto.local/sse-auto")
- .send()
- .await
- .expect("send")
- .stream()
- .await
- .expect("stream");
-
- let msg = timeout(Duration::from_secs(1), stream.next())
- .await
- .expect("timeout")
- .expect("option")
- .expect("event");
-
- assert_eq!(msg.text_data().unwrap_or_default(), "retry");
- assert!(CALLS.load(Ordering::SeqCst) >= 2);
-}
diff --git a/attic/net/http/sse/types.rs b/attic/net/http/sse/types.rs
deleted file mode 100644
index 0bc5b5662..000000000
@@ -1,789 +0,0 @@
-use std::{
- borrow::Borrow,
- collections::BTreeMap,
- fmt, mem,
- ops::Deref,
- str::{self, FromStr},
-};
-
-use bytes::{BufMut, Bytes, BytesMut};
-use bytes_utils::Str;
-use serde::{Deserialize, Serialize, de::MapAccess, ser::SerializeMap};
-use strum::{EnumCount, IntoEnumIterator};
-use thiserror::Error;
-
-use crate::http::{sse::stream::EventFormat, utils};
-
-#[derive(Default, Clone, Serialize, Deserialize, PartialEq, PartialOrd, Eq, Ord, Hash)]
-#[serde(transparent)]
-pub struct Value {
- inner: Str,
-}
-
-impl fmt::Debug for Value {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- let s: &str = &self.inner;
- write!(f, "{s:?}")
- }
-}
-
-impl From<String> for Value {
- fn from(value: String) -> Self {
- let bytes = Bytes::from_owner(value.into_bytes());
- unsafe { Self::new_unchecked(bytes) }
- }
-}
-impl From<Str> for Value {
- fn from(value: Str) -> Self {
- Self { inner: value }
- }
-}
-impl Borrow<str> for Value {
- fn borrow(&self) -> &str {
- &self.inner
- }
-}
-impl AsRef<str> for Value {
- fn as_ref(&self) -> &str {
- &self.inner
- }
-}
-impl Deref for Value {
- type Target = str;
- fn deref(&self) -> &Self::Target {
- &self.inner
- }
-}
-
-impl Value {
- /// Creates a new [`Value`] from a static string.
- pub const fn from_static(data: &'static str) -> Self {
- let bytes = Bytes::from_static(data.as_bytes());
- unsafe { Self::new_unchecked(bytes) }
- }
-
- /// Creates a new [`Value`] from a [`Bytes`] instance without checking if
- /// the bytes are valid UTF-8.
- /// # Safety
- /// This method is unsafe because it does not check if the bytes are valid
- /// UTF-8. It is only safe to use if you are sure that the bytes are valid
- /// UTF-8.
- pub const unsafe fn new_unchecked(bytes: Bytes) -> Self {
- Self {
- inner: unsafe { Str::from_inner_unchecked(bytes) },
- }
- }
-
- /// Creates a new [`Value`] from a [`fmt::Display`] instance by rendering it
- /// to a [`Str`].
- pub fn from_display<D: fmt::Display + ?Sized>(data: &D) -> Self {
- use std::fmt::Write;
- let mut buf = BytesMut::new();
- write!(buf, "{data}").unwrap();
- unsafe { Self::new_unchecked(buf.freeze()) }
- }
-
- /// Attempts to parse the [`Value`] as a [`FromStr`] instance.
- pub fn parse_str<T: FromStr<Err: Into<anyhow::Error>>>(&self) -> Result<T> {
- self
- .as_non_empty_str()
- .ok_or(Error::NoData)
- .and_then(|s| T::from_str(s).map_err(Error::user_data_parse))
- }
-
- /// Creates a new [`Value`] from a [`serde::Serialize`] instance by
- /// rendering it to a [`Str`].
- pub fn from_json<J: serde::Serialize + ?Sized>(data: &J) -> Self {
- let mut buf = BytesMut::new().writer();
- serde_json::to_writer(&mut buf, data).unwrap();
- unsafe { Self::new_unchecked(buf.into_inner().freeze()) }
- }
-
- /// Attempts to parse the [`Value`] as a [`serde::Deserialize`] instance
- /// using [`serde_json`].
- pub fn parse_json<'de, T: serde::de::Deserialize<'de>>(&'de self) -> Result<T> {
- self
- .as_non_empty_str()
- .ok_or(Error::NoData)
- .and_then(|s| serde_json::from_slice::<'de, T>(s.as_bytes()).map_err(Error::UserData))
- }
-
- /// Returns `true` when the [`Value`] is empty.
- pub fn is_empty(&self) -> bool {
- self.inner.is_empty()
- }
-
- /// Appends a [`Str`] to the [`Value`].
- pub fn append_str(&mut self, line: Str) {
- if self.is_empty() {
- *self = Self::from(line);
- } else {
- let mut buf = mem::take(&mut self.inner)
- .into_inner()
- .try_into_mut()
- .unwrap_or_default();
- buf.reserve(1 + line.len());
- buf.extend_from_slice(b"\n");
- buf.extend_from_slice(line.as_bytes());
- *self = unsafe { Self::new_unchecked(buf.freeze()) };
- }
- }
-
- /// Appends a [`Bytes`] to the [`Value`] without checking if the bytes are
- /// valid UTF-8.
- /// # Safety
- /// This method is unsafe because it does not check if the bytes are valid
- /// UTF-8. It is only safe to use if you are sure that the bytes are valid
- /// UTF-8.
- pub unsafe fn append_unchecked(&mut self, line: Bytes) {
- let line = unsafe { Str::from_inner_unchecked(line) };
- self.append_str(line);
- }
-
- /// Returns the [`Value`] as a [`Str`].
- pub fn as_str(&self) -> &str {
- &self.inner
- }
-
- /// Returns the [`Value`] as a [`Str`].
- pub const fn as_bstr(&self) -> &Str {
- &self.inner
- }
-
- /// Returns the [`Value`] as a [`Bytes`].
- pub fn as_bytes(&self) -> &Bytes {
- self.inner.inner()
- }
-
- /// Returns the [`Value`] as a [`str`] if it is not empty.
- pub fn as_non_empty_str(&self) -> Option<&str> {
- if self.is_empty() {
- None
- } else {
- Some(&self.inner)
- }
- }
-}
-
-#[derive(
- Debug,
- Clone,
- Copy,
- PartialEq,
- Eq,
- Hash,
- strum::EnumString,
- strum::EnumCount,
- strum::IntoStaticStr,
- strum::EnumIter,
-)]
-pub enum IntrinsicField {
- #[strum(serialize = "id", ascii_case_insensitive)]
- Id,
- #[strum(serialize = "data", ascii_case_insensitive)]
- Data,
- #[strum(serialize = "event", ascii_case_insensitive)]
- Event,
-}
-
-/// A single [Server-Sent Event](https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events)
-/// that can be sent **or** received through the [`crate::sse`] module.
-///
-/// The type is purposely minimal and is mainly a thin wrapper over the
-/// wire-format used by browsers and by the [`axum::response::sse`] primitives.
-/// It keeps the raw textual representation so that *unknown* or *vendor
-/// specific* fields are preserved during a round-trip.
-///
-/// Construction of an event follows the "builder" pattern so that calls can be
-/// chained fluently:
-///
-/// ```ignore
-/// use tetra_net::http::sse::Event;
-///
-/// let evt = Event::new()
-/// .with_id(&42)
-/// .with_event("tick")
-/// .with_raw_data("hello world");
-/// ```
-///
-/// # Parsing
-/// Instances are produced by the internal parser when a
-/// [`crate::http::sse::EventStream`] is consumed. Consumers should use the
-/// high-level accessors such as [`Event::data`] or [`Event::field`] instead of
-/// inspecting the private fields directly.
-#[derive(Clone, Default)]
-pub struct Event {
- int: [Value; IntrinsicField::COUNT],
- aux: BTreeMap<Value, Value>,
-}
-
-impl fmt::Debug for Event {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- let mut map = f.debug_struct("Event");
- for (k, v) in self.fields() {
- map.field(k, &v.as_str());
- }
- map.finish()
- }
-}
-
-impl serde::Serialize for Event {
- fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
- where
- S: serde::Serializer,
- {
- let mut map = serializer.serialize_map(Some(self.len()))?;
- for (k, v) in self.fields() {
- map.serialize_entry(k, v)?;
- }
- map.end()
- }
-}
-
-impl<'de> serde::Deserialize<'de> for Event {
- fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
- where
- D: serde::Deserializer<'de>,
- {
- struct EventVisitor;
- impl<'de> serde::de::Visitor<'de> for EventVisitor {
- type Value = Event;
- fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
- formatter.write_str("an SSE event")
- }
- fn visit_map<A>(self, mut map: A) -> std::result::Result<Self::Value, A::Error>
- where
- A: MapAccess<'de>,
- {
- let mut event = Event::new();
- while let Some((k, v)) = map.next_entry()? {
- event.set_field_value(k, v);
- }
- Ok(event)
- }
- }
- deserializer.deserialize_map(EventVisitor)
- }
-}
-
-impl Event {
- pub const EMPTY: Self = Self::new();
-
- /// Creates an empty [`Event`].
- ///
- /// The returned value carries no field. Use the builder helpers or the
- /// individual setters such as [`Event::with_raw_data`] to populate it.
- ///
- /// # Examples
- /// ```ignore
- /// use tetra_net::http::sse::Event;
- /// let heartbeat = Event::new();
- /// assert!(heartbeat.is_empty());
- /// ```
- pub const fn new() -> Self {
- Self {
- int: [const { Value::from_static("") }; IntrinsicField::COUNT],
- aux: BTreeMap::new(),
- }
- }
-
- /// Returns an iterator over the fields in the event.
- pub fn fields(&self) -> impl Iterator<Item = (&str, &Value)> {
- IntrinsicField::iter()
- .map(|field| (field.into(), &self.int[field as usize]))
- .filter(|(_, v)| !v.is_empty())
- .chain(self.aux.iter().map(|(k, v)| (k as &str, v)))
- }
-
- /// Returns the number of fields in the event.
- pub fn len(&self) -> usize {
- let mut n = 0;
- for field in IntrinsicField::iter() {
- if !self.int[field as usize].is_empty() {
- n += 1;
- }
- }
- n += self.aux.len(); // aux is guaranteed to be non-empty
- n
- }
-
- /// Returns `true` when the event does not carry any field whatsoever.
- ///
- /// This can be useful to ignore *keep-alive* messages that some servers
- /// periodically send configured with `:\n`.
- pub fn is_empty(&self) -> bool {
- IntrinsicField::iter().all(|field| self.int[field as usize].is_empty()) && self.aux.is_empty()
- }
-
- /// Sets the value of an intrinsic field.
- pub fn with_intrinsic(mut self, field: IntrinsicField, value: Value) -> Self {
- self.int[field as usize] = value;
- self
- }
-
- /// Sets the `id:` field.
- ///
- /// The identifier will be converted to a string using [`fmt::Display`].
- /// Although the *SSE* specification does not mandate any particular format,
- /// using an increasing integer is a common pattern as it allows the browser
- /// to resume the stream by sending the `Last-Event-ID` header.
- pub fn with_id<D: fmt::Display + ?Sized>(self, id: &D) -> Self {
- self.with_intrinsic(IntrinsicField::Id, Value::from_display(id))
- }
-
- /// Writes raw UTF-8 data into the `data:` field.
- ///
- /// The contents are **not** escaped or transformed in any way; make sure it
- /// does not contain control characters other than **LF** (`\n`). For JSON
- /// payloads prefer [`Event::with_json_data`] which automatically serialises
- /// the value for you.
- pub fn with_raw_data<D: fmt::Display + ?Sized>(self, data: &D) -> Self {
- self.with_intrinsic(IntrinsicField::Data, Value::from_display(data))
- }
-
- /// Serialises `data` into JSON and stores the resulting string in the
- /// `data:` field.
- ///
- /// # Errors
- /// The method panics if `serde_json` cannot serialise the value. The panic
- /// is not expected in practice — if it does occur it signals a bug in the
- /// caller or during the derivation of [`serde::Serialize`].
- pub fn with_json_data<J: serde::Serialize + ?Sized>(self, data: &J) -> Self {
- self.with_intrinsic(IntrinsicField::Data, Value::from_json(data))
- }
-
- /// Explicitly sets the [`event`](https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation)
- /// name of the message.
- ///
- /// This is a convenience wrapper around writing to the `event:` field.
- /// It exists because the field name collides with the [`Event::event`]
- /// accessor.
- pub fn with_event<D: fmt::Display + ?Sized>(self, name: &D) -> Self {
- self.with_intrinsic(IntrinsicField::Event, Value::from_display(name))
- }
-
- /// Inserts an arbitrary, user-defined field.
- ///
- /// According to the _WHATWG_ specification any unrecognised field must be
- /// forwarded to the client unchanged. This method therefore gives you a
- /// fully future-proof escape hatch.
- ///
- /// The provided key **must** already comply with the field-name grammar —
- /// no validation is performed.
- pub fn with_field(mut self, key: impl AsRef<str> + Into<Str>, value: impl Into<Value>) -> Self {
- let kstr = key.as_ref();
- if let Ok(field) = kstr.parse() {
- return self.with_intrinsic(field, value.into());
- }
- if kstr.is_empty() {
- return self;
- }
-
- let value: Value = value.into();
- if value.is_empty() {
- self.aux.remove(kstr);
- } else {
- let owned: Str = key.into();
- self.aux.insert(owned.into(), value);
- }
- self
- }
- /// Returns a mutable reference to the value of the field identified by
- /// `key`.
- ///
- /// If the field is an intrinsic field (like `data`, `id`, or `event`),
- /// returns a reference to that field's value. Otherwise, looks up the
- /// field in the map of custom fields.
- ///
- /// # Returns
- /// - `Some(&mut Value)` if the field exists
- /// - `None` if the field doesn't exist in custom fields
- pub fn field_value_mut(&mut self, key: &str) -> Option<&mut Value> {
- if let Ok(field) = key.parse::<IntrinsicField>() {
- return Some(&mut self.int[field as usize]);
- }
- self.aux.get_mut(key)
- }
-
- /// Returns a reference to the value of the field identified by `key`.
- ///
- /// If the field is an intrinsic field (like `data`, `id`, or `event`),
- /// returns a reference to that field's value. Otherwise, looks up the
- /// field in the map of custom fields.
- ///
- /// # Returns
- /// Returns the field's value, or an empty value if the field doesn't exist.
- pub fn field_value(&self, key: &str) -> &Value {
- if let Ok(field) = key.parse::<IntrinsicField>() {
- return &self.int[field as usize];
- }
- static DEFAULT: Value = Value::from_static("");
- self.aux.get(key).unwrap_or(&DEFAULT)
- }
-
- /// Sets the value of a field identified by `key`.
- ///
- /// If the field is an intrinsic field (like `data`, `id`, or `event`),
- /// updates that field's value. Otherwise, inserts or updates the field in
- /// the map of custom fields.
- pub fn set_field_value(&mut self, key: Str, value: Value) {
- if let Ok(field) = key.parse::<IntrinsicField>() {
- self.int[field as usize] = value;
- } else {
- self.aux.insert(key.into(), value);
- }
- }
-
- /// Returns the identifier carried by the `id:` field, if any.
- ///
- /// The caller **must not** assume any particular semantics: the value may
- /// be a monotonic counter, a UUID or even a timestamp. Its interpretation
- /// is left to the application layer.
- pub fn id(&self) -> Option<&str> {
- self.int[IntrinsicField::Id as usize].as_non_empty_str()
- }
-
- /// Returns the raw payload as a UTF-8 string.
- ///
- /// Contrary to [`Event::json_data`], this accessor does **not** attempt to
- /// parse the value as JSON — it simply exposes the bytes as text. Use it
- /// when you expect the `data:` field to hold arbitrary text or when
- /// performance is critical.
- pub fn text_data(&self) -> Option<&str> {
- self.int[IntrinsicField::Data as usize].as_non_empty_str()
- }
-
- /// Returns the _event type_ (value of the `event:` field) if present.
- pub fn event(&self) -> Option<&str> {
- self.int[IntrinsicField::Event as usize].as_non_empty_str()
- }
-
- /// Attempts to deserialize the `data:` field from JSON into `J`.
- pub fn json_data<'de, J: serde::de::Deserialize<'de>>(&'de self) -> Result<J> {
- self.int[IntrinsicField::Data as usize].parse_json()
- }
-
- /// Attempts to deserialize the `data:` field from string into `T`.
- pub fn fmt_data<T: FromStr<Err: Into<anyhow::Error>>>(&self) -> Result<T> {
- self.int[IntrinsicField::Data as usize].parse_str()
- }
-
- /// Returns an arbitrary field previously added with [`Event::with_field`].
- ///
- /// The lookup is case-sensitive and uses the field name *as sent on the
- /// wire*.
- pub fn field(&self, key: &str) -> Option<&str> {
- self.field_value(key).as_non_empty_str()
- }
-
- /// Appends a string value to an existing field or creates a new one.
- ///
- /// # Arguments
- /// * `key` - The field name to append to
- /// * `value` - The bytes value to append
- ///
- /// If the field is an intrinsic field (data, id, event), appends to that
- /// field. Otherwise, inserts or replaces the custom field with the given
- /// value.
- pub(crate) fn append_field_value(&mut self, key: Str, value: Str) {
- if value.is_empty() || key.is_empty() {
- return;
- }
-
- if let Ok(field) = key.parse::<IntrinsicField>() {
- self.int[field as usize].append_str(value);
- } else {
- self.aux.insert(key.into(), Value::from(value));
- }
- }
-
- /// Converts this event to a chunk of bytes.
- pub fn to_body(&self, format: EventFormat) -> Bytes {
- match format {
- EventFormat::Dom => self.to_dom_body(),
- EventFormat::NdJson => self.to_ndjson_body(),
- }
- }
-
- /// Converts this event to a chunk of bytes in DOM format.
- pub fn to_dom_body(&self) -> Bytes {
- if self.is_empty() {
- return Bytes::from_static(b":\n\n");
- }
-
- let mut size_estimation = self
- .fields()
- .fold(0, |acc, (k, v)| acc + k.len() + v.len() + 3);
- if size_estimation == 0 {
- return Bytes::new();
- }
- size_estimation += 1;
-
- let mut buf = BytesMut::with_capacity(size_estimation);
- for (k, v) in self.fields() {
- buf.extend_from_slice(k.as_bytes());
- buf.extend_from_slice(b": ");
- buf.extend_from_slice(v.as_bytes());
- buf.put_u8(b'\n');
- }
- if !buf.is_empty() {
- buf.put_u8(b'\n');
- }
- buf.freeze()
- }
-
- /// Converts this event to a chunk of bytes in NDJSON format.
- pub fn to_ndjson_body(&self) -> Bytes {
- if self.is_empty() {
- return Bytes::from_static(b"{}\n");
- }
-
- let mut size_estimation = self
- .fields()
- .fold(0, |acc, (k, v)| acc + k.len() + v.len() + 8);
- if size_estimation == 0 {
- return Bytes::new();
- }
- size_estimation += 3;
-
- let mut wr = BytesMut::with_capacity(size_estimation).writer();
- serde_json::to_writer(&mut wr, self).expect("Failed to serialize event");
- let mut buf = wr.into_inner();
- buf.put_u8(b'\n');
- buf.freeze()
- }
-
- /// Parses an event in DOM format from a buffer of bytes.
- ///
- /// The DOM format separates events with double newlines (`\n\n`).
- ///
- /// # Arguments
- /// * `lines` - A mutable reference to a buffer containing SSE data
- ///
- /// # Returns
- /// * `Ok(Some(Event))` - If an event was successfully parsed
- /// * `Ok(None)` - If no complete event was found in the buffer
- /// * `Err(Error)` - If an error occurred during parsing
- pub fn parse_dom_body(lines: &mut BytesMut) -> Result<Option<Self>> {
- // If there's no \n\n, nothing to read.
- let lines = {
- let Some(i) = memchr::memmem::find(lines.as_ref(), b"\n\n") else {
- return Ok(None);
- };
- let mut cursor = lines.split_to(i + 2);
- cursor.truncate(i);
- Str::try_from(cursor.freeze()).map_err(|x| Error::Utf8(x.utf8_error()))?
- };
-
- // Parse the event.
- let mut event = Self::default();
- let mut seen_non_empty = false;
- for line in lines.split('\n') {
- // Ignore comments.
- if line.starts_with(':') {
- continue;
- }
-
- let Some(i) = line.find(": ") else {
- // error?
- continue;
- };
-
- let key = &line[..i];
- let val = &line[i + 2..];
- if key.is_empty() || val.is_empty() {
- continue;
- }
- seen_non_empty = true;
-
- let key = lines.slice_ref(key);
- let val = lines.slice_ref(val);
- event.append_field_value(key, val);
- }
-
- if seen_non_empty {
- Ok(Some(event))
- } else {
- Ok(None)
- }
- }
-
- /// Parses an event in NDJSON format from a buffer of bytes.
- ///
- /// The NDJSON format separates events with newlines (`\n`).
- ///
- /// # Arguments
- /// * `lines` - A mutable reference to a buffer containing NDJSON data
- ///
- /// # Returns
- /// * `Ok(Some(Event))` - If an event was successfully parsed
- /// * `Ok(None)` - If no complete event was found in the buffer
- /// * `Err(Error)` - If an error occurred during parsing
- pub fn parse_ndjson_body(lines: &mut BytesMut) -> Result<Option<Self>> {
- // If there's no \n, nothing to read.
- let cursor = {
- let Some(i) = memchr::memchr(b'\n', lines.as_ref()) else {
- return Ok(None);
- };
- let mut cursor = lines.split_to(i + 1);
- cursor.truncate(i);
- cursor.freeze()
- };
-
- // Parse the event as JSON.
- if cursor.as_ref() == b"{}" {
- return Ok(Some(Self::EMPTY));
- }
- let event: Self = serde_json::from_slice(cursor.as_ref())?;
- if event.is_empty() {
- return Ok(None);
- }
- Ok(Some(event))
- }
-
- /// Parses an event from a buffer using the specified format.
- ///
- /// # Arguments
- /// * `lines` - A mutable reference to a buffer containing event data
- /// * `format` - The format of the events (DOM or NDJSON)
- ///
- /// # Returns
- /// * `Ok(Some(Event))` - If an event was successfully parsed
- /// * `Ok(None)` - If no complete event was found in the buffer
- /// * `Err(Error)` - If an error occurred during parsing
- pub fn parse_body(lines: &mut BytesMut, format: EventFormat) -> Result<Option<Self>> {
- match format {
- EventFormat::Dom => Self::parse_dom_body(lines),
- EventFormat::NdJson => Self::parse_ndjson_body(lines),
- }
- }
-}
-
-/// Errors that can be emitted while parsing or streaming [`Event`]s.
-#[derive(Debug, Error)]
-pub enum Error {
- #[error(transparent)]
- Remote(#[from] utils::Error),
- #[error("UTF-8 error: {0}")]
- Utf8(#[from] str::Utf8Error),
- /// Error from the HTTP client.
- #[error(transparent)]
- Reqwest(#[from] reqwest::Error),
- /// Errors during deserialisation of the `data:` field.
- #[error("Failed to parse user data using serde_json: {0}")]
- UserData(#[from] serde_json::Error),
- /// Error from the `FromStr` implementation.
- #[error("Failed to parse user data using FromStr: {0}")]
- UserDataParse(#[source] anyhow::Error),
- #[error("Event has no associated data")]
- NoData,
-}
-
-impl Error {
- pub fn user_data_parse<E: Into<anyhow::Error>>(err: E) -> Self {
- Self::UserDataParse(err.into())
- }
-}
-
-/// Convenience alias over [`std::result::Result`] that defaults the error type
-/// to [`Error`].
-pub type Result<T, E = Error> = std::result::Result<T, E>;
-
-#[cfg(test)]
-mod tests {
- use bytes::BytesMut;
-
- use super::*;
-
- /// Tests the roundtrip serialization and deserialization of Events in DOM
- /// format.
- #[test]
- fn test_event_roundtrip_dom() {
- // Create test events with different combinations of fields
- let events = vec![
- Event::new().with_raw_data("simple data"),
- Event::new().with_raw_data("data with id").with_id("1234"),
- Event::new()
- .with_raw_data("complete event")
- .with_id("5678")
- .with_event("custom-event"),
- ];
-
- for original_event in events {
- // Serialize the event to DOM format
- let serialized = original_event.to_body(EventFormat::Dom);
- // Parse the event back from the serialized bytes
- let mut buffer = BytesMut::from(&serialized[..]);
- let parsed_event = Event::parse_dom_body(&mut buffer)
- .expect("Failed to parse event")
- .expect("No event parsed");
-
- // Verify the parsed event matches the original
- assert_eq!(parsed_event.text_data(), original_event.text_data());
- assert_eq!(parsed_event.id(), original_event.id());
- assert_eq!(parsed_event.event(), original_event.event());
- }
- }
-
- /// Tests the roundtrip serialization and deserialization of Events in
- /// NDJSON format.
- #[test]
- fn test_event_roundtrip_ndjson() {
- // Create test events with different combinations of fields
- let events = vec![
- Event::new().with_raw_data("simple data"),
- Event::new().with_raw_data("data with id").with_id("1234"),
- Event::new()
- .with_raw_data("complete event")
- .with_id("5678")
- .with_event("custom-event"),
- ];
-
- for original_event in events {
- // Serialize the event to NDJSON format
- let serialized = original_event.to_body(EventFormat::NdJson);
-
- // Parse the event back from the serialized bytes
- let mut buffer = BytesMut::from(&serialized[..]);
- let parsed_event = Event::parse_ndjson_body(&mut buffer)
- .expect("Failed to parse event")
- .expect("No event parsed");
-
- // Verify the parsed event matches the original
- assert_eq!(parsed_event.text_data(), original_event.text_data());
- assert_eq!(parsed_event.id(), original_event.id());
- assert_eq!(parsed_event.event(), original_event.event());
- }
- }
-
- /// Tests the generic `parse_as` method with both formats.
- #[test]
- fn test_event_parse_as() {
- let event = Event::new()
- .with_raw_data("test data")
- .with_id("123")
- .with_event("test");
-
- // Test DOM format
- let dom_bytes = event.to_body(EventFormat::Dom);
- let mut dom_buffer = BytesMut::from(&dom_bytes[..]);
- let parsed_dom = Event::parse_body(&mut dom_buffer, EventFormat::Dom)
- .expect("Failed to parse DOM format")
- .expect("No DOM event parsed");
-
- assert_eq!(parsed_dom.text_data(), event.text_data());
- assert_eq!(parsed_dom.id(), event.id());
- assert_eq!(parsed_dom.event(), event.event());
-
- // Test NDJSON format
- let ndjson_bytes = event.to_body(EventFormat::NdJson);
- let mut ndjson_buffer = BytesMut::from(&ndjson_bytes[..]);
- let parsed_ndjson = Event::parse_body(&mut ndjson_buffer, EventFormat::NdJson)
- .expect("Failed to parse NDJSON format")
- .expect("No NDJSON event parsed");
-
- assert_eq!(parsed_ndjson.text_data(), event.text_data());
- assert_eq!(parsed_ndjson.id(), event.id());
- assert_eq!(parsed_ndjson.event(), event.event());
- }
-}
diff --git a/attic/net/http/upgrade.rs b/attic/net/http/upgrade.rs
deleted file mode 100644
index 921bbbff6..000000000
@@ -1,192 +0,0 @@
-use std::{
- fmt,
- future::Future,
- io, mem,
- pin::Pin,
- sync::Arc,
- task,
- task::{Context, Poll},
-};
-
-use futures::FutureExt;
-use hyper::upgrade::OnUpgrade as HyperOnUpgrade;
-use parking_lot::Mutex;
-use tokio::{
- io::{AsyncRead, AsyncWrite, ReadBuf},
- sync::oneshot,
-};
-
-#[derive(Debug, thiserror::Error)]
-pub enum UpgradeError {
- #[error("hyper error: {0}")]
- Hyper(hyper::Error),
- #[error("timeout")]
- Timeout,
- #[error("cancelled")]
- Cancelled,
- #[error("use after lifetime")]
- UseAfterLifetime,
- #[error("connection not upgradable")]
- ConnectionNotUpgradable,
-}
-
-pub trait UpgradedDuplex: AsyncRead + AsyncWrite + Send + Sync + 'static {}
-impl<T: AsyncRead + AsyncWrite + Send + Sync + 'static> UpgradedDuplex for T {}
-
-pub enum Upgraded {
- Hyper(reqwest::Upgraded), // nice wrapper around it with TokioIo
- Custom(Pin<Box<dyn UpgradedDuplex>>),
-}
-
-impl From<hyper::upgrade::Upgraded> for Upgraded {
- fn from(value: hyper::upgrade::Upgraded) -> Self {
- Self::Hyper(value.into())
- }
-}
-
-impl Upgraded {
- pub fn custom<T: UpgradedDuplex>(dup: T) -> Self {
- Self::Custom(Box::pin(dup))
- }
-}
-
-impl AsyncRead for Upgraded {
- fn poll_read(
- mut self: Pin<&mut Self>,
- cx: &mut task::Context<'_>,
- buf: &mut ReadBuf<'_>,
- ) -> Poll<io::Result<()>> {
- match &mut *self {
- Self::Hyper(h) => Pin::new(h).poll_read(cx, buf),
- Self::Custom(c) => Pin::new(c).poll_read(cx, buf),
- }
- }
-}
-
-impl AsyncWrite for Upgraded {
- fn poll_write(
- mut self: Pin<&mut Self>,
- cx: &mut task::Context<'_>,
- buf: &[u8],
- ) -> Poll<io::Result<usize>> {
- match &mut *self {
- Self::Hyper(h) => Pin::new(h).poll_write(cx, buf),
- Self::Custom(c) => Pin::new(c).poll_write(cx, buf),
- }
- }
-
- fn poll_write_vectored(
- mut self: Pin<&mut Self>,
- cx: &mut task::Context<'_>,
- bufs: &[io::IoSlice<'_>],
- ) -> Poll<io::Result<usize>> {
- match &mut *self {
- Self::Hyper(h) => Pin::new(h).poll_write_vectored(cx, bufs),
- Self::Custom(c) => Pin::new(c).poll_write_vectored(cx, bufs),
- }
- }
-
- fn poll_flush(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll<io::Result<()>> {
- match &mut *self {
- Self::Hyper(h) => Pin::new(h).poll_flush(cx),
- Self::Custom(c) => Pin::new(c).poll_flush(cx),
- }
- }
-
- fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll<io::Result<()>> {
- match &mut *self {
- Self::Hyper(h) => Pin::new(h).poll_shutdown(cx),
- Self::Custom(c) => Pin::new(c).poll_shutdown(cx),
- }
- }
-
- fn is_write_vectored(&self) -> bool {
- match self {
- Self::Hyper(h) => h.is_write_vectored(),
- Self::Custom(c) => c.is_write_vectored(),
- }
- }
-}
-
-impl fmt::Debug for Upgraded {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- if let Self::Hyper(h) = self {
- f.debug_struct("Upgraded").field("hyper", &h).finish()
- } else {
- f.debug_struct("Upgraded").field("custom", &self).finish()
- }
- }
-}
-
-/// A custom upgrade future for mock transports.
-///
-/// This can be used in place of [`hyper::upgrade::OnUpgrade`] when
-/// constructing HTTP requests manually.
-#[derive(Clone, Debug)]
-pub enum OnUpgrade {
- None,
- Custom(Arc<Mutex<oneshot::Receiver<Result<Upgraded, UpgradeError>>>>),
- Hyper(HyperOnUpgrade),
-}
-
-pub type Promise = oneshot::Sender<Result<Upgraded, UpgradeError>>;
-
-impl OnUpgrade {
- /// Returns a sender paired with a new `OnUpgrade` future.
- pub fn pending() -> (Promise, Self) {
- let (tx, rx) = oneshot::channel();
- (tx, Self::Custom(Arc::new(Mutex::new(rx))))
- }
-
- /// Returns a sender paired with a new `OnUpgrade` future.
- pub fn pending_at(ext: &mut http::Extensions) -> Promise {
- let (pr, up) = Self::pending();
- ext.insert(up);
- pr
- }
-
- /// Extracts an `OnUpgrade` from the extensions.
- pub fn extract(ext: &mut http::Extensions) -> Result<Self, UpgradeError> {
- if let Some(on_upgrade) = ext.remove::<hyper::upgrade::OnUpgrade>() {
- Ok(Self::Hyper(on_upgrade))
- } else if let Some(on_upgrade) = ext.remove::<Self>() {
- Ok(on_upgrade)
- } else {
- Err(UpgradeError::ConnectionNotUpgradable)
- }
- }
-
- pub const fn take(&mut self) -> Self {
- mem::replace(self, Self::None)
- }
-}
-
-impl Future for OnUpgrade {
- type Output = Result<Upgraded, UpgradeError>;
-
- fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
- match self.take() {
- Self::Custom(rx) => {
- if let Poll::Ready(res) = rx.lock().poll_unpin(cx) {
- return Poll::Ready(res.map_err(|_| UpgradeError::Cancelled).flatten());
- }
- *self = Self::Custom(rx);
- Poll::Pending
- },
- Self::Hyper(mut up) => {
- if let Poll::Ready(res) = up.poll_unpin(cx) {
- return Poll::Ready(res.map_err(UpgradeError::Hyper).map(Upgraded::from));
- }
- *self = Self::Hyper(up);
- Poll::Pending
- },
- Self::None => Poll::Ready(Err(UpgradeError::UseAfterLifetime)),
- }
- }
-}
-
-impl From<HyperOnUpgrade> for OnUpgrade {
- fn from(up: HyperOnUpgrade) -> Self {
- Self::Hyper(up)
- }
-}
diff --git a/attic/net/http/utils.rs b/attic/net/http/utils.rs
deleted file mode 100644
index b83c9e547..000000000
@@ -1,534 +0,0 @@
-use std::{
- borrow::Cow,
- mem,
- time::{Duration, SystemTime},
-};
-
-use axum::response::IntoResponse;
-use futures::StreamExt;
-use http::{HeaderMap, HeaderName, HeaderValue, Method, StatusCode};
-use httpdate::parse_http_date;
-use smol_str::{SmolStr, ToSmolStr};
-use tokio::{pin, select};
-
-#[derive(derive_more::Debug)]
-pub enum ErrorBody {
- #[debug("{:?}", 0)]
- Read(Cow<'static, str>),
- #[debug("{:?}... (truncated)", 1)]
- Trunc(Cow<'static, str>),
- #[debug("<Io error: {:?}>", 2)]
- Errored(anyhow::Error),
- #[debug("<Io timeout>")]
- Timeout,
-}
-
-/// Extension type inserted into [`reqwest::Response`] when the server returns
-/// a `Retry-After` header.
-#[derive(Debug, Clone, Copy)]
-pub struct RetryAfter(pub Duration);
-
-#[derive(Debug, thiserror::Error)]
-pub enum Error {
- /// The server did not respond with the expected status code.
- #[error("unexpected status code {code}. response: {body:?}")]
- UnexpectedStatus {
- code: StatusCode,
- body: ErrorBody,
- retry_after: Option<Duration>,
- },
-
- /// The server responded with a client error status code (400-499).
- #[error("client error {code}. response: {body:?}")]
- ClientError {
- code: StatusCode,
- body: ErrorBody,
- retry_after: Option<Duration>,
- },
-
- /// The server responded with a server error status code (500-599).
- #[error("server error {code}. response: {body:?}")]
- ServerError {
- code: StatusCode,
- body: ErrorBody,
- retry_after: Option<Duration>,
- },
-
- /// A required header was missing.
- #[error("missing header {0}")]
- MissingHeader(&'static str),
-
- /// The server did not respond with the expected method.
- #[error("unexpected method. got {got:?}, expected {expected}")]
- UnexpectedMethod {
- got: Method,
- expected: &'static Method,
- },
-
- /// The server did not respond with the expected header value.
- #[error("unexpected header value for {header}. got {got:?}, expected {expected}")]
- UnexpectedHeaderValue {
- header: &'static str,
- got: Option<HeaderValue>,
- expected: SmolStr,
- },
-
- /// The content-type header does not match the expected value.
- #[error("unexpected content-type. server sent {got:?}, client expected {expected:?}")]
- UnexpectedContentType {
- got: SmolStr,
- expected: Vec<&'static str>,
- },
-
- /// Content-type negotiation failed.
- #[error("content-type negotiation failed. client accepts {got:?}, server supports {expected:?}")]
- ContentTypeNegotiationFailed {
- got: SmolStr,
- expected: Vec<&'static str>,
- },
-
- /// Wrapped error
- #[error(transparent)]
- Wrapped(anyhow::Error),
-}
-
-impl Error {
- pub const fn code(&self) -> StatusCode {
- match self {
- Self::UnexpectedStatus { .. } => StatusCode::UNPROCESSABLE_ENTITY,
- &Self::ClientError { code, .. } => code,
- &Self::ServerError { code, .. } => code,
- Self::MissingHeader(_) => StatusCode::BAD_REQUEST,
- Self::UnexpectedMethod { .. } => StatusCode::METHOD_NOT_ALLOWED,
- Self::UnexpectedHeaderValue { .. } => StatusCode::PRECONDITION_FAILED,
- Self::UnexpectedContentType { .. } => StatusCode::UNSUPPORTED_MEDIA_TYPE,
- Self::ContentTypeNegotiationFailed { .. } => StatusCode::NOT_ACCEPTABLE,
- Self::Wrapped(_) => StatusCode::INTERNAL_SERVER_ERROR,
- }
- }
-
- pub fn wrap(e: impl Into<anyhow::Error>) -> Self {
- Self::Wrapped(e.into())
- }
-}
-
-impl IntoResponse for Error {
- fn into_response(self) -> axum::response::Response {
- axum::response::Response::builder()
- .status(self.code())
- .body(axum::body::Body::from(self.to_string()))
- .unwrap()
- }
-}
-
-pub type Result<T, E = Error> = std::result::Result<T, E>;
-
-const ERROR_TIMEOUT: Duration = Duration::from_secs(4);
-const MAX_ERROR_BODY: usize = 1024;
-
-/// Parses the `Retry-After` header from the given set of headers.
-pub fn retry_after(headers: &HeaderMap) -> Option<Duration> {
- headers
- .get("Retry-After")
- .and_then(|v| v.to_str().ok())
- .and_then(|s| {
- s.trim()
- .parse::<u64>()
- .map(Duration::from_secs)
- .ok()
- .or_else(|| {
- parse_http_date(s)
- .ok()
- .and_then(|t| t.duration_since(SystemTime::now()).ok())
- })
- })
-}
-
-/// Helper function to read the error body of a response.
-pub async fn read_remote_error(response: reqwest::Response) -> Error {
- let status = response.status();
- let retry = retry_after(response.headers());
-
- let mut buffer: Vec<u8> = Vec::new();
- let mut stream = response.bytes_stream();
-
- let mut body = ErrorBody::Read(Cow::Borrowed(""));
- let timeout = tokio::time::sleep(ERROR_TIMEOUT);
- pin!(timeout);
- while buffer.len() < MAX_ERROR_BODY {
- select! {
- biased;
- res = stream.next() => {
- match res {
- Some(Ok(bytes)) => {
- let max_read = MAX_ERROR_BODY - buffer.len();
- if bytes.len() > max_read {
- buffer.extend_from_slice(&bytes[..max_read]);
- body = ErrorBody::Trunc(String::from_utf8_lossy_owned(mem::take(&mut buffer)).into());
- break;
- }
- buffer.extend_from_slice(&bytes);
- },
- Some(Err(e)) => {
- body = ErrorBody::Errored(e.into());
- break;
- },
- None => {
- if !buffer.is_empty() {
- body = ErrorBody::Read(String::from_utf8_lossy_owned(mem::take(&mut buffer)).into());
- }
- break;
- }
- }
- },
- () = &mut timeout => {
- if buffer.is_empty() {
- body = ErrorBody::Timeout;
- }
- break;
- },
- }
- }
-
- if status.is_client_error() {
- Error::ClientError {
- code: status,
- body,
- retry_after: retry,
- }
- } else if status.is_server_error() {
- Error::ServerError {
- code: status,
- body,
- retry_after: retry,
- }
- } else {
- Error::UnexpectedStatus {
- code: status,
- body,
- retry_after: retry,
- }
- }
-}
-
-/// Helper function to get a header as a string.
-pub fn header_as_str<'a>(
- headers: &'a HeaderMap,
- header: &'static HeaderName,
-) -> Result<&'a str, Error> {
- headers
- .get(header)
- .and_then(|h| h.to_str().ok())
- .ok_or(Error::MissingHeader(header.as_str()))
-}
-
-/// Helper function to check if a header matches an expected value.
-pub fn header_must_eq(
- headers: &HeaderMap,
- header: &'static HeaderName,
- expected: &str,
-) -> Result<(), Error> {
- let value = header_as_str(headers, header)?;
- if !value.eq_ignore_ascii_case(expected) {
- return Err(Error::UnexpectedHeaderValue {
- header: header.as_str(),
- got: headers.get(header).cloned(),
- expected: expected.to_smolstr(),
- });
- }
- Ok(())
-}
-
-pub trait ToMime: Clone + PartialEq + Eq {
- fn to_mime(&self) -> &'static str;
-}
-
-impl ToMime for &'static str {
- fn to_mime(&self) -> &'static str {
- self
- }
-}
-
-/// Performs content negotiation to select the best MIME type from
-/// server-supported types.
-///
-/// This function compares the client's `Accept` header against a list of
-/// server-supported MIME types, selecting the best match according to HTTP
-/// content negotiation rules. It handles wildcards (e.g. `*/*`), quality
-/// values, and character set extensions.
-///
-/// # Examples
-/// ```
-/// # use http::HeaderMap;
-/// # use tetra_net::http::utils::negotiate_content_type;
-/// let mut headers = HeaderMap::new();
-/// headers.insert(
-/// "Accept",
-/// "text/html, application/json;q=0.8, */*;q=0.1"
-/// .parse()
-/// .unwrap(),
-/// );
-///
-/// // Server supports these formats
-/// let supported = &["application/json", "text/plain"];
-///
-/// assert_eq!(
-/// negotiate_content_type(&headers, supported).ok(),
-/// Some("application/json")
-/// );
-/// ```
-///
-/// # Return value
-///
-/// Returns the best matching MIME type from the supported list, or `None` if
-/// no acceptable match could be found.
-pub fn negotiate_content_type<T: ToMime>(headers: &HeaderMap, supported_types: &[T]) -> Result<T> {
- // If no supported types, return None
- let Some(preferred) = supported_types.first() else {
- return Err(Error::ContentTypeNegotiationFailed {
- got: header_as_str(headers, &http::header::ACCEPT)?.to_smolstr(),
- expected: Vec::new(),
- });
- };
-
- // If no Accept header present, return first supported type
- let accept_header = match headers.get(http::header::ACCEPT) {
- Some(value) => match value.to_str() {
- Ok(v) => v,
- Err(_) => return Ok(preferred.clone()),
- },
- None => return Ok(preferred.clone()),
- };
-
- accept_header
- // Split the Accept header into parts
- .split(',')
- // Parse the Accept header into (mime, quality) pairs
- .filter_map(|part| {
- let part = part.trim();
- if part.is_empty() {
- return None;
- }
-
- // Split into mime and params
- let mut parts = part.split(';');
- let mime = parts.next()?.trim();
-
- // Default quality is 1.0
- let mut quality = 100;
-
- // Parse quality value if present
- for param in parts {
- let param = param.trim();
- if let Some(q_val) = param.strip_prefix("q=")
- && let Ok(q) = q_val.parse::<f32>()
- {
- // Skip if quality is zero
- if q <= f32::EPSILON {
- return None;
- }
- quality = (q.min(1.0) * 100.0) as u8;
- break;
- }
- }
- Some((mime, quality))
- })
- // Skip those that are not supported
- .filter_map(|(accept_mime, q)| {
- // Handle wildcard
- if accept_mime == "*/*" {
- return Some((preferred.clone(), q));
- }
-
- // Handle type wildcards like "text/*"
- if let Some(prefix) = accept_mime.strip_suffix("/*") {
- for supported in supported_types {
- let mime = supported.to_mime();
- if mime.starts_with(prefix)
- && mime.len() > prefix.len()
- && mime.as_bytes()[prefix.len()] == b'/'
- {
- return Some((supported.clone(), q));
- }
- }
- }
-
- // Strip charset and other parameters from mime for comparison
- let base_mime = accept_mime.split(';').next().unwrap_or(accept_mime).trim();
-
- // Direct match
- for supported in supported_types {
- let mime = supported.to_mime();
- if base_mime.eq_ignore_ascii_case(mime) {
- return Some((supported.clone(), q));
- }
- }
-
- None
- })
- // Find the best match
- .max_by_key(|&(_, quality)| quality)
- // Return the best match
- .map(|(mime, _)| mime)
- .ok_or_else(|| Error::ContentTypeNegotiationFailed {
- got: accept_header.to_smolstr(),
- expected: supported_types.iter().map(|t| t.to_mime()).collect(),
- })
-}
-
-/// Matches a Content-Type header against a list of supported MIME types.
-///
-/// This function checks if the Content-Type header of a request or response
-/// matches any of the provided supported MIME types. It handles parameters
-/// in the Content-Type header (like charset) by comparing only the base MIME
-/// type.
-///
-/// # Examples
-/// ```
-/// # use http::HeaderMap;
-/// # use tetra_net::http::utils::match_content_type;
-/// let mut headers = HeaderMap::new();
-/// headers.insert(
-/// "Content-Type",
-/// "application/json; charset=utf-8".parse().unwrap(),
-/// );
-///
-/// // Server supports these formats
-/// let supported = &["application/json", "text/plain"];
-///
-/// assert_eq!(
-/// match_content_type(&headers, supported).ok(),
-/// Some("application/json")
-/// );
-/// ```
-///
-/// # Return value
-///
-/// Returns the matching MIME type from the supported list, or an error if
-/// the Content-Type header is missing or no match could be found.
-pub fn match_content_type<T: ToMime>(headers: &HeaderMap, supported_types: &[T]) -> Result<T> {
- // If no supported types, return error
- let Some(preferred) = supported_types.first() else {
- return Err(Error::ContentTypeNegotiationFailed {
- got: header_as_str(headers, &http::header::CONTENT_TYPE)?.to_smolstr(),
- expected: Vec::new(),
- });
- };
-
- // If no Content-Type header present, return first supported type
- let content_type_header = match headers.get(http::header::CONTENT_TYPE) {
- Some(value) => match value.to_str() {
- Ok(v) => v,
- Err(_) => return Ok(preferred.clone()),
- },
- None => return Ok(preferred.clone()),
- };
-
- // Get base MIME type without parameters
- let base_mime = content_type_header
- .split(';')
- .next()
- .unwrap_or(content_type_header)
- .trim();
-
- // Find first matching MIME type
- for supported in supported_types {
- if base_mime.eq_ignore_ascii_case(supported.to_mime()) {
- return Ok(supported.clone());
- }
- }
-
- // No match found
- Err(Error::UnexpectedContentType {
- got: content_type_header.to_smolstr(),
- expected: supported_types.iter().map(|t| t.to_mime()).collect(),
- })
-}
-
-/// Helper method to ensure the method.
-pub fn method_must_eq(method: &Method, expected: &'static Method) -> Result<()> {
- if method != expected {
- return Err(Error::UnexpectedMethod {
- got: method.clone(),
- expected,
- });
- }
- Ok(())
-}
-
-#[cfg(test)]
-mod tests {
- use http::HeaderMap;
-
- use super::*;
-
- #[test]
- fn test_negotiate_content_type() {
- fn to_accept(accept: &str) -> HeaderMap {
- let mut headers = HeaderMap::new();
- headers.insert("Accept", accept.parse().unwrap());
- headers
- }
-
- // Test with simple direct match
- let accept = "text/plain";
- let supported = &["text/plain", "application/json"];
- assert_eq!(
- negotiate_content_type(&to_accept(accept), supported).ok(),
- Some("text/plain")
- );
-
- // Test with multiple accept values
- let accept = "application/xml, application/json";
- let supported = &["text/plain", "application/json"];
- assert_eq!(
- negotiate_content_type(&to_accept(accept), supported).ok(),
- Some("application/json")
- );
-
- // Test with quality values
- let accept = "text/html;q=0.8, application/json;q=0.9, */*;q=0.1";
- let supported = &["text/plain", "application/json"];
- assert_eq!(
- negotiate_content_type(&to_accept(accept), supported).ok(),
- Some("application/json")
- );
-
- // Test with wildcards
- let accept = "text/*";
- let supported = &["text/plain", "application/json"];
- assert_eq!(
- negotiate_content_type(&to_accept(accept), supported).ok(),
- Some("text/plain")
- );
-
- // Test with global wildcard
- let accept = "*/*";
- let supported = &["text/plain", "application/json"];
- assert_eq!(
- negotiate_content_type(&to_accept(accept), supported).ok(),
- Some("text/plain")
- );
-
- // Test with no match
- let accept = "application/xml";
- let supported = &["text/plain", "application/json"];
- assert!(negotiate_content_type(&to_accept(accept), supported).is_err());
-
- // Test with charset and other parameters
- let accept = "text/plain; charset=utf-8";
- let supported = &["text/plain", "application/json"];
- assert_eq!(
- negotiate_content_type(&to_accept(accept), supported).ok(),
- Some("text/plain")
- );
-
- // Test case insensitivity
- let accept = "TEXT/PLAIN";
- let supported = &["text/plain", "application/json"];
- assert_eq!(
- negotiate_content_type(&to_accept(accept), supported).ok(),
- Some("text/plain")
- );
- }
-}
diff --git a/attic/net/http/ws/config.rs b/attic/net/http/ws/config.rs
deleted file mode 100644
index 8eeca740c..000000000
@@ -1,89 +0,0 @@
-use smol_str::ToSmolStr;
-use tokio_tungstenite::tungstenite::protocol::WebSocketConfig;
-
-/// Trait for configuring WebSocket parameters.
-pub trait ConfigBuilder {
- /// Returns a mutable reference to the `WebSocketConfig`.
- fn websocket_config(&mut self) -> &mut WebSocketConfig;
-
- /// Adds supported protocols to the configuration.
- fn protocols(&mut self, extend: impl IntoIterator<Item: ToSmolStr>) -> &mut Self;
-
- /// Read buffer capacity. This buffer is eagerly allocated and used for
- /// receiving messages.
- ///
- /// For high read load scenarios a larger buffer, e.g. 128 KiB, improves
- /// performance.
- ///
- /// For scenarios where you expect a lot of connections and don't need high
- /// read load performance a smaller buffer, e.g. 4 KiB, would be
- /// appropriate to lower total memory usage.
- ///
- /// The default value is 128 KiB.
- fn read_buffer_size(&mut self, read_buffer_size: usize) -> &mut Self {
- self.websocket_config().read_buffer_size = read_buffer_size;
- self
- }
-
- /// The target minimum size of the write buffer to reach before writing the
- /// data to the underlying stream.
- /// The default value is 128 KiB.
- ///
- /// If set to `0` each message will be eagerly written to the underlying
- /// stream. It is often more optimal to allow them to buffer a little,
- /// hence the default value.
- ///
- /// Note: [`flush`](WebSocket::flush) will always fully write the buffer
- /// regardless.
- fn write_buffer_size(&mut self, write_buffer_size: usize) -> &mut Self {
- self.websocket_config().write_buffer_size = write_buffer_size;
- self
- }
-
- /// The max size of the write buffer in bytes. Setting this can provide
- /// backpressure in the case the write buffer is filling up due to write
- /// errors. The default value is unlimited.
- ///
- /// Note: The write buffer only builds up past
- /// [`write_buffer_size`](Self::write_buffer_size) when writes to the
- /// underlying stream are failing. So the **write buffer can not
- /// fill up if you are not observing write errors even if not flushing**.
- ///
- /// Note: Should always be at least [`write_buffer_size + 1
- /// message`](Self::write_buffer_size) and probably a little more
- /// depending on error handling strategy.
- fn max_write_buffer_size(&mut self, max_write_buffer_size: usize) -> &mut Self {
- self.websocket_config().max_write_buffer_size = max_write_buffer_size;
- self
- }
-
- /// The maximum size of an incoming message. `None` means no size limit. The
- /// default value is 64 MiB which should be reasonably big for all normal
- /// use-cases but small enough to prevent memory eating by a malicious
- /// user.
- fn max_message_size(&mut self, max_message_size: Option<usize>) -> &mut Self {
- self.websocket_config().max_message_size = max_message_size;
- self
- }
-
- /// The maximum size of a single incoming message frame. `None` means no
- /// size limit. The limit is for frame payload NOT including the frame
- /// header. The default value is 16 MiB which should be reasonably big
- /// for all normal use-cases but small enough to prevent memory eating
- /// by a malicious user.
- fn max_frame_size(&mut self, max_frame_size: Option<usize>) -> &mut Self {
- self.websocket_config().max_frame_size = max_frame_size;
- self
- }
-
- /// When set to `true`, the server will accept and handle unmasked frames
- /// from the client. According to the RFC 6455, the server must close the
- /// connection to the client in such cases, however it seems like there are
- /// some popular libraries that are sending unmasked frames, ignoring the
- /// RFC. By default this option is set to `false`, i.e. according to RFC
- /// 6455.
- fn accept_unmasked_frames(&mut self, accept_unmasked_frames: bool) -> &mut Self {
- self.websocket_config().accept_unmasked_frames = accept_unmasked_frames;
- self
- }
-}
diff --git a/attic/net/http/ws/mod.rs b/attic/net/http/ws/mod.rs
deleted file mode 100644
index 6caeb29f8..000000000
@@ -1,18 +0,0 @@
-pub mod config;
-pub mod request;
-pub mod response;
-pub mod security;
-pub mod socket;
-pub mod types;
-
-pub use config::ConfigBuilder;
-pub use request::Request;
-pub use response::Response;
-pub use security::Nonce;
-pub use socket::WebSocket;
-// Re-export WebSocketConfig from tungstenite
-pub use tokio_tungstenite::tungstenite::protocol::WebSocketConfig;
-pub use types::{Code, Error, Message, Result};
-
-#[cfg(test)]
-pub mod tests;
diff --git a/attic/net/http/ws/request.rs b/attic/net/http/ws/request.rs
deleted file mode 100644
index da144f7d9..000000000
@@ -1,164 +0,0 @@
-use std::{convert::Infallible, future, future::Future, mem, ops::Deref};
-
-use axum::response::IntoResponse;
-use http::{HeaderValue, Method, StatusCode, Version, header, request::Parts};
-use smol_str::{SmolStr, ToSmolStr};
-use tokio_tungstenite::tungstenite::protocol::{Role, WebSocketConfig};
-
-use super::{
- config::ConfigBuilder,
- security::Nonce,
- socket::WebSocket,
- types::{Error, Result},
-};
-use crate::http::{upgrade::OnUpgrade, utils};
-
-/// Represents a WebSocket upgrade request on the server side.
-#[derive(Debug)]
-pub struct Request {
- /// The HTTP request parts.
- pub request: Option<Parts>,
- /// The supported WebSocket protocols.
- pub protocols: Vec<SmolStr>,
- /// The WebSocket configuration.
- pub websocket_config: Option<WebSocketConfig>,
-}
-
-impl ConfigBuilder for Request {
- fn websocket_config(&mut self) -> &mut WebSocketConfig {
- self.websocket_config.get_or_insert_default()
- }
- fn protocols(&mut self, extend: impl IntoIterator<Item: ToSmolStr>) -> &mut Self {
- self
- .protocols
- .extend(extend.into_iter().map(|s| s.to_smolstr()));
- self
- }
-}
-
-impl Request {
- /// Creates a new WebSocket upgrade request from HTTP request parts.
- pub const fn new(request: Parts) -> Self {
- Self {
- request: Some(request),
- protocols: Vec::new(),
- websocket_config: None,
- }
- }
-
- /// Attempts to upgrade an HTTP connection to a WebSocket connection.
- /// Handles errors by returning an appropriate HTTP response.
- pub fn upgrade<F, Fut>(&mut self, callback: F) -> axum::response::Response
- where
- F: FnOnce(WebSocket) -> Fut + Send + 'static,
- Fut: Future<Output = ()> + Send,
- {
- let response = self.try_upgrade(callback);
- match response {
- Ok(response) => response,
- Err(error) => (StatusCode::BAD_REQUEST, error.to_string()).into_response(),
- }
- }
-
- /// Attempts to upgrade an HTTP connection to a WebSocket connection.
- /// Returns a Result with either the response for a successful upgrade or an
- /// error.
- fn try_upgrade<F, Fut>(&mut self, callback: F) -> Result<axum::response::Response>
- where
- F: FnOnce(WebSocket) -> Fut + Send + 'static,
- Fut: Future<Output = ()> + Send,
- {
- let mut req = self.request.take().ok_or(Error::RequestAlreadyBuilt)?;
- let protocols = mem::take(&mut self.protocols);
- let websocket_config = mem::take(&mut self.websocket_config);
-
- let on_upgrade = OnUpgrade::extract(&mut req.extensions).map_err(Error::Upgrade)?;
-
- utils::header_must_eq(&req.headers, &header::SEC_WEBSOCKET_VERSION, "13")?;
-
- let sec_websocket_key = if req.version <= Version::HTTP_11 {
- utils::method_must_eq(&req.method, &Method::GET)?;
- utils::header_must_eq(&req.headers, &header::CONNECTION, "upgrade")?;
- utils::header_must_eq(&req.headers, &header::UPGRADE, "websocket")?;
- let sec_websocket_key = utils::header_as_str(&req.headers, &header::SEC_WEBSOCKET_KEY)?;
- Some(sec_websocket_key)
- } else {
- None
- };
-
- let protocol = if protocols.is_empty() {
- None
- } else {
- let protocol = req
- .headers
- .get(header::SEC_WEBSOCKET_PROTOCOL)
- .map(|h| h.to_str().unwrap_or_default())
- .unwrap_or_default();
- protocols.into_iter().find(|p| p == protocol)
- };
-
- tokio::spawn({
- let protocol = protocol.clone();
- async move {
- let upgraded = match on_upgrade.await {
- Ok(upgraded) => upgraded,
- Err(_) => return,
- };
- callback(WebSocket::create(upgraded, protocol, Role::Server, websocket_config).await)
- .await;
- }
- });
-
- let mut response = if let Some(sec_websocket_key) = sec_websocket_key {
- // If `sec_websocket_key` was `Some`, we are using HTTP/1.1.
- axum::response::Response::builder()
- .status(StatusCode::SWITCHING_PROTOCOLS)
- .header(header::CONNECTION, HeaderValue::from_static("upgrade"))
- .header(header::UPGRADE, HeaderValue::from_static("websocket"))
- .header(
- header::SEC_WEBSOCKET_ACCEPT,
- Nonce::accept(sec_websocket_key).as_str(),
- )
- .body(axum::body::Body::empty())
- .unwrap()
- } else {
- // Otherwise, we are HTTP/2+. As established in RFC 9113 section 8.5, we just
- // respond with a 2XX with an empty body:
- // <https://datatracker.ietf.org/doc/html/rfc9113#name-the-connect-method>.
- axum::response::Response::new(axum::body::Body::empty())
- };
-
- if let Some(protocol) = protocol {
- response.headers_mut().insert(
- header::SEC_WEBSOCKET_PROTOCOL,
- HeaderValue::from_str(protocol.as_str()).unwrap(),
- );
- }
-
- Ok(response)
- }
-
- /// Consumes the request and returns the [`Parts`] of the request.
- pub fn into_inner(self) -> Parts {
- self.request.expect("request used after finalized")
- }
-}
-
-impl<S> axum::extract::FromRequestParts<S> for Request {
- type Rejection = Infallible;
-
- fn from_request_parts(
- parts: &mut Parts,
- _state: &S,
- ) -> impl Future<Output = Result<Self, Self::Rejection>> + Send {
- let result = Self::new(parts.clone());
- future::ready(Ok(result))
- }
-}
-
-impl Deref for Request {
- type Target = Parts;
- fn deref(&self) -> &Self::Target {
- self.request.as_ref().expect("request used after finalized")
- }
-}
diff --git a/attic/net/http/ws/response.rs b/attic/net/http/ws/response.rs
deleted file mode 100644
index 2874a538a..000000000
@@ -1,73 +0,0 @@
-use std::ops::Deref;
-
-use http::{StatusCode, header};
-use smol_str::SmolStr;
-use tokio_tungstenite::tungstenite::protocol::{Role, WebSocketConfig};
-
-use super::{
- security::Nonce,
- socket::WebSocket,
- types::{Error, Result},
-};
-use crate::http::{upgrade::OnUpgrade, utils};
-
-/// The server's response to the WebSocket upgrade request.
-#[derive(Debug)]
-pub struct Response {
- /// The HTTP response from the server.
- pub response: reqwest::Response,
- /// The security nonce used during the handshake.
- pub nonce: Nonce,
- /// The supported WebSocket protocols.
- pub protocols: Vec<SmolStr>,
- /// The WebSocket configuration.
- pub websocket_config: Option<WebSocketConfig>,
-}
-
-impl Response {
- /// Attempts to upgrade the HTTP connection to a WebSocket connection based
- /// on the server's response.
- pub async fn upgrade(mut self) -> Result<WebSocket> {
- let headers = self.response.headers();
-
- if self.response.status() != StatusCode::SWITCHING_PROTOCOLS {
- return Err(utils::read_remote_error(self.response).await.into());
- }
-
- utils::header_must_eq(headers, &header::CONNECTION, "upgrade")?;
- utils::header_must_eq(headers, &header::UPGRADE, "websocket")?;
- if !self.nonce.accept.is_empty() {
- utils::header_must_eq(headers, &header::SEC_WEBSOCKET_ACCEPT, &self.nonce.accept)?;
- }
-
- let protocol = if self.protocols.is_empty() {
- None
- } else {
- let protocol = utils::header_as_str(headers, &header::SEC_WEBSOCKET_PROTOCOL)
- .map_err(|_| Error::ExpectedAProtocol)?;
- Some(
- self
- .protocols
- .into_iter()
- .find(|p| p == protocol)
- .ok_or_else(|| Error::UnexpectedProtocol(protocol.to_string()))?,
- )
- };
-
- let upgraded = OnUpgrade::extract(self.response.extensions_mut())?.await?;
- Ok(WebSocket::create(upgraded, protocol, Role::Client, self.websocket_config).await)
- }
-
- /// Consumes the response and returns the inner [`reqwest::Response`].
- #[must_use]
- pub fn into_inner(self) -> reqwest::Response {
- self.response
- }
-}
-
-impl Deref for Response {
- type Target = reqwest::Response;
- fn deref(&self) -> &Self::Target {
- &self.response
- }
-}
diff --git a/attic/net/http/ws/security.rs b/attic/net/http/ws/security.rs
deleted file mode 100644
index 18f824129..000000000
@@ -1,48 +0,0 @@
-use data_encoding::BASE64;
-use sha1::{digest::Update, Digest, Sha1};
-
-/// Represents a WebSocket security key pair used during the handshake process.
-#[derive(Debug)]
-pub struct Nonce {
- /// The security key sent to the server.
- pub key: heapless::String<32>,
- /// The expected accept value that should be returned by the server.
- pub accept: heapless::String<32>,
-}
-
-impl Default for Nonce {
- fn default() -> Self {
- Self::new()
- }
-}
-
-impl Nonce {
- /// Computes the accept value for a given security key according to the
- /// WebSocket protocol.
- pub fn accept(key: &str) -> heapless::String<32> {
- use std::fmt::Write;
-
- let hash = Sha1::new()
- .chain(key.as_bytes())
- .chain(b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
- .finalize();
-
- let mut accept = heapless::String::new();
- BASE64.encode_write(&hash, &mut accept).unwrap();
- accept
- }
-
- /// Creates a new security key pair with a randomly generated key.
- pub fn new() -> Self {
- use std::fmt::Write;
-
- let seed: [u8; 16] = rand::random();
- let mut key = heapless::String::new();
- BASE64.encode_write(&seed, &mut key).unwrap();
-
- Self {
- accept: Self::accept(&key),
- key,
- }
- }
-}
diff --git a/attic/net/http/ws/socket.rs b/attic/net/http/ws/socket.rs
deleted file mode 100644
index 6d7858e3d..000000000
@@ -1,90 +0,0 @@
-use std::{
- pin::Pin,
- task::{Context, Poll},
-};
-
-use futures::{Sink, SinkExt, Stream, StreamExt};
-use smol_str::SmolStr;
-use tokio_tungstenite::{
- WebSocketStream,
- tungstenite::protocol::{CloseFrame, Role, WebSocketConfig},
-};
-
-use super::types::{Code, Error, Message, Result};
-use crate::http::upgrade::Upgraded;
-
-/// A WebSocket connection. Implements [`futures::Stream`] and
-/// [`futures::Sink`].
-#[derive(Debug)]
-pub struct WebSocket {
- /// The underlying WebSocket stream.
- inner: WebSocketStream<Upgraded>,
- /// The negotiated protocol, if any.
- protocol: Option<SmolStr>,
-}
-
-impl WebSocket {
- /// Creates a new WebSocket from a upgraded HTTP connection, a protocol, the
- /// client/server role, and a configuration.
- pub async fn create(
- upgraded: Upgraded,
- protocol: Option<SmolStr>,
- role: Role,
- config: Option<WebSocketConfig>,
- ) -> Self {
- Self {
- inner: WebSocketStream::from_raw_socket(upgraded, role, config).await,
- protocol,
- }
- }
-
- /// Returns the protocol negotiated during the handshake.
- pub fn protocol(&self) -> Option<&str> {
- self.protocol.as_deref()
- }
-
- /// Closes the connection with a given code and (optional) reason.
- pub async fn close(self, code: Code, reason: Option<&str>) -> Result<()> {
- let mut inner = self.inner;
- inner
- .close(Some(CloseFrame {
- code,
- reason: reason.unwrap_or_default().into(),
- }))
- .await?;
- Ok(())
- }
-}
-
-impl Stream for WebSocket {
- type Item = Result<Message>;
-
- fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
- match self.inner.poll_next_unpin(cx) {
- Poll::Ready(None) => Poll::Ready(None),
- Poll::Ready(Some(Err(error))) => Poll::Ready(Some(Err(error.into()))),
- Poll::Ready(Some(Ok(message))) => Poll::Ready(Some(Ok(message.into()))),
- Poll::Pending => Poll::Pending,
- }
- }
-}
-
-impl Sink<Message> for WebSocket {
- type Error = Error;
-
- fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<()>> {
- self.inner.poll_ready_unpin(cx).map_err(Into::into)
- }
-
- fn start_send(mut self: Pin<&mut Self>, item: Message) -> Result<()> {
- self.inner.start_send_unpin(item.into()).map_err(Into::into)
- }
-
- fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<()>> {
- self.inner.poll_flush_unpin(cx).map_err(Into::into)
- }
-
- fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<()>> {
- self.inner.poll_close_unpin(cx).map_err(Into::into)
- }
-}
diff --git a/attic/net/http/ws/tests.rs b/attic/net/http/ws/tests.rs
deleted file mode 100644
index e2b1d15d8..000000000
@@ -1,184 +0,0 @@
-use std::{
- sync::atomic::{AtomicUsize, Ordering},
- time::Duration,
-};
-
-use axum::{Router, response::IntoResponse, routing::get};
-use futures::{SinkExt, StreamExt};
-use tokio::time::timeout;
-
-use super::ConfigBuilder;
-use crate::http::{client, server::SharedRouter, utils, ws};
-
-/// Tests the WebSocket functionality by setting up a server and client,
-/// exchanging messages, and verifying proper connection handling.
-#[tokio::test]
-async fn test_websocket_communication() {
- // Create axum router with WebSocket support
- let app = SharedRouter::new(Router::new().route("/ws", get(websocket_handler)));
- app.bind(["ws-example.local"]);
-
- // Connect to WebSocket server
- let mut websocket = client::Client::global()
- .websocket("wss://ws-example.local/ws")
- .protocols(["tetra-protocol"])
- .read_buffer_size(64 * 1024)
- .max_message_size(Some(10 * 1024 * 1024))
- .send()
- .await
- .expect("Failed to send WebSocket request")
- .upgrade()
- .await
- .expect("Failed to upgrade to WebSocket");
-
- assert_eq!(
- websocket.protocol(),
- Some("tetra-protocol"),
- "WebSocket should connect with the expected protocol"
- );
-
- // Send a text message
- let text_message = "Hello from WebSocket client!";
- websocket
- .send(ws::Message::Text(text_message.to_string()))
- .await
- .expect("Failed to send text message");
-
- // Send a binary message
- let binary_data = b"Binary data example";
- websocket
- .send(ws::Message::Binary(binary_data.to_vec().into()))
- .await
- .expect("Failed to send binary message");
-
- // Read messages from server (with timeout to avoid blocking)
- let mut received_messages = Vec::new();
- for _ in 0..2 {
- if let Ok(Some(msg)) = timeout(std::time::Duration::from_secs(1), websocket.next()).await {
- let msg = msg.expect("Failed to receive message");
- received_messages.push(msg);
- } else {
- panic!("Timed out waiting for message");
- }
- }
-
- // Verify received messages
- assert_eq!(received_messages.len(), 2, "Should receive two messages");
-
- match &received_messages[0] {
- ws::Message::Text(text) => assert_eq!(
- text, text_message,
- "Text message should echo back correctly"
- ),
- _ => panic!("First message should be text"),
- }
-
- match &received_messages[1] {
- ws::Message::Binary(bin) => assert_eq!(
- bin.as_ref(),
- binary_data,
- "Binary message should echo back correctly"
- ),
- _ => panic!("Second message should be binary"),
- }
-
- // Close the WebSocket connection
- websocket
- .close(ws::Code::Normal, Some("Test complete"))
- .await
- .expect("Failed to close WebSocket connection");
-}
-
-async fn websocket_handler(mut ws: ws::Request) -> impl IntoResponse {
- ws.protocols(["tetra-protocol"]).upgrade(handle_socket)
-}
-
-/// Handles the WebSocket connection by echoing back any received messages.
-async fn handle_socket(mut socket: ws::WebSocket) {
- while let Some(msg) = socket.next().await {
- if let Ok(msg) = msg {
- // Echo the message back
- if socket.send(msg).await.is_err() {
- break;
- }
- } else {
- // Client disconnected or error occurred
- break;
- }
- }
-}
-
-#[tokio::test]
-async fn websocket_retry_after_error() {
- let app = SharedRouter::new(Router::new().route(
- "/ws",
- get(|| async {
- axum::response::Response::builder()
- .status(axum::http::StatusCode::SERVICE_UNAVAILABLE)
- .header("Retry-After", "7")
- .body(axum::body::Body::empty())
- .unwrap()
- }),
- ));
- app.bind(["ws-retry.local"]);
-
- let err = client::Client::global()
- .websocket("wss://ws-retry.local/ws")
- .send()
- .await
- .expect("send should succeed")
- .upgrade()
- .await
- .unwrap_err();
-
- match err {
- ws::Error::Remote(utils::Error::ServerError { retry_after, .. }) => {
- assert_eq!(retry_after, Some(Duration::from_secs(7)));
- },
- other => panic!("unexpected error: {other:?}"),
- }
-}
-
-#[tokio::test]
-async fn websocket_retry_automatically() {
- static CALLS: AtomicUsize = AtomicUsize::new(0);
-
- async fn handler(mut ws: ws::Request) -> impl IntoResponse {
- if CALLS.fetch_add(1, Ordering::SeqCst) == 0 {
- axum::response::Response::builder()
- .status(axum::http::StatusCode::SERVICE_UNAVAILABLE)
- .header("Retry-After", "0")
- .body(axum::body::Body::empty())
- .unwrap()
- } else {
- ws.protocols(["tetra-protocol"]).upgrade(handle_socket)
- }
- }
-
- let app = SharedRouter::new(Router::new().route("/ws-auto", get(handler)));
- app.bind(["ws-auto.local"]);
-
- let mut socket = client::Client::global()
- .websocket("wss://ws-auto.local/ws-auto")
- .protocols(["tetra-protocol"])
- .send()
- .await
- .expect("send")
- .upgrade()
- .await
- .expect("upgrade");
-
- socket
- .send(ws::Message::Text("ping".into()))
- .await
- .expect("send ping");
-
- let msg = timeout(Duration::from_secs(1), socket.next())
- .await
- .expect("timeout")
- .expect("option")
- .expect("msg");
-
- assert!(matches!(msg, ws::Message::Text(_)));
- assert!(CALLS.load(Ordering::SeqCst) >= 2);
-}
diff --git a/attic/net/http/ws/types.rs b/attic/net/http/ws/types.rs
deleted file mode 100644
index 60923353c..000000000
@@ -1,142 +0,0 @@
-use bytes::Bytes;
-use tokio_tungstenite::tungstenite;
-pub use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode as Code;
-
-use crate::http::{upgrade, utils};
-
-/// Represents different types of WebSocket messages that can be sent or
-/// received.
-#[derive(Clone, Debug)]
-pub enum Message {
- /// A text message containing UTF-8 encoded data.
- Text(String),
- /// A binary message.
- Binary(Bytes),
- /// A ping message used for keep-alive and latency measurement.
- Ping(Bytes),
- /// A pong message sent in response to a ping.
- Pong(Bytes),
- /// A close message indicating the connection is being terminated.
- Close {
- /// The close code indicating the reason for closing.
- code: Code,
- /// A human-readable explanation for the close.
- reason: String,
- },
-}
-
-impl From<String> for Message {
- #[inline]
- fn from(value: String) -> Self {
- Self::Text(value)
- }
-}
-
-impl From<&str> for Message {
- #[inline]
- fn from(value: &str) -> Self {
- Self::from(value.to_owned())
- }
-}
-
-impl From<Bytes> for Message {
- #[inline]
- fn from(value: Bytes) -> Self {
- Self::Binary(value)
- }
-}
-
-impl From<Vec<u8>> for Message {
- #[inline]
- fn from(value: Vec<u8>) -> Self {
- Self::from(Bytes::from(value))
- }
-}
-
-impl From<&[u8]> for Message {
- #[inline]
- fn from(value: &[u8]) -> Self {
- Self::from(Bytes::copy_from_slice(value))
- }
-}
-
-impl From<tungstenite::Message> for Message {
- fn from(value: tungstenite::Message) -> Self {
- match value {
- tungstenite::Message::Text(text) => Self::Text(text.as_str().to_owned()),
- tungstenite::Message::Binary(data) => Self::Binary(data),
- tungstenite::Message::Ping(data) => Self::Ping(data),
- tungstenite::Message::Pong(data) => Self::Pong(data),
- tungstenite::Message::Close(Some(tungstenite::protocol::CloseFrame { code, reason })) => {
- Self::Close {
- code,
- reason: reason.as_str().to_owned(),
- }
- },
- tungstenite::Message::Close(None) => Self::Close {
- code: Code::Normal,
- reason: String::new(),
- },
- tungstenite::Message::Frame(value) => panic!("unexpected frame: {value:?}"),
- }
- }
-}
-
-impl From<Message> for tungstenite::Message {
- fn from(value: Message) -> Self {
- match value {
- Message::Text(text) => Self::Text(tungstenite::Utf8Bytes::from(text)),
- Message::Binary(data) => Self::Binary(data),
- Message::Ping(data) => Self::Ping(data),
- Message::Pong(data) => Self::Pong(data),
- Message::Close { code, reason } => Self::Close(Some(tungstenite::protocol::CloseFrame {
- code,
- reason: reason.into(),
- })),
- }
- }
-}
-
-/// Error during `Websocket` handshake.
-#[derive(Debug, thiserror::Error)]
-pub enum Error {
- #[error(transparent)]
- Remote(#[from] utils::Error),
-
- /// Error when the connection cannot be upgraded to a WebSocket.
- #[error("connection not upgradable")]
- ConnectionNotUpgradable,
-
- /// Error when the server didn't select a protocol when one was expected.
- #[error("expected the server to select a protocol.")]
- ExpectedAProtocol,
-
- /// Error when the server selected an unexpected protocol.
- #[error("unexpected protocol: {0}")]
- UnexpectedProtocol(String),
-
- /// Error during the upgrade process.
- #[error("upgrade error: {0}")]
- Upgrade(#[from] upgrade::UpgradeError),
-
- /// Error when trying to use a request that has already been built.
- #[error("request already built")]
- RequestAlreadyBuilt,
-
- /// Error from the underlying WebSocket implementation.
- #[error("socket error: {0}")]
- Socket(Box<tungstenite::Error>),
-
- /// Error from the HTTP client.
- #[error("server error: {0}")]
- Server(#[from] reqwest::Error),
-}
-
-impl From<tungstenite::Error> for Error {
- fn from(value: tungstenite::Error) -> Self {
- Self::Socket(Box::new(value))
- }
-}
-
-/// A specialized Result type for WebSocket operations.
-pub type Result<T, E = Error> = std::result::Result<T, E>;
diff --git a/attic/net/identity_service.rs b/attic/net/identity_service.rs
deleted file mode 100644
index 5eeb02c79..000000000
@@ -1,164 +0,0 @@
-use std::borrow::Cow;
-
-use axum::{
- Json, Router,
- body::Body,
- extract::{Request, State},
- response::{IntoResponse, Response},
- routing::get,
-};
-use bytes::Bytes;
-use chrono::DurationRound;
-use futures::FutureExt;
-use http::StatusCode;
-
-use super::{Hostname, Identifiable, Identity, id};
-use crate::http::client;
-
-#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, derive_more::Display)]
-#[display("BuildInfo {{ time: {time}, commit: {commit} }}")]
-pub struct BuildInfo {
- pub time: Cow<'static, str>,
- pub commit: Cow<'static, str>,
-}
-
-impl Default for BuildInfo {
- fn default() -> Self {
- Self {
- //time: env!("BUILD_TIMESTAMP").into(),
- //commit: env!("GIT_COMMIT", "unknown").into(),
- time: "TODO: 2025-06-30T12:00:00Z".into(),
- commit: "TODO: Python script for 1234567890".into(),
- }
- }
-}
-
-#[derive(Debug, thiserror::Error)]
-pub enum IdentityServiceError {
- #[error("Failed to fetch identity")]
- Fetch(#[from] client::Error),
- #[error("Failed to validate identity")]
- Validate(#[from] id::Error),
- #[error("Fetched identity belongs to a different host {host:?}, expected {expected:?}")]
- Host { host: Hostname, expected: String },
-}
-
-impl From<reqwest::Error> for IdentityServiceError {
- fn from(err: reqwest::Error) -> Self {
- Self::Fetch(client::Error::RequestFailed(err))
- }
-}
-
-#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, derive_more::Display)]
-#[display("{{ identity: {identity}, build: {build} }}")]
-pub struct IdentityResponse {
- pub identity: Identity,
- pub build: BuildInfo,
-}
-
-#[derive(Clone, Debug)]
-pub struct IdentityService {
- prerender: Response<Bytes>,
- init_time: chrono::DateTime<chrono::Utc>,
-}
-
-impl IdentityService {
- pub const ROOT: &'static str = "/.well-known/tetra-id";
-
- pub fn new(identity: Identity) -> Self {
- let data = IdentityResponse {
- identity,
- build: BuildInfo::default(),
- };
-
- let (mut parts, body) = (StatusCode::OK, Json(data)).into_response().into_parts();
-
- // Convert body to bytes for caching
- let body = axum::body::to_bytes(body, usize::MAX)
- .now_or_never()
- .expect("Non-async body should not block")
- .expect("Failed to convert body to bytes");
-
- // Generate ETag from body content
- let etag = blake3::hash(&body[..]).to_hex();
-
- // Add caching headers
- parts
- .headers
- .insert(http::header::ETAG, etag.parse().unwrap());
- parts.headers.insert(
- http::header::CACHE_CONTROL,
- "public, max-age=31536000, immutable".parse().unwrap(),
- );
- parts.headers.insert(
- http::header::EXPIRES,
- "Thu, 31 Dec 2099 23:59:59 GMT".parse().unwrap(),
- );
- parts.headers.insert(
- http::header::CONTENT_TYPE,
- "application/json; charset=utf-8".parse().unwrap(),
- );
-
- let init_time = chrono::Utc::now()
- .duration_trunc(chrono::Duration::seconds(1))
- .unwrap();
- parts.headers.insert(
- http::header::LAST_MODIFIED,
- init_time.to_rfc3339().parse().unwrap(),
- );
-
- Self {
- prerender: Response::from_parts(parts, body),
- init_time,
- }
- }
-
- pub async fn fetch(host: &str) -> Result<IdentityResponse, IdentityServiceError> {
- let url = format!("https://{host}{}/data.json", Self::ROOT);
- let id: IdentityResponse = client::Client::global()
- .get(url)
- .send()
- .await?
- .error_for_status()?
- .json()
- .await?;
-
- if id.identity.host() != host {
- return Err(IdentityServiceError::Host {
- host: id.identity.host().clone(),
- expected: host.into(),
- });
- }
- Ok(id)
- }
-
- pub fn router(&self) -> Router {
- Router::new()
- .route("/data.json", get(Self::handler))
- .with_state(self.clone())
- }
-
- pub async fn handler(State(node): State<Self>, req: Request) -> impl IntoResponse {
- // Check If-None-Match header
- if let Some(etag) = node.prerender.headers().get(http::header::ETAG) {
- for if_none_match in req.headers().get_all(http::header::IF_NONE_MATCH) {
- if if_none_match == etag {
- return StatusCode::NOT_MODIFIED.into_response();
- }
- }
- }
-
- // Check If-Modified-Since header
- if let Some(tm) = req.headers().get(http::header::IF_MODIFIED_SINCE)
- && let Ok(tm) = chrono::DateTime::parse_from_rfc3339(tm.to_str().unwrap_or_default())
- && tm >= node.init_time
- {
- return StatusCode::NOT_MODIFIED.into_response();
- }
-
- // Otherwise return the full response
- let (parts, body) = node.prerender.into_parts();
- let body = Body::from(body);
- Response::from_parts(parts, body)
- }
-}
diff --git a/attic/net/netsim.rs b/attic/net/netsim.rs
deleted file mode 100644
index 7bb2167a8..000000000
@@ -1,145 +0,0 @@
-use std::{iter, time::Duration};
-
-use rand::{Rng, prelude::Distribution};
-
-#[derive(Debug, Clone, Copy)]
-enum Distr {
- Fixed(f64),
- Uniform(rand_distr::Uniform<f64>),
- Normal(rand_distr::Normal<f64>),
- LogNormal(rand_distr::LogNormal<f64>),
-}
-
-impl Distr {
- pub fn normal(mean: f64, cv: f64) -> Self {
- Self::Normal(rand_distr::Normal::from_mean_cv(mean, cv).unwrap())
- }
-
- pub fn lognormal(mean: f64, cv: f64) -> Self {
- Self::LogNormal(rand_distr::LogNormal::from_mean_cv(mean, cv).unwrap())
- }
-
- pub fn uniform(min: f64, max: f64) -> Self {
- if (max - min) < 2.0 * f64::EPSILON {
- return Self::fixed(min);
- }
- Self::Uniform(rand_distr::Uniform::new(min, max).unwrap())
- }
- pub const fn fixed(val: f64) -> Self {
- Self::Fixed(val)
- }
-
- pub fn next(&self, r: &mut impl Rng) -> f64 {
- match self {
- Self::Normal(d) => d.sample(r),
- Self::LogNormal(d) => d.sample(r),
- Self::Uniform(d) => d.sample(r),
- &Self::Fixed(d) => d,
- }
- }
-}
-
-#[derive(Debug, Clone, Copy)]
-pub struct LatencyModel(Distr);
-
-impl LatencyModel {
- pub const MIN: Duration = Duration::from_secs(0);
- pub const MAX: Duration = Duration::from_secs(10);
-
- pub fn normal(mean: Duration, cv: f64) -> Self {
- let mean = mean.clamp(Self::MIN, Self::MAX);
- Self(Distr::normal(mean.as_secs_f64(), cv))
- }
-
- pub fn lognormal(mean: Duration, cv: f64) -> Self {
- let mean = mean.clamp(Self::MIN, Self::MAX);
- Self(Distr::lognormal(mean.as_secs_f64(), cv))
- }
-
- pub fn uniform(min: Duration, max: Duration) -> Self {
- let min = min.clamp(Self::MIN, Self::MAX);
- let max = max.clamp(min, Self::MAX);
- Self(Distr::uniform(min.as_secs_f64(), max.as_secs_f64()))
- }
- pub fn fixed(val: Duration) -> Self {
- let val = val.clamp(Self::MIN, Self::MAX);
- Self(Distr::fixed(val.as_secs_f64()))
- }
-
- pub fn next(&self, r: &mut impl Rng) -> Duration {
- Duration::from_secs_f64(
- self
- .0
- .next(r)
- .clamp(Self::MIN.as_secs_f64(), Self::MAX.as_secs_f64()),
- )
- }
-
- pub fn build(self, n: usize, r: &mut impl Rng) -> LatencySim {
- LatencySim::new(n, self, r)
- }
-}
-
-impl Default for LatencyModel {
- fn default() -> Self {
- Self::normal(Duration::from_millis(100), 0.2)
- }
-}
-
-#[derive(Debug, Clone)]
-pub struct LatencySim {
- e2e_latency: Box<[Duration]>,
-}
-
-impl LatencySim {
- pub fn basic(n: usize) -> Self {
- Self::new(n, LatencyModel::default(), &mut rand::rng())
- }
- pub fn with_rng<R: Rng>(n: usize, r: &mut R) -> Self {
- Self::new(n, LatencyModel::default(), r)
- }
- pub fn with_model(n: usize, latency: LatencyModel) -> Self {
- Self::new(n, latency, &mut rand::rng())
- }
- pub fn new(n: usize, latency: LatencyModel, r: &mut impl Rng) -> Self {
- let k = Self::locate(n, 0).unwrap_or_default();
- let lat_vec = iter::repeat_with(|| latency.next(r))
- .take(k)
- .collect::<Vec<_>>();
- Self {
- e2e_latency: lat_vec.into_boxed_slice(),
- }
- }
-
- pub const fn locate(i: usize, j: usize) -> Option<usize> {
- if i == j {
- None
- } else {
- let (i, j) = if i < j { (j, i) } else { (i, j) };
- Some(i * (i - 1) / 2 + j)
- }
- }
-
- pub fn get_base(&self, i: usize, j: usize) -> &Duration {
- if let Some(idx) = Self::locate(i, j) {
- &self.e2e_latency[idx]
- } else {
- &const { Duration::from_secs(0) }
- }
- }
-
- pub fn get_jitter(&self, i: usize, j: usize, jitter: f64, r: &mut impl Rng) -> Duration {
- let base = *self.get_base(i, j);
- if base <= Duration::from_millis(1) {
- return base;
- }
- let jitter = LatencyModel::normal(base, jitter).next(r);
- base + jitter
- }
-
- pub fn set(&mut self, i: usize, j: usize, val: Duration) {
- if let Some(idx) = Self::locate(i, j) {
- self.e2e_latency[idx] = val;
- }
- }
-}
diff --git a/attic/vm/Cargo.toml b/attic/vm/Cargo.toml
deleted file mode 100644
index 1c96fc37a..000000000
@@ -1,17 +0,0 @@
-[package]
-name = "vm"
-publish = false
-version.workspace = true
-edition.workspace = true
-authors.workspace = true
-homepage.workspace = true
-description = "Tetra VM: tree-sitter based exploration"
-repository.workspace = true
-
-[lints]
-workspace = true
-
-[dependencies]
-tree-sitter = "0.25.3"
-tree-sitter-javascript = "0.23.1"
-anyhow.workspace = true
diff --git a/attic/vm/README.md b/attic/vm/README.md
deleted file mode 100644
index eb14f70c9..000000000
@@ -1,4 +0,0 @@
-# vm
-
-Early experiments with JavaScript parsing and virtual machine concepts using
-Tree‑sitter.
diff --git a/attic/vm/src/main.rs b/attic/vm/src/main.rs
deleted file mode 100644
index 71fd8cc0c..000000000
@@ -1,482 +0,0 @@
-// A *very* small JavaScript engine prototype written in Rust.
-// -----------------------------------------------------------
-// Supported features (MVP)
-// - Numeric literals (e.g., 42, 3.14)
-// - Identifiers & immutable const bindings
-// - Binary expressions with +, -, *, /
-// - console.log(<expr>); for side‑effect output
-// - Function declarations and calls
-// - Return statements
-//
-// This is intentionally tiny so you can flesh out additional
-// language features incrementally (conditionals, etc.).
-//
-// Build & run (requires Rust 1.77+):
-// $ cargo add tree-sitter tree-sitter-javascript anyhow
-// $ cargo run --release -- <file.js>
-// If no file is provided, an inline sample is executed.
-// -----------------------------------------------------------
-use std::{collections::HashMap, fmt, fs, path::Path};
-
-use anyhow::{Context, Result, anyhow};
-use tree_sitter::{Node, Parser};
-
-// ──────────────────────────────────────────────────────────────
-// AST definitions (greatly simplified)
-// ──────────────────────────────────────────────────────────────
-#[derive(Debug, Clone)]
-enum BinOp {
- Add,
- Sub,
- Mul,
- Div,
-}
-
-#[derive(Debug, Clone)]
-enum Expr {
- Number(f64),
- Ident(String),
- Str(String),
- Binary(Box<Expr>, BinOp, Box<Expr>),
- Call { callee: String, args: Vec<Expr> },
-}
-
-#[derive(Debug, Clone)]
-enum Stmt {
- VarDecl(String, Expr),
- Expr(Expr),
- FuncDecl {
- name: String,
- params: Vec<String>,
- body: Vec<Stmt>,
- },
- Return(Option<Expr>),
-}
-
-#[derive(Debug, Clone)]
-struct Program {
- body: Vec<Stmt>,
-}
-
-// ──────────────────────────────────────────────────────────────
-// Simple interpreter ‑ NOT spec‑compliant, just good enough ☺
-// ──────────────────────────────────────────────────────────────
-#[derive(Debug, Clone)]
-enum Value {
- Number(f64),
- Str(String),
- Function {
- params: Vec<String>,
- body: Vec<Stmt>,
- closure: HashMap<String, Value>,
- },
- Undefined,
-}
-
-impl fmt::Display for Value {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- Self::Number(n) => write!(f, "{n}"),
- Self::Str(s) => write!(f, "{s}"),
- Self::Function { .. } => write!(f, "[Function]"),
- Self::Undefined => write!(f, "undefined"),
- }
- }
-}
-
-// Control flow for returns
-#[derive(Debug)]
-enum ControlFlow {
- Normal(Value),
- Return(Value),
-}
-
-#[derive(Clone, Default)]
-struct Interpreter {
- env: HashMap<String, Value>,
-}
-
-impl Interpreter {
- fn run(&mut self, program: &Program) {
- for stmt in &program.body {
- match self.exec_stmt(stmt) {
- Ok(ControlFlow::Return(_)) => {
- eprintln!("Runtime error: Return outside function");
- return;
- },
- Err(e) => eprintln!("Runtime error: {e}"),
- _ => {},
- }
- }
- }
-
- fn exec_stmt(&mut self, stmt: &Stmt) -> Result<ControlFlow> {
- match stmt {
- Stmt::VarDecl(name, init) => {
- let val = self.eval_expr(init)?;
- self.env.insert(name.clone(), val.clone());
- Ok(ControlFlow::Normal(val))
- },
- Stmt::Expr(expr) => {
- let val = self.eval_expr(expr)?;
- Ok(ControlFlow::Normal(val))
- },
- Stmt::FuncDecl { name, params, body } => {
- // Capture current environment as closure
- let func = Value::Function {
- params: params.clone(),
- body: body.clone(),
- closure: self.env.clone(),
- };
- self.env.insert(name.clone(), func);
- Ok(ControlFlow::Normal(Value::Undefined))
- },
- Stmt::Return(expr) => {
- let val = match expr {
- Some(e) => self.eval_expr(e)?,
- None => Value::Undefined,
- };
- Ok(ControlFlow::Return(val))
- },
- }
- }
-
- fn eval_expr(&mut self, expr: &Expr) -> Result<Value> {
- match expr {
- Expr::Number(n) => Ok(Value::Number(*n)),
- Expr::Str(s) => Ok(Value::Str(s.clone())),
- Expr::Ident(name) => self
- .env
- .get(name)
- .cloned()
- .ok_or_else(|| anyhow!("ReferenceError: {name} is not defined")),
- Expr::Binary(lhs, op, rhs) => {
- let l = self.eval_expr(lhs)?;
- let r = self.eval_expr(rhs)?;
- self.apply_bin_op(op, l, r)
- },
- Expr::Call { callee, args } => {
- // Check for built-ins first
- if callee == "console.log" {
- let printed: Vec<String> = args
- .iter()
- .map(|a| self.eval_expr(a))
- .collect::<Result<Vec<_>>>()?
- .into_iter()
- .map(|v| v.to_string())
- .collect();
- println!("{}", printed.join(" "));
- return Ok(Value::Undefined);
- }
-
- // Otherwise, look up the function
- let func = self
- .env
- .get(callee)
- .ok_or_else(|| anyhow!("ReferenceError: {callee} is not defined"))?
- .clone();
-
- match func {
- Value::Function {
- params,
- body,
- closure,
- } => {
- // Evaluate arguments
- let arg_vals: Vec<Value> = args
- .iter()
- .map(|a| self.eval_expr(a))
- .collect::<Result<Vec<_>>>()?;
-
- // Check arity
- if params.len() != arg_vals.len() {
- return Err(anyhow!(
- "TypeError: {callee} expects {} arguments, got {}",
- params.len(),
- arg_vals.len()
- ));
- }
-
- // Create new environment for function execution
- let saved_env = self.env.clone();
- self.env = closure; // Start with closure
-
- // Bind parameters
- for (param, arg_val) in params.iter().zip(arg_vals.iter()) {
- self.env.insert(param.clone(), arg_val.clone());
- }
-
- // Execute function body
- let mut result = Value::Undefined;
- for stmt in &body {
- match self.exec_stmt(stmt)? {
- ControlFlow::Return(val) => {
- result = val;
- break;
- },
- ControlFlow::Normal(val) => {
- result = val;
- },
- }
- }
-
- // Restore environment
- self.env = saved_env;
- Ok(result)
- },
- _ => Err(anyhow!("TypeError: {callee} is not a function")),
- }
- },
- }
- }
-
- fn apply_bin_op(&self, op: &BinOp, l: Value, r: Value) -> Result<Value> {
- match (l, r) {
- (Value::Number(a), Value::Number(b)) => {
- let res = match op {
- BinOp::Add => a + b,
- BinOp::Sub => a - b,
- BinOp::Mul => a * b,
- BinOp::Div => a / b,
- };
- Ok(Value::Number(res))
- },
- _ => Err(anyhow!("TypeError: binary operands must be numbers")),
- }
- }
-}
-
-// ──────────────────────────────────────────────────────────────
-// Entry point & plumbing
-// ──────────────────────────────────────────────────────────────
-fn main() -> Result<()> {
- // 1) Load source (file or sample)
- let source_code = if let Some(path) = std::env::args().nth(1) {
- fs::read_to_string(Path::new(&path)).with_context(|| format!("Could not read {path}"))?
- } else {
- r"// demo snippet with functions
- function add(a, b) {
- return a + b;
- }
-
- function multiply(x, y) {
- const result = x * y;
- return result;
- }
-
- const sum = add(5, 3);
- console.log('5 + 3 =', sum);
-
- const product = multiply(4, 7);
- console.log('4 * 7 =', product);
-
- // Nested function calls
- console.log('add(10, multiply(2, 3)) =', add(10, multiply(2, 3)));
- "
- .into()
- };
-
- // 2) Parse with tree‑sitter‑javascript
- let mut parser = Parser::new();
- parser
- .set_language(&tree_sitter_javascript::LANGUAGE.into())
- .expect("Loading JS grammar failed");
- let tree = parser
- .parse(&source_code, None)
- .ok_or_else(|| anyhow!("Parse failure"))?;
-
- // 2.1) Print the tree
- // println!("tree: {:?}", tree.root_node());
- // fn rec_print(node: Node, indent: usize) {
- // println!("{:indent$}{:?}", "", node.kind(), indent = indent);
- // let mut cursor = node.walk();
- // for child in node.children(&mut cursor) {
- // rec_print(child, indent + 2);
- // }
- // }
- // rec_print(tree.root_node(), 0);
-
- // 3) Convert to *our* tiny AST subset
- let program = build_ast(tree.root_node(), &source_code)?;
-
- // 4) Run interpreter
- Interpreter::default().run(&program);
-
- Ok(())
-}
-
-// ──────────────────────────────────────────────────────────────
-// AST builder helpers (subset only!)
-// ──────────────────────────────────────────────────────────────
-fn build_ast(root: Node, src: &str) -> Result<Program> {
- let mut body = Vec::new();
- let mut cursor = root.walk();
- for child in root.children(&mut cursor) {
- match child.kind() {
- "lexical_declaration" => body.extend(parse_lexical_decl(child, src)?),
- "expression_statement" => {
- let expr_node = child
- .named_child(0)
- .ok_or_else(|| anyhow!("Malformed expression statement"))?;
- body.push(Stmt::Expr(parse_expr(expr_node, src)?));
- },
- "function_declaration" => {
- body.push(parse_function_decl(child, src)?);
- },
- "comment" | ";" => {}, // ignore
- other => eprintln!("⚠️ Unhandled top‑level node: {other} (ignored)"),
- }
- }
- Ok(Program { body })
-}
-
-fn parse_function_decl(node: Node, src: &str) -> Result<Stmt> {
- let name_node = node
- .child_by_field_name("name")
- .ok_or_else(|| anyhow!("Missing function name"))?;
- let name = src[name_node.byte_range()].to_string();
-
- let params_node = node
- .child_by_field_name("parameters")
- .ok_or_else(|| anyhow!("Missing function parameters"))?;
- let params = parse_params(params_node, src)?;
-
- let body_node = node
- .child_by_field_name("body")
- .ok_or_else(|| anyhow!("Missing function body"))?;
- let body = parse_block_stmt(body_node, src)?;
-
- Ok(Stmt::FuncDecl { name, params, body })
-}
-
-fn parse_params(node: Node, src: &str) -> Result<Vec<String>> {
- let mut params = Vec::new();
- let mut cursor = node.walk();
- for child in node.named_children(&mut cursor) {
- if child.kind() == "identifier" {
- params.push(src[child.byte_range()].to_string());
- }
- }
- Ok(params)
-}
-
-fn parse_block_stmt(node: Node, src: &str) -> Result<Vec<Stmt>> {
- let mut stmts = Vec::new();
- let mut cursor = node.walk();
- for child in node.children(&mut cursor) {
- match child.kind() {
- "lexical_declaration" => stmts.extend(parse_lexical_decl(child, src)?),
- "expression_statement" => {
- let expr_node = child
- .named_child(0)
- .ok_or_else(|| anyhow!("Malformed expression statement"))?;
- stmts.push(Stmt::Expr(parse_expr(expr_node, src)?));
- },
- "return_statement" => {
- stmts.push(parse_return_stmt(child, src)?);
- },
- "{" | "}" | "comment" => {}, // ignore
- other => eprintln!("⚠️ Unhandled block node: {other} (ignored)"),
- }
- }
- Ok(stmts)
-}
-
-fn parse_return_stmt(node: Node, src: &str) -> Result<Stmt> {
- // Check if there's an expression after 'return'
- let expr = if let Some(expr_node) = node.named_child(0) {
- Some(parse_expr(expr_node, src)?)
- } else {
- None
- };
- Ok(Stmt::Return(expr))
-}
-
-fn parse_lexical_decl(node: Node, src: &str) -> Result<Vec<Stmt>> {
- let mut stmts = Vec::new();
- let mut cursor = node.walk();
- for child in node.children(&mut cursor) {
- if child.kind() == "variable_declarator" {
- let id = child
- .child_by_field_name("name")
- .ok_or_else(|| anyhow!("Missing declarator name"))?;
- let init = child
- .child_by_field_name("value")
- .ok_or_else(|| anyhow!("Missing initializer"))?;
- let name = src[id.byte_range()].to_string();
- let expr = parse_expr(init, src)?;
- stmts.push(Stmt::VarDecl(name, expr));
- }
- }
- Ok(stmts)
-}
-
-fn parse_expr(node: Node, src: &str) -> Result<Expr> {
- match node.kind() {
- "number" => Ok(Expr::Number(src[node.byte_range()].parse()?)),
- "identifier" => Ok(Expr::Ident(src[node.byte_range()].into())),
- "string" | "string_fragment" => {
- let raw = &src[node.byte_range()];
- let unquoted = raw.trim_matches('\'').trim_matches('"').to_string();
- Ok(Expr::Str(unquoted))
- },
- "binary_expression" => {
- let left = node
- .child_by_field_name("left")
- .ok_or_else(|| anyhow!("Missing lhs"))?;
- let right = node
- .child_by_field_name("right")
- .ok_or_else(|| anyhow!("Missing rhs"))?;
- let op_node = node
- .child_by_field_name("operator")
- .or_else(|| node.child(1))
- .ok_or_else(|| anyhow!("Missing operator"))?;
- let op_char = src[op_node.byte_range()].trim();
- let op = match op_char {
- "+" => BinOp::Add,
- "-" => BinOp::Sub,
- "*" => BinOp::Mul,
- "/" => BinOp::Div,
- _ => return Err(anyhow!("Unsupported operator {op_char}")),
- };
- Ok(Expr::Binary(
- Box::new(parse_expr(left, src)?),
- op,
- Box::new(parse_expr(right, src)?),
- ))
- },
- "call_expression" => {
- let callee_node = node
- .child_by_field_name("function")
- .ok_or_else(|| anyhow!("Malformed call expression"))?;
- let callee = match callee_node.kind() {
- "member_expression" => {
- let obj = callee_node
- .child_by_field_name("object")
- .ok_or_else(|| anyhow!("Malformed member expression"))?;
- let prop = callee_node
- .child_by_field_name("property")
- .ok_or_else(|| anyhow!("Malformed member expression"))?;
- format!(
- "{}.{}",
- src[obj.byte_range()].trim(),
- src[prop.byte_range()].trim()
- )
- },
- "identifier" => src[callee_node.byte_range()].trim().into(),
- other => return Err(anyhow!("Unsupported callee type: {other}")),
- };
- // Arguments
- let args_node = node
- .child_by_field_name("arguments")
- .ok_or_else(|| anyhow!("Missing arguments"))?;
- let mut args = Vec::<Expr>::new();
- let mut cur = args_node.walk();
- for arg in args_node.named_children(&mut cur) {
- args.push(parse_expr(arg, src)?);
- }
- Ok(Expr::Call { callee, args })
- },
- other => Err(anyhow!("Unhandled expr kind: {other}")),
- }
-}
diff --git a/attic/vmi-precompile/.gitignore b/attic/vmi-precompile/.gitignore
deleted file mode 100644
index 3bbc62499..000000000
@@ -1,2 +0,0 @@
-rsrc/
-src/wasm_modules.rs
\ No newline at end of file
diff --git a/attic/vmi-precompile/Cargo.toml b/attic/vmi-precompile/Cargo.toml
deleted file mode 100644
index 8ebc044c8..000000000
@@ -1,30 +0,0 @@
-[package]
-name = "tetra-vmi-precompile"
-version.workspace = true
-edition.workspace = true
-authors.workspace = true
-homepage.workspace = true
-description = "Tetra VMI precompile: bundled Wasm modules"
-repository.workspace = true
-
-[lints]
-workspace = true
-
-[dependencies]
-serde_json.workspace = true
-serde.workspace = true
-
-[build-dependencies]
-pathdiff = "0.2.3"
-cargo_metadata = "0.19.2"
-serde_json.workspace = true
-serde.workspace = true
-blake3.workspace = true
-
-[[package.metadata.wasm-bundle.projects]]
-name = "scripthost"
-crate = "tetra-scripthost"
-
-[[package.metadata.wasm-bundle.projects]]
-name = "swcbundler"
-crate = "tetra-swcbundler"
diff --git a/attic/vmi-precompile/README.md b/attic/vmi-precompile/README.md
deleted file mode 100644
index 3a5e4c352..000000000
@@ -1,3 +0,0 @@
-# tetra-vmi-precompile
-
-Collection of prebuilt WebAssembly modules bundled for use by the script host.
diff --git a/attic/vmi-precompile/build.rs b/attic/vmi-precompile/build.rs
deleted file mode 100644
index 4ebf9ab8e..000000000
@@ -1,140 +0,0 @@
-use std::{
- env, fs,
- path::{Path, PathBuf},
- sync::LazyLock,
-};
-
-use cargo_metadata::{CargoOpt, MetadataCommand};
-use pathdiff::diff_paths;
-use serde::Deserialize;
-
-#[derive(Deserialize)]
-struct Project {
- name: String,
- //#[serde(rename = "crate")]
- // crate_name: String,
- #[serde(default)]
- metadata: serde_json::Value,
-}
-
-#[derive(Deserialize)]
-struct Metadata {
- projects: Vec<Project>,
-}
-
-static METADATA: LazyLock<Metadata> = LazyLock::new(|| {
- let manifest_path = env::var("CARGO_MANIFEST_DIR").unwrap() + "/Cargo.toml";
- let metadata = MetadataCommand::new()
- .manifest_path(&manifest_path)
- .features(CargoOpt::AllFeatures)
- .exec()
- .expect("Failed to get cargo metadata");
-
- let pkg = metadata
- .packages
- .iter()
- .find(|p| p.name == env!("CARGO_PKG_NAME"))
- .expect(concat!(
- "Could not find ",
- env!("CARGO_PKG_NAME"),
- " package"
- ));
-
- serde_json::from_value(
- pkg.metadata
- .get("wasm-bundle")
- .expect("Failed to get wasm-bundle metadata")
- .clone(),
- )
- .expect("Failed to parse wasm-bundle metadata")
-});
-
-fn main() {
- // Find the workspace root directory (where your project is located)
- println!("cargo:rerun-if-changed=build.rs"); // Rerun if this script changes
-
- // Check if rsrc directory exists and contains WASM files
- let src_dir = "./src/";
- let wasm_dir = Path::new("./rsrc/");
-
- if !wasm_dir.exists() {
- println!("cargo:warning=WASM directory not found. Run scripts/build_wasm.py first.");
- fs::create_dir_all(wasm_dir).unwrap();
- return;
- }
-
- // Mark all WASM files as dependencies for the build
- if let Ok(entries) = fs::read_dir(wasm_dir) {
- for entry in entries.flatten() {
- let path = entry.path();
- if path.extension().is_some_and(|ext| ext == "wasm") {
- println!("cargo:rerun-if-changed={}", path.display());
- }
- }
- }
-
- let mut projects = Vec::new();
-
- for project in &METADATA.projects {
- let wasm_path = wasm_dir.join(format!("{}.wasm", project.name));
- if wasm_path.exists() {
- projects.push((project, wasm_path));
- } else {
- println!(
- "cargo:warning=WASM file for {} not found at {}. Run scripts/build_wasm.py.",
- project.name,
- wasm_path.display()
- );
- }
- }
-
- if projects.is_empty() {
- println!("cargo:warning=No WASM files found. Run scripts/build_wasm.py first.");
- return;
- }
-
- // Generate a Rust module that embeds the WASM files
- generate_wasm_module(&projects, Path::new(&src_dir));
-}
-
-fn generate_wasm_module(projects: &Vec<(&'static Project, PathBuf)>, src_dir: &Path) {
- let mut mod_content = String::from(
- r"//! WASM Modules generated during build
-//! This file is auto-generated. Do not edit.
-
-#[derive(Debug, Clone, Copy)]
-pub struct WasmModule<'a> {
- pub name: &'a str,
- pub bytes: &'a [u8],
- pub metadata: &'a str,
- pub sum: [u8; 32],
-}
-
-// Use a static array for direct access if needed
-pub static ALL_MODULES: &[WasmModule<'static>] = & [
-",
- );
-
- for (project, path) in projects {
- let relative_path =
- diff_paths(path, src_dir).expect("Could not create relative path for include_bytes!");
- let include_path = relative_path.to_str().unwrap().replace('\\', "/");
- mod_content.push_str("\tWasmModule {\n");
- mod_content.push_str(&format!("\t\tname: {:?},\n", project.name));
- mod_content.push_str(&format!("\t\tbytes: include_bytes!({include_path:?}),\n"));
- mod_content.push_str(&format!(
- "\t\tmetadata: {:?},\n",
- project.metadata.to_string()
- ));
- let bin = fs::read(path).unwrap();
- let sum = blake3::hash(&bin);
- mod_content.push_str(&format!("\t\tsum: {:?},\n", sum.as_bytes()));
- mod_content.push_str("\t},\n");
- }
-
- mod_content.push_str("];\n\n");
-
- let mod_path = src_dir.join("wasm_modules.rs");
- fs::write(&mod_path, &mod_content).expect("Failed to write generated wasm_modules.rs");
- println!("Generated module at: {}", mod_path.display());
-}
diff --git a/attic/vmi-precompile/src/lib.rs b/attic/vmi-precompile/src/lib.rs
deleted file mode 100644
index a9da061d1..000000000
@@ -1,16 +0,0 @@
-mod wasm_modules;
-pub use wasm_modules::*;
-
-impl WasmModule<'_> {
- pub fn metadata<'b, T: serde::de::Deserialize<'b>>(&'b self) -> T {
- serde_json::from_str(self.metadata).unwrap()
- }
-
- pub const fn hash(&self) -> [u8; 32] {
- self.sum
- }
-}
-
-pub fn lookup_module(name: &str) -> Option<WasmModule<'static>> {
- ALL_MODULES.iter().find(|m| m.name == name).copied()
-}
diff --git a/lib/services/examples/graphql.rs b/lib/services/examples/graphql.rs
index 97111e48d..ca1c9534b 100644
@@ -144,14 +144,14 @@ impl MutationRoot {
let user = users
.get_mut(&id)
- .ok_or_else(|| async_graphql::Error::new(format!("User with id {id} not found")))?;
+ .ok_or_else(|| async_graphql::Error::from(format!("User with id {id} not found")))?;
if let Some(name) = input.name {
user.name = name;
}
if let Some(email) = input.email {
if !email.contains('@') {
- return Err(async_graphql::Error::new("Invalid email format"));
+ return Err("Invalid email format".into());
}
user.email = email;
}
diff --git a/package.json b/package.json
index 649af38b3..1a673ca6e 100644
@@ -21,13 +21,13 @@
"lint:fix": "bun scripts/lint.ts --fix",
"lint:fmt": "bun scripts/lint.ts --fmt",
"lint:sync-deps": "bun scripts/lint.ts --sync-deps",
- "py:build": "cd bin/py && maturin build --release",
+ "py:build": "cd bin/py && VIRTUAL_ENV=$PWD/../../.venv maturin build --release",
"py:clean": "rm -rf target/wheels/tetra-* bin/py/build/ bin/py/dist/",
"py:dev": "bun run py:stubs && bun run py:develop",
- "py:develop": "cd bin/py && maturin develop",
- "py:install": "pip install --force-reinstall $(ls target/wheels/tetra-*.whl | head -1)",
+ "py:develop": "cd bin/py && VIRTUAL_ENV=$PWD/../../.venv maturin develop",
+ "py:install": "uv run --project . pip install --force-reinstall $(ls target/wheels/tetra-*.whl | head -1)",
"py:stubs": "cd bin/py && cargo run --bin stub_gen --no-default-features --features stub-gen",
- "py:test": "python3 bin/py/examples/simple_demo.py",
+ "py:test": "uv run --project . python bin/py/examples/simple_demo.py",
"sim:tpm": "bun scripts/tpm-sim.ts",
"test": "cargo nextest run",
"test:actor": "cargo nextest run -p tetra-actor",
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 000000000..39a7f3a59
@@ -0,0 +1,8 @@
+[project]
+name = "tetra-workspace"
+version = "0.1.0"
+requires-python = ">=3.11"
+description = "Tetra workspace Python environment"
+
+[tool.uv.workspace]
+members = ["bin/py"]