diff --git a/.config/hakari.toml b/.config/hakari.toml index 51825f59..e7b336f1 100644 --- a/.config/hakari.toml +++ b/.config/hakari.toml @@ -35,7 +35,11 @@ platforms = [ # # `nexus-nostd-smoketest` is the no_std derive-macro compile-gate (#304): it is # built for thumbv7em/wasm32, so a std workspace-hack edge would break it. -workspace-members = ["nexus", "nexus-nostd-smoketest"] +# +# `nexus-store` is no_std (core+alloc) — workspace-hack force-enables +# std-implying futures-util features (io/channel/sink) that feature +# unification would leak into its --no-default-features build. (#301) +workspace-members = ["nexus", "nexus-nostd-smoketest", "nexus-store"] third-party = [ { name = "futures-core" }, ] diff --git a/Cargo.lock b/Cargo.lock index 4f0a2ce9..0104363f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1340,7 +1340,6 @@ dependencies = [ "thiserror", "tokio", "trybuild", - "workspace-hack", ] [[package]] @@ -2666,13 +2665,18 @@ checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" name = "workspace-hack" version = "0.1.0" dependencies = [ + "aligned-vec", "bitflags", + "bytes", "crossbeam-utils", "crypto-common", "digest", "either", + "futures", + "futures-channel", "futures-io", "futures-sink", + "futures-task", "futures-util", "getrandom 0.3.4", "memchr", @@ -2684,6 +2688,7 @@ dependencies = [ "serde_core", "serde_json", "sha2", + "slab", "smallvec", "sqlx-core", "sqlx-postgres", diff --git a/Cargo.toml b/Cargo.toml index d64a3f13..099a9975 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,15 +21,15 @@ members = [ ] [workspace.dependencies] -aligned-vec = "=0.6.4" +aligned-vec = { version = "=0.6.4", default-features = false } arrayvec = { version = "0.7.6", default-features = false } bytemuck = { version = "1", features = ["derive"] } -bytes = "1" +bytes = { version = "1", default-features = false } crc32c = "0.6.8" criterion = { version = "0.8", features = ["html_reports"] } fjall = { version = "3", features = ["bytes_1"] } foldhash = "0.2.0" -futures = "0.3" +futures = { version = "0.3", default-features = false, features = ["alloc"] } futures-core = { version = "0.3", default-features = false } insta = "1.47.2" minicbor = { version = "2.2.2", features = ["alloc"] } diff --git a/crates/nexus-fjall/Cargo.toml b/crates/nexus-fjall/Cargo.toml index d270837a..d010a345 100644 --- a/crates/nexus-fjall/Cargo.toml +++ b/crates/nexus-fjall/Cargo.toml @@ -19,9 +19,9 @@ import = ["nexus-store/import"] [dependencies] arrayvec = { workspace = true } -bytes = { workspace = true } +bytes = { workspace = true, features = ["std"] } fjall = { workspace = true } -futures.workspace = true +futures = { workspace = true, features = ["std", "async-await", "executor"] } nexus = { version = "0.1.0", path = "../nexus" } nexus-store = { version = "0.1.0", path = "../nexus-store", features = ["subscription"] } nexus-wake = { version = "0.1.0", path = "../nexus-wake" } diff --git a/crates/nexus-inmemory/Cargo.toml b/crates/nexus-inmemory/Cargo.toml index 5dbb1ca4..26b45418 100644 --- a/crates/nexus-inmemory/Cargo.toml +++ b/crates/nexus-inmemory/Cargo.toml @@ -16,8 +16,8 @@ export = ["nexus-store/export"] import = ["nexus-store/import"] [dependencies] -bytes = { workspace = true } -futures = { workspace = true } +bytes = { workspace = true, features = ["std"] } +futures = { workspace = true, features = ["std", "async-await", "executor"] } nexus = { version = "0.1.0", path = "../nexus" } nexus-store = { version = "0.1.0", path = "../nexus-store", features = ["subscription"] } nexus-wake = { version = "0.1.0", path = "../nexus-wake" } diff --git a/crates/nexus-postgres/Cargo.toml b/crates/nexus-postgres/Cargo.toml index e94d9d82..b67efc96 100644 --- a/crates/nexus-postgres/Cargo.toml +++ b/crates/nexus-postgres/Cargo.toml @@ -12,8 +12,8 @@ keywords = ["event-sourcing", "event-store", "postgres"] categories = ["database"] [dependencies] -bytes = { workspace = true } -futures = { workspace = true } +bytes = { workspace = true, features = ["std"] } +futures = { workspace = true, features = ["std", "async-await", "executor"] } nexus = { version = "0.1.0", path = "../nexus" } nexus-store = { version = "0.1.0", path = "../nexus-store", features = ["subscription"] } nexus-wake = { version = "0.1.0", path = "../nexus-wake" } diff --git a/crates/nexus-store-testing/Cargo.toml b/crates/nexus-store-testing/Cargo.toml index 35df505e..670c434f 100644 --- a/crates/nexus-store-testing/Cargo.toml +++ b/crates/nexus-store-testing/Cargo.toml @@ -12,7 +12,7 @@ keywords = ["event-sourcing", "testing", "conformance"] categories = ["data-structures", "development-tools::testing"] [dependencies] -futures.workspace = true +futures = { workspace = true, features = ["std", "async-await", "executor"] } nexus = { version = "0.1.0", path = "../nexus" } nexus-store = { version = "0.1.0", path = "../nexus-store" } tokio = { workspace = true, features = ["macros", "rt"] } diff --git a/crates/nexus-store/Cargo.toml b/crates/nexus-store/Cargo.toml index e3a357b7..d82d6fc1 100644 --- a/crates/nexus-store/Cargo.toml +++ b/crates/nexus-store/Cargo.toml @@ -12,6 +12,17 @@ keywords = ["event-sourcing", "event-store", "cqrs", "persistence"] categories = ["data-structures"] [features] +default = ["std"] +# Additive: enabling `std` only ADDS (the std::error::Error bridge via +# thiserror + the deps' std impls); disabling it (`--no-default-features`) +# yields a no_std + alloc + core::error::Error store. (#301, mirrors #279) +std = [ + "thiserror/std", + "nexus/std", + "futures/std", + "bytes/std", + "aligned-vec/std", +] # Generic catch-up-then-live-tail subscription loop + the `wake` traits. # Dep-free: the loop is generic over `WakeSource`, so no runtime is pulled. # The in-process wake registry (`StreamNotifiers`, tokio-backed) lives in the @@ -49,16 +60,15 @@ crc32c = { workspace = true, optional = true } futures = { workspace = true } futures-core = { workspace = true } minicbor = { workspace = true, features = ["derive"], optional = true } -nexus = { version = "0.1.0", path = "../nexus" } +nexus = { version = "0.1.0", path = "../nexus", default-features = false } rkyv = { workspace = true, optional = true } serde = { workspace = true, optional = true } serde_json = { workspace = true, optional = true } -thiserror = { workspace = true, features = ["std"] } -workspace-hack = { version = "0.1", path = "../workspace-hack" } +thiserror = { workspace = true } [dev-dependencies] criterion = { workspace = true } -futures = { workspace = true } +futures = { workspace = true, features = ["std", "async-await", "executor"] } insta = { workspace = true } # Self dev-dependency unifies feature-gated integration tests into the # default-feature nextest gate (which builds no explicit features). Without diff --git a/crates/nexus-store/src/builder.rs b/crates/nexus-store/src/builder.rs index 274bdcbe..c6350876 100644 --- a/crates/nexus-store/src/builder.rs +++ b/crates/nexus-store/src/builder.rs @@ -1,4 +1,4 @@ -use std::marker::PhantomData; +use core::marker::PhantomData; use crate::repository::EventStore; use crate::store::{RawEventStore, Store}; @@ -44,7 +44,7 @@ pub struct NoSnapshot; pub struct WithSnapshot { store: SS, trigger: T, - schema_version: std::num::NonZeroU32, + schema_version: core::num::NonZeroU32, snapshot_on_read: bool, } @@ -152,7 +152,7 @@ use super::snapshot::Snapshotting; #[cfg(feature = "snapshot")] use crate::state; #[cfg(feature = "snapshot")] -use std::num::NonZeroU64; +use core::num::NonZeroU64; /// Default snapshot interval. #[cfg(feature = "snapshot")] @@ -160,7 +160,7 @@ const DEFAULT_SNAPSHOT_INTERVAL: u64 = 100; /// Default snapshot schema version. #[cfg(feature = "snapshot")] -const DEFAULT_SCHEMA_VERSION: std::num::NonZeroU32 = std::num::NonZeroU32::MIN; +const DEFAULT_SCHEMA_VERSION: core::num::NonZeroU32 = core::num::NonZeroU32::MIN; #[cfg(feature = "snapshot-json")] impl RepositoryBuilder { @@ -292,7 +292,7 @@ impl RepositoryBuilder> { /// Set the schema version for snapshot invalidation. #[must_use] - pub const fn snapshot_schema_version(mut self, version: std::num::NonZeroU32) -> Self { + pub const fn snapshot_schema_version(mut self, version: core::num::NonZeroU32) -> Self { self.snapshot.schema_version = version; self } diff --git a/crates/nexus-store/src/catchup.rs b/crates/nexus-store/src/catchup.rs index ff4109df..e06093be 100644 --- a/crates/nexus-store/src/catchup.rs +++ b/crates/nexus-store/src/catchup.rs @@ -8,8 +8,8 @@ //! [`subscription_cursor`](crate::subscription_cursor), which the user-facing //! [`Subscription`](crate::Subscription) assembles per call site. +use alloc::sync::Arc; use core::future::Future; -use std::sync::Arc; use futures::StreamExt; use nexus::Version; diff --git a/crates/nexus-store/src/cbor.rs b/crates/nexus-store/src/cbor.rs index 7f59986f..adf36a97 100644 --- a/crates/nexus-store/src/cbor.rs +++ b/crates/nexus-store/src/cbor.rs @@ -6,6 +6,7 @@ //! followed by block arrays `[crc32c, body]`. See //! `docs/plans/2026-06-21-export-import-cbor-box-design.md`. +use alloc::vec::Vec; use core::convert::Infallible; use core::ops::Range; diff --git a/crates/nexus-store/src/codec.rs b/crates/nexus-store/src/codec.rs index 02387d1c..a9a15908 100644 --- a/crates/nexus-store/src/codec.rs +++ b/crates/nexus-store/src/codec.rs @@ -17,7 +17,7 @@ //! ```ignore //! pub trait Decode: Send + Sync + 'static { //! type Output<'a> where Self: 'a; -//! type Error: std::error::Error + Send + Sync + 'static; +//! type Error: core::error::Error + Send + Sync + 'static; //! fn decode<'a>(&'a self, env: &'a PersistedEnvelope) //! -> Result, Self::Error>; //! } @@ -53,7 +53,7 @@ use crate::envelope::PersistedEnvelope; /// `E: ?Sized` allows unsized event types (e.g. `Archived`). pub trait Encode: Send + Sync + 'static { /// The error type for serialization failures. - type Error: std::error::Error + Send + Sync + 'static; + type Error: core::error::Error + Send + Sync + 'static; /// Serialize a typed value to bytes. /// @@ -111,7 +111,7 @@ pub trait Decode: Send + Sync + 'static { Self: 'a; /// The error type for deserialization failures. - type Error: std::error::Error + Send + Sync + 'static; + type Error: core::error::Error + Send + Sync + 'static; /// Decode the envelope's payload to [`Output<'a>`](Self::Output). /// @@ -152,6 +152,8 @@ impl OwningCodec for C where C: for<'a> Decode = #[cfg(feature = "serde")] pub mod serde { + use alloc::vec::Vec; + use ::serde::{Serialize, de::DeserializeOwned}; use super::{Decode, Encode}; @@ -170,7 +172,7 @@ pub mod serde { /// - Errors must accurately describe the failure (not erase the cause). pub trait SerdeFormat: Send + Sync + 'static { /// The error type for serialization/deserialization failures. - type Error: std::error::Error + Send + Sync + 'static; + type Error: core::error::Error + Send + Sync + 'static; /// Serialize a value to bytes. /// @@ -260,16 +262,18 @@ pub mod serde { } /// Seal `Debug` — show the format type, not its internals. - impl std::fmt::Debug for SerdeCodec { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + impl core::fmt::Debug for SerdeCodec { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.debug_struct("SerdeCodec") - .field("format", &std::any::type_name::()) + .field("format", &core::any::type_name::()) .finish() } } #[cfg(feature = "json")] pub mod json { + use alloc::vec::Vec; + use ::serde::{Serialize, de::DeserializeOwned}; use super::{SerdeCodec, SerdeFormat}; @@ -310,9 +314,9 @@ pub mod bytemuck { use super::{Decode, Encode}; use crate::envelope::PersistedEnvelope; - /// Wrapper around [`PodCastError`] that satisfies the `std::error::Error` bound. + /// Wrapper around [`PodCastError`] that satisfies the `core::error::Error` bound. /// - /// Upstream `PodCastError` does not implement `std::error::Error` + /// Upstream `PodCastError` does not implement `core::error::Error` /// (`bytemuck` is `no_std` by default and skips the impl), so /// `Decode::Error` requires a wrapper. /// diff --git a/crates/nexus-store/src/envelope.rs b/crates/nexus-store/src/envelope.rs index ef67b483..a3df0c1a 100644 --- a/crates/nexus-store/src/envelope.rs +++ b/crates/nexus-store/src/envelope.rs @@ -1,4 +1,4 @@ -use std::ops::Range; +use core::ops::Range; use bytes::Bytes; use nexus::{DomainEvent, Version}; @@ -37,7 +37,7 @@ pub enum EnvelopeError { start: u32, end: u32, #[source] - source: std::str::Utf8Error, + source: core::str::Utf8Error, }, /// The `event_type` range length exceeds the wire-format cap. @@ -397,7 +397,7 @@ impl PersistedEnvelope { // UTF-8 validation of event_type once at construction. let et_start = idx(event_type_range.start); let et_end = idx(event_type_range.end); - std::str::from_utf8(&value[et_start..et_end]).map_err(|e| EnvelopeError::InvalidUtf8 { + core::str::from_utf8(&value[et_start..et_end]).map_err(|e| EnvelopeError::InvalidUtf8 { start: event_type_range.start, end: event_type_range.end, source: e, @@ -443,7 +443,7 @@ impl PersistedEnvelope { reason = "UTF-8 invariant established at construction; ranges validated" )] unsafe { - std::str::from_utf8_unchecked(&self.value[start..end]) + core::str::from_utf8_unchecked(&self.value[start..end]) } } @@ -489,7 +489,7 @@ impl PersistedEnvelope { pub fn event_type_value(&self) -> EventType { // SAFETY: `from_validated_bytes` requires (1) valid UTF-8 and // (2) `bytes.len() <= MAX_EVENT_TYPE_LEN`. Both invariants are - // established by `try_new`: UTF-8 via `std::str::from_utf8`, and + // established by `try_new`: UTF-8 via `core::str::from_utf8`, and // the cap via the `EventTypeRangeTooLong` check on the range // length. #[allow( diff --git a/crates/nexus-store/src/export.rs b/crates/nexus-store/src/export.rs index edc32f25..aa021da1 100644 --- a/crates/nexus-store/src/export.rs +++ b/crates/nexus-store/src/export.rs @@ -59,7 +59,7 @@ pub trait StreamLister: RawEventStore { /// guaranteed order, terminating when exhausted. fn list_streams( &self, - ) -> impl std::future::Future> + Send; + ) -> impl core::future::Future> + Send; } /// Export a single stream's events — a raw pass-through read. @@ -93,7 +93,7 @@ pub trait EventExporter: RawEventStore { &self, id: &StreamKey, from: Version, - ) -> impl std::future::Future> + Send; + ) -> impl core::future::Future> + Send; } /// Every [`RawEventStore`] is an [`EventExporter`] — export is just a read. @@ -109,7 +109,7 @@ impl EventExporter for S { &self, id: &StreamKey, from: Version, - ) -> impl std::future::Future> + Send { + ) -> impl core::future::Future> + Send { self.read_stream(id, from) } } diff --git a/crates/nexus-store/src/import.rs b/crates/nexus-store/src/import.rs index b688668f..434f2e1b 100644 --- a/crates/nexus-store/src/import.rs +++ b/crates/nexus-store/src/import.rs @@ -29,6 +29,9 @@ //! the report, the error) and the [`EventImporter`] trait. The concrete //! ingest impl is a later card. +use alloc::vec; +use alloc::vec::Vec; + use bytes::Bytes; use nexus::Version; use thiserror::Error; @@ -265,7 +268,7 @@ pub trait AtomicAppend: RawEventStore { fn atomic_append_many( &self, writes: &[PlannedAppend], - ) -> impl std::future::Future>> + Send; + ) -> impl core::future::Future>> + Send; } /// `Store` forwards [`AtomicAppend`] to its inner backend (issue #247). With @@ -308,7 +311,7 @@ pub trait EventImporter: RawEventStore + AtomicAppend { sections: &[StreamSection], route: R, atomicity: Atomicity, - ) -> impl std::future::Future>> + Send + ) -> impl core::future::Future>> + Send where R: Fn(&[u8]) -> StreamKey + Send; } diff --git a/crates/nexus-store/src/lib.rs b/crates/nexus-store/src/lib.rs index 382b5084..90fa6488 100644 --- a/crates/nexus-store/src/lib.rs +++ b/crates/nexus-store/src/lib.rs @@ -83,6 +83,12 @@ //! (kernel-pure → store-persistence → adapters); the boundary that //! didn't matter was inside `nexus-store`. +#![cfg_attr(not(feature = "std"), no_std)] + +// The store is alloc-dependent by design (Bytes, Vec, Arc are its working +// vocabulary) — unlike the pure-core kernel, `alloc` is unconditional. +extern crate alloc; + pub mod batch; pub mod builder; #[cfg(feature = "subscription")] diff --git a/crates/nexus-store/src/repository.rs b/crates/nexus-store/src/repository.rs index 92d33b18..8821ff5a 100644 --- a/crates/nexus-store/src/repository.rs +++ b/crates/nexus-store/src/repository.rs @@ -7,11 +7,12 @@ reason = "per-iteration Arc clones in try_fold closures intentionally re-bind" )] -use std::borrow::Borrow; -use std::future::Future; -use std::marker::PhantomData; -use std::num::NonZeroU32; -use std::sync::Arc; +use alloc::sync::Arc; +use alloc::vec::Vec; +use core::borrow::Borrow; +use core::future::Future; +use core::marker::PhantomData; +use core::num::NonZeroU32; use nexus::{Aggregate, AggregateRoot, DomainEvent, EventOf, Events, Version}; @@ -83,7 +84,7 @@ use crate::value::SchemaVersion; /// as `Self::Error` or define a custom error with `From` impls. pub trait Repository: Send + Sync { /// The error type for repository operations. - type Error: std::error::Error + Send + Sync + 'static; + type Error: core::error::Error + Send + Sync + 'static; /// Load an aggregate by replaying its event stream. /// @@ -123,7 +124,7 @@ pub trait Repository: Send + Sync { /// replay logic. Not public API. pub(crate) trait ReplayFrom: Send + Sync { /// The error type for replay operations. - type Error: std::error::Error + Send + Sync + 'static; + type Error: core::error::Error + Send + Sync + 'static; /// Replay events starting from `from` version (inclusive) into `root`. /// @@ -360,7 +361,7 @@ impl EventStore { for<'a> C: Encode> + Decode, Output<'a>: Borrow>> + 'static, F: for<'a> Fn(EventMorsel<'a>) -> Result, E> + Send + Sync + 'static, - E: std::error::Error + Send + Sync + 'static, + E: core::error::Error + Send + Sync + 'static, EventOf: DomainEvent, S::Stream: Send, { diff --git a/crates/nexus-store/src/snapshot.rs b/crates/nexus-store/src/snapshot.rs index 8e890e71..66edced1 100644 --- a/crates/nexus-store/src/snapshot.rs +++ b/crates/nexus-store/src/snapshot.rs @@ -1,4 +1,4 @@ -use std::num::NonZeroU32; +use core::num::NonZeroU32; use nexus::{Aggregate, AggregateRoot, DomainEvent, EventOf, Events, KernelError, Version}; diff --git a/crates/nexus-store/src/state.rs b/crates/nexus-store/src/state.rs index 8abcf412..44583e08 100644 --- a/crates/nexus-store/src/state.rs +++ b/crates/nexus-store/src/state.rs @@ -1,5 +1,6 @@ -use std::future::Future; -use std::num::{NonZeroU32, NonZeroU64}; +use alloc::vec::Vec; +use core::future::Future; +use core::num::{NonZeroU32, NonZeroU64}; use nexus::{Id, Version}; diff --git a/crates/nexus-store/src/store.rs b/crates/nexus-store/src/store.rs index 6ff6ed12..a25b25f0 100644 --- a/crates/nexus-store/src/store.rs +++ b/crates/nexus-store/src/store.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use alloc::sync::Arc; use nexus::Version; @@ -114,7 +114,7 @@ impl Clone for Store { /// calls this trait after encoding events into `PendingEnvelope`. pub trait RawEventStore: Send + Sync { /// The error type for store operations. - type Error: std::error::Error + Send + Sync + 'static; + type Error: core::error::Error + Send + Sync + 'static; /// The stream type for reading events. /// @@ -191,7 +191,7 @@ pub trait RawEventStore: Send + Sync { id: &StreamKey, expected_version: Option, envelopes: &[PendingEnvelope], - ) -> impl std::future::Future>> + Send; + ) -> impl core::future::Future>> + Send; /// Open a stream of events. /// @@ -219,7 +219,7 @@ pub trait RawEventStore: Send + Sync { &self, id: &StreamKey, from: Version, - ) -> impl std::future::Future> + Send; + ) -> impl core::future::Future> + Send; /// Open a one-shot read over **all** streams, ordered by /// [`AllPosition`](Self::AllPosition). @@ -252,7 +252,7 @@ pub trait RawEventStore: Send + Sync { fn read_all( &self, from: Option, - ) -> impl std::future::Future> + Send; + ) -> impl core::future::Future> + Send; /// Wrap this backend in a shared [`Store`] handle. /// diff --git a/crates/nexus-store/src/stream.rs b/crates/nexus-store/src/stream.rs index 1143f723..c8fc675f 100644 --- a/crates/nexus-store/src/stream.rs +++ b/crates/nexus-store/src/stream.rs @@ -42,13 +42,13 @@ pub trait EventStream: futures_core::Stream::Error>> + Send { /// The error type yielded by the stream's `Result` items. - type Error: std::error::Error + Send + Sync + 'static; + type Error: core::error::Error + Send + Sync + 'static; } impl EventStream for S where S: futures_core::Stream> + Send, - E: std::error::Error + Send + Sync + 'static, + E: core::error::Error + Send + Sync + 'static, { type Error = E; } diff --git a/crates/nexus-store/src/subscription.rs b/crates/nexus-store/src/subscription.rs index 63f37220..42e0ca25 100644 --- a/crates/nexus-store/src/subscription.rs +++ b/crates/nexus-store/src/subscription.rs @@ -26,7 +26,7 @@ //! assembled here from [`StreamCatchup`] / [`AllCatchup`] + the internal //! `live_stepped` loop, one monomorphized state machine per call site. -use std::sync::Arc; +use alloc::sync::Arc; use futures::StreamExt; use nexus::{Id, Version}; @@ -47,7 +47,7 @@ use crate::wake::WakeSource; /// # Example /// /// ```ignore -/// use std::pin::pin; +/// use core::pin::pin; /// use futures::StreamExt; /// use nexus_store::{Step, StepStreamExt, Store, Subscription}; /// diff --git a/crates/nexus-store/src/upcasting.rs b/crates/nexus-store/src/upcasting.rs index c9c10513..036f5ea3 100644 --- a/crates/nexus-store/src/upcasting.rs +++ b/crates/nexus-store/src/upcasting.rs @@ -1,4 +1,5 @@ -use std::borrow::Cow; +use alloc::borrow::{Cow, ToOwned}; +use alloc::vec::Vec; use nexus::Version; diff --git a/crates/nexus-store/src/value.rs b/crates/nexus-store/src/value.rs index 47bf07d9..6cde942b 100644 --- a/crates/nexus-store/src/value.rs +++ b/crates/nexus-store/src/value.rs @@ -10,7 +10,7 @@ //! `Bytes::from_static` path makes literal event-type names //! allocation-free, matching the previous `&'static str` ergonomics. -use std::num::NonZeroU32; +use core::num::NonZeroU32; use bytes::Bytes; use thiserror::Error; @@ -47,7 +47,7 @@ pub enum ValueError { EventTypeInvalidUtf8 { valid_up_to: usize, #[source] - source: std::str::Utf8Error, + source: core::str::Utf8Error, }, #[error("payload length {actual} exceeds maximum {MAX_PAYLOAD_LEN}")] PayloadTooLong { actual: usize }, @@ -108,7 +108,7 @@ impl EventType { actual: bytes.len(), }); } - std::str::from_utf8(&bytes).map_err(|e| ValueError::EventTypeInvalidUtf8 { + core::str::from_utf8(&bytes).map_err(|e| ValueError::EventTypeInvalidUtf8 { valid_up_to: e.valid_up_to(), source: e, })?; @@ -135,7 +135,7 @@ impl EventType { "from_validated_bytes invariant: length ≤ MAX_EVENT_TYPE_LEN" ); debug_assert!( - std::str::from_utf8(&bytes).is_ok(), + core::str::from_utf8(&bytes).is_ok(), "from_validated_bytes invariant: valid UTF-8" ); Self { inner: bytes } @@ -151,7 +151,7 @@ impl EventType { reason = "UTF-8 invariant established by every constructor" )] unsafe { - std::str::from_utf8_unchecked(&self.inner) + core::str::from_utf8_unchecked(&self.inner) } } diff --git a/crates/workspace-hack/Cargo.toml b/crates/workspace-hack/Cargo.toml index 49f35b51..62f82ff8 100644 --- a/crates/workspace-hack/Cargo.toml +++ b/crates/workspace-hack/Cargo.toml @@ -12,12 +12,17 @@ publish = false ### BEGIN HAKARI SECTION [dependencies] +aligned-vec = { version = "0.6", default-features = false, features = ["std"] } +bytes = { version = "1" } crossbeam-utils = { version = "0.8" } crypto-common = { version = "0.1", default-features = false, features = ["std"] } digest = { version = "0.10", features = ["mac", "std"] } either = { version = "1", features = ["serde", "use_std"] } +futures = { version = "0.3", default-features = false, features = ["async-await", "executor"] } +futures-channel = { version = "0.3", default-features = false, features = ["sink", "std"] } futures-io = { version = "0.3" } futures-sink = { version = "0.3", default-features = false, features = ["std"] } +futures-task = { version = "0.3", default-features = false, features = ["std"] } futures-util = { version = "0.3", default-features = false, features = ["async-await-macro", "channel", "io", "sink"] } memchr = { version = "2" } num-traits = { version = "0.2" } @@ -27,6 +32,7 @@ serde = { version = "1", features = ["alloc", "derive", "rc"] } serde_core = { version = "1", default-features = false, features = ["alloc", "rc", "result", "std"] } serde_json = { version = "1", features = ["raw_value"] } sha2 = { version = "0.10" } +slab = { version = "0.4", default-features = false, features = ["std"] } smallvec = { version = "1", default-features = false, features = ["const_generics", "serde"] } sqlx-core = { version = "0.8", features = ["_rt-tokio", "any", "json", "migrate", "offline"] } sqlx-postgres = { version = "0.8", default-features = false, features = ["migrate", "offline"] } @@ -36,12 +42,17 @@ trybuild = { version = "1", default-features = false, features = ["diff"] } zerocopy = { version = "0.8", default-features = false, features = ["derive", "simd"] } [build-dependencies] +aligned-vec = { version = "0.6", default-features = false, features = ["std"] } +bytes = { version = "1" } crossbeam-utils = { version = "0.8" } crypto-common = { version = "0.1", default-features = false, features = ["std"] } digest = { version = "0.10", features = ["mac", "std"] } either = { version = "1", features = ["serde", "use_std"] } +futures = { version = "0.3", default-features = false, features = ["async-await", "executor"] } +futures-channel = { version = "0.3", default-features = false, features = ["sink", "std"] } futures-io = { version = "0.3" } futures-sink = { version = "0.3", default-features = false, features = ["std"] } +futures-task = { version = "0.3", default-features = false, features = ["std"] } futures-util = { version = "0.3", default-features = false, features = ["async-await-macro", "channel", "io", "sink"] } memchr = { version = "2" } num-traits = { version = "0.2" } @@ -52,6 +63,7 @@ serde = { version = "1", features = ["alloc", "derive", "rc"] } serde_core = { version = "1", default-features = false, features = ["alloc", "rc", "result", "std"] } serde_json = { version = "1", features = ["raw_value"] } sha2 = { version = "0.10" } +slab = { version = "0.4", default-features = false, features = ["std"] } smallvec = { version = "1", default-features = false, features = ["const_generics", "serde"] } sqlx-core = { version = "0.8", features = ["_rt-tokio", "any", "json", "migrate", "offline"] } sqlx-postgres = { version = "0.8", default-features = false, features = ["migrate", "offline"] } diff --git a/docs/superpowers/plans/2026-07-09-no-std-store.md b/docs/superpowers/plans/2026-07-09-no-std-store.md new file mode 100644 index 00000000..7c121df8 --- /dev/null +++ b/docs/superpowers/plans/2026-07-09-no-std-store.md @@ -0,0 +1,375 @@ +# no_std nexus-store (#301) Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Port `nexus-store` to `no_std` + `alloc` behind an additive default `std` feature, switching all public error bounds to `core::error::Error` (the freeze-critical, post-1.0-irreversible piece), mirroring the merged #279 kernel port. + +**Architecture:** `#![cfg_attr(not(feature = "std"), no_std)]` + unconditional `extern crate alloc` (the store always needs an allocator — `Vec`, `Arc`, `Bytes`). All production `std::` paths move to `core::`/`alloc::`; `#[cfg(test)]` code keeps `std::` freely (tests always build with the default `std` feature). Deps flip to `default-features = false` at the workspace root (Cargo cannot override `default-features` through workspace inheritance — verified against the Cargo reference, stated in #301); std consumers re-add the features they lose. `nexus-store` joins the hakari `[final-excludes]` because `workspace-hack` force-enables std-implying `futures-util` features (`io`, `channel`, `sink`), which feature-unification would silently leak into the no_std build. + +**Tech Stack:** Rust stable (pinned via `rust-toolchain.toml`), `core::error::Error` (stable since 1.81), cargo-hakari, Nix flake CI gate (wasm32-unknown-unknown + host `--no-default-features`). + +**Non-breaking note:** `std::error::Error` has been a re-export of `core::error::Error` since Rust 1.81 — the bounds swap is semantically a no-op for std consumers. The PR still carries `!` (mirrors #303): the dependency default-feature flips can affect `--no-default-features` consumers of the workspace deps. + +**Scope decisions (flag at review):** +1. The `std` feature forwards `thiserror/std`, `nexus/std`, `futures/std`, `bytes/std`, `aligned-vec/std` — strict additivity: the default build produces byte-identical dep configurations to today, so "no regression on the std path" holds by construction. +2. The wasm32 gate also builds the dep-free features (`subscription,export,import,snapshot,projection`) under `--no-default-features`, because this plan ports those modules' imports too and without a gate they'd rot. **Empirical checkpoint:** if `futures`' `alloc`-only combinator set (`unfold`, `try_fold`, …) turns out to need `std`, drop the features-variant from the gate and record the deviation here. +3. Optional codec features (`serde`, `json`, `rkyv`, `bytemuck`, `cbor`) stay orthogonal to `std` — they are not in the gate and not claimed no_std-compatible. `crc32c` (behind `cbor`) exposes **no** feature toggles at all, so `cbor` cannot be no_std today; out of scope. + +**Deviation log:** record divergences here as they happen (per project convention for multi-step plans). + +- **Task 7 (2026-07-10):** the issue card's premise "a bare `thumbv7em` build would need a `#[global_allocator]`" holds only for binaries — an rlib build links no allocator. Quality review *measured* it: `nexus-store --no-default-features` (and with the dep-free features) builds clean on `thumbv7em-none-eabihf`. The gate therefore includes thumbv7em lines — the strong target the kernel already uses — because wasm32/host ship std and structurally cannot catch a dependency-level std leak. Also confirmed empirically: optional codec features `json`/`rkyv`/`cbor` do NOT build on thumbv7em (serde_json→memchr/std, rkyv→ptr_meta/std, crc32c→std) — accepted per scope decision 3; a follow-up card can flip those deps no-default at the workspace root. +- **Task 2 (2026-07-10):** the plan's "known consumers" list was incomplete — the mandated sweep found 5 more std consumers of `futures`/`bytes` (`nexus-store-testing` + examples `store-and-kernel`, `store-inmemory`, `projection-tokio`, `fjall-end-to-end`); all got the identical feature restore. Additionally, review caught that `nexus-fjall`'s `futures` line (not listed in the plan, which named only its `bytes`) also needed the restore — it compiled only via workspace-hack/dev-dep feature unification. Lesson recorded: `cargo check --workspace` cannot prove a restore is self-sufficient (features unify across the whole graph, dev-deps included); only a per-crate isolated check or removing the unification source can. + +--- + +### Task 1: Branch setup + +**Files:** none (git only) + +- [ ] **Step 1: Fetch and branch off origin/main** (never stale local main — project rule) + +```bash +git fetch origin +git checkout -b feat/301-no-std-store origin/main +``` + +- [ ] **Step 2: Verify clean state** + +Run: `git status` +Expected: `nothing to commit, working tree clean`, branch `feat/301-no-std-store` + +--- + +### Task 2: Workspace dependency flips + std-consumer restores + +**Files:** +- Modify: `Cargo.toml` (workspace root, `[workspace.dependencies]` lines 24–32) +- Modify: `crates/nexus-fjall/Cargo.toml` (line 22) +- Modify: `crates/nexus-inmemory/Cargo.toml` (lines 19–20) +- Modify: `crates/nexus-postgres/Cargo.toml` (lines 15–16) +- Modify: `crates/nexus-store/Cargo.toml` (dev-dependency `futures`, line 61) + +- [ ] **Step 1: Flip workspace declarations to `default-features = false`** + +In root `Cargo.toml` `[workspace.dependencies]`: + +```toml +aligned-vec = { version = "=0.6.4", default-features = false } +bytes = { version = "1", default-features = false } +futures = { version = "0.3", default-features = false, features = ["alloc"] } +``` + +(`thiserror` is already `default-features = false` from #279; `futures-core` already is; `arrayvec` already is; `minicbor` already uses `features = ["alloc"]` with no default pull.) + +- [ ] **Step 2: Restore lost default features in std consumers** + +`futures`' default set is `["std", "async-await", "executor"]`; `bytes`' and `aligned-vec`'s is `["std"]`. Restore exactly that set so consumer builds are unchanged: + +`crates/nexus-fjall/Cargo.toml`: +```toml +bytes = { workspace = true, features = ["std"] } +``` + +`crates/nexus-inmemory/Cargo.toml`: +```toml +bytes = { workspace = true, features = ["std"] } +futures = { workspace = true, features = ["std", "async-await", "executor"] } +``` + +`crates/nexus-postgres/Cargo.toml`: +```toml +bytes = { workspace = true, features = ["std"] } +futures = { workspace = true, features = ["std", "async-await", "executor"] } +``` + +`crates/nexus-store/Cargo.toml` `[dev-dependencies]` (tests always link std): +```toml +futures = { workspace = true, features = ["std", "async-await", "executor"] } +``` + +- [ ] **Step 3: Check the workspace still builds** + +Run: `cargo check --workspace` +Expected: clean. If a crate fails on a missing futures/bytes item, that consumer needs the corresponding feature added — fix it there (deviation-log it), never by re-enabling workspace defaults. + +--- + +### Task 3: nexus-store feature surface + hakari exclusion + +**Files:** +- Modify: `crates/nexus-store/Cargo.toml` (`[features]` + `[dependencies]`) +- Modify: `.config/hakari.toml` (`[final-excludes] workspace-members`) +- Modify (generated): `crates/workspace-hack/Cargo.toml`, `Cargo.lock` + +- [ ] **Step 1: Add the additive `std` default feature** + +In `crates/nexus-store/Cargo.toml` `[features]`: + +```toml +default = ["std"] +# Additive: enabling `std` only ADDS (the std::error::Error bridge via +# thiserror + the deps' std impls); disabling it (`--no-default-features`) +# yields a no_std + alloc + core::error::Error store. (#301, mirrors #279) +std = [ + "thiserror/std", + "nexus/std", + "futures/std", + "bytes/std", + "aligned-vec/std", +] +``` + +- [ ] **Step 2: Re-point the store's own deps** + +In `[dependencies]`: + +```toml +nexus = { version = "0.1.0", path = "../nexus", default-features = false } +thiserror = { workspace = true } +``` + +(`thiserror` loses its hardcoded `features = ["std"]`; `futures`/`bytes`/`aligned-vec` lines stay plain `{ workspace = true }` — the workspace decl is now no-default, and `std` forwards through the feature above.) + +- [ ] **Step 3: Exclude nexus-store from workspace-hack** + +In `.config/hakari.toml`, extend the existing entry: + +```toml +workspace-members = ["nexus", "nexus-nostd-smoketest", "nexus-store"] +``` + +Add a comment line above mirroring the existing ones: +```toml +# `nexus-store` is no_std (core+alloc) — workspace-hack force-enables +# std-implying futures-util features (io/channel/sink) that feature +# unification would leak into its --no-default-features build. (#301) +``` + +- [ ] **Step 4: Regenerate hakari and remove the dep edge** + +```bash +cargo hakari generate +cargo hakari manage-deps +``` + +Expected: `workspace-hack = { … }` line removed from `crates/nexus-store/Cargo.toml`; `crates/workspace-hack/Cargo.toml` regenerated. Verify with `cargo hakari verify` → exit 0. + +- [ ] **Step 5: Workspace still builds** + +Run: `cargo check --workspace` +Expected: clean. + +- [ ] **Step 6: Commit (workspace prep, green standalone)** + +```bash +git add Cargo.toml Cargo.lock .config/hakari.toml crates/workspace-hack crates/nexus-fjall/Cargo.toml crates/nexus-inmemory/Cargo.toml crates/nexus-postgres/Cargo.toml crates/nexus-store/Cargo.toml +git commit -m "build(store): flip futures/bytes/aligned-vec to no-default workspace deps, hakari-exclude nexus-store (#301)" +``` + +(The pre-commit hook runs `nix flake check` itself — do not pre-run it.) + +--- + +### Task 4: no_std attribute — make the build fail + +**Files:** +- Modify: `crates/nexus-store/src/lib.rs` (top of file) + +- [ ] **Step 1: Add the attribute + alloc** + +At the very top of `crates/nexus-store/src/lib.rs`, before the doc comment/attrs that exist today: + +```rust +#![cfg_attr(not(feature = "std"), no_std)] + +// The store is alloc-dependent by design (Bytes, Vec, Arc are its working +// vocabulary) — unlike the pure-core kernel, `alloc` is unconditional. +extern crate alloc; +``` + +- [ ] **Step 2: Run the failing build (this is the port's "failing test")** + +Run: `cargo build -p nexus-store --no-default-features` +Expected: FAIL with many `error[E0433]: failed to resolve: use of unresolved module or unlinked crate 'std'` and missing-prelude errors (`Vec`, `String`, `format!`, `Box` not found). Save the error list — it is the authoritative worklist for Task 5. + +--- + +### Task 5: Mechanical sweep — `std::` → `core::`/`alloc::` + +**Files (production code only — leave `#[cfg(test)]` modules untouched):** +- Modify: `crates/nexus-store/src/builder.rs` +- Modify: `crates/nexus-store/src/codec.rs` +- Modify: `crates/nexus-store/src/envelope.rs` +- Modify: `crates/nexus-store/src/value.rs` +- Modify: `crates/nexus-store/src/store.rs` +- Modify: `crates/nexus-store/src/state.rs` +- Modify: `crates/nexus-store/src/catchup.rs` +- Modify: `crates/nexus-store/src/subscription.rs` +- Modify: `crates/nexus-store/src/subscription_cursor.rs` +- Modify: `crates/nexus-store/src/repository.rs` +- Modify: `crates/nexus-store/src/export.rs` +- Modify: `crates/nexus-store/src/import.rs` +- Modify: `crates/nexus-store/src/snapshot.rs` +- Modify: `crates/nexus-store/src/stream.rs` +- Modify: `crates/nexus-store/src/upcasting.rs` +- Modify: `crates/nexus-store/src/wire.rs` +- Modify: `crates/nexus-store/src/cbor.rs` +- Modify (alloc imports only, as compiler demands): `error.rs`, `batch.rs`, `conflict.rs`, `step.rs`, `stream_id.rs`, `decoded.rs`, `projection.rs`, `saga.rs`, `execute.rs` + +- [ ] **Step 1: Apply the path mapping (exact substitutions)** + +| From | To | Known sites | +|---|---|---| +| `std::error::Error` (bounds — **the freeze gate**) | `core::error::Error` | `codec.rs:56,114,173`, `store.rs:117`, `repository.rs:86,126,363`, `stream.rs:45,51` | +| `std::sync::Arc` | `alloc::sync::Arc` | `store.rs:1`, `catchup.rs:12`, `subscription.rs:29`, `repository.rs:14` | +| `std::future::Future` | `core::future::Future` | `store.rs:194,222,255`, `state.rs:1`, `export.rs:62,96,112`, `import.rs:268,311`, `repository.rs:11` | +| `std::num::{NonZeroU32, NonZeroU64}` | `core::num::*` | `builder.rs:47,155,163,295`, `value.rs:13`, `state.rs:2`, `snapshot.rs:1`, `repository.rs:13` | +| `std::fmt` / `std::any::type_name` | `core::fmt` / `core::any::type_name` | `codec.rs:263–266` | +| `std::str::{from_utf8, from_utf8_unchecked, Utf8Error}` | `core::str::*` | `envelope.rs:40,400,446`, `value.rs:50,111,138,154` | +| `std::ops::Range` | `core::ops::Range` | `envelope.rs:1` | +| `std::ptr::eq` | `core::ptr::eq` | `envelope.rs:912` (verify cfg; test-only sites stay) | +| `std::marker::PhantomData` | `core::marker::PhantomData` | `builder.rs:1`, `repository.rs:12` | +| `std::borrow::{Borrow, Cow}` | `core::borrow::Borrow` / `alloc::borrow::Cow` | `repository.rs:10`, `upcasting.rs:1` | +| prelude `Vec`, `String`, `Box`, `format!`, `vec!`, `ToString`, `ToOwned` | explicit `use alloc::vec::Vec;`, `use alloc::string::{String, ToString};`, `use alloc::boxed::Box;`, `use alloc::borrow::ToOwned;`, `use alloc::{format, vec};` | wherever the Task 4 error list points (heaviest: `cbor.rs`, `wire.rs`, `import.rs`, `catchup.rs`, `upcasting.rs`) | + +Rules while sweeping (project conventions): +- All `use` imports at the top of the file — no inline paths introduced by the sweep. +- `#[cfg(test)]` modules keep `std::` (they always build with the `std` default feature; do NOT churn them). +- Doc comments that *teach* the bound (`codec.rs:20`, `subscription.rs:50` `std::pin::pin`) get updated to the `core::` spelling for accuracy. +- Do not touch `#[non_exhaustive]` on error enums, lint levels, or anything outside the path sweep. + +- [ ] **Step 2: Iterate until the no_std build is green** + +Run (repeat until clean): `cargo build -p nexus-store --no-default-features` +Expected: PASS. + +- [ ] **Step 3: Verify no production `std::` remains** + +Run: `rg -n 'std::' crates/nexus-store/src --no-heading | rg -v 'cfg\(test\)' | rg -v '^\S+:\d+:\s*//'` +Expected: hits only inside `#[cfg(test)]` modules (spot-check each survivor's enclosing cfg). + +--- + +### Task 6: Verify all four build surfaces + +**Files:** none (verification only) + +- [ ] **Step 1: Host no_std** + +Run: `cargo build -p nexus-store --no-default-features` +Expected: PASS. (With `#![no_std]` active, any in-crate `std::` path fails to resolve even on a host with std — this is the leak detector.) + +- [ ] **Step 2: Host no_std + dep-free features** (scope decision 2's empirical checkpoint) + +Run: `cargo build -p nexus-store --no-default-features --features subscription,export,import,snapshot,projection` +Expected: PASS. If `futures`' alloc-only set is missing a combinator the subscription loop needs (`unfold`, `try_fold`): record the deviation, drop this variant from the flake gate in Task 7, and open a follow-up card instead of forcing `futures/std` in. + +- [ ] **Step 3: wasm32 no_std** + +Run: `cargo build -p nexus-store --target wasm32-unknown-unknown --no-default-features` +Expected: PASS (target is already installed — the kernel gate builds it). + +- [ ] **Step 4: std path regression check** + +Run: `cargo nextest run -p nexus-store` +Expected: all tests pass, zero failures — the default `std` build is behaviorally unchanged. + +- [ ] **Step 5: Clippy under the full matrix** (project rule: clean under `--all-features --all-targets`) + +Run: `cargo clippy --workspace --all-features --all-targets` +Expected: zero warnings. + +--- + +### Task 7: Flake CI gate + +**Files:** +- Modify: `flake.nix` (checks section, next to the existing `nexus-wasm`/`nexus-nostd` derivations, ~line 140) + +- [ ] **Step 1: Add the store gate as its own check** + +```nix +# nexus-store no_std gate (#301). The store needs an allocator, so there +# is no bare-metal thumbv7em build (that would require a +# #[global_allocator]); wasm32 + host --no-default-features is the +# pragmatic gate. The host build is the std-leak detector: with +# #![no_std] active, an in-crate `std::` path fails to resolve even +# though the host ships std. The features variant keeps the dep-free +# feature set (subscription/export/import/snapshot/projection) honest. +nexus-store-nostd = craneLib.mkCargoDerivation (commonArgs // { + inherit cargoArtifacts; + pname = "nexus-store-nostd"; + buildPhaseCargoCommand = '' + cargo build -p nexus-store --no-default-features + cargo build -p nexus-store --target wasm32-unknown-unknown --no-default-features + cargo build -p nexus-store --target wasm32-unknown-unknown --no-default-features --features subscription,export,import,snapshot,projection + ''; +}); +``` + +(Adjust the features line per Task 6 Step 2's outcome; if dropped, log the deviation.) + +- [ ] **Step 2: Verify the flake evaluates** + +Run: `nix flake show 2>/dev/null | head -40` +Expected: `nexus-store-nostd` listed under checks, no eval errors. + +--- + +### Task 8: Format, commit, PR + +**Files:** none new + +- [ ] **Step 1: Format** + +```bash +nix develop -c cargo fmt --all +``` + +- [ ] **Step 2: Stage everything (including any new files) and commit** + +```bash +git add -A +git commit -m "feat(store)!: port nexus-store to no_std (core+alloc) (#301)" +``` + +The pre-commit hook runs `nix flake check` (never bypass it; never pre-run it). Expect several minutes. + +- [ ] **Step 3: Push and open the PR** (gh account `joeldsouzax`) + +```bash +git push -u origin feat/301-no-std-store +gh pr create \ + --title "feat(store)!: port nexus-store to no_std (core+alloc) (#301)" \ + --body "$(cat <<'EOF' +## Summary +- `#![cfg_attr(not(feature = "std"), no_std)]` + unconditional `extern crate alloc` behind an additive default `std` feature (mirrors #279 / #303) +- **Freeze gate:** all public error bounds `std::error::Error` → `core::error::Error` (stable since 1.81; a re-export, so no semantic change for std consumers — but irreversible post-1.0 in the other direction) +- `futures`/`bytes`/`aligned-vec` flip to `default-features = false` at the workspace root (workspace inheritance cannot override `default-features`); std consumers restore their exact former feature sets +- `nexus-store` joins hakari `[final-excludes]` — workspace-hack force-enables std-implying `futures-util` features that unification would leak into the no_std build +- New flake check `nexus-store-nostd`: host + wasm32 `--no-default-features`, plus the dep-free feature set + +Closes #301 + +## Test plan +- [ ] `nix flake check` green (std path + new no_std gate) +- [ ] `cargo nextest run -p nexus-store` unchanged +- [ ] `cargo clippy --workspace --all-features --all-targets` clean + +🤖 Generated with [Claude Code](https://claude.com/claude-code) +EOF +)" +``` + +- [ ] **Step 4: Verify CI, then squash-merge** + +Run: `gh pr checks --watch` +Expected: Nix Flake Check green. Merge with `gh pr merge --squash --delete-branch` (squash-only ruleset) — after user confirmation. + +--- + +## Self-Review + +- **Spec coverage:** #301's five "What" bullets → Task 4 (attribute), Task 5 (bounds + core/alloc sweep), test-only HashMap bullet is obsolete (`test_support.rs` is `#[cfg(all(test, feature = "subscription"))]` — always std; verified), thiserror bullet → Tasks 2–3 (workspace already no-default from #279; the store's hardcoded `["std"]` becomes feature-forwarded), CI gate → Task 7, hakari → Task 3 (verified needed: workspace-hack enables std-implying futures-util features). Acceptance boxes → Task 5 Step 1 (bounds) + Tasks 6–7 (gates, no regression). +- **Placeholder scan:** the Task 5 sweep intentionally uses a mapping table + compiler-driven worklist instead of per-line diffs — the exact sites are enumerated where known; prelude-import sites are discovered by the Task 4 failing build (deterministic, complete). +- **Type consistency:** feature name `std`, check name `nexus-store-nostd`, branch `feat/301-no-std-store` used consistently throughout. diff --git a/examples/fjall-end-to-end/Cargo.toml b/examples/fjall-end-to-end/Cargo.toml index ccb87ff9..a413240f 100644 --- a/examples/fjall-end-to-end/Cargo.toml +++ b/examples/fjall-end-to-end/Cargo.toml @@ -5,8 +5,8 @@ edition.workspace = true publish = false [dependencies] -bytes.workspace = true -futures.workspace = true +bytes = { workspace = true, features = ["std"] } +futures = { workspace = true, features = ["std", "async-await", "executor"] } nexus = { path = "../../crates/nexus", features = ["derive"] } # json → built-in JsonCodec + the `.json()` builder convenience # cbor → the CBOR backup box (implies import + export) diff --git a/examples/projection-tokio/Cargo.toml b/examples/projection-tokio/Cargo.toml index ce364e54..8247847c 100644 --- a/examples/projection-tokio/Cargo.toml +++ b/examples/projection-tokio/Cargo.toml @@ -5,8 +5,8 @@ edition.workspace = true publish = false [dependencies] -bytes.workspace = true -futures.workspace = true +bytes = { workspace = true, features = ["std"] } +futures = { workspace = true, features = ["std", "async-await", "executor"] } nexus = { path = "../../crates/nexus", features = ["derive"] } nexus-inmemory = { path = "../../crates/nexus-inmemory" } nexus-store = { path = "../../crates/nexus-store", features = ["projection", "subscription"] } diff --git a/examples/store-and-kernel/Cargo.toml b/examples/store-and-kernel/Cargo.toml index 0dcbd73f..45b29bb7 100644 --- a/examples/store-and-kernel/Cargo.toml +++ b/examples/store-and-kernel/Cargo.toml @@ -5,8 +5,8 @@ edition.workspace = true publish = false [dependencies] -bytes.workspace = true -futures.workspace = true +bytes = { workspace = true, features = ["std"] } +futures = { workspace = true, features = ["std", "async-await", "executor"] } nexus = { path = "../../crates/nexus", features = ["derive"] } nexus-inmemory = { path = "../../crates/nexus-inmemory" } nexus-store = { path = "../../crates/nexus-store" } diff --git a/examples/store-inmemory/Cargo.toml b/examples/store-inmemory/Cargo.toml index def5c9e8..21b73d12 100644 --- a/examples/store-inmemory/Cargo.toml +++ b/examples/store-inmemory/Cargo.toml @@ -5,8 +5,8 @@ edition.workspace = true publish = false [dependencies] -bytes.workspace = true -futures.workspace = true +bytes = { workspace = true, features = ["std"] } +futures = { workspace = true, features = ["std", "async-await", "executor"] } nexus = { path = "../../crates/nexus", features = ["derive"] } nexus-inmemory = { path = "../../crates/nexus-inmemory" } nexus-store = { path = "../../crates/nexus-store" } diff --git a/flake.nix b/flake.nix index 78ef9e0c..f65bdf35 100644 --- a/flake.nix +++ b/flake.nix @@ -135,6 +135,8 @@ # uses `#[nexus::aggregate]` + `#[derive(DomainEvent)]`, so the macro # OUTPUT — not just its source — is compiled for the target. A macro # emitting a `std::` path fails the thumbv7em build here. + # `nexus-store-nostd` (#301) is the store-crate sibling gate — see its + # own comment below for what host vs thumbv7em each catch there. nexus-wasm = craneLib.mkCargoDerivation (commonArgs // { inherit cargoArtifacts; pname = "nexus-wasm"; @@ -152,6 +154,26 @@ cargo build -p nexus-nostd-smoketest --target thumbv7em-none-eabihf --no-default-features --features derive ''; }); + + # nexus-store no_std gate (#301). The host build is the in-crate std-leak + # detector (with #![no_std] active a `std::` path fails to resolve even + # though the host ships std); thumbv7em is the STRONG dep-level gate — an + # rlib build links no allocator, so the alloc-dependent store builds + # bare-metal even though a *binary* would need a #[global_allocator]. + # The features variant keeps the dep-free surface (subscription/export/ + # import/snapshot/projection) no_std-clean; optional codec features + # (json/rkyv/cbor) are deliberately NOT gated — they cannot build no_std + # today (serde_json/rkyv/crc32c pull std). + nexus-store-nostd = craneLib.mkCargoDerivation (commonArgs // { + inherit cargoArtifacts; + pname = "nexus-store-nostd"; + buildPhaseCargoCommand = '' + cargo build -p nexus-store --no-default-features + cargo build -p nexus-store --target wasm32-unknown-unknown --no-default-features + cargo build -p nexus-store --target thumbv7em-none-eabihf --no-default-features + cargo build -p nexus-store --target thumbv7em-none-eabihf --no-default-features --features subscription,export,import,snapshot,projection + ''; + }); }; packages = {