Skip to content

Latest commit

 

History

History
379 lines (314 loc) · 35.2 KB

File metadata and controls

379 lines (314 loc) · 35.2 KB

Changelog - aimdb-core

All notable changes to the aimdb-core crate will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

Added

  • json-serialize feature + codec module (M16, Design 032). New crate::codec module with RemoteSerialize (capability trait, blanket-impl'd for every serde Serialize + DeserializeOwned type), the object-safe JsonCodec<T> storage trait, and the zero-sized SerdeJsonCodec. All three are re-exported from the crate root. The feature is no_std + alloc compatible (serde_json runs on alloc), so RecordValue::as_json() now works on embedded targets, not just std. std enables json-serialize transitively, so existing std builds are unaffected.
  • DynBuffer::peek(&self) -> Option<T> (M15, Design 031). Non-destructive, buffer-native point-in-time read; the default impl returns None (correct for buffers with no canonical latest, e.g. broadcast/SPMC rings). AimX record.get and TypedRecord::latest() now route through it. Adapters implement it per buffer type — see the tokio/embassy adapter changelogs.

Internal refactors

  • AimX remote-access path is now spawn-free (Issue #114, Design 030). Every remaining tokio::spawn in aimdb-core/src/remote/ was removed; the supervisor's accept loop and each connection handler now own their own FuturesUnordered<BoxFuture> driven by tokio::select! { biased; }. Cancellation collapsed to one mechanism — dropping the future.
    • New aimdb-core/src/remote/stream.rs exports a pub(crate) stream_record_updates helper that adapts a record's JsonBufferReader into a Stream<Item = serde_json::Value> via futures_util::stream::unfold. No task, no channel — drop the stream to cancel.
    • AimDb::subscribe_record_updates deleted. The method had no out-of-tree callers (the only caller was the AimX handler); replaced by stream_record_updates above.
    • Per-subscription oneshot::Sender<()> cancel channels and the SubscriptionHandle struct deleted. ConnectionState::subscriptions is now HashMap<String, Arc<tokio::sync::Notify>>; record.unsubscribe calls notify_one(), waking the per-sub future immediately (even when parked on stream.next()).
    • The two-task chain per subscription (buffer-reader task + JSON-event forwarder task) collapsed into one run_subscription future per subscription, held in the connection's FuturesUnordered.

Changed (breaking)

  • latest_snapshot removed from TypedRecord; latest() / AimX record.get read the buffer via peek() (M15, Design 031). Eliminates one snapshot-mutex lock + Option<T> clone per produce() on the hot path. Behavioural consequences:

    • A record configured with .with_remote_access() but no buffer now fails build() with a clear error (previously a silent runtime no-op — reads returned not_found, writes were discarded). Add a buffer, e.g. .buffer(BufferCfg::SingleLatest).
    • record.get / latest() on an SpmcRing record now returns not_found / None — a ring keeps per-consumer history with no canonical latest. Use record.drain (history) or record.subscribe (live). SingleLatest and Mailbox are unaffected.
    • On no_std/embedded, latest() now depends on the adapter implementing peek() (the Embassy adapter does — see its changelog).
  • with_remote_access() is now gated on json-serialize and bounded on T: codec::RemoteSerialize (M16, Design 032). Same effective bound as before (Serialize + DeserializeOwned, blanket-impl'd), but the stored serializer/deserializer closures are replaced by a single type-erased Arc<dyn JsonCodec<T>>. std enables json-serialize, so std callers see no change; no_std + alloc callers must enable the json-serialize feature to call it.

  • producer_service renamed to producer (M15). TypedRecord::set_producer_serviceset_producer, and has_producer_servicehas_producer (the latter also on the AnyRecord trait). Affects code that called these methods directly; the public .source() registrar API is unchanged. Also collapses the std/no_std cfg split on AnyRecord::buffer_info / transform_input_keys into single signatures.

  • AimxConfig lost subscription_queue_size (Issue #114, Design 030). The field bounded a per-subscription mpsc channel that no longer exists — subscriptions are now one future in a FuturesUnordered. The builder method .subscription_queue_size(n) is removed; replace it with .max_subs_per_connection(n) if you were using the value as a soft cap on subscription count, or just delete the call.

  • AimX Welcome.max_subscriptions now reports the real per-connection cap. Previously it returned subscription_queue_size (default 100) while the actual cap was implicit; it now returns max_subs_per_connection (default 32). Clients that displayed this value will see the change.

  • AimX record.subscribe response no longer carries queue_size. Result object is now { "subscription_id": "..." } — the previous "queue_size" reported a number that no longer corresponded to anything in the implementation.

  • AimxConfig gains max_subs_per_connection: usize (default 32) — the dedicated per-connection subscription cap. The existing max_connections: usize (previously declared but unread) is now actually enforced by the supervisor; over-cap connections are refused by closing the accepted UnixStream pre-handshake.

  • Producer::produce is now sync + infallible; Consumer::subscribe is now infallible (Design 029 follow-up, M14). The pre-resolved WriteHandle::push cannot fail and the pre-resolved buffer Arc makes subscribe() infallible. Call sites collapse: producer.produce(x).await?producer.produce(x); and let Ok(reader) = consumer.subscribe() else { ... }let reader = consumer.subscribe();. The ProducerTrait::produce_any / ConsumerTrait::subscribe_any trait surfaces stay Result/async because the type-erasure downcast remains fallible.

    • AimDb::produce<T>(key, value) -> DbResult<()> is now sync; .await on the call site goes away. Only the key lookup can fail.
    • Database::produce likewise sync.
    • TypedRecord::produce was made sync here (was pub async fn produce), then removed entirely in M15 — see Removed (breaking) below.
    • aimdb-wasm-adapter: bindings::poll_sync helper deleted — no remaining callers now that TypedRecord::produce is sync.
    • Dead consumer.subscribe() error arms in transform/single.rs and transform/join.rs removed (the Err branch was unreachable after M14).
  • Producer<T> / Consumer<T> drop the runtime parameter R and pre-resolve the record at build time (Design 029, M14). Producer/Consumer become handles to a buffer rather than tickets to look one up: produce() is one virtual call (no HashMap<key> probe, no TypeId check, no downcast), and subscribe() collapses to buffer.subscribe_boxed(). The internal mechanic is a new crate-private WriteHandle<T> trait backed by RecordWriter<T> (in aimdb-core/src/buffer/writer.rs), pre-bound to the record's Arc<dyn DynBuffer<T>> + snapshot mutex + metadata tracker.

    • Producer<T, R>Producer<T>; Consumer<T, R>Consumer<T>. User code that names the two-parameter form must drop the trailing adapter arg.
    • Producer::key(&self) -> &str is removed. Capture the record key at the registration site instead.
    • Producer::produce(value) -> () and Consumer::subscribe() -> Box<dyn BufferReader<T> + Send> (v0.4 revision — see the sync/infallible bullet above for the rationale and migration). The ProducerTrait::produce_any / ConsumerTrait::subscribe_any trait surfaces retain async/Result for the type-erased downcast that can still fail.
    • AimDb::producer<T>(key) / AimDb::consumer<T>(key) now return DbResult<…> (was infallible). They resolve the typed record up front, so callers that previously assumed inference must add ?.
    • Consumer<T> cannot exist without a buffer: .tap() on a record with no .buffer(...) now surfaces as MissingConfiguration at build time (was a deferred subscribe-time error).
    • TypedRecord::buffer field is Option<Arc<dyn DynBuffer<T>>> (was Box); TypedRecord::set_buffer(Box<…>) keeps its public signature and converts via Arc::from(box_) internally.
    • TypedRecord::create_producer_trait(&self) no longer takes db / record_key — it uses the new writer_handle().
    • ConnectorBuilder<R> cascade is zero-LOC: no connector struct carried R after M13. The outbound consumer_factory / inbound producer_factory callbacks now resolve the record once at link-startup time (via db.inner().get_typed_record_by_key) and construct the new handles.
    • Codegen-emitted task scaffolds use Producer<T> / Consumer<T> (no , TokioAdapter).
    • data-contracts log_tap parameter is Consumer<T>.
  • Spawn trait removed; AimDbBuilder::build() now returns (AimDb<R>, AimDbRunner) (Issue #88, Design 028). Every future the database needs — .source()/.tap()/.transform() tasks, on_start hooks, connector loops, the remote-access supervisor — is collected at build time into the new AimDbRunner, then driven by a single FuturesUnordered from runner.run().await. No background work runs until the runner is polled.

    • AimDb::spawn_task is deleted. Migrate to on_start() (collected at build) or to a private FuturesUnordered inside your own future.
    • The Runtime bundle no longer supertrait-requires Spawn. Custom adapters drop impl Spawn.
    • R: Spawn bounds are gone everywhere in aimdb-core (Producer, Consumer, TypedRecord, TransformDescriptor, RecordRegistrar, RecordT, AnyRecordExt::as_typed, remote handler/supervisor, Database<A>) — replaced by R: RuntimeAdapter.
    • RecordSpawner<T> renamed to RecordFutureCollector<T>; its spawn_all_taskscollect_all_futures. Internal spawn_consumer_tasks/spawn_producer_service/spawn_transform_task on TypedRecord become collect_consumer_futures/collect_producer_future/collect_transform_futures.
    • Join transforms now hoist their per-input forwarder construction to build time — JoinPipeline::into_descriptor() returns a CollectedTransform { task_future, fanin_futures } and the lazy runtime.spawn(forwarder) inside run_join_transform is gone.
    • ConnectorBuilder::build() now returns Vec<BoxFuture<'static, ()>> instead of Arc<dyn Connector> (which AimDbBuilder already discarded).
    • Unsafe impl Send/Sync blocks on Producer<T, R> / Consumer<T, R> deleted — they auto-derive now.
    • On the AimX remote-access path, three runtime.spawn(...) call sites were temporarily bridged to bare tokio::spawn under #[cfg(feature = "std")]. These have since been removed by the AimX spawn-free follow-up — see the "AimX remote-access path is now spawn-free" entry above.
  • on_start no_std bifurcation collapsed: a single StartFnType<R> alias replaces the byte-identical std/no_std pair.

Removed (breaking)

  • 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.

1.1.0 - 2026-05-22

Added

  • Automatic stage profiling (Issue #58, RFC 014, feature profiling): AimDB now measures wall-clock time per .source(), .tap(), and .link() stage with no user instrumentation. Feature is off by default and adds zero overhead when disabled; alloc + a runtime clock is enough, so it works on no_std + alloc targets too.
    • New profiling module exporting StageMetrics (atomic call_count / total_time_ns / avg_time_ns / min_time_ns / max_time_ns counters), RecordProfilingMetrics per-record container, and serializable StageProfilingInfo snapshot.
    • Source-stage timing measures the interval between successive Producer::produce() calls via a new ProducerProfilingState. Tap- and link-stage timing wraps the BufferReader returned by Consumer::subscribe() in a new ProfilingBufferReader that times the interval between successive recv() yields. The whole-task closure shape of .source() / .tap() is preserved — no per-value handler changes.
    • RecordRegistrar::with_name("...") assigns a human-readable name to the most recently registered source/tap/link; surfaces in MCP output. Always callable — a no-op when the feature is disabled.
    • New StageKind enum (Source / Tap / Link / Transform); .transform() is stubbed for future instrumentation.
    • RecordMetadata gains an optional stage_profiling: Vec<StageProfilingInfo> field (feature-gated) attached automatically in TypedRecord::collect_metadata. New helper RecordMetadata::with_stage_profiling.
    • AimDb::reset_stage_profiling() clears every record's counters. New profiling.reset AimX RPC method (write-permission gated) wired through remote::handler.
    • New RuntimeForProfiling marker trait — blanket-implemented for every R when the feature is off, requires aimdb_executor::TimeOps when on. Surfaces on AimDbBuilder::run / build and AimDb::build_with. Public API is unchanged when the feature is disabled.
    • New Time::duration_as_nanos accessor on the context (delegates to TimeOps).
    • Dependency: portable-atomic (with the fallback + critical-section features enabled by the profiling feature) for 64-bit-atomic emulation on targets without native AtomicU64 (e.g. thumbv7em-none-eabihf).
  • Writer-exclusivity validation for .link_from() (Issue #89): .source(), .transform(), and .link_from() are now mutually exclusive on a single record — combining any two now panics at configuration time instead of silently racing on the buffer (last-writer-wins). The check fires from LinkFromBuilder::finish() (panic message includes the offending URL), with symmetric defense-in-depth checks added to TypedRecord::set_producer_service, set_transform, and add_inbound_connector. Multiple .link_from() calls on the same record (fan-in) remain permitted.
  • no_std support for the full Transform API (Design 027): .transform() and .transform_join() are now available on no_std + alloc targets. Multi-input join fan-in is no longer hardcoded to tokio::sync::mpsc; it uses the runtime-agnostic JoinFanInRuntime traits from aimdb-executor, implemented by Tokio, Embassy, and WASM adapters.
  • JoinEventRx — type-erased trigger receiver passed to the on_triggers handler. Call .recv().await in a loop to consume JoinTrigger events from all input forwarders.
  • transform_join as an inherent method on RecordRegistrar (gated feature = "alloc", R: JoinFanInRuntime). Previously only exposed via the impl_record_registrar_ext! macro under feature = "std".
  • Context-Aware Deserializers (Design 026): Inbound connector deserializers can now receive a RuntimeContext<R> for platform-independent timestamps and logging during deserialization
    • New ContextDeserializerFn type alias for context-aware type-erased deserializer callbacks
    • New DeserializerKind enum (Raw / Context) to enforce mutual exclusivity between plain and context-aware deserializers
    • .with_deserializer(|ctx, bytes| ...) now accepts a context-aware closure receiving RuntimeContext<R>
    • .with_deserializer_raw(|bytes| ...) added for plain bytes-only deserialization (no context needed)
    • Router::route() now accepts an optional type-erased runtime context (Option<&Arc<dyn Any + Send + Sync>>)
    • Context deserializer routes are gracefully skipped when no context is provided
  • Context-Aware Serializers: Outbound connector serializers can now receive a RuntimeContext<R>, symmetric with deserializers
    • New ContextSerializerFn type alias for context-aware type-erased serializer callbacks
    • New SerializerKind enum (Raw / Context) to enforce mutual exclusivity between plain and context-aware serializers
    • .with_serializer(|ctx, value| ...) now accepts a context-aware closure receiving RuntimeContext<R>
    • .with_serializer_raw(|value| ...) added for plain value-only serialization (no context needed)

Changed

  • Breaking — Join handler API redesign (Design 027 §Q4): JoinBuilder::with_state(...).on_trigger(Fn(...) -> Pin<Box<dyn Future>>) replaced with task-model JoinBuilder::on_triggers(FnOnce(JoinEventRx, Producer) -> impl Future). The handler now owns the event loop, eliminating per-event heap allocation and allowing state to be borrowed across .await points.
  • transform.rs split into transform/{mod,single,join}.rs — internal reorganization to keep the alloc-only join path separate from the runtime-agnostic single-input path. JoinBuilder, JoinPipeline, JoinTrigger, JoinEventRx are now re-exported from transform::join.
  • transform_join_raw now requires R: JoinFanInRuntime (was feature = "std").
  • ExecutorError::QueueClosed mapped to DbError::RuntimeError in From<ExecutorError>.
  • Breaking: InboundConnectorLink::deserializer field type changed from DeserializerFn to DeserializerKind
  • Breaking: InboundConnectorLink::new() now takes DeserializerKind instead of DeserializerFn
  • Breaking: Router::route() signature changed to accept an additional ctx parameter
  • Breaking: RouterBuilder::from_routes() and RouterBuilder::add_route() now take DeserializerKind instead of DeserializerFn
  • Breaking: ConnectorLink::serializer field type changed from Option<SerializerFn> to Option<SerializerKind>
  • Breaking: .with_serializer() renamed to .with_serializer_raw() — old single-argument pattern
  • Breaking: OutboundRoute type alias updated to use SerializerKind
  • Breaking: .with_deserializer() on InboundConnectorBuilder now expects Fn(RuntimeContext<R>, &[u8]) -> Result<T, String> instead of Fn(&[u8]) -> Result<T, String> — use .with_deserializer_raw() for the previous bytes-only signature
  • AimDb::collect_inbound_routes() return type updated to use DeserializerKind

1.0.0 - 2026-03-11

Added

  • ConnectorUrl::default_port() now handles ws:// (→ 1883) and wss:// (→ 8883) URL schemes for WebSocket connectors
  • ConnectorUrl::is_secure() now includes the wss scheme
  • Documentation updated with WebSocket URL format examples

0.5.0 - 2026-02-21

Added

  • Transform API (Design 020): Reactive data transformations between records
    • Single-Input Transforms: transform_raw() method on RecordRegistrar for creating reactive derivations
    • Multi-Input Joins: transform_join_raw() for combining multiple input records with stateful handlers
    • TransformBuilder: Fluent API with .with_state() and .map() for transform configuration
    • JoinBuilder: Multi-input builder with .input::<T>(key) and .with_state().on_trigger() pattern
    • TransformDescriptor: Type-erased descriptor for storing transform configuration
    • JoinTrigger: Event type for multi-input join handlers with index and type-erased value
    • Transforms are spawned as tasks during AimDb::build() and subscribe to input buffers
    • Mutual exclusion enforced: a record cannot have both .source() and .transform()
    • Full tracing integration for transform lifecycle events
  • Graph Introspection API (Design 021): Dependency graph visualization and introspection
    • RecordOrigin enum: Classifies record data sources (Source, Link, Transform, TransformJoin, Passive)
    • GraphNode struct: Node metadata including origin, buffer config, tap count, outbound status
    • GraphEdge struct: Directed edge with from, to, and edge type classification
    • DependencyGraph struct: Full graph with nodes, edges, and topological ordering
    • New AnyRecord trait methods: has_transform(), record_origin(), buffer_info(), transform_input_keys()
    • RecordId::new() now accepts RecordOrigin parameter for accurate metadata
    • Graph methods in AimDbInner: build_dependency_graph(), graph_nodes(), graph_edges(), graph_topo_order()
  • Record Drain API (Design 019): Non-blocking batch history access
    • try_recv() on BufferReader: Non-blocking receive returning Ok(T), Err(BufferEmpty), or Err(BufferLagged)
    • try_recv_json() on JsonBufferReader: JSON-serialized non-blocking receive for remote access
    • record.drain AimX protocol method: Drain accumulated values since last call with optional limit
    • Cold-start semantics: first drain creates reader and returns empty
    • Supports SpmcRing (full history), SingleLatest (at most 1), Mailbox (at most 1)
    • Handler maintains per-connection drain readers via ConnectionState
  • Extension Macro: impl_record_registrar_ext! macro in ext_macros.rs for generating runtime adapter extension traits
  • Dynamic Topic/Destination Routing (Design 018): Complete support for dynamic topic resolution
    • Outbound (TopicProvider trait): Dynamically determine MQTT topics or KNX group addresses based on data being published
      • New TopicProvider<T> trait for type-safe topic determination
      • New TopicProviderAny trait for type-erased storage
      • New TopicProviderWrapper<T, P> struct for type erasure
      • New TopicProviderFn type alias for stored providers
      • New topic_provider field in ConnectorLink struct
      • New with_topic_provider() method on OutboundConnectorBuilder
      • OutboundRoute tuple now includes optional TopicProviderFn
    • Inbound (TopicResolverFn): Late-binding topic resolution at connector startup
      • New TopicResolverFn type alias for closure-based topic resolution
      • New topic_resolver field in InboundConnectorLink struct
      • New with_topic_resolver() method on InboundConnectorBuilder
      • New resolve_topic() method on InboundConnectorLink
      • collect_inbound_routes() now resolves topics dynamically at route collection time
    • Enables runtime topic determination from smart contracts, service discovery, or configuration
    • Works in both std and no_std + alloc environments
  • Unit Tests: Added comprehensive tests for TopicProvider and TopicResolverFn

Changed

  • Renamed .with_serialization() to .with_remote_access(): Clearer naming for JSON serialization configuration
  • RecordId constructor: Now requires RecordOrigin parameter for dependency graph support
  • set_from_json protection: Now also rejects writes on records with active transforms (in addition to sources)
  • Outbound Route Collection: collect_outbound_routes() now returns OutboundRoute tuples with optional TopicProviderFn
  • Inbound Topic Resolution: collect_inbound_routes() now calls link.resolve_topic() instead of link.url.resource_id() directly, enabling dynamic topic resolution

0.4.0 - 2025-12-25

Added

  • RecordKey Trait (Issue #65): RecordKey is now a trait instead of a struct
    • Enables user-defined enum keys with #[derive(RecordKey)] for compile-time safety
    • as_str() method for string representation
    • link_address() method for connector metadata (MQTT topics, KNX addresses)
    • Borrow<str> bound for O(1) HashMap lookups by string
    • Blanket implementation for &'static str
  • StringKey Type: New struct replacing old RecordKey struct
    • Static(&'static str) variant for zero-allocation keys
    • Interned(&'static str) variant using Box::leak for O(1) Copy/Clone
    • Implements RecordKey trait
  • derive Feature: New feature flag enabling #[derive(RecordKey)] macro via aimdb-derive

Changed

  • Breaking: RecordKey struct → trait: The RecordKey struct is now a trait. Use StringKey for string-based keys or define custom enum keys with #[derive(RecordKey)]
  • StringKey Memory Model: Dynamic keys now use Box::leak (interning) instead of Arc<str>. This optimizes for O(1) cloning at the cost of never freeing dynamic key memory (acceptable for startup-time registration pattern)

Migration Guide

RecordKey is now a trait (breaking change)

If you were using RecordKey directly, switch to StringKey:

// Before (this PR)
use aimdb_core::RecordKey;
let key: RecordKey = "sensors.temp".into();

// After
use aimdb_core::StringKey;
let key: StringKey = "sensors.temp".into();

For compile-time safe keys (recommended for embedded)

Use the new derive macro:

use aimdb_core::RecordKey;  // Now a trait, re-exported from aimdb-derive

#[derive(RecordKey, Clone, Copy, PartialEq, Eq)]
pub enum AppKey {
    #[key = "temp.indoor"]
    TempIndoor,
    #[key = "temp.outdoor"]
    TempOutdoor,
}

// Compile-time typo detection!
let producer = db.producer::<Temperature>(AppKey::TempIndoor);

Note on StringKey memory model

StringKey::intern() leaks memory intentionally for O(1) Copy/Clone. This is designed for startup-time registration (<1000 keys). In debug builds, a warning fires if you exceed 1000 interned keys.

0.3.0 - 2025-12-15

Added

  • RecordId + RecordKey Architecture (Issue #60): Complete rewrite of internal storage for stable record identification
    • RecordId: u32 index wrapper for O(1) Vec-based hot-path access
    • StringKey: Hybrid &'static str / interned with Borrow<str> for zero-alloc static keys and flexible dynamic keys
    • O(1) key resolution via HashMap<RecordKey, RecordId>
    • Type introspection via HashMap<TypeId, Vec<RecordId>>
  • Key-Based Producer/Consumer API:
    • produce::<T>(key, value): Produce to specific record by key
    • subscribe::<T>(key): Subscribe to specific record by key
    • producer::<T>(key): Get key-bound producer
    • consumer::<T>(key): Get key-bound consumer
  • New Types: Producer<T, R> and Consumer<T, R> for key-bound access with .key() accessor
  • Introspection Methods:
    • records_of_type::<T>(): Returns &[RecordId] for all records of type T
    • resolve_key(key): O(1) lookup returning Option<RecordId>
  • New Error Variants:
    • RecordKeyNotFound: Key doesn't exist in registry
    • InvalidRecordId: RecordId out of bounds
    • TypeMismatch: Type assertion failed during downcast
    • AmbiguousType: Multiple records of same type (use key-based API)
    • DuplicateRecordKey: Key already registered
  • RecordMetadata Extensions: Now includes record_id: u32 and record_key: String fields
  • Buffer Metrics API: New BufferMetrics trait and BufferMetricsSnapshot struct for buffer introspection (feature-gated behind metrics)
    • produced_count: Total items pushed to the buffer
    • consumed_count: Total items consumed across all readers
    • dropped_count: Total items dropped due to lag (documents per-reader semantics)
    • occupancy: Current buffer fill level as (current, capacity) tuple
  • DynBuffer Metrics Method: Added metrics_snapshot() method to DynBuffer trait (returns Option<BufferMetricsSnapshot>)

Removed

  • Breaking: Legacy Type-Only Methods: Removed methods that accessed records by type alone:
    • get_typed_record<T>(): Use get_typed_record_by_key::<T>(key) instead
    • produce<T>(value): Use produce::<T>(key, value) instead
    • subscribe<T>(): Use subscribe::<T>(key) instead
    • producer<T>(): Use producer::<T>(key) instead
    • consumer<T>(): Use consumer::<T>(key) instead
    • This eliminates AmbiguousType errors - all record access now requires explicit keys

Changed

  • Breaking: configure<T>() Signature: Now requires key parameter: configure::<T>("key", |reg| ...)
  • Breaking: Internal Storage: Changed from BTreeMap<TypeId, Box<dyn AnyRecord>> to:
    • Vec<Box<dyn AnyRecord>> for O(1) hot-path access by RecordId
    • HashMap<RecordKey, RecordId> for O(1) name lookups
    • HashMap<TypeId, Vec<RecordId>> for type introspection
  • Breaking: Key-Based API Only: All producer/consumer methods now require a record key parameter. This properly supports multi-instance records (e.g., multiple temperature sensors with the same type)
  • SecurityPolicy: ReadWrite variant now uses HashSet<String> for writable record keys
  • Dependencies: Added hashbrown for no_std-compatible HashMap with default-hasher feature
  • Breaking: DynBuffer No Longer Has Blanket Impl: The blanket impl<T, B: Buffer<T>> DynBuffer<T> for B has been removed. Each adapter now provides its own explicit DynBuffer implementation. This enables adapters to provide metrics support via metrics_snapshot(). Custom Buffer<T> implementations must now also implement DynBuffer<T> explicitly.

0.2.0 - 2025-11-20

Added

  • Bidirectional Connector Support: New ConnectorBuilder trait enables connectors to support both outbound (AimDB → External) and inbound (External → AimDB) data flows
  • Type-Erased Router System: New Router and RouterBuilder in src/router.rs automatically route incoming messages to correct typed producers without manual dispatch
  • Inbound Connector API: New .link_from() method for configuring inbound connections (External → AimDB) with deserializer callbacks
  • Outbound Connector API: New .link_to() method for configuring outbound connections (AimDB → External), replacing generic .link()
  • Producer Trait: New ProducerTrait for type-erased producer calls, enabling dynamic routing of messages to different record types
  • Resource ID Extraction: Added ConnectorUrl::resource_id() method to extract protocol-specific resource identifiers (topics, keys, paths)
  • Producer Factory Pattern: New ProducerFactoryFn and InboundConnectorLink types for creating producers dynamically at runtime
  • Consumer Trait for Outbound Routing: New ConsumerTrait and AnyReader traits in src/connector.rs enable type-erased outbound message publishing, mirroring ProducerTrait architecture
  • Outbound Route Collection: Added AimDb::collect_outbound_routes() method to gather all configured outbound connectors with their type-erased consumers, serializers, and configs
  • Consumer Factory Pattern: New ConsumerFactoryFn type alias and factory storage in OutboundConnectorLink for capturing types at configuration time
  • Type-Erased Consumer Adapter: Added TypedAnyReader in src/typed_api.rs implementing AnyReader trait for Consumer<T, R>

Changed

  • Breaking: Connector Registration: Changed from .with_connector(scheme, instance) to .with_connector(builder) - connectors now registered as builders
  • Breaking: Async Build: AimDbBuilder::build() is now async to support connector initialization during database construction
  • Breaking: ConnectorBuilder Trait: Updated build() signature to take &AimDb<R>, enabling route collection via db.collect_inbound_routes()
  • Breaking: Sync Bound on Records: Added Sync bound to AnyRecord trait and TypedRecord<T, R> implementation. Record types must now be Send + Sync (previously only Send). This enables safe concurrent access from multiple connector tasks in the bidirectional routing system. Types that were Send but not Sync must be wrapped in Arc<Mutex<_>> or Arc<RwLock<_>> for interior mutability.
  • Breaking: Connector Naming Refactor: Renamed connector-related APIs to explicitly distinguish outbound (AimDB → External) from inbound (External → AimDB) flows:
    • TypedRecord::connectors field → outbound_connectors
    • TypedRecord::add_connector()add_outbound_connector()
    • TypedRecord::connectors()outbound_connectors()
    • TypedRecord::connector_count()outbound_connector_count()
    • AnyRecord::connector_count()outbound_connector_count()
    • AnyRecord::connector_urls()outbound_connector_urls()
    • AnyRecord::connectors()outbound_connectors()
    • RecordMetadata::connector_countoutbound_connector_count
    • Inbound methods (inbound_connectors(), add_inbound_connector()) remain unchanged for clarity
  • Deprecated .link(): Generic .link() method deprecated in favor of explicit .link_to() and .link_from()
  • Builder API: Enhanced builder to validate buffer requirements for inbound connectors at configuration time
  • Breaking: Outbound Consumer Architecture: Refactored outbound connector system to use trait-based type erasure:
    • Removed: TypedRecord::spawn_outbound_consumers() method (automatic spawning)
    • Changed: OutboundConnectorLink now stores consumer_factory: Arc<ConsumerFactoryFn> instead of direct consumer
    • Changed: Connectors must now implement spawn_outbound_publishers() and explicitly call db.collect_outbound_routes()
    • Impact: All connectors require code changes to support outbound publishing via the new trait system

Fixed

  • Memory Management: Removed async-trait dependency that leaked std into no_std builds, replaced with manual Pin<Box<dyn Future>>
  • Type-Erased Routing: Fixed producer storage in collections by implementing ProducerTrait with Box<dyn Any> downcasting
  • Buffer Validation: Added validation ensuring inbound connectors have configured buffers before link creation
  • Consumer Factory Downcasting: Fixed type downcasting in typed_api.rs line 565 - changed from downcasting to Arc<AimDb<R>> to downcasting to AimDb<R> then wrapping in Arc, resolving "Invalid db type in consumer factory" runtime panic

Removed

  • Channel Stream: Removed obsolete channel-based stream abstraction in favor of router-based routing
  • Automatic Outbound Spawning: Removed TypedRecord::spawn_outbound_consumers() method - outbound publisher spawning now explicit via ConsumerTrait and spawn_outbound_publishers()

0.1.0 - 2025-11-06

Added

  • Initial release of AimDB async in-memory database engine
  • Type-safe record system using TypeId-based routing
  • Three buffer types for different data flow patterns:
    • SPMC Ring Buffer: High-frequency data streams with bounded memory
    • SingleLatest: State synchronization and configuration updates
    • Mailbox: Commands and one-shot events
  • Producer-consumer model with async task spawning
  • Runtime adapter abstraction for cross-platform support
  • no_std compatibility for embedded targets
  • Error handling with comprehensive DbResult<T> and DbError types
  • Remote access protocol (AimX v1) for cross-process introspection
  • Connector abstraction for external system integration
  • Builder pattern API for database configuration
  • Record lifecycle management with type-safe APIs