//! The mem engine's store: `MemStore::new`, the engine-native //! constructor (ADR-024 §4 — the one documented exception to the //! open prose), and the guarded state it owns — plus the `Store` trait //! surface this crate's whole point rides on. //! //! Isolation per open: each `new()` yields a fully isolated instance //! sharing nothing — a fresh `State` (the guarded per-mechanism core), //! a fresh `WakeBus` (the wake-routing substrate), a fresh `Clock` — //! exactly the suite factory's isolation contract; two instances never //! contend and never observe each other. There is no connection, no //! file, no pool; teardown is drop — the engine-wide `Drop` runs the //! close path (the wake bus empties, every open feed disconnects), and //! the state is gone with the process's references, the honest- //! ephemeral posture (ADR-016 carriers, ADR-024 §2: the dependency on //! `alkstore-mem` *is* the ephemerality declaration). //! //! The `Store` surface wires the mechanism handles (core-owned trait //! objects — engine types never appear in consumer signatures): `queue` //! carries its `QueueOpts` stamps (queues are names, not config //! owners), `stream`/`outbox` carry validated names, `try_lock` rides //! the TTL registry, `schedule`/`unschedule`/`run_schedules` the //! collapsed scheduler, `notify`/`listen` the wake substrate, and //! `begin_tx` allocates the tx overlay (`crate::tx` — the load-bearing //! seam; `with_tx` is the trait's provided composition). use std::sync::Arc; use alkstore::{BoxedFuture, Result, StopToken, Store, TxHandle}; use alkstore::{QueueOpts, ScheduleOpts}; use serde_json::Value; use crate::clock::Clock; use crate::queue::MemQueueHandle; use crate::scheduler; use crate::state::State; use crate::stream::MemStreamHandle; use crate::tx::MemTxHandle; use crate::wakes::WakeBus; /// The mem engine's store handle. Consumers hold /// [`alkstore::Store`] — the trait is the contract surface, never the /// engine type; `MemStore::new` is the engine-native constructor. pub struct MemStore { pub(crate) state: Arc, pub(crate) wakes: Arc, pub(crate) clock: Arc, } impl Default for MemStore { fn default() -> Self { Self::new() } } impl std::fmt::Debug for MemStore { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("MemStore").finish_non_exhaustive() } } impl MemStore { /// Construct a fully isolated mem engine instance. Engine-native: /// no connection string, no file path (ADR-024 §4's documented /// exception to the open prose). Teardown is drop; each instance /// shares nothing with any other. pub fn new() -> Self { Self { state: Arc::new(State::new()), wakes: Arc::new(WakeBus::new()), clock: Arc::new(Clock::new()), } } #[cfg(test)] pub(crate) fn wakes(&self) -> &Arc { &self.wakes } #[cfg(test)] pub(crate) fn clock(&self) -> &Arc { &self.clock } } impl Drop for MemStore { fn drop(&mut self) { self.wakes.close(); } } impl Store for MemStore { fn begin_tx(&self) -> BoxedFuture<'_, Result>> { let handle = MemTxHandle::begin(self.state.clone(), self.clock.clone(), self.wakes.clone()); Box::pin(async move { Ok(Box::new(handle) as Box) }) } fn notify<'a>(&'a self, channel: &str, payload: Value) -> BoxedFuture<'a, Result<()>> { crate::notify::notify(self.wakes.clone(), channel, payload) } fn listen<'a>( &'a self, channel: &str, ) -> BoxedFuture<'a, Result>> { let wakes = self.wakes.clone(); let owned = channel.to_string(); Box::pin(async move { crate::notify::listen(wakes, &owned) }) } fn stream<'a>( &'a self, name: &str, ) -> BoxedFuture<'a, Result>> { let handle = alkstore::validate_shared_name(name).map(|()| { MemStreamHandle::new( name.to_string(), self.state.clone(), self.clock.clone(), self.wakes.clone(), ) }); Box::pin(async move { handle.map(|h| Box::new(h) as Box) }) } fn queue<'a>( &'a self, name: &str, opts: QueueOpts, ) -> BoxedFuture<'a, Result>> { let handle = alkstore::validate_shared_name(name).map(|()| { Box::new(MemQueueHandle::new( name.to_string(), opts, self.state.clone(), self.clock.clone(), )) as Box }); Box::pin(async move { handle }) } fn outbox<'a>(&'a self, name: &str) -> BoxedFuture<'a, Result>> { let handle = alkstore::validate_shared_name(name).map(|()| { Box::new(crate::outbox::MemOutbox::new( name.to_string(), self.state.clone(), self.clock.clone(), )) as Box }); Box::pin(async move { handle }) } fn try_lock<'a>( &'a self, name: &str, owner: &str, ttl: i64, ) -> BoxedFuture<'a, Result>>> { let outcome = crate::lock::try_lock(self.state.clone(), self.clock.clone(), name, owner, ttl); Box::pin(async move { outcome }) } fn schedule<'a>( &'a self, name: &str, spec: &str, queue: &str, payload: Value, opts: ScheduleOpts, ) -> BoxedFuture<'a, Result> { scheduler::schedule( self.state.clone(), self.clock.clone(), name, spec, queue, payload, opts, ) } fn unschedule<'a>(&'a self, name: &str) -> BoxedFuture<'a, Result> { scheduler::unschedule(self.state.clone(), name) } fn run_schedules<'a>(&'a self, stop: StopToken) -> BoxedFuture<'a, Result<()>> { scheduler::run_schedules(self.state.clone(), self.clock.clone(), stop) } } #[cfg(test)] mod tests { use super::*; use alkstore::EnqueueOpts; #[test] fn two_new_instances_share_nothing() { let a = MemStore::new(); let b = MemStore::new(); assert!( !std::sync::Arc::ptr_eq(&a.state, &b.state), "the state cores are distinct instances" ); assert!( !std::sync::Arc::ptr_eq(&a.wakes, &b.wakes), "the wake substrates are distinct instances" ); assert!( !std::sync::Arc::ptr_eq(&a.clock, &b.clock), "the clocks are distinct instances" ); } #[test] fn instances_observed_through_their_futures_share_nothing() { let a = MemStore::new(); let b = MemStore::new(); a.clock().freeze(1_000_000); assert_eq!(a.clock().unix_now(), 1_000_000, "instance a's clock froze"); assert!( b.clock().unix_now() > 1_700_000_000, "instance b's clock kept the process time" ); } #[test] fn wake_substrates_do_not_cross_instances() { let a = MemStore::new(); let b = MemStore::new(); a.wakes().fire_commit_wakes(vec!["orders".into()]); let mut late_feed = b.wakes().attach("orders"); assert!( late_feed.rx().try_recv().is_err(), "instance a's commit fired before the feed attached — instance b's substrate never replayed it, and instance a's substrate is unreachable from b" ); let mut a_feed = a.wakes().attach("orders"); a.wakes().fire_commit_wakes(vec!["orders".into()]); assert!( matches!( a_feed.rx().try_recv(), Ok(wake) if wake.channel == "orders" ), "instance a's substrate routes its own attaches" ); } #[tokio::test(flavor = "current_thread")] async fn engine_drop_closes_open_feeds() { let store = MemStore::new(); let mut feed = store.wakes().attach("orders"); std::mem::drop(store); assert!( feed.rx().recv().await.is_none(), "teardown is drop: the engine's close path disconnects every feed" ); } #[tokio::test(flavor = "current_thread")] async fn constructor_entry_validation() { let store = MemStore::new(); for bad in ["", " ", "__alkstore_q"] { match store.queue(bad, QueueOpts::default()).await { Err(alkstore::Error::InvalidName { .. } | alkstore::Error::ReservedName { .. }) => { } _other => panic!("queue({bad:?}) rejects"), } assert!(store.stream(bad).await.is_err()); assert!(store.outbox(bad).await.is_err()); } // Nothing was created on a rejected name — the next queue with // a fresh name starts clean (ids allocate nowhere). let good = store.queue("good", QueueOpts::default()).await.unwrap(); let id = good .enqueue(serde_json::json!(1), EnqueueOpts::default()) .await .unwrap(); assert_eq!(id, 1, "ids count from 1"); } #[tokio::test(flavor = "current_thread")] async fn begin_tx_grants_a_boxed_handle_that_commits() { let store = MemStore::new(); let mut tx = store.begin_tx().await.unwrap(); tx.enqueue_tx("q", EnqueueOpts::default(), serde_json::json!(1)) .await .unwrap(); tx.commit().await.unwrap(); let q = store.queue("q", QueueOpts::default()).await.unwrap(); assert!(q.get_job(1).await.unwrap().is_some()); } }