diff --git a/CHANGELOG.md b/CHANGELOG.md index 82cf569..e658a28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed (breaking) +- **Design 034 Phase 1 — mechanical debt cleanup (Issues #129, #132, [review doc](docs/design/034-technical-debt-review.md)).** `DbError` is unified on `alloc::string::String`: every variant has one shape on all targets, `thiserror` derives `Display`/`Error` unconditionally (now a mandatory no_std dependency of `aimdb-core`), and no_std builds produce the same error messages as std builds instead of `Error 0xNNNN` codes — on no_std the `_field: ()` placeholders become the real `String` fields. The dead `Database` wrapper, the `TokioDatabase`/`EmbassyDatabase` aliases, and the deprecated `RecordRegistrar::link()` are removed. `ConsumerTrait::subscribe_any` is infallible (returns `Box`), `OutboundRoute` is a struct with named fields, and `aimdb-core`'s `std` feature no longer pulls a `tokio` dependency (tests cover it via dev-deps). Internally: all dual std/alloc import pairs and the duplicated `RuntimeContext` impl blocks are collapsed, and crate-private `log_*!` macros replace the 62 per-call-site `#[cfg(feature = "tracing")]` gates. The `aimdb-wasm-adapter` ring-lag error now carries a `buffer_name` like the other adapters. ([aimdb-core](aimdb-core/CHANGELOG.md), [aimdb-tokio-adapter](aimdb-tokio-adapter/CHANGELOG.md), [aimdb-embassy-adapter](aimdb-embassy-adapter/CHANGELOG.md), [aimdb-mqtt-connector](aimdb-mqtt-connector/CHANGELOG.md), [aimdb-knx-connector](aimdb-knx-connector/CHANGELOG.md)) + - **CLI/MCP endpoint surface reworked to `--connect ` (Issue #123).** `aimdb`'s per-command `--socket ` is replaced by a global `--connect ` (`AIMDB_CONNECT` env; bare paths still work). `aimdb-mcp` renames the tools' `socket_path` param to `endpoint`, the startup `--socket` to `--connect`, and `AIMDB_SOCKET` to `AIMDB_CONNECT`; the pool is keyed by endpoint URL. `aimdb-client`'s `AimxConnection::connect` takes a `&str` endpoint (was a path) and `ClientError::ConnectionFailed.socket` is renamed `endpoint`. Serial endpoints (`serial://…`) require building the CLI/MCP with `--features transport-serial`. ([aimdb-client](aimdb-client/CHANGELOG.md), [tools/aimdb-cli](tools/aimdb-cli/CHANGELOG.md), [tools/aimdb-mcp](tools/aimdb-mcp/CHANGELOG.md)) - **`AimDbBuilder::with_remote_access(config)` removed — remote-access servers are now registered like any other connector (Issue #39).** Replace `.with_remote_access(config)` with `.with_connector(aimdb_uds_connector::UdsServer::from_config(config))`. The AimX wire was reshaped to **v2** (NDJSON tagged frames mapping onto the engine's role-neutral message set) and is **not** backward-compatible with the legacy AimX v1 framing; the bundled `aimdb-client` / CLI / MCP speak v2. The UDS transport types moved out of `aimdb-core` into `aimdb-uds-connector`. ([aimdb-core](aimdb-core/CHANGELOG.md), [aimdb-uds-connector](aimdb-uds-connector/CHANGELOG.md), [aimdb-client](aimdb-client/CHANGELOG.md)) diff --git a/aimdb-core/CHANGELOG.md b/aimdb-core/CHANGELOG.md index f763687..b24bfba 100644 --- a/aimdb-core/CHANGELOG.md +++ b/aimdb-core/CHANGELOG.md @@ -25,6 +25,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Internal refactors +- **Phase 1 mechanical cleanup (Issue #132, [design doc](../docs/design/034-technical-debt-review.md)).** No behavior change: + - All dual `#[cfg(feature = "std")] use std::… / #[cfg(not(…))] use alloc::…` import pairs replaced by single unconditional `use alloc::…` imports; redundant per-module `extern crate alloc;` declarations dropped (the crate root has one). The duplicated std/no_std `RuntimeContext` impl blocks in `context.rs` (character-identical except for the `Arc` path) are merged into one. + - New crate-private `log_debug!`/`log_info!`/`log_warn!`/`log_error!` macros (`src/log.rs`) forward to `tracing` when the feature is on and expand to an argument-borrowing no-op otherwise — deleting all 62 per-call-site `#[cfg(feature = "tracing")]` gates. `defmt` is deliberately not folded in (most sites use `{:?}` with non-`defmt::Format` types); router.rs keeps its paired explicit defmt gates. + - `build()` resolves each topo-order key with one O(1) `by_key.get_key_value(&str)` lookup (via `StringKey: Borrow`) instead of an O(n) `keys().find()` scan plus a redundant second resolve. + - **`RecordMetadataTracker` deleted; `TypedRecord` keeps only a bare `writable` flag (Issue #120).** The per-record tracker (an `Arc>>` + `Arc` + `SystemTime::now()` on every `produce()` via `RecordWriter::push`) is gone; the surviving `writable` bit is now a single `portable_atomic::AtomicBool` field on `TypedRecord`, and `collect_metadata` reads the type name + `writable` directly with no shared state. New `pub(crate)` `DbError::{runtime_error, permission_denied, record_key_not_found}` constructors let the JSON/remote paths write one error expression across `std` (message carried) and `no_std` (unit placeholder) — replacing the inline `#[cfg]` splits at each call site. The `graph` types' `serde` derives (`RecordOrigin` / `GraphNode` / `GraphEdge` / `EdgeType`) move from the `std` gate to the `serde` feature (always on with `alloc`), so `RecordMetadata` can serialize on `no_std`. - **AimX server/client ported onto the shared session engine; the hand-rolled loops deleted (Issue #39, [design doc](../docs/design/remote-access-via-connectors.md)).** Building on the spawn-free work below, `remote/handler.rs` (the per-connection `select!` loop) and `remote/supervisor.rs` (the accept loop) are **removed** — their behavior is now `run_session` + `serve` in `session`, driven by the AimX-v2 `AimxDispatch`/`AimxCodec`. `remote/stream.rs`'s `stream_record_updates` survives and is reused by `AimxSession::subscribe`. The UDS transport (socket bind/connect, NDJSON framing) relocated out of core into the new `aimdb-uds-connector` crate; core keeps only the protocol (codec + dispatch) and the generic session connectors. The query handler type-erasure moved to `remote/query.rs` (`QueryHandlerFn`/`QueryHandlerParams`). New dependencies: `async-channel` (runtime-neutral mpsc), `futures-channel` (oneshot), `futures-util`'s `async-await-macro` (`select_biased!`), and `serde_json`'s `raw_value` feature — all `no_std + alloc`-compatible, none entering the no_std contracts build. @@ -36,6 +41,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed (breaking) +- **`DbError` unified on `alloc::string::String` — one shape per variant on every target (Issue #129, [design doc §3.1.1](../docs/design/034-technical-debt-review.md)).** The std/no_std dual-field design (`key: String` under `std`, `_key: ()` otherwise) is gone: every context field is a `String` unconditionally (the crate already requires `alloc` everywhere), and `Display`/`Error` derive from `thiserror` on every target (thiserror 2.x is no_std-capable, now a mandatory `default-features = false` dependency; the `remote-access` feature no longer lists it). Consequences: + - **no_std only:** field renames (`_endpoint`/`_reason`/`_buffer_name`/`_key`/… `: ()` → the real `String` fields); `Display` now prints the same full messages as std builds instead of the `Error 0xNNNN` numeric-code table (which is deleted); `DbError` now implements `core::error::Error`. `error_code()` / `error_category()` are unchanged. + - **std:** no change to variant shapes or messages. + - The helper constructors `DbError::runtime_error` / `permission_denied` / `record_key_not_found` are now **public**, un-gated from `remote-access`, single-bodied (the message is carried on every target), and joined by a new `DbError::missing_configuration`. Every dual `#[cfg]` construction branch in core, the adapters, and the MQTT/KNX connectors is collapsed. +- **`ConsumerTrait::subscribe_any` is infallible (Issue #132).** The future now resolves to `Box` instead of `DbResult>` — subscription has been infallible since M14 (pre-resolved buffer handle); the `Result` was kept only for caller compatibility. Implementors drop the `Ok(...)` wrap; callers drop the dead `Err` arm. +- **`OutboundRoute` is a struct (Issue #132).** Was a 5-tuple type alias destructured positionally; now named fields: `topic`, `consumer`, `serializer`, `config`, `topic_provider`. +- **The `std` feature no longer enables a `tokio` dependency (Issue #132).** Since the session-engine refactor (030/033) the only `tokio::` references in `aimdb-core/src` are in `#[cfg(test)]` modules and doc comments — covered by the dev-dependency. Core no longer pulls tokio (`net`, `io-util`, `sync`, `time`) into every std build; depend on tokio directly if you relied on the transitive dependency. + - **`RecordMetadata` drops `created_at` / `last_update`; `AimxConfig.socket_path` is now `String` (Issue #120).** Part of de-std'ing the remote data model for the `no_std` AimX server. Core no longer keeps per-record timestamp state — the `std::sync::Mutex`-+-`SystemTime`-backed `RecordMetadataTracker` is deleted — so `RecordMetadata` loses the `created_at` and `last_update` fields, `RecordMetadata::new` loses its `created_at` parameter, and `with_last_update` / `with_last_update_opt` are removed. AimX `record.list` (and downstream MCP / CLI output derived from it) therefore no longer carry those two fields; every other metadata field is unchanged. `AimxConfig.socket_path` becomes `String` (was `PathBuf`) and `AimxConfig::socket_path(impl Into)` replaces `impl Into` — `&str` / `String` callers are unaffected, `&Path` / `PathBuf` callers must convert (the UDS transport in `aimdb-uds-connector` does the `Path`-based bind). `SecurityPolicy`'s writable set and the `remote` module's collections move from `std::collections` to `hashbrown`. - **`AimDbBuilder::with_remote_access(config)` removed (Issue #39).** Register a session connector instead: `.with_connector(aimdb_uds_connector::UdsServer::from_config(config))`. The connector binds its transport at `build` time (bind errors surface synchronously, as before), applies the security policy's writable-record marking, and drives the shared `serve` engine. The builder's private `remote_config` field is gone; the per-record `TypedRecord::with_remote_access()` is unrelated and unchanged. The reshaped **AimX-v2** wire is not backward-compatible with the legacy v1 framing. @@ -82,6 +95,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed (breaking) +- **`Database` wrapper removed (Issue #132, design doc §3.8).** The thin façade over `AimDb` (and its `pub use database::Database` re-export) had no users beyond the `TokioDatabase`/`EmbassyDatabase` adapter aliases, themselves used nowhere — both aliases are removed from the adapters in the same change. Use `AimDb` via `AimDbBuilder` directly. +- **Deprecated `RecordRegistrar::link()` removed (Issue #132).** Deprecated since 0.2.0; use `.link_to()` / `.link_from()`. + - **`TypedRecord::produce` removed (M15, Design 031).** The M14 step (above) made it sync; M15 removes it entirely. All writes now go through `WriteHandle::push` via `TypedRecord::writer_handle()`. `AimDb::produce` and AimX `set_from_json` route through it; as a side effect `set_from_json` now marks record metadata as updated (previously skipped on that path). `WriteHandle` / `RecordWriter` no longer carry the snapshot mutex. - **`with_read_only_serialization()` removed (M16, Design 032).** A `Serialize`-only record can no longer be exposed read-only over remote access. Use `with_remote_access()`, which additionally requires `DeserializeOwned`. No in-tree callers existed. diff --git a/aimdb-core/Cargo.toml b/aimdb-core/Cargo.toml index 98f70eb..3c7b3a8 100644 --- a/aimdb-core/Cargo.toml +++ b/aimdb-core/Cargo.toml @@ -23,7 +23,6 @@ std = [ "serde", "anyhow", "remote-access", - "tokio", "aimdb-executor/std", # AimX remote access now rides the shared session engine: `with_remote_access` # builds the `session::aimx` server, so std pulls in the connector-session module. @@ -42,11 +41,11 @@ json-serialize = ["alloc", "serde_json"] # AimX remote-access data model (`crate::remote`): record metadata + # introspection (`RecordMetadata`, `AimDb::list_records`), the JSON # read/write/subscribe API (`try_latest_as_json` / `set_record_from_json` / -# `JsonBufferReader`), and the wire protocol/config/security/error types. Builds on -# the `json-serialize` value codec and adds `thiserror` (no_std-capable; see below) -# for `RemoteError`. The AimX *server* dispatch (`session::aimx`) additionally -# needs `connector-session`. All compiles on `no_std + alloc`. -remote-access = ["json-serialize", "thiserror"] +# `JsonBufferReader`), and the wire protocol/config/security/error types. Builds +# on the `json-serialize` value codec. The AimX *server* dispatch +# (`session::aimx`) additionally needs `connector-session`. All compiles on +# `no_std + alloc`. +remote-access = ["json-serialize"] # The connector-session substrate (`crate::session`): the dyn-safe trait set # (Connection/Listener/Dialer, Dispatch/EnvelopeCodec, Source + shared types) and @@ -117,8 +116,8 @@ async-channel = { version = "2", default-features = false } # Serialization (optional) serde = { workspace = true, optional = true } -# Error handling -thiserror = { version = "2.0.16", default-features = false, optional = true } +# Error handling (thiserror 2.x is no_std-capable; DbError derives it on every target) +thiserror = { version = "2.0.16", default-features = false } anyhow = { workspace = true, optional = true } # `raw_value` lets the connector-session `EnvelopeCodec` splice an already- # serialized record-value `Payload` into a JSON envelope without re-escaping @@ -126,14 +125,6 @@ anyhow = { workspace = true, optional = true } # json-serialize); the no_std `connector-session` contracts build never sees it. serde_json = { workspace = true, optional = true, features = ["raw_value"] } -# Async runtime - only for std environments with remote access -tokio = { workspace = true, features = [ - "net", - "io-util", - "sync", - "time", -], optional = true } - # Atomic operations for all platforms portable-atomic = { version = "1.9", default-features = false } @@ -155,6 +146,7 @@ hashbrown = { version = "0.15", default-features = false, features = [ [dev-dependencies] # For no_std testing heapless = "0.9.1" -# For async testing -tokio = { workspace = true, features = ["macros", "rt", "time"] } +# For async testing (`sync` covers tokio::sync::mpsc in tests/session_engine.rs, +# which previously leaked in from the optional [dependencies] entry) +tokio = { workspace = true, features = ["macros", "rt", "time", "sync"] } futures = { version = "0.3", default-features = false, features = ["alloc"] } diff --git a/aimdb-core/src/buffer/cfg.rs b/aimdb-core/src/buffer/cfg.rs index 6fc96d1..898ce6a 100644 --- a/aimdb-core/src/buffer/cfg.rs +++ b/aimdb-core/src/buffer/cfg.rs @@ -4,9 +4,6 @@ use core::fmt; -#[cfg(not(feature = "std"))] -extern crate alloc; - /// Buffer configuration for a record type /// /// Selects buffering strategy: SPMC Ring (backlog), SingleLatest (state sync), or Mailbox (commands). diff --git a/aimdb-core/src/buffer/mod.rs b/aimdb-core/src/buffer/mod.rs index 56b74ca..7d4067f 100644 --- a/aimdb-core/src/buffer/mod.rs +++ b/aimdb-core/src/buffer/mod.rs @@ -52,9 +52,6 @@ //! .tap(|em, cfg| async { ... }); //! ``` -#[cfg(not(feature = "std"))] -extern crate alloc; - // Module structure mod cfg; #[cfg(feature = "metrics")] diff --git a/aimdb-core/src/buffer/traits.rs b/aimdb-core/src/buffer/traits.rs index 842b45a..5f324ad 100644 --- a/aimdb-core/src/buffer/traits.rs +++ b/aimdb-core/src/buffer/traits.rs @@ -8,15 +8,8 @@ use core::future::Future; use core::pin::Pin; -#[cfg(not(feature = "std"))] -extern crate alloc; - -#[cfg(not(feature = "std"))] use alloc::boxed::Box; -#[cfg(feature = "std")] -use std::boxed::Box; - use super::BufferCfg; use crate::DbError; @@ -265,6 +258,7 @@ pub trait BufferMetrics { #[cfg(test)] mod tests { use super::*; + use alloc::string::ToString; // Mock implementation for testing trait bounds struct MockBuffer { @@ -321,10 +315,7 @@ mod tests { Box::pin(async { // Return closed for testing Err(DbError::BufferClosed { - #[cfg(feature = "std")] buffer_name: "mock".to_string(), - #[cfg(not(feature = "std"))] - _buffer_name: (), }) }) } diff --git a/aimdb-core/src/buffer/writer.rs b/aimdb-core/src/buffer/writer.rs index 6156ad8..374bf43 100644 --- a/aimdb-core/src/buffer/writer.rs +++ b/aimdb-core/src/buffer/writer.rs @@ -3,15 +3,8 @@ //! Pre-binds the buffer so `Producer` can push values without holding an //! `Arc>` or running a `HashMap` lookup per call. -#[cfg(not(feature = "std"))] -extern crate alloc; - -#[cfg(not(feature = "std"))] use alloc::sync::Arc; -#[cfg(feature = "std")] -use std::sync::Arc; - use super::traits::{DynBuffer, WriteHandle}; pub(crate) struct RecordWriter { diff --git a/aimdb-core/src/builder.rs b/aimdb-core/src/builder.rs index 558c612..34a2093 100644 --- a/aimdb-core/src/builder.rs +++ b/aimdb-core/src/builder.rs @@ -7,20 +7,14 @@ use core::any::TypeId; use core::fmt::Debug; use core::marker::PhantomData; -extern crate alloc; - -use alloc::vec::Vec; +use alloc::{ + boxed::Box, + string::{String, ToString}, + sync::Arc, + vec::Vec, +}; use hashbrown::HashMap; -#[cfg(not(feature = "std"))] -use alloc::{boxed::Box, sync::Arc}; - -#[cfg(not(feature = "std"))] -use alloc::string::{String, ToString}; - -#[cfg(feature = "std")] -use std::{boxed::Box, sync::Arc}; - use crate::extensions::Extensions; use crate::graph::DependencyGraph; @@ -46,21 +40,20 @@ use crate::typed_api::{RecordRegistrar, RecordT}; use crate::typed_record::{AnyRecord, AnyRecordExt, RecordFutureCollector, TypedRecord}; use crate::{DbError, DbResult}; -/// Type alias for outbound route tuples returned by `collect_outbound_routes` -/// -/// Each tuple contains: -/// - `String` - Default topic/destination from the URL path -/// - `Box` - Consumer for subscribing to record values -/// - `SerializerKind` - User-provided serializer for the record type (raw or context-aware) -/// - `Vec<(String, String)>` - Configuration options from the URL query -/// - `Option` - Optional dynamic topic provider -pub type OutboundRoute = ( - String, - Box, - crate::connector::SerializerKind, - Vec<(String, String)>, - Option, -); +/// One outbound route returned by [`AimDb::collect_outbound_routes`] +pub struct OutboundRoute { + /// Default topic/destination from the URL path; used when no + /// `topic_provider` overrides it per value. + pub topic: String, + /// Type-erased consumer for subscribing to record values + pub consumer: Box, + /// User-provided serializer for the record type (raw or context-aware) + pub serializer: crate::connector::SerializerKind, + /// Configuration options from the URL query + pub config: Vec<(String, String)>, + /// Optional dynamic topic provider + pub topic_provider: Option, +} /// Marker type for untyped builder (before runtime is set) pub struct NoRuntime; @@ -182,18 +175,9 @@ impl AimDbInner { let key_str = key.as_ref(); // Resolve key to RecordId - let id = self.resolve_str(key_str).ok_or({ - #[cfg(feature = "std")] - { - DbError::RecordKeyNotFound { - key: key_str.to_string(), - } - } - #[cfg(not(feature = "std"))] - { - DbError::RecordKeyNotFound { _key: () } - } - })?; + let id = self + .resolve_str(key_str) + .ok_or_else(|| DbError::record_key_not_found(key_str))?; self.get_typed_record_by_id::(id) } @@ -215,32 +199,21 @@ impl AimDbInner { let expected = TypeId::of::(); let actual = self.types[id.index()]; if expected != actual { - #[cfg(feature = "std")] return Err(DbError::TypeMismatch { record_id: id.raw(), expected_type: core::any::type_name::().to_string(), }); - #[cfg(not(feature = "std"))] - return Err(DbError::TypeMismatch { - record_id: id.raw(), - _expected_type: (), - }); } // Safe to downcast (type validated above) let record = &self.storages[id.index()]; - #[cfg(feature = "std")] - let typed_record = record.as_typed::().ok_or(DbError::InvalidOperation { - operation: "get_typed_record_by_id".to_string(), - reason: "type mismatch during downcast".to_string(), - })?; - - #[cfg(not(feature = "std"))] - let typed_record = record.as_typed::().ok_or(DbError::InvalidOperation { - _operation: (), - _reason: (), - })?; + let typed_record = record + .as_typed::() + .ok_or_else(|| DbError::InvalidOperation { + operation: "get_typed_record_by_id".to_string(), + reason: "type mismatch during downcast".to_string(), + })?; Ok(typed_record) } @@ -655,13 +628,11 @@ where where R: crate::RuntimeForProfiling, { - #[cfg(feature = "tracing")] - tracing::info!("Building database and spawning background tasks..."); + log_info!("Building database and spawning background tasks..."); let (_db, runner) = self.build().await?; - #[cfg(feature = "tracing")] - tracing::info!("Database running, runner driving collected futures."); + log_info!("Database running, runner driving collected futures."); runner.run().await; @@ -706,35 +677,19 @@ where { // Validate all records for (key, _, record) in &self.records { - record.validate().map_err(|_msg| { - #[cfg(feature = "std")] - { - DbError::RuntimeError { - message: format!("Record '{}' validation failed: {}", key.as_str(), _msg), - } - } - #[cfg(not(feature = "std"))] - { - // Suppress unused warning for key in no_std - let _ = &key; - DbError::RuntimeError { _message: () } - } + record.validate().map_err(|msg| { + DbError::runtime_error(alloc::format!( + "Record '{}' validation failed: {}", + key.as_str(), + msg + )) })?; } // Ensure runtime is set - let runtime = self.runtime.ok_or({ - #[cfg(feature = "std")] - { - DbError::RuntimeError { - message: "runtime not set (use .runtime())".into(), - } - } - #[cfg(not(feature = "std"))] - { - DbError::RuntimeError { _message: () } - } - })?; + let runtime = self + .runtime + .ok_or_else(|| DbError::runtime_error("runtime not set (use .runtime())"))?; // Build the new index structures let record_count = self.records.len(); @@ -749,12 +704,9 @@ where // Check for duplicate keys (should not happen if configure() is used correctly) if by_key.contains_key(&key) { - #[cfg(feature = "std")] return Err(DbError::DuplicateRecordKey { key: key.as_str().to_string(), }); - #[cfg(not(feature = "std"))] - return Err(DbError::DuplicateRecordKey { _key: () }); } // Build index structures @@ -791,8 +743,7 @@ where // Build and validate the dependency graph let dependency_graph = DependencyGraph::build_and_validate(&record_infos)?; - #[cfg(feature = "tracing")] - tracing::debug!( + log_debug!( "Dependency graph built successfully ({} nodes, {} edges, topo order: {:?})", dependency_graph.nodes.len(), dependency_graph.edges.len(), @@ -822,38 +773,24 @@ where // Accumulator for every future the runner will drive. let mut futures_acc: Vec = Vec::new(); - #[cfg(feature = "tracing")] - tracing::info!("Collecting futures for {} records", self.spawn_fns.len()); + log_info!("Collecting futures for {} records", self.spawn_fns.len()); // Build a lookup map from spawn_fns for topological ordering let mut spawn_fn_map: HashMap> = self.spawn_fns.into_iter().collect(); // Execute collectors in topological order — transforms collect after their inputs. + // `StringKey: Borrow` with content-based Hash, so the map lookup by + // `&str` is O(1) and yields both the interned key and the RecordId. for key_str in inner.dependency_graph.topo_order() { - let key = match inner.by_key.keys().find(|k| k.as_str() == key_str) { - Some(k) => *k, - None => continue, + let Some((&key, &id)) = inner.by_key.get_key_value(key_str.as_str()) else { + continue; }; - let spawn_fn_any = match spawn_fn_map.remove(&key) { - Some(f) => f, - None => continue, + let Some(spawn_fn_any) = spawn_fn_map.remove(&key) else { + continue; }; - let id = inner.resolve(&key).ok_or({ - #[cfg(feature = "std")] - { - DbError::RecordKeyNotFound { - key: key.as_str().to_string(), - } - } - #[cfg(not(feature = "std"))] - { - DbError::RecordKeyNotFound { _key: () } - } - })?; - let spawn_fn = spawn_fn_any .downcast::>() .expect("spawn function type mismatch"); @@ -861,8 +798,7 @@ where futures_acc.extend((*spawn_fn)(&runtime, &db, id)?); } - #[cfg(feature = "tracing")] - tracing::info!("Record future collection complete"); + log_info!("Record future collection complete"); // AimX remote-access servers are no longer stood up here: register a // session connector (`UdsServer::from_config(...)`) via `with_connector` @@ -874,25 +810,22 @@ where // a `Vec` instead of an `Arc` (which previously // was discarded anyway — see design doc 028 §"The dropped Arc object"). for builder in self.connector_builders { - #[cfg(feature = "tracing")] - let scheme = builder.scheme().to_string(); - - #[cfg(feature = "tracing")] - tracing::debug!("Building connector for scheme: {}", scheme); + log_debug!("Building connector for scheme: {}", builder.scheme()); let connector_futures = builder.build(&db).await?; - #[cfg(feature = "tracing")] let n_futures = connector_futures.len(); futures_acc.extend(connector_futures); - #[cfg(feature = "tracing")] - tracing::info!("Connector '{}' contributed {} future(s)", scheme, n_futures); + log_info!( + "Connector '{}' contributed {} future(s)", + builder.scheme(), + n_futures + ); } // Collect on_start futures (registered by external crates like aimdb-persistence). if !self.start_fns.is_empty() { - #[cfg(feature = "tracing")] - tracing::debug!("Collecting {} on_start future(s)", self.start_fns.len()); + log_debug!("Collecting {} on_start future(s)", self.start_fns.len()); for (idx, start_fn_any) in self.start_fns.into_iter().enumerate() { let start_fn = start_fn_any @@ -1163,17 +1096,8 @@ impl AimDb { // than panicking later inside `subscribe()`. let key_str: alloc::string::String = key.into(); let typed_rec = self.inner.get_typed_record_by_key::(&key_str)?; - let buffer = typed_rec.buffer_handle().ok_or({ - #[cfg(feature = "std")] - { - DbError::MissingConfiguration { - parameter: alloc::format!("buffer for record '{}'", key_str), - } - } - #[cfg(not(feature = "std"))] - { - DbError::MissingConfiguration { _parameter: () } - } + let buffer = typed_rec.buffer_handle().ok_or_else(|| { + DbError::missing_configuration(alloc::format!("buffer for record '{}'", key_str)) })?; Ok(crate::typed_api::Consumer::new(buffer)) } @@ -1364,9 +1288,8 @@ impl AimDb { } } - #[cfg(feature = "tracing")] if !routes.is_empty() { - tracing::debug!( + log_debug!( "Collected {} inbound routes for scheme '{}'", routes.len(), scheme @@ -1447,27 +1370,25 @@ impl AimDb { // Skip links without serializer let Some(serializer) = link.serializer.clone() else { - #[cfg(feature = "tracing")] - tracing::warn!("Outbound link '{}' has no serializer, skipping", link.url); + log_warn!("Outbound link '{}' has no serializer, skipping", link.url); continue; }; // Create consumer using the stored factory if let Some(consumer) = link.create_consumer(db_any.clone()) { - routes.push(( - destination, + routes.push(OutboundRoute { + topic: destination, consumer, serializer, - link.config.clone(), - link.topic_provider.clone(), - )); + config: link.config.clone(), + topic_provider: link.topic_provider.clone(), + }); } } } - #[cfg(feature = "tracing")] if !routes.is_empty() { - tracing::debug!( + log_debug!( "Collected {} outbound routes for scheme '{}'", routes.len(), scheme diff --git a/aimdb-core/src/connector.rs b/aimdb-core/src/connector.rs index d68ee11..b3a4538 100644 --- a/aimdb-core/src/connector.rs +++ b/aimdb-core/src/connector.rs @@ -31,8 +31,6 @@ use core::fmt::{self, Debug}; use core::future::Future; use core::pin::Pin; -extern crate alloc; - use alloc::{ boxed::Box, string::{String, ToString}, @@ -40,7 +38,6 @@ use alloc::{ vec::Vec, }; -#[cfg(feature = "std")] use alloc::format; use crate::{builder::AimDb, DbResult}; @@ -661,12 +658,12 @@ pub trait ConsumerTrait: Send + Sync { /// /// Returns a type-erased reader that can be polled for `Box` values. /// The connector will downcast to the expected type after deserialization. + /// Infallible since M14 — subscription is a pre-resolved buffer handle. fn subscribe_any<'a>(&'a self) -> SubscribeAnyFuture<'a>; } /// Type alias for the future returned by `ConsumerTrait::subscribe_any` -type SubscribeAnyFuture<'a> = - Pin>> + Send + 'a>>; +type SubscribeAnyFuture<'a> = Pin> + Send + 'a>>; /// Type alias for the future returned by `AnyReader::recv_any` type RecvAnyFuture<'a> = @@ -812,22 +809,12 @@ fn parse_connector_url(url: &str) -> DbResult { use crate::DbError; // Split scheme from rest - let (scheme, rest) = url.split_once("://").ok_or({ - #[cfg(feature = "std")] - { - DbError::InvalidOperation { - operation: "parse_connector_url".into(), - reason: format!("Missing scheme in URL: {}", url), - } - } - #[cfg(not(feature = "std"))] - { - DbError::InvalidOperation { - _operation: (), - _reason: (), - } - } - })?; + let (scheme, rest) = url + .split_once("://") + .ok_or_else(|| DbError::InvalidOperation { + operation: "parse_connector_url".into(), + reason: format!("Missing scheme in URL: {}", url), + })?; // Extract credentials if present (user:pass@host) let (credentials, host_part) = if let Some(at_idx) = rest.find('@') { diff --git a/aimdb-core/src/context.rs b/aimdb-core/src/context.rs index 0610662..3cf28a7 100644 --- a/aimdb-core/src/context.rs +++ b/aimdb-core/src/context.rs @@ -3,10 +3,8 @@ //! Provides a unified interface to runtime capabilities like sleep and timestamp //! functions, abstracting away the specific runtime adapter implementation. -#[cfg(not(feature = "std"))] -extern crate alloc; - use aimdb_executor::Runtime; +use alloc::sync::Arc; use core::future::Future; /// Unified runtime context for AimDB services @@ -21,69 +19,33 @@ pub struct RuntimeContext where R: Runtime, { - #[cfg(feature = "std")] - runtime: std::sync::Arc, - #[cfg(not(feature = "std"))] - runtime: alloc::sync::Arc, -} - -#[cfg(feature = "std")] -impl RuntimeContext -where - R: Runtime, -{ - /// Create a new RuntimeContext (std version uses Arc internally) - pub fn new(runtime: R) -> Self { - Self { - runtime: std::sync::Arc::new(runtime), - } - } - - /// Create from an existing Arc to avoid double-wrapping - pub fn from_arc(runtime: std::sync::Arc) -> Self { - Self { runtime } - } - - /// Extract runtime context from type-erased Arc (std version) - /// - /// This is a helper for runtime adapters to convert the raw `Arc` - /// context passed to `.source_raw()` and `.tap_raw()` into a typed `RuntimeContext`. - /// - /// # Panics - /// Panics if the runtime type doesn't match `R`. - pub fn extract_from_any(ctx_any: std::sync::Arc) -> Self { - let runtime = ctx_any - .downcast::() - .expect("Runtime type mismatch - expected matching runtime adapter"); - Self::from_arc(runtime.clone()) - } + runtime: Arc, } -#[cfg(not(feature = "std"))] impl RuntimeContext where R: Runtime, { - /// Create a new RuntimeContext (no_std version uses Arc internally) + /// Create a new RuntimeContext (wraps the runtime in an Arc internally) pub fn new(runtime: R) -> Self { Self { - runtime: alloc::sync::Arc::new(runtime), + runtime: Arc::new(runtime), } } /// Create from an existing Arc to avoid double-wrapping - pub fn from_arc(runtime: alloc::sync::Arc) -> Self { + pub fn from_arc(runtime: Arc) -> Self { Self { runtime } } - /// Extract runtime context from type-erased Arc (no_std version) + /// Extract runtime context from type-erased Arc /// /// This is a helper for runtime adapters to convert the raw `Arc` /// context passed to `.source_raw()` and `.tap_raw()` into a typed `RuntimeContext`. /// /// # Panics /// Panics if the runtime type doesn't match `R`. - pub fn extract_from_any(ctx_any: alloc::sync::Arc) -> Self { + pub fn extract_from_any(ctx_any: Arc) -> Self { let runtime = ctx_any .downcast::() .expect("Runtime type mismatch - expected matching runtime adapter"); @@ -110,30 +72,22 @@ where /// Get access to the underlying runtime /// /// This provides direct access to the runtime for advanced use cases. - #[cfg(feature = "std")] - pub fn runtime(&self) -> &R { - &self.runtime - } - - #[cfg(not(feature = "std"))] pub fn runtime(&self) -> &R { &self.runtime } } -#[cfg(feature = "std")] impl RuntimeContext where R: Runtime, { - /// Create a RuntimeContext from a runtime adapter (std version with Arc) + /// Create a RuntimeContext from a runtime adapter pub fn from_runtime(runtime: R) -> Self { Self::new(runtime) } } -/// Create a RuntimeContext from any Runtime implementation (std version) -#[cfg(feature = "std")] +/// Create a RuntimeContext from any Runtime implementation pub fn create_runtime_context(runtime: R) -> RuntimeContext where R: Runtime, diff --git a/aimdb-core/src/database.rs b/aimdb-core/src/database.rs deleted file mode 100644 index fec597c..0000000 --- a/aimdb-core/src/database.rs +++ /dev/null @@ -1,140 +0,0 @@ -//! Core database implementation -//! -//! `AimDb` is the central coordination point for AimDB, managing type-safe -//! in-memory storage with type-safe records and data synchronization across -//! MCU → edge → cloud environments. - -use crate::{AimDb, DbResult, RuntimeAdapter, RuntimeContext}; -use core::fmt::Debug; - -#[cfg(not(feature = "std"))] -extern crate alloc; - -#[cfg(not(feature = "std"))] -use alloc::{boxed::Box, sync::Arc}; - -#[cfg(feature = "std")] -use std::{boxed::Box, sync::Arc}; - -/// AimDB Database implementation -/// -/// Unified database combining runtime adapter management with type-safe record -/// registration and producer-consumer patterns. See `examples/` for usage patterns. -/// -/// This is a thin wrapper around `AimDb` that adds adapter-specific functionality. -/// Most users should use `AimDbBuilder` directly to create databases. -pub struct Database { - adapter: A, - aimdb: AimDb, -} - -impl Database { - /// Internal accessor for the AimDb instance - /// - /// Used by adapter crates. Should not be used by application code. - #[doc(hidden)] - pub fn inner_aimdb(&self) -> &AimDb { - &self.aimdb - } - - /// Creates a new database from adapter and AimDb - /// - /// # Arguments - /// * `adapter` - The runtime adapter - /// * `aimdb` - The configured AimDb instance - /// - /// Most users should use `AimDbBuilder` directly instead of this constructor. - pub fn new(adapter: A, aimdb: AimDb) -> DbResult { - #[cfg(feature = "tracing")] - tracing::info!("Initializing unified database with typed records"); - - Ok(Self { adapter, aimdb }) - } - - /// Gets a reference to the runtime adapter - /// - /// # Example - /// ```rust,ignore - /// # use aimdb_core::Database; - /// # #[cfg(feature = "tokio-runtime")] - /// # { - /// # async fn example(db: Database) { - /// let adapter = db.adapter(); - /// // Use adapter directly - /// # } - /// # } - /// ``` - pub fn adapter(&self) -> &A { - &self.adapter - } - - /// Produces typed data to a specific record by key - /// - /// # Example - /// ```rust,ignore - /// # fn example(db: aimdb_core::Database) -> aimdb_core::DbResult<()> { - /// db.produce("sensor.temp", SensorData { temp: 23.5 })?; - /// # Ok(()) - /// # } - /// ``` - pub fn produce(&self, key: impl AsRef, data: T) -> DbResult<()> - where - T: Send + 'static + Clone + core::fmt::Debug, - { - self.aimdb.produce(key, data) - } - - /// Subscribes to a record by key - /// - /// Creates a subscription to the configured buffer for the given record key. - /// Returns a boxed reader for receiving values asynchronously. - /// - /// # Example - /// ```rust,ignore - /// # async fn example(db: aimdb_core::Database) -> aimdb_core::DbResult<()> { - /// let mut reader = db.subscribe::("sensor.temp")?; - /// - /// loop { - /// match reader.recv().await { - /// Ok(data) => println!("Received: {:?}", data), - /// Err(e) => { - /// eprintln!("Error: {:?}", e); - /// break; - /// } - /// } - /// } - /// # Ok(()) - /// # } - /// ``` - pub fn subscribe( - &self, - key: impl AsRef, - ) -> DbResult + Send>> - where - T: Send + Sync + 'static + Debug + Clone, - { - self.aimdb.subscribe(key) - } - - /// Creates a RuntimeContext for this database - /// - /// Provides services with access to runtime capabilities (timing, logging) plus the emitter. - /// - /// # Example - /// ```rust,ignore - /// # use aimdb_core::Database; - /// # #[cfg(feature = "tokio-runtime")] - /// # { - /// # async fn example(db: Database) { - /// let ctx = db.context(); - /// // Pass ctx to services - /// # } - /// # } - /// ``` - pub fn context(&self) -> RuntimeContext - where - A: aimdb_executor::Runtime + Clone, - { - RuntimeContext::from_arc(Arc::new(self.adapter.clone())) - } -} diff --git a/aimdb-core/src/error.rs b/aimdb-core/src/error.rs index 740ca2e..f9b5cc0 100644 --- a/aimdb-core/src/error.rs +++ b/aimdb-core/src/error.rs @@ -5,12 +5,11 @@ //! //! # Platform Compatibility //! -//! The error system is designed with conditional compilation to optimize for -//! different deployment targets: -//! -//! - **MCU/Embedded**: Minimal memory footprint with `no_std` compatibility -//! - **Edge/Desktop**: Rich error context with standard library features -//! - **Cloud**: Full error chains and debugging capabilities with thiserror integration +//! Every variant has a single shape on all targets: context fields use +//! `alloc::string::String`, which is available everywhere (the crate +//! unconditionally requires `alloc`), and `Display`/`Error` come from +//! `thiserror` (no_std-capable since 2.x). Only variants that wrap std types +//! (`Io`, `Json`, sync-API timeouts) are gated on the `std` feature. //! //! # Error Categories //! @@ -25,7 +24,8 @@ //! - **Hardware**: MCU hardware errors (embedded only) //! - **I/O & JSON**: Standard library integrations (std only) -#[cfg(feature = "std")] +use alloc::string::String; +use alloc::vec::Vec; use thiserror::Error; #[cfg(feature = "std")] @@ -35,192 +35,96 @@ use std::io; /// /// Only includes errors that are actually used in the codebase, /// removing theoretical/unused error variants for simplicity. -#[derive(Debug)] -#[cfg_attr(feature = "std", derive(Error))] +#[derive(Debug, Error)] pub enum DbError { // ===== Network Errors (0x1000-0x1FFF) ===== /// Connection or timeout failures - #[cfg_attr(feature = "std", error("Connection failed to {endpoint}: {reason}"))] - ConnectionFailed { - #[cfg(feature = "std")] - endpoint: String, - #[cfg(feature = "std")] - reason: String, - #[cfg(not(feature = "std"))] - _endpoint: (), - #[cfg(not(feature = "std"))] - _reason: (), - }, + #[error("Connection failed to {endpoint}: {reason}")] + ConnectionFailed { endpoint: String, reason: String }, // ===== Buffer Errors (0x2000-0x2FFF & 0xA000-0xAFFF) ===== /// Buffer is full and cannot accept more items - #[cfg_attr(feature = "std", error("Buffer full: {buffer_name} ({size} items)"))] - BufferFull { - size: u32, - #[cfg(feature = "std")] - buffer_name: String, - #[cfg(not(feature = "std"))] - _buffer_name: (), - }, + #[error("Buffer full: {buffer_name} ({size} items)")] + BufferFull { size: u32, buffer_name: String }, /// Consumer lagged behind producer (SPMC ring buffers) - #[cfg_attr(feature = "std", error("Consumer lagged by {lag_count} messages"))] - BufferLagged { - lag_count: u64, - #[cfg(feature = "std")] - buffer_name: String, - #[cfg(not(feature = "std"))] - _buffer_name: (), - }, + #[error("Consumer lagged by {lag_count} messages")] + BufferLagged { lag_count: u64, buffer_name: String }, /// Buffer channel has been closed (shutdown) - #[cfg_attr(feature = "std", error("Buffer channel closed: {buffer_name}"))] - BufferClosed { - #[cfg(feature = "std")] - buffer_name: String, - #[cfg(not(feature = "std"))] - _buffer_name: (), - }, + #[error("Buffer channel closed: {buffer_name}")] + BufferClosed { buffer_name: String }, /// Non-blocking receive found no pending values - #[cfg_attr(feature = "std", error("Buffer empty: no pending values"))] + #[error("Buffer empty: no pending values")] BufferEmpty, // ===== Database Errors (0x7003-0x7009) ===== /// Record type not found in database (legacy, by type name) - #[cfg_attr(feature = "std", error("Record type not found: {record_name}"))] - RecordNotFound { - #[cfg(feature = "std")] - record_name: String, - #[cfg(not(feature = "std"))] - _record_name: (), - }, + #[error("Record type not found: {record_name}")] + RecordNotFound { record_name: String }, /// Record key not found in registry - #[cfg_attr(feature = "std", error("Record key not found: {key}"))] - RecordKeyNotFound { - #[cfg(feature = "std")] - key: String, - #[cfg(not(feature = "std"))] - _key: (), - }, + #[error("Record key not found: {key}")] + RecordKeyNotFound { key: String }, /// RecordId out of bounds or invalid - #[cfg_attr(feature = "std", error("Invalid record ID: {id}"))] + #[error("Invalid record ID: {id}")] InvalidRecordId { id: u32 }, /// Type mismatch when accessing record by ID - #[cfg_attr( - feature = "std", - error("Type mismatch: expected {expected_type}, record {record_id} has different type") - )] + #[error("Type mismatch: expected {expected_type}, record {record_id} has different type")] TypeMismatch { record_id: u32, - #[cfg(feature = "std")] expected_type: String, - #[cfg(not(feature = "std"))] - _expected_type: (), }, /// Multiple records of same type exist (ambiguous type-only lookup) - #[cfg_attr( - feature = "std", - error("Ambiguous type lookup: {type_name} has {count} records, use explicit key") - )] - AmbiguousType { - count: u32, - #[cfg(feature = "std")] - type_name: String, - #[cfg(not(feature = "std"))] - _type_name: (), - }, + #[error("Ambiguous type lookup: {type_name} has {count} records, use explicit key")] + AmbiguousType { count: u32, type_name: String }, /// Duplicate record key during registration - #[cfg_attr(feature = "std", error("Duplicate record key: {key}"))] - DuplicateRecordKey { - #[cfg(feature = "std")] - key: String, - #[cfg(not(feature = "std"))] - _key: (), - }, + #[error("Duplicate record key: {key}")] + DuplicateRecordKey { key: String }, /// Invalid operation attempted - #[cfg_attr(feature = "std", error("Invalid operation '{operation}': {reason}"))] - InvalidOperation { - #[cfg(feature = "std")] - operation: String, - #[cfg(feature = "std")] - reason: String, - #[cfg(not(feature = "std"))] - _operation: (), - #[cfg(not(feature = "std"))] - _reason: (), - }, + #[error("Invalid operation '{operation}': {reason}")] + InvalidOperation { operation: String, reason: String }, /// Permission denied for operation - #[cfg_attr(feature = "std", error("Permission denied: {operation}"))] - PermissionDenied { - #[cfg(feature = "std")] - operation: String, - #[cfg(not(feature = "std"))] - _operation: (), - }, + #[error("Permission denied: {operation}")] + PermissionDenied { operation: String }, // ===== Configuration Errors (0x4000-0x4FFF) ===== /// Missing required configuration parameter - #[cfg_attr(feature = "std", error("Missing configuration parameter: {parameter}"))] - MissingConfiguration { - #[cfg(feature = "std")] - parameter: String, - #[cfg(not(feature = "std"))] - _parameter: (), - }, + #[error("Missing configuration parameter: {parameter}")] + MissingConfiguration { parameter: String }, // ===== Runtime Errors (0x7002 & 0x5000-0x5FFF) ===== /// Runtime execution error (task spawning, scheduling, etc.) - #[cfg_attr(feature = "std", error("Runtime error: {message}"))] - RuntimeError { - #[cfg(feature = "std")] - message: String, - #[cfg(not(feature = "std"))] - _message: (), - }, + #[error("Runtime error: {message}")] + RuntimeError { message: String }, /// Resource temporarily unavailable (used by adapters) - #[cfg_attr(feature = "std", error("Resource unavailable: {resource_name}"))] + #[error("Resource unavailable: {resource_name}")] ResourceUnavailable { resource_type: u8, - #[cfg(feature = "std")] resource_name: String, - #[cfg(not(feature = "std"))] - _resource_name: (), }, // ===== Hardware Errors (0x6000-0x6FFF) - Embedded Only ===== /// Hardware-specific errors for embedded/MCU environments - #[cfg_attr( - feature = "std", - error("Hardware error: component {component}, code 0x{error_code:04X}") - )] + #[error("Hardware error: component {component}, code 0x{error_code:04X}")] HardwareError { component: u8, error_code: u16, - #[cfg(feature = "std")] description: String, - #[cfg(not(feature = "std"))] - _description: (), }, // ===== Internal Errors (0x7001) ===== /// Internal error for unexpected conditions - #[cfg_attr(feature = "std", error("Internal error (0x{code:04X}): {message}"))] - Internal { - code: u32, - #[cfg(feature = "std")] - message: String, - #[cfg(not(feature = "std"))] - _message: (), - }, + #[error("Internal error (0x{code:04X}): {message}")] + Internal { code: u32, message: String }, // ===== Sync API Errors (0xB000-0xBFFF) - std only ===== /// Failed to attach database to runtime thread @@ -250,31 +154,14 @@ pub enum DbError { // ===== Transform / Dependency Graph Errors ===== /// Transform dependency graph contains a cycle - #[cfg_attr( - feature = "std", - error("Cyclic dependency detected among records: {records:?}") - )] - CyclicDependency { - #[cfg(feature = "std")] - records: Vec, - #[cfg(not(feature = "std"))] - _records: (), - }, + #[error("Cyclic dependency detected among records: {records:?}")] + CyclicDependency { records: Vec }, /// Transform input key references a record that was not registered - #[cfg_attr( - feature = "std", - error("Transform on '{output_key}' references unregistered input '{input_key}'") - )] + #[error("Transform on '{output_key}' references unregistered input '{input_key}'")] TransformInputNotFound { - #[cfg(feature = "std")] output_key: String, - #[cfg(feature = "std")] input_key: String, - #[cfg(not(feature = "std"))] - _output_key: (), - #[cfg(not(feature = "std"))] - _input_key: (), }, // ===== Standard Library Integrations (std only) ===== @@ -313,36 +200,6 @@ pub enum DbError { }, } -// ===== no_std Display Implementation ===== -#[cfg(not(feature = "std"))] -impl core::fmt::Display for DbError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - let (code, message) = match self { - DbError::ConnectionFailed { .. } => (0x1002, "Connection failed"), - DbError::BufferFull { .. } => (0x2002, "Buffer full"), - DbError::BufferLagged { .. } => (0xA001, "Buffer consumer lagged"), - DbError::BufferClosed { .. } => (0xA002, "Buffer channel closed"), - DbError::BufferEmpty => (0xA003, "Buffer empty"), - DbError::RecordNotFound { .. } => (0x7003, "Record not found"), - DbError::RecordKeyNotFound { .. } => (0x7006, "Record key not found"), - DbError::InvalidRecordId { .. } => (0x7007, "Invalid record ID"), - DbError::TypeMismatch { .. } => (0x7008, "Type mismatch"), - DbError::AmbiguousType { .. } => (0x7009, "Ambiguous type lookup"), - DbError::DuplicateRecordKey { .. } => (0x700A, "Duplicate record key"), - DbError::InvalidOperation { .. } => (0x7004, "Invalid operation"), - DbError::PermissionDenied { .. } => (0x7005, "Permission denied"), - DbError::MissingConfiguration { .. } => (0x4002, "Missing configuration"), - DbError::RuntimeError { .. } => (0x7002, "Runtime error"), - DbError::ResourceUnavailable { .. } => (0x5002, "Resource unavailable"), - DbError::HardwareError { .. } => (0x6001, "Hardware error"), - DbError::Internal { .. } => (0x7001, "Internal error"), - DbError::CyclicDependency { .. } => (0xC001, "Cyclic dependency in transforms"), - DbError::TransformInputNotFound { .. } => (0xC002, "Transform input not found"), - }; - write!(f, "Error 0x{:04X}: {}", code, message) - } -} - // ===== DbError Implementation ===== impl DbError { // Resource type constants @@ -361,10 +218,7 @@ impl DbError { DbError::HardwareError { component, error_code, - #[cfg(feature = "std")] description: String::new(), - #[cfg(not(feature = "std"))] - _description: (), } } @@ -372,48 +226,33 @@ impl DbError { pub fn internal(code: u32) -> Self { DbError::Internal { code, - #[cfg(feature = "std")] message: String::new(), - #[cfg(not(feature = "std"))] - _message: (), } } - /// Builds a [`RuntimeError`](DbError::RuntimeError). The message is carried - /// on `std` and dropped on `no_std` (where the variant holds a unit - /// placeholder); lets callers write one expression across both targets. - /// Gated on `remote-access` — its only callers are the JSON/remote paths. - #[cfg(feature = "remote-access")] - pub(crate) fn runtime_error(_message: impl Into) -> Self { + /// Builds a [`RuntimeError`](DbError::RuntimeError). + pub fn runtime_error(message: impl Into) -> Self { DbError::RuntimeError { - #[cfg(feature = "std")] - message: _message.into(), - #[cfg(not(feature = "std"))] - _message: (), + message: message.into(), } } - /// Builds a [`PermissionDenied`](DbError::PermissionDenied). The operation - /// detail is carried on `std` and dropped on `no_std`. - #[cfg(feature = "remote-access")] - pub(crate) fn permission_denied(_operation: impl Into) -> Self { + /// Builds a [`PermissionDenied`](DbError::PermissionDenied). + pub fn permission_denied(operation: impl Into) -> Self { DbError::PermissionDenied { - #[cfg(feature = "std")] - operation: _operation.into(), - #[cfg(not(feature = "std"))] - _operation: (), + operation: operation.into(), } } - /// Builds a [`RecordKeyNotFound`](DbError::RecordKeyNotFound). The key is - /// carried on `std` and dropped on `no_std`. - #[cfg(feature = "remote-access")] - pub(crate) fn record_key_not_found(_key: impl Into) -> Self { - DbError::RecordKeyNotFound { - #[cfg(feature = "std")] - key: _key.into(), - #[cfg(not(feature = "std"))] - _key: (), + /// Builds a [`RecordKeyNotFound`](DbError::RecordKeyNotFound). + pub fn record_key_not_found(key: impl Into) -> Self { + DbError::RecordKeyNotFound { key: key.into() } + } + + /// Builds a [`MissingConfiguration`](DbError::MissingConfiguration). + pub fn missing_configuration(parameter: impl Into) -> Self { + DbError::MissingConfiguration { + parameter: parameter.into(), } } @@ -562,15 +401,13 @@ impl DbError { } /// Helper to prepend context to a message string - #[cfg(feature = "std")] fn prepend_context>(existing: &mut String, new_context: S) { let new_context = new_context.into(); existing.insert_str(0, ": "); existing.insert_str(0, &new_context); } - /// Adds additional context to an error (std only) - #[cfg(feature = "std")] + /// Adds additional context to an error pub fn with_context>(self, context: S) -> Self { match self { DbError::ConnectionFailed { @@ -760,41 +597,15 @@ impl From for DbError { fn from(err: aimdb_executor::ExecutorError) -> Self { use aimdb_executor::ExecutorError; + // `ExecutorError`'s `message` field is `String` on std and + // `&'static str` on no_std; `.into()` is required for the latter. + #[allow(clippy::useless_conversion)] match err { - ExecutorError::RuntimeUnavailable { message } => { - #[cfg(feature = "std")] - { - DbError::RuntimeError { message } - } - #[cfg(not(feature = "std"))] - { - let _ = message; // Avoid unused warnings - DbError::RuntimeError { _message: () } - } - } - ExecutorError::TaskJoinFailed { message } => { - #[cfg(feature = "std")] - { - DbError::RuntimeError { message } - } - #[cfg(not(feature = "std"))] - { - let _ = message; // Avoid unused warnings - DbError::RuntimeError { _message: () } - } - } - ExecutorError::QueueClosed => { - #[cfg(feature = "std")] - { - DbError::RuntimeError { - message: "join queue closed".to_string(), - } - } - #[cfg(not(feature = "std"))] - { - DbError::RuntimeError { _message: () } - } - } + ExecutorError::RuntimeUnavailable { message } + | ExecutorError::TaskJoinFailed { message } => DbError::RuntimeError { + message: message.into(), + }, + ExecutorError::QueueClosed => DbError::runtime_error("join queue closed"), } } } @@ -802,6 +613,8 @@ impl From for DbError { #[cfg(test)] mod tests { use super::*; + use alloc::string::ToString; + use alloc::vec; #[test] fn test_error_size_constraint() { @@ -816,24 +629,15 @@ mod tests { #[test] fn test_error_codes() { let connection_error = DbError::ConnectionFailed { - #[cfg(feature = "std")] endpoint: "localhost".to_string(), - #[cfg(feature = "std")] reason: "timeout".to_string(), - #[cfg(not(feature = "std"))] - _endpoint: (), - #[cfg(not(feature = "std"))] - _reason: (), }; assert_eq!(connection_error.error_code(), 0x1002); assert_eq!(connection_error.error_category(), 0x1000); let buffer_error = DbError::BufferFull { size: 1024, - #[cfg(feature = "std")] buffer_name: String::new(), - #[cfg(not(feature = "std"))] - _buffer_name: (), }; assert_eq!(buffer_error.error_code(), 0x2002); } @@ -841,14 +645,8 @@ mod tests { #[test] fn test_helper_methods() { let connection_error = DbError::ConnectionFailed { - #[cfg(feature = "std")] endpoint: "localhost".to_string(), - #[cfg(feature = "std")] reason: "timeout".to_string(), - #[cfg(not(feature = "std"))] - _endpoint: (), - #[cfg(not(feature = "std"))] - _reason: (), }; assert!(connection_error.is_network_error()); @@ -865,7 +663,6 @@ mod tests { )); } - #[cfg(feature = "std")] #[test] fn test_error_context() { let error = DbError::ConnectionFailed { @@ -901,7 +698,6 @@ mod tests { assert!(!buffer.is_network_error()); } - #[cfg(feature = "std")] #[test] fn test_configuration_error() { let configuration = DbError::MissingConfiguration { @@ -911,7 +707,6 @@ mod tests { assert!(!configuration.is_buffer_error()); } - #[cfg(feature = "std")] #[test] fn test_runtime_error() { let runtime = DbError::RuntimeError { @@ -928,7 +723,6 @@ mod tests { assert!(!database.is_buffer_error()); } - #[cfg(feature = "std")] #[test] fn test_transform_error() { let transform = DbError::CyclicDependency { diff --git a/aimdb-core/src/extensions.rs b/aimdb-core/src/extensions.rs index 86fbde1..174a68a 100644 --- a/aimdb-core/src/extensions.rs +++ b/aimdb-core/src/extensions.rs @@ -18,8 +18,6 @@ //! let state = db.extensions().get::().expect("MyState not configured"); //! ``` -extern crate alloc; - use alloc::boxed::Box; use core::any::{Any, TypeId}; use hashbrown::HashMap; diff --git a/aimdb-core/src/graph.rs b/aimdb-core/src/graph.rs index 5699296..dfb2751 100644 --- a/aimdb-core/src/graph.rs +++ b/aimdb-core/src/graph.rs @@ -10,7 +10,6 @@ //! - Spawn ordering (topological sort) //! - Runtime introspection (AimX protocol, MCP tools, CLI) -extern crate alloc; use alloc::{ string::{String, ToString}, vec::Vec, @@ -162,20 +161,13 @@ impl DependencyGraph { } // Validate: check all transform input keys exist - #[allow(unused_variables)] // output_key, input_key used only in std error messages for (output_key, input_keys) in &transform_inputs { for input_key in input_keys { if !all_keys.contains(input_key) { - #[cfg(feature = "std")] return Err(crate::DbError::TransformInputNotFound { output_key: output_key.to_string(), input_key: input_key.to_string(), }); - #[cfg(not(feature = "std"))] - return Err(crate::DbError::TransformInputNotFound { - _output_key: (), - _input_key: (), - }); } } } @@ -221,21 +213,16 @@ impl DependencyGraph { } if topo_order.len() != all_keys.len() { - #[cfg(feature = "std")] - { - // Find the cycle participants for a helpful error message - let cycle_records: Vec = in_degree - .iter() - .filter(|(_, °)| deg > 0) - .map(|(&k, _)| k.to_string()) - .collect(); - - return Err(crate::DbError::CyclicDependency { - records: cycle_records, - }); - } - #[cfg(not(feature = "std"))] - return Err(crate::DbError::CyclicDependency { _records: () }); + // Find the cycle participants for a helpful error message + let cycle_records: Vec = in_degree + .iter() + .filter(|(_, °)| deg > 0) + .map(|(&k, _)| k.to_string()) + .collect(); + + return Err(crate::DbError::CyclicDependency { + records: cycle_records, + }); } // Build nodes from record infos diff --git a/aimdb-core/src/lib.rs b/aimdb-core/src/lib.rs index 5e26208..6b9e902 100644 --- a/aimdb-core/src/lib.rs +++ b/aimdb-core/src/lib.rs @@ -8,7 +8,6 @@ //! # Architecture //! //! - **RecordKey/RecordId**: Stable identifiers for multi-instance records -//! - **Unified API**: Single `Database` type for all operations //! - **Runtime Agnostic**: Works with Tokio (std) or Embassy (embedded) //! - **Producer-Consumer**: Built-in typed message passing //! @@ -18,13 +17,16 @@ extern crate alloc; +// Must precede the other modules: `macro_rules!` visibility is textual. +#[macro_use] +mod log; + pub mod buffer; pub mod builder; #[cfg(feature = "json-serialize")] pub mod codec; pub mod connector; pub mod context; -pub mod database; mod error; pub mod ext_macros; pub mod extensions; @@ -74,9 +76,6 @@ pub use aimdb_executor::{ ExecutorError, ExecutorResult, Logger, Runtime, RuntimeAdapter, RuntimeInfo, TimeOps, }; -// Database implementation exports -pub use database::Database; - // Producer-Consumer Pattern exports pub use builder::OutboundRoute; pub use builder::{AimDb, AimDbBuilder}; diff --git a/aimdb-core/src/log.rs b/aimdb-core/src/log.rs new file mode 100644 index 0000000..a7666f9 --- /dev/null +++ b/aimdb-core/src/log.rs @@ -0,0 +1,51 @@ +//! Crate-private logging macros (design 034 / #132). +//! +//! `log_debug!`/`log_info!`/`log_warn!`/`log_error!` forward to the matching +//! `tracing` event macro when the `tracing` feature is enabled; otherwise they +//! expand to a no-op that still borrows the arguments, so call sites compile +//! identically (no unused-variable warnings) under every feature combination. +//! This replaces the per-call-site `#[cfg(feature = "tracing")]` gates. +//! +//! Notes: +//! - The no-op branch *borrows* (and therefore evaluates) the arguments — keep +//! them cheap (getters, lengths, references), never allocate in hot paths. +//! - `defmt` is intentionally not folded in: most call sites use `{:?}` with +//! types that do not implement `defmt::Format` (e.g. `DbError`, `String`, +//! `Vec`). The few sites that mirror events to defmt (router.rs) +//! keep explicit `#[cfg(feature = "defmt")]` gates next to these macros. + +macro_rules! log_debug { + ($s:literal $(, $x:expr)* $(,)?) => {{ + #[cfg(feature = "tracing")] + ::tracing::debug!($s $(, $x)*); + #[cfg(not(feature = "tracing"))] + { let _ = ($( & $x ),*); } + }}; +} + +macro_rules! log_info { + ($s:literal $(, $x:expr)* $(,)?) => {{ + #[cfg(feature = "tracing")] + ::tracing::info!($s $(, $x)*); + #[cfg(not(feature = "tracing"))] + { let _ = ($( & $x ),*); } + }}; +} + +macro_rules! log_warn { + ($s:literal $(, $x:expr)* $(,)?) => {{ + #[cfg(feature = "tracing")] + ::tracing::warn!($s $(, $x)*); + #[cfg(not(feature = "tracing"))] + { let _ = ($( & $x ),*); } + }}; +} + +macro_rules! log_error { + ($s:literal $(, $x:expr)* $(,)?) => {{ + #[cfg(feature = "tracing")] + ::tracing::error!($s $(, $x)*); + #[cfg(not(feature = "tracing"))] + { let _ = ($( & $x ),*); } + }}; +} diff --git a/aimdb-core/src/profiling/info.rs b/aimdb-core/src/profiling/info.rs index 5b7bfad..f419126 100644 --- a/aimdb-core/src/profiling/info.rs +++ b/aimdb-core/src/profiling/info.rs @@ -1,6 +1,5 @@ //! Serializable snapshot of stage profiling metrics, for remote introspection. -extern crate alloc; use alloc::{string::String, vec::Vec}; use serde::{Deserialize, Serialize}; diff --git a/aimdb-core/src/profiling/mod.rs b/aimdb-core/src/profiling/mod.rs index ba3118b..95c2f2e 100644 --- a/aimdb-core/src/profiling/mod.rs +++ b/aimdb-core/src/profiling/mod.rs @@ -23,7 +23,6 @@ pub use info::StageProfilingInfo; pub use record_profiling::{RecordProfilingMetrics, StageEntry}; pub use stage_metrics::StageMetrics; -extern crate alloc; use alloc::{boxed::Box, sync::Arc}; use core::future::Future; use core::pin::Pin; diff --git a/aimdb-core/src/profiling/record_profiling.rs b/aimdb-core/src/profiling/record_profiling.rs index 014628e..04972c5 100644 --- a/aimdb-core/src/profiling/record_profiling.rs +++ b/aimdb-core/src/profiling/record_profiling.rs @@ -1,6 +1,5 @@ //! Per-record container of stage profiling metrics. -extern crate alloc; use alloc::{string::String, sync::Arc, vec::Vec}; use crate::profiling::StageMetrics; diff --git a/aimdb-core/src/record_id.rs b/aimdb-core/src/record_id.rs index 9098fba..8d2bb6d 100644 --- a/aimdb-core/src/record_id.rs +++ b/aimdb-core/src/record_id.rs @@ -50,15 +50,8 @@ //! let producer = db.producer::(AppKey::TempIndoor); //! ``` -#[cfg(not(feature = "std"))] -extern crate alloc; - -#[cfg(not(feature = "std"))] use alloc::{boxed::Box, string::ToString}; -#[cfg(feature = "std")] -use std::boxed::Box; - #[cfg(all(debug_assertions, feature = "std"))] use core::sync::atomic::{AtomicUsize, Ordering}; diff --git a/aimdb-core/src/remote/stream.rs b/aimdb-core/src/remote/stream.rs index a594d63..78e9ce4 100644 --- a/aimdb-core/src/remote/stream.rs +++ b/aimdb-core/src/remote/stream.rs @@ -48,29 +48,24 @@ where // hides which subscription is misbehaving in mixed-record traces. let state = (reader, record_key.to_string()); - Ok(unfold(state, |(mut reader, _key)| async move { + Ok(unfold(state, |(mut reader, key)| async move { loop { match reader.recv_json().await { - Ok(value) => return Some((value, (reader, _key))), - Err(DbError::BufferLagged { - lag_count: _lag_count, - .. - }) => { - #[cfg(feature = "tracing")] - tracing::warn!( - record = %_key, - "stream_record_updates: subscription lagged by {} messages", - _lag_count + Ok(value) => return Some((value, (reader, key))), + Err(DbError::BufferLagged { lag_count, .. }) => { + log_warn!( + "stream_record_updates: record '{}' subscription lagged by {} messages", + key, + lag_count ); continue; } Err(DbError::BufferClosed { .. }) => return None, - Err(_e) => { - #[cfg(feature = "tracing")] - tracing::error!( - record = %_key, - "stream_record_updates: terminating on error: {:?}", - _e + Err(e) => { + log_error!( + "stream_record_updates: record '{}' terminating on error: {:?}", + key, + e ); return None; } diff --git a/aimdb-core/src/router.rs b/aimdb-core/src/router.rs index 9b590c2..894a722 100644 --- a/aimdb-core/src/router.rs +++ b/aimdb-core/src/router.rs @@ -10,15 +10,8 @@ //! - DDS: Routes topics to producers //! - Shared Memory: Routes segment names to producers -#[cfg(not(feature = "std"))] -extern crate alloc; - -#[cfg(not(feature = "std"))] use alloc::{boxed::Box, string::String, sync::Arc, vec::Vec}; -#[cfg(feature = "std")] -use std::sync::Arc; - use crate::connector::{DeserializerKind, ProducerTrait}; /// A single routing entry @@ -116,8 +109,7 @@ impl Router { DeserializerKind::Context(deser) => match ctx { Some(ctx) => (deser)(ctx.clone(), payload), None => { - #[cfg(feature = "tracing")] - tracing::warn!( + log_warn!( "Context deserializer on '{}' but no context provided, skipping", resource_id ); @@ -140,12 +132,10 @@ impl Router { Ok(()) => { routed = true; - #[cfg(feature = "tracing")] - tracing::debug!("Routed message on '{}' to producer", resource_id); + log_debug!("Routed message on '{}' to producer", resource_id); } Err(_e) => { - #[cfg(feature = "tracing")] - tracing::error!( + log_error!( "Failed to produce message on '{}': {}", resource_id, _e @@ -161,12 +151,7 @@ impl Router { } } Err(_e) => { - #[cfg(feature = "tracing")] - tracing::warn!( - "Failed to deserialize message on '{}': {}", - resource_id, - _e - ); + log_warn!("Failed to deserialize message on '{}': {}", resource_id, _e); #[cfg(feature = "defmt")] defmt::warn!( @@ -181,14 +166,12 @@ impl Router { if !routed { if matched { - #[cfg(feature = "tracing")] - tracing::debug!("Route matched for '{}' but message was not produced (missing context or errors)", resource_id); + log_debug!("Route matched for '{}' but message was not produced (missing context or errors)", resource_id); #[cfg(feature = "defmt")] defmt::debug!("Route matched for '{}' but not produced", resource_id); } else { - #[cfg(feature = "tracing")] - tracing::debug!("No route found for resource: '{}'", resource_id); + log_debug!("No route found for resource: '{}'", resource_id); #[cfg(feature = "defmt")] defmt::debug!("No route found for resource: '{}'", resource_id); diff --git a/aimdb-core/src/session/client.rs b/aimdb-core/src/session/client.rs index 11389aa..6062efb 100644 --- a/aimdb-core/src/session/client.rs +++ b/aimdb-core/src/session/client.rs @@ -249,8 +249,7 @@ async fn client_loop( conn } Err(_e) => { - #[cfg(feature = "tracing")] - tracing::warn!("client dial failed: {:?}", _e); + log_warn!("client dial failed: {:?}", _e); match reconnect_after(&mut attempt, &config, &cmd_rx, &*clock).await { true => continue, false => return, @@ -284,8 +283,7 @@ async fn reconnect_after( } *attempt += 1; if config.max_reconnect_attempts != 0 && *attempt >= config.max_reconnect_attempts { - #[cfg(feature = "tracing")] - tracing::warn!( + log_warn!( "client giving up after {} reconnect attempts", config.max_reconnect_attempts ); @@ -503,16 +501,18 @@ where let mut pumps: Vec> = Vec::new(); // --- outbound: local record updates -> remote `write` ------------------ - for (destination, consumer, serializer, _config, topic_provider) in - db.collect_outbound_routes(scheme) + for crate::OutboundRoute { + topic: destination, + consumer, + serializer, + topic_provider, + .. + } in db.collect_outbound_routes(scheme) { let handle = handle.clone(); let ctx = ctx.clone(); pumps.push(Box::pin(async move { - let mut reader = match consumer.subscribe_any().await { - Ok(r) => r, - Err(_e) => return, - }; + let mut reader = consumer.subscribe_any().await; loop { let value = match reader.recv_any().await { Ok(v) => v, diff --git a/aimdb-core/src/session/mod.rs b/aimdb-core/src/session/mod.rs index 783d6aa..9ae2968 100644 --- a/aimdb-core/src/session/mod.rs +++ b/aimdb-core/src/session/mod.rs @@ -12,8 +12,6 @@ //! All contracts are `dyn`-safe and compile on `std` and `no_std + alloc`. See //! `docs/design/remote-access-via-connectors.md` for the design. -extern crate alloc; - use alloc::{boxed::Box, string::String, sync::Arc, vec::Vec}; use core::future::Future; use core::pin::Pin; diff --git a/aimdb-core/src/session/pump.rs b/aimdb-core/src/session/pump.rs index aa2dc28..e0d5964 100644 --- a/aimdb-core/src/session/pump.rs +++ b/aimdb-core/src/session/pump.rs @@ -13,8 +13,6 @@ //! //! Both are `no_std + alloc`-native (boxed futures, no `tokio`). -extern crate alloc; - use alloc::boxed::Box; use alloc::sync::Arc; use alloc::vec; @@ -45,28 +43,23 @@ where let routes = db.collect_outbound_routes(scheme); let mut futures: Vec = Vec::with_capacity(routes.len()); - for (default_topic, consumer, serializer, config, topic_provider) in routes { + for crate::OutboundRoute { + topic: default_topic, + consumer, + serializer, + config, + topic_provider, + } in routes + { let sink = sink.clone(); let runtime_ctx = db.runtime_any(); let cfg = ConnectorConfig::from_query(&config); futures.push(Box::pin(async move { // Subscribe to typed values (type-erased). - let mut reader = match consumer.subscribe_any().await { - Ok(r) => r, - Err(_e) => { - #[cfg(feature = "tracing")] - tracing::error!( - "pump_sink: failed to subscribe for destination '{}': {:?}", - default_topic, - _e - ); - return; - } - }; + let mut reader = consumer.subscribe_any().await; - #[cfg(feature = "tracing")] - tracing::info!( + log_info!( "pump_sink: publisher started for destination: {}", default_topic ); @@ -79,14 +72,12 @@ where // gap and keep pumping — a transient lag must not permanently // kill the publisher. Err(crate::DbError::BufferLagged { .. }) => { - #[cfg(feature = "tracing")] - tracing::warn!("pump_sink: consumer lagged for '{}'", default_topic); + log_warn!("pump_sink: consumer lagged for '{}'", default_topic); continue; } // Buffer closed / fatal — the record is gone; end the publisher. Err(_e) => { - #[cfg(feature = "tracing")] - tracing::info!( + log_info!( "pump_sink: publisher stopping for '{}': {:?}", default_topic, _e @@ -105,8 +96,7 @@ where SerializerKind::Raw(ser) => match ser(&*value_any) { Ok(b) => b, Err(_e) => { - #[cfg(feature = "tracing")] - tracing::error!( + log_error!( "pump_sink: failed to serialize for destination '{}': {:?}", dest, _e @@ -117,8 +107,7 @@ where SerializerKind::Context(ser) => match ser(runtime_ctx.clone(), &*value_any) { Ok(b) => b, Err(_e) => { - #[cfg(feature = "tracing")] - tracing::error!( + log_error!( "pump_sink: failed to serialize for destination '{}': {:?}", dest, _e @@ -130,16 +119,13 @@ where // Publish through the connector's pure I/O adapter. if let Err(_e) = sink.publish(&dest, &cfg, &bytes).await { - #[cfg(feature = "tracing")] - tracing::error!("pump_sink: failed to publish to '{}': {:?}", dest, _e); + log_error!("pump_sink: failed to publish to '{}': {:?}", dest, _e); } else { - #[cfg(feature = "tracing")] - tracing::debug!("pump_sink: published to: {}", dest); + log_debug!("pump_sink: published to: {}", dest); } } - #[cfg(feature = "tracing")] - tracing::info!( + log_info!( "pump_sink: publisher stopped for destination: {}", default_topic ); @@ -170,8 +156,7 @@ where let ctx = db.runtime_any(); vec![Box::pin(async move { - #[cfg(feature = "tracing")] - tracing::info!( + log_info!( "pump_source: reader started ({} topics)", router.resource_ids().len() ); @@ -180,8 +165,7 @@ where // `route` deserializes and fans out to producers; it drops + logs on a // full producer buffer and never returns a fatal error. if let Err(_e) = router.route(&topic, &payload, Some(&ctx)).await { - #[cfg(feature = "tracing")] - tracing::error!( + log_error!( "pump_source: failed to route message on '{}': {}", topic, _e @@ -189,7 +173,6 @@ where } } - #[cfg(feature = "tracing")] - tracing::info!("pump_source: reader stopped"); + log_info!("pump_source: reader stopped"); })] } diff --git a/aimdb-core/src/session/server.rs b/aimdb-core/src/session/server.rs index c16d599..4327466 100644 --- a/aimdb-core/src/session/server.rs +++ b/aimdb-core/src/session/server.rs @@ -100,8 +100,7 @@ pub async fn run_session( let ctx = match dispatch.authenticate(conn.peer(), first.as_deref()).await { Ok(ctx) => ctx, Err(_e) => { - #[cfg(feature = "tracing")] - tracing::warn!("session authenticate rejected: {:?}", _e); + log_warn!("session authenticate rejected: {:?}", _e); return; } }; @@ -373,8 +372,7 @@ where // under an accept flood (the biased `accept` arm starves the reap // arm) it may read high transiently — acceptable for a soft cap. if conns.len() >= config.limits.max_connections { - #[cfg(feature = "tracing")] - tracing::warn!( + log_warn!( "max_connections={} reached, refusing client", config.limits.max_connections ); @@ -389,8 +387,7 @@ where })); } Err(_e) => { - #[cfg(feature = "tracing")] - tracing::error!("accept failed: {:?}", _e); + log_error!("accept failed: {:?}", _e); // Keep serving existing connections despite a transient accept error. } } diff --git a/aimdb-core/src/time.rs b/aimdb-core/src/time.rs index 1c3810f..95f0dac 100644 --- a/aimdb-core/src/time.rs +++ b/aimdb-core/src/time.rs @@ -70,16 +70,10 @@ pub mod utils { } /// Creates a generic timeout error for operations that exceed their time limit - pub fn create_timeout_error(_operation_name: &str) -> crate::DbError { + pub fn create_timeout_error(operation_name: &str) -> crate::DbError { crate::DbError::ConnectionFailed { - #[cfg(feature = "std")] - endpoint: "timeout".to_string(), - #[cfg(feature = "std")] - reason: format!("{} operation timed out", _operation_name), - #[cfg(not(feature = "std"))] - _endpoint: (), - #[cfg(not(feature = "std"))] - _reason: (), + endpoint: alloc::string::String::from("timeout"), + reason: alloc::format!("{} operation timed out", operation_name), } } } diff --git a/aimdb-core/src/transform/join.rs b/aimdb-core/src/transform/join.rs index 8f6cd5c..6455768 100644 --- a/aimdb-core/src/transform/join.rs +++ b/aimdb-core/src/transform/join.rs @@ -166,8 +166,7 @@ where let consumer = match db.consumer::(&key_for_factory) { Ok(c) => c, Err(_e) => { - #[cfg(feature = "tracing")] - tracing::error!( + log_error!( "🔄 Join input '{}' (index {}) consumer resolution failed: {:?}", key_for_factory, index, @@ -260,7 +259,6 @@ where // Join Transform Build (forwarders + handler future, both collected at build time) // ============================================================================ -#[allow(unused_variables)] fn build_join_collected( db: Arc>, inputs: Vec<(String, JoinInputFactory)>, @@ -277,28 +275,21 @@ where { // Output key is threaded in from the descriptor so diagnostics stay // unambiguous when multiple records share output type `O` (design 029). - #[cfg(feature = "tracing")] - let output_key_owned = output_key.to_string(); - #[cfg(not(feature = "tracing"))] - let _ = output_key; - - #[cfg(feature = "tracing")] - { - let input_keys: Vec = inputs.iter().map(|(k, _)| k.clone()).collect(); - tracing::info!( - "🔄 Join transform building: {:?} → '{}'", - input_keys, - output_key_owned - ); - } + // Owned copies are build-time, one-shot allocations. + let output_key = output_key.to_string(); + let input_keys: Vec = inputs.iter().map(|(k, _)| k.clone()).collect(); + log_info!( + "🔄 Join transform building: {:?} → '{}'", + input_keys, + output_key + ); let queue = match runtime.create_join_queue::() { Ok(q) => q, Err(_e) => { - #[cfg(feature = "tracing")] - tracing::error!( + log_error!( "🔄 Join transform '{}' FATAL: failed to create join queue", - output_key_owned + output_key ); // Empty collected transform — caller still receives a valid descriptor. return CollectedTransform { @@ -323,16 +314,14 @@ where drop(tx); let task_future: BoxFuture<'static, ()> = Box::pin(async move { - #[cfg(feature = "tracing")] - tracing::debug!( + log_debug!( "✅ Join transform '{}' handing receiver to user task", - output_key_owned + output_key ); handler(JoinEventRx::new(rx), producer).await; - #[cfg(feature = "tracing")] - tracing::warn!("🔄 Join transform '{}' user task exited", output_key_owned); + log_warn!("🔄 Join transform '{}' user task exited", output_key); }); CollectedTransform { diff --git a/aimdb-core/src/transform/single.rs b/aimdb-core/src/transform/single.rs index 8b74cbb..6b0ec27 100644 --- a/aimdb-core/src/transform/single.rs +++ b/aimdb-core/src/transform/single.rs @@ -169,14 +169,12 @@ pub(crate) async fn run_single_transform( // `Producer` no longer exposes a `.key()` accessor (design 029) — the // output key is threaded in by the transform descriptor so diagnostics // remain unambiguous when multiple records share type `O`. - #[cfg(feature = "tracing")] - tracing::info!("🔄 Transform started: '{}' → '{}'", input_key, output_key); + log_info!("🔄 Transform started: '{}' → '{}'", input_key, output_key); let consumer = match db.consumer::(&input_key) { Ok(c) => c, Err(_e) => { - #[cfg(feature = "tracing")] - tracing::error!( + log_error!( "🔄 Transform '{}' → '{}' FATAL: failed to resolve consumer: {:?}", input_key, output_key, @@ -187,8 +185,7 @@ pub(crate) async fn run_single_transform( }; let mut reader = consumer.subscribe(); - #[cfg(feature = "tracing")] - tracing::debug!( + log_debug!( "✅ Transform '{}' → '{}' subscribed, entering event loop", input_key, output_key @@ -202,8 +199,7 @@ pub(crate) async fn run_single_transform( } } Err(crate::DbError::BufferLagged { .. }) => { - #[cfg(feature = "tracing")] - tracing::warn!( + log_warn!( "🔄 Transform '{}' → '{}' lagged behind, some values skipped", input_key, output_key @@ -211,8 +207,7 @@ pub(crate) async fn run_single_transform( continue; } Err(_) => { - #[cfg(feature = "tracing")] - tracing::warn!( + log_warn!( "🔄 Transform '{}' → '{}' input closed, task exiting", input_key, output_key diff --git a/aimdb-core/src/transport.rs b/aimdb-core/src/transport.rs index 35ea369..99813ae 100644 --- a/aimdb-core/src/transport.rs +++ b/aimdb-core/src/transport.rs @@ -11,8 +11,6 @@ //! - **Multi-transport publishing**: Same data can be published to multiple protocols //! - **Protocol-agnostic core**: Core doesn't know about MQTT, Kafka, etc. - just routes by scheme -extern crate alloc; - use alloc::{boxed::Box, string::String, vec::Vec}; use core::future::Future; use core::pin::Pin; diff --git a/aimdb-core/src/typed_api.rs b/aimdb-core/src/typed_api.rs index a6320c4..2970c40 100644 --- a/aimdb-core/src/typed_api.rs +++ b/aimdb-core/src/typed_api.rs @@ -55,7 +55,6 @@ use core::future::Future; use core::marker::PhantomData; use core::pin::Pin; -extern crate alloc; use alloc::{ boxed::Box, format, @@ -203,7 +202,7 @@ where pub struct Consumer { /// Pre-resolved buffer handle to the record's buffer. buffer: Arc>, - /// Stage profiling state (set by the spawn machinery for `.tap()` / `.link()`). + /// Stage profiling state (set by the spawn machinery for `.tap()` / `.link_to()`). #[cfg(feature = "profiling")] profiling: Option<(Arc, crate::profiling::Clock)>, // See Producer: `fn() -> T` keeps Send/Sync independent of T. @@ -297,14 +296,10 @@ where { fn subscribe_any<'a>( &'a self, - ) -> Pin>> + Send + 'a>> - { + ) -> Pin> + Send + 'a>> { Box::pin(async move { - // `subscribe()` is infallible after M14; keep the `DbResult` on - // the trait surface so connector code stays unchanged. let reader = self.subscribe(); - Ok(Box::new(TypedAnyReader:: { inner: reader }) - as Box) + Box::new(TypedAnyReader:: { inner: reader }) as Box }) } } @@ -429,7 +424,7 @@ where /// /// This method accepts the raw runtime context as `Arc` and is used by: /// - Runtime adapter implementations to provide convenient wrappers - /// - Internal connector implementations (e.g., `.link()` creates consumers via this method) + /// - Internal connector implementations (e.g., `.link_to()` creates consumers via this method) /// - Advanced use cases requiring direct control pub fn tap_raw(&'a mut self, f: F) -> &'a mut Self where @@ -574,22 +569,6 @@ where self.transform_join_raw(build_fn) } - /// Adds a connector link for external system integration (DEPRECATED) - /// - /// **Deprecated**: Use `.link_to()` for outbound connectors or `.link_from()` for inbound. - /// This method will be removed in a future version. - #[deprecated(since = "0.2.0", note = "Use link_to() or link_from() instead")] - pub fn link(&'a mut self, url: &str) -> OutboundConnectorBuilder<'a, T, R> { - OutboundConnectorBuilder { - registrar: self, - url: url.to_string(), - config: Vec::new(), - serializer: None, - context_serializer: None, - topic_provider: None, - } - } - /// Link TO external system (outbound: AimDB → External) /// /// Subscribes to buffer updates and publishes them to an external system. diff --git a/aimdb-core/src/typed_record.rs b/aimdb-core/src/typed_record.rs index 91ece48..2b5e0ee 100644 --- a/aimdb-core/src/typed_record.rs +++ b/aimdb-core/src/typed_record.rs @@ -20,17 +20,12 @@ use core::any::Any; use core::fmt::Debug; -#[cfg(not(feature = "std"))] -extern crate alloc; - -#[cfg(not(feature = "std"))] -use alloc::{boxed::Box, string::String, sync::Arc, vec::Vec}; - -#[cfg(not(feature = "std"))] -use alloc::string::ToString; - -#[cfg(feature = "std")] -use std::{boxed::Box, string::String, sync::Arc, vec::Vec}; +use alloc::{ + boxed::Box, + string::{String, ToString}, + sync::Arc, + vec::Vec, +}; #[cfg(feature = "profiling")] use crate::profiling::RecordProfilingMetrics; @@ -438,18 +433,11 @@ where use crate::DbError; // Downcast to TypedRecord - let typed_record: &TypedRecord = - record.as_any().downcast_ref::>().ok_or({ - #[cfg(feature = "std")] - { - DbError::RecordNotFound { - record_name: core::any::type_name::().to_string(), - } - } - #[cfg(not(feature = "std"))] - { - DbError::RecordNotFound { _record_name: () } - } + let typed_record: &TypedRecord = record + .as_any() + .downcast_ref::>() + .ok_or_else(|| DbError::RecordNotFound { + record_name: core::any::type_name::().to_string(), })?; let mut futures = Vec::new(); @@ -740,8 +728,7 @@ impl crate::DbResult + Send>> { - let buffer = self.buffer.as_ref().ok_or({ - #[cfg(feature = "std")] - { - crate::DbError::MissingConfiguration { - parameter: "buffer".to_string(), - } - } - #[cfg(not(feature = "std"))] - { - crate::DbError::MissingConfiguration { _parameter: () } - } - })?; + let buffer = self + .buffer + .as_ref() + .ok_or_else(|| crate::DbError::missing_configuration("buffer"))?; Ok(buffer.subscribe_boxed()) } @@ -872,8 +851,7 @@ impl() ); @@ -956,8 +934,7 @@ impl() @@ -976,20 +953,11 @@ impl() @@ -1278,8 +1245,7 @@ impl Option { - #[cfg(feature = "tracing")] - tracing::debug!( + log_debug!( "latest_json called for type: {}", core::any::type_name::() ); @@ -1289,8 +1255,7 @@ impl crate::DbResult> { use crate::DbError; - #[cfg(feature = "tracing")] - tracing::debug!( + log_debug!( "subscribe_json called for type: {}", core::any::type_name::() ); @@ -1324,8 +1288,7 @@ impl() ); @@ -1338,16 +1301,14 @@ impl crate::DbResult<()> { use crate::DbError; - #[cfg(feature = "tracing")] - tracing::debug!( + log_debug!( "set_from_json called for type: {}", core::any::type_name::() ); // SAFETY CHECK 1: Enforce "No Producer Override" rule if self.has_producer() || self.has_transform() { - #[cfg(feature = "tracing")] - tracing::warn!( + log_warn!( "Rejected set_from_json for '{}': has active producer or transform", core::any::type_name::() ); @@ -1387,8 +1348,7 @@ impl() ); @@ -1397,8 +1357,7 @@ impl() ); diff --git a/aimdb-embassy-adapter/CHANGELOG.md b/aimdb-embassy-adapter/CHANGELOG.md index 061b419..a38f153 100644 --- a/aimdb-embassy-adapter/CHANGELOG.md +++ b/aimdb-embassy-adapter/CHANGELOG.md @@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **Buffer and `nb` errors now carry their context on `no_std` (Issue #129).** With `DbError` unified on `alloc::String`, the adapter's error sites fill the real fields instead of `_field: ()` placeholders: the SPMC-ring/watch paths report `buffer_name` (`"embassy spmc ring"` / `"embassy watch"`), and `from_nb_error(WouldBlock)` reports `resource_name: "nb::WouldBlock"`. Allocation happens only on the error path. - **`buffer()` / `buffer_sized()` now record the `BufferCfg` (via `buffer_with_cfg`).** `buffer_info()` therefore reports the real buffer type and capacity in the dependency graph on `no_std` too (previously `"unknown"`), matching std behaviour. Mirrors the `aimdb-core` `impl_record_registrar_ext!` change (M15). ### Changed (breaking) @@ -36,6 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed (breaking) +- **`EmbassyDatabase` type alias removed (Issue #132, design 034 Phase 1).** It aliased the dead `aimdb_core::Database` wrapper (also removed) and had no users in the workspace. Use `AimDb` via `AimDbBuilder`. - **`impl Spawn for EmbassyAdapter` deleted (Issue #88).** Static `generic_task_runner` task pool gone, along with `BoxedFuture` and the `unsafe Pin::new_unchecked` cast that fed the pool. Drive database futures by awaiting `AimDbRunner::run()` from inside the Embassy main task. - **`embassy-task-pool-8` / `embassy-task-pool-16` / `embassy-task-pool-32` Cargo features deleted.** No pool — `FuturesUnordered` grows as needed within a single Embassy task's heap budget. - **`EmbassyAdapter::new_with_spawner(spawner)` constructor deleted.** diff --git a/aimdb-embassy-adapter/src/buffer.rs b/aimdb-embassy-adapter/src/buffer.rs index cf248e9..6390b20 100644 --- a/aimdb-embassy-adapter/src/buffer.rs +++ b/aimdb-embassy-adapter/src/buffer.rs @@ -41,6 +41,7 @@ extern crate alloc; use alloc::boxed::Box; +use alloc::string::String; use alloc::sync::Arc; use aimdb_core::buffer::{Buffer, BufferCfg, BufferReader}; @@ -432,7 +433,9 @@ impl< Count one slot per .tap(), .link_to() connector, and each transform_join input.", SUBS ); - DbError::BufferClosed { _buffer_name: () } + DbError::BufferClosed { + buffer_name: String::from("embassy spmc ring"), + } })?, ); } @@ -447,7 +450,7 @@ impl< self.metrics.add_dropped(n); Err(DbError::BufferLagged { lag_count: n, - _buffer_name: (), + buffer_name: String::from("embassy spmc ring"), }) } } @@ -467,7 +470,9 @@ impl< self.watch_receiver = watch_static.receiver(); if self.watch_receiver.is_none() { - return Err(DbError::BufferClosed { _buffer_name: () }); + return Err(DbError::BufferClosed { + buffer_name: String::from("embassy watch"), + }); } } @@ -478,7 +483,9 @@ impl< self.metrics.increment_consumed(); Ok(value) } else { - Err(DbError::BufferClosed { _buffer_name: () }) + Err(DbError::BufferClosed { + buffer_name: String::from("embassy watch"), + }) } } EmbassyBufferInner::Mailbox(channel) => { @@ -512,7 +519,9 @@ impl< Count one slot per .tap(), .link_to() connector, and each transform_join input.", SUBS ); - DbError::BufferClosed { _buffer_name: () } + DbError::BufferClosed { + buffer_name: String::from("embassy spmc ring"), + } })?, ); } diff --git a/aimdb-embassy-adapter/src/connectors.rs b/aimdb-embassy-adapter/src/connectors.rs index f03bace..9f5f52e 100644 --- a/aimdb-embassy-adapter/src/connectors.rs +++ b/aimdb-embassy-adapter/src/connectors.rs @@ -58,12 +58,7 @@ type BuildFuture<'a> = Pin>> + S /// The spine's one-shot peripheral was already consumed — `build` ran twice. The /// framework calls it once, so this is unreachable in practice. fn connector_consumed() -> DbError { - DbError::MissingConfiguration { - #[cfg(feature = "std")] - parameter: String::from("embassy connector already built"), - #[cfg(not(feature = "std"))] - _parameter: (), - } + DbError::missing_configuration("embassy connector already built") } // =========================================================================== diff --git a/aimdb-embassy-adapter/src/error.rs b/aimdb-embassy-adapter/src/error.rs index f6cbe8f..c64d669 100644 --- a/aimdb-embassy-adapter/src/error.rs +++ b/aimdb-embassy-adapter/src/error.rs @@ -3,13 +3,15 @@ //! This module provides traits and implementations that add Embassy //! and embedded-hal specific functionality to AimDB's core error types. //! -//! Embassy is a no_std async runtime, so this adapter always works in no_std mode -//! and uses the no_std field names from DbError. +//! Embassy is a no_std async runtime, so this adapter always works in no_std mode. //! //! This crate is excluded from the main workspace to prevent feature unification issues //! that would enable std mode. Build it separately with: cargo build -p aimdb-embassy-adapter +extern crate alloc; + use aimdb_core::DbError; +use alloc::string::String; use embedded_hal_nb::nb; /// Trait that provides Embassy-specific error conversions for DbError @@ -26,7 +28,7 @@ pub trait EmbassyErrorSupport { /// pattern, which is commonly used in embedded async code for hardware interfaces. /// /// # Returns - /// - `nb::Error::WouldBlock` → `DbError::ResourceUnavailable { resource_type: 1, _resource_name: () }` + /// - `nb::Error::WouldBlock` → `DbError::ResourceUnavailable` with `RESOURCE_TYPE_WOULD_BLOCK` /// - `nb::Error::Other(e)` → The underlying error `e` converted to DbError /// /// # Example @@ -57,7 +59,7 @@ impl EmbassyErrorSupport for DbError { nb::Error::Other(e) => e.into(), nb::Error::WouldBlock => DbError::ResourceUnavailable { resource_type: DbError::RESOURCE_TYPE_WOULD_BLOCK, - _resource_name: (), + resource_name: String::from("nb::WouldBlock"), }, } } @@ -83,11 +85,8 @@ mod tests { #[test] fn test_nb_error_other_conversion() { // Test nb::Error::Other conversion with a hardware error - let hardware_error = DbError::HardwareError { - component: 4, // UART component - error_code: 0x6242, // UART error code - _description: (), - }; + let hardware_error = + DbError::hardware_error(4 /* UART */, 0x6242 /* UART error */); let nb_other_error: nb::Error = nb::Error::Other(hardware_error); let converted_error = DbError::from_nb_error(nb_other_error); @@ -111,11 +110,8 @@ mod tests { } // Practical example: unwrapping a hardware error from nb::Error::Other - let underlying_error = DbError::HardwareError { - component: 4, // UART component - error_code: 0x6220, // UART error code - _description: (), - }; + let underlying_error = + DbError::hardware_error(4 /* UART */, 0x6220 /* UART error */); let nb_wrapped: nb::Error = nb::Error::Other(underlying_error); let unwrapped_error = DbError::from_nb_error(nb_wrapped); diff --git a/aimdb-embassy-adapter/src/lib.rs b/aimdb-embassy-adapter/src/lib.rs index cba0828..239e91c 100644 --- a/aimdb-embassy-adapter/src/lib.rs +++ b/aimdb-embassy-adapter/src/lib.rs @@ -39,12 +39,8 @@ //! let nb_error: embedded_hal_nb::nb::Error = embedded_hal_nb::nb::Error::WouldBlock; //! let db_error = DbError::from_nb_error(nb_error); //! -//! // Create hardware errors directly (recommended approach) -//! let uart_error = DbError::HardwareError { -//! component: 4, // UART component ID -//! error_code: 0x6210, -//! _description: (), -//! }; +//! // Create hardware errors via the helper (recommended approach) +//! let uart_error = DbError::hardware_error(4 /* UART component ID */, 0x6210); //! # } //! ``` //! @@ -106,13 +102,6 @@ pub use runtime::EmbassyNetwork; #[cfg(all(not(feature = "std"), feature = "embassy-sync"))] pub use buffer::EmbassyBuffer; -/// Type alias for Embassy database -/// -/// This provides a convenient type for working with databases on the Embassy runtime. -/// Most users should use `AimDbBuilder` directly to create databases. -#[cfg(feature = "embassy-runtime")] -pub type EmbassyDatabase = aimdb_core::Database; - // Re-export core types for convenience #[cfg(all(not(feature = "std"), feature = "embassy-runtime"))] pub use embassy_executor::Spawner; diff --git a/aimdb-knx-connector/CHANGELOG.md b/aimdb-knx-connector/CHANGELOG.md index 2294ff1..0a9d81a 100644 --- a/aimdb-knx-connector/CHANGELOG.md +++ b/aimdb-knx-connector/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **Connector-build errors carry their message on `no_std` too (Issue #129).** With `DbError` unified on `alloc::String`, the dual `#[cfg]` error-construction branches in both clients collapse to one `DbError::runtime_error(...)` expression; the Embassy client's "Failed to build KNX connector" detail is no longer dropped on embedded targets. No API change. - **Tokio client rebuilt on the shared data-plane toolkit (Issue #39, [design doc](../docs/design/remote-access-via-connectors.md)).** The hand-rolled consume-serialize-publish and telegram read-route loops are replaced by `aimdb-core`'s `pump_sink` / `pump_source` helpers: the connector now writes only a `KnxSink` (`Connector`, parses the destination group address and forwards a fire-and-forget `GroupValueWrite`) and a `KnxSource` (`Source`, yields each inbound `(group_address, payload)`) and composes the pumps in `build()`. The routing `Router` is (re)built inside `pump_source`. `std` enables `aimdb-core/connector-session` (where the pump helpers live; `std` implies it transitively). No public API change. - **Outbound publishers survive a consumer lag (Tokio + Embassy).** A `BufferLagged` (SPMC-ring overflow) on the outbound reader now skips the gap and keeps publishing instead of terminating the publisher; only a closed buffer stops it. - **M17 — Embassy client rebuilt on core's pumps via the adapter spine ([Design 033](../docs/design/033-M17-unify-connectors-drop-send.md)).** The hand-rolled outbound publisher loops are gone; this crate now contributes only the KNX/IP **protocol** (UDP socket + tunnelling state machine), force-`Send`ed once via `aimdb_embassy_adapter::connectors::into_box_future` — **no `unsafe`, no `SendFutureWrapper`** remain in this crate. Data-flow changes: diff --git a/aimdb-knx-connector/src/embassy_client.rs b/aimdb-knx-connector/src/embassy_client.rs index ef6f461..8087ae2 100644 --- a/aimdb-knx-connector/src/embassy_client.rs +++ b/aimdb-knx-connector/src/embassy_client.rs @@ -175,19 +175,13 @@ where // `CriticalSectionRawMutex`, i.e. `Send`). Box::pin(async move { let (command_channel, inbound_rx, connection_task) = - KnxConnectorImpl::setup(self.gateway_url.as_str(), db.runtime()).map_err(|_e| { + KnxConnectorImpl::setup(self.gateway_url.as_str(), db.runtime()).map_err(|e| { #[cfg(feature = "defmt")] defmt::error!("Failed to build KNX connector"); - #[cfg(feature = "std")] - { - aimdb_core::DbError::RuntimeError { - message: format!("Failed to build KNX connector: {}", _e), - } - } - #[cfg(not(feature = "std"))] - { - aimdb_core::DbError::RuntimeError { _message: () } - } + aimdb_core::DbError::runtime_error(alloc::format!( + "Failed to build KNX connector: {}", + e + )) })?; // Outbound: records → KNX telegrams via the existing `Connector` impl. diff --git a/aimdb-knx-connector/src/tokio_client.rs b/aimdb-knx-connector/src/tokio_client.rs index d4b4e1c..7d96043 100644 --- a/aimdb-knx-connector/src/tokio_client.rs +++ b/aimdb-knx-connector/src/tokio_client.rs @@ -112,17 +112,11 @@ impl ConnectorBuilder for KnxCon let (command_tx, telegram_rx, connection_future) = KnxConnectorImpl::build_internal(&self.gateway_url, self.command_queue_size) .await - .map_err(|_e| { - #[cfg(feature = "std")] - { - aimdb_core::DbError::RuntimeError { - message: format!("Failed to build KNX connector: {}", _e), - } - } - #[cfg(not(feature = "std"))] - { - aimdb_core::DbError::RuntimeError { _message: () } - } + .map_err(|e| { + aimdb_core::DbError::runtime_error(format!( + "Failed to build KNX connector: {}", + e + )) })?; let mut futures: Vec = vec![connection_future]; diff --git a/aimdb-mqtt-connector/CHANGELOG.md b/aimdb-mqtt-connector/CHANGELOG.md index 648ebf0..068f0a9 100644 --- a/aimdb-mqtt-connector/CHANGELOG.md +++ b/aimdb-mqtt-connector/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **Connector-build errors carry their message on `no_std` too (Issue #129).** With `DbError` unified on `alloc::String`, the dual `#[cfg]` error-construction branches in both clients collapse to one `DbError::runtime_error(...)` expression; the Embassy client's "Failed to build MQTT connector" detail is no longer dropped on embedded targets. No API change. - **Tokio client rebuilt on the shared data-plane toolkit (Issue #39, [design doc](../docs/design/remote-access-via-connectors.md)).** The hand-rolled consume-serialize-publish and read-route loops are replaced by `aimdb-core`'s `pump_sink` / `pump_source` helpers (the connector now writes only its `Connector`/`Source` I/O adapters and composes the pumps in `build()`). Per-route configuration (`qos` / `retain` / `timeout_ms` / …) is threaded from each link URL's query via `ConnectorConfig::from_query`. `std` now enables `aimdb-core/connector-session` (where the pump helpers live; `std` implies it transitively). No public API change. - **Outbound publisher survives a consumer lag (Embassy client, Issue #39).** A `BufferLagged` (SPMC-ring overflow) on the outbound reader now skips the gap and keeps publishing instead of terminating the publisher; only a closed buffer stops it. - **M17 — Embassy client rebuilt on core's pumps via the adapter spine ([Design 033](../docs/design/033-M17-unify-connectors-drop-send.md)).** The hand-rolled outbound publisher and inbound event-router loops are gone: the Embassy half now rides core's `pump_sink` / `pump_source` through the force-`Send` `EmbassySink` / `EmbassySource` bridges in `aimdb-embassy-adapter::connectors`, exactly like the Tokio half rides them — this crate contributes only the broker **manager task** (mountain-mqtt's `run`, force-`Send`ed once via `into_box_future`) and the `MqttSink` / `MqttSource` over its action/event channels. **No `unsafe`, no `SendFutureWrapper`** remain in this crate. Per-route `qos` / `retain` still arrive from each link URL's query (now via `ConnectorConfig::protocol_options`, parsed per publish). Note: per-message inbound routing logs moved from this crate's `defmt` calls into core's `pump_source` (`tracing` feature), so defmt-only MCU builds no longer log per-message routing failures. diff --git a/aimdb-mqtt-connector/src/embassy_client.rs b/aimdb-mqtt-connector/src/embassy_client.rs index 2bc9d7f..94ac4a9 100644 --- a/aimdb-mqtt-connector/src/embassy_client.rs +++ b/aimdb-mqtt-connector/src/embassy_client.rs @@ -364,19 +364,10 @@ fn setup_manager( where R: aimdb_executor::RuntimeAdapter + aimdb_embassy_adapter::EmbassyNetwork + 'static, { - let build_err = |_msg: &str| { + let build_err = |msg: &str| { #[cfg(feature = "defmt")] - defmt::error!("Failed to build MQTT connector: {}", _msg); - #[cfg(feature = "std")] - { - aimdb_core::DbError::RuntimeError { - message: format!("Failed to build MQTT connector: {}", _msg), - } - } - #[cfg(not(feature = "std"))] - { - aimdb_core::DbError::RuntimeError { _message: () } - } + defmt::error!("Failed to build MQTT connector: {}", msg); + aimdb_core::DbError::runtime_error(format!("Failed to build MQTT connector: {}", msg)) }; // Parse the broker URL (add a dummy topic if none, so parsing succeeds). diff --git a/aimdb-mqtt-connector/src/tokio_client.rs b/aimdb-mqtt-connector/src/tokio_client.rs index 9f5af65..171467b 100644 --- a/aimdb-mqtt-connector/src/tokio_client.rs +++ b/aimdb-mqtt-connector/src/tokio_client.rs @@ -112,17 +112,11 @@ impl ConnectorBuilder for MqttCo let (client, event_loop) = MqttConnectorImpl::build_internal(&self.broker_url, self.client_id.clone(), router) .await - .map_err(|_e| { - #[cfg(feature = "std")] - { - aimdb_core::DbError::RuntimeError { - message: format!("Failed to build MQTT connector: {}", _e), - } - } - #[cfg(not(feature = "std"))] - { - aimdb_core::DbError::RuntimeError { _message: () } - } + .map_err(|e| { + aimdb_core::DbError::runtime_error(format!( + "Failed to build MQTT connector: {}", + e + )) })?; let mut futures: Vec = Vec::new(); diff --git a/aimdb-tokio-adapter/CHANGELOG.md b/aimdb-tokio-adapter/CHANGELOG.md index 30688c7..b862de0 100644 --- a/aimdb-tokio-adapter/CHANGELOG.md +++ b/aimdb-tokio-adapter/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Removed (breaking) + +- **`TokioDatabase` type alias removed (Issue #132, design 034 Phase 1).** It aliased the dead `aimdb_core::Database` wrapper (also removed) and had no users in the workspace. Use `AimDb` via `AimDbBuilder`. + ### Added - **`TimeOps::unix_time()` implemented from the OS wall clock (Issue #120).** Returns `SystemTime::now()` since the Unix epoch as `(secs, subsec_nanos)`; `now()` stays monotonic for duration measurement. Supplies absolute timestamps to the runtime-neutral AimX server / remote-display paths. diff --git a/aimdb-tokio-adapter/src/lib.rs b/aimdb-tokio-adapter/src/lib.rs index 9259852..9911538 100644 --- a/aimdb-tokio-adapter/src/lib.rs +++ b/aimdb-tokio-adapter/src/lib.rs @@ -43,13 +43,6 @@ pub use error::TokioErrorSupport; #[cfg(feature = "tokio-runtime")] pub use runtime::TokioAdapter; -/// Type alias for Tokio database -/// -/// This provides a convenient type for working with databases on the Tokio runtime. -/// Most users should use `AimDbBuilder` directly to create databases. -#[cfg(feature = "tokio-runtime")] -pub type TokioDatabase = aimdb_core::Database; - // Generate extension trait for Tokio adapter using the macro aimdb_core::impl_record_registrar_ext! { TokioRecordRegistrarExt, diff --git a/aimdb-wasm-adapter/src/buffer.rs b/aimdb-wasm-adapter/src/buffer.rs index 61d6746..45d0110 100644 --- a/aimdb-wasm-adapter/src/buffer.rs +++ b/aimdb-wasm-adapter/src/buffer.rs @@ -232,7 +232,7 @@ impl BufferReader for WasmBufferReader { *read_seq = oldest_seq; return Err(DbError::BufferLagged { lag_count, - _buffer_name: (), + buffer_name: alloc::string::String::from("wasm ring"), }); }