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.
json-serializefeature +codecmodule (M16, Design 032). Newcrate::codecmodule withRemoteSerialize(capability trait, blanket-impl'd for everyserdeSerialize + DeserializeOwnedtype), the object-safeJsonCodec<T>storage trait, and the zero-sizedSerdeJsonCodec. All three are re-exported from the crate root. The feature isno_std + alloccompatible (serde_jsonruns onalloc), soRecordValue::as_json()now works on embedded targets, not juststd.stdenablesjson-serializetransitively, 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 returnsNone(correct for buffers with no canonical latest, e.g. broadcast/SPMC rings). AimXrecord.getandTypedRecord::latest()now route through it. Adapters implement it per buffer type — see the tokio/embassy adapter changelogs.
- AimX remote-access path is now spawn-free (Issue #114, Design 030). Every remaining
tokio::spawninaimdb-core/src/remote/was removed; the supervisor's accept loop and each connection handler now own their ownFuturesUnordered<BoxFuture>driven bytokio::select! { biased; }. Cancellation collapsed to one mechanism — dropping the future.- New
aimdb-core/src/remote/stream.rsexports apub(crate) stream_record_updateshelper that adapts a record'sJsonBufferReaderinto aStream<Item = serde_json::Value>viafutures_util::stream::unfold. No task, no channel — drop the stream to cancel. AimDb::subscribe_record_updatesdeleted. The method had no out-of-tree callers (the only caller was the AimX handler); replaced bystream_record_updatesabove.- Per-subscription
oneshot::Sender<()>cancel channels and theSubscriptionHandlestruct deleted.ConnectionState::subscriptionsis nowHashMap<String, Arc<tokio::sync::Notify>>;record.unsubscribecallsnotify_one(), waking the per-sub future immediately (even when parked onstream.next()). - The two-task chain per subscription (buffer-reader task + JSON-event forwarder task) collapsed into one
run_subscriptionfuture per subscription, held in the connection'sFuturesUnordered.
- New
-
latest_snapshotremoved fromTypedRecord;latest()/ AimXrecord.getread the buffer viapeek()(M15, Design 031). Eliminates one snapshot-mutex lock +Option<T>clone perproduce()on the hot path. Behavioural consequences:- A record configured with
.with_remote_access()but no buffer now failsbuild()with a clear error (previously a silent runtime no-op — reads returnednot_found, writes were discarded). Add a buffer, e.g..buffer(BufferCfg::SingleLatest). record.get/latest()on anSpmcRingrecord now returnsnot_found/None— a ring keeps per-consumer history with no canonical latest. Userecord.drain(history) orrecord.subscribe(live).SingleLatestandMailboxare unaffected.- On
no_std/embedded,latest()now depends on the adapter implementingpeek()(the Embassy adapter does — see its changelog).
- A record configured with
-
with_remote_access()is now gated onjson-serializeand bounded onT: 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-erasedArc<dyn JsonCodec<T>>.stdenablesjson-serialize, so std callers see no change;no_std + alloccallers must enable thejson-serializefeature to call it. -
producer_servicerenamed toproducer(M15).TypedRecord::set_producer_service→set_producer, andhas_producer_service→has_producer(the latter also on theAnyRecordtrait). Affects code that called these methods directly; the public.source()registrar API is unchanged. Also collapses the std/no_stdcfgsplit onAnyRecord::buffer_info/transform_input_keysinto single signatures. -
AimxConfiglostsubscription_queue_size(Issue #114, Design 030). The field bounded a per-subscription mpsc channel that no longer exists — subscriptions are now one future in aFuturesUnordered. 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_subscriptionsnow reports the real per-connection cap. Previously it returnedsubscription_queue_size(default 100) while the actual cap was implicit; it now returnsmax_subs_per_connection(default 32). Clients that displayed this value will see the change. -
AimX
record.subscriberesponse no longer carriesqueue_size. Result object is now{ "subscription_id": "..." }— the previous"queue_size"reported a number that no longer corresponded to anything in the implementation. -
AimxConfiggainsmax_subs_per_connection: usize(default 32) — the dedicated per-connection subscription cap. The existingmax_connections: usize(previously declared but unread) is now actually enforced by the supervisor; over-cap connections are refused by closing the acceptedUnixStreampre-handshake. -
Producer::produceis now sync + infallible;Consumer::subscribeis now infallible (Design 029 follow-up, M14). The pre-resolvedWriteHandle::pushcannot fail and the pre-resolved buffer Arc makessubscribe()infallible. Call sites collapse:producer.produce(x).await?→producer.produce(x);andlet Ok(reader) = consumer.subscribe() else { ... }→let reader = consumer.subscribe();. TheProducerTrait::produce_any/ConsumerTrait::subscribe_anytrait surfaces stayResult/asyncbecause the type-erasure downcast remains fallible.AimDb::produce<T>(key, value) -> DbResult<()>is now sync;.awaiton the call site goes away. Only the key lookup can fail.Database::producelikewise sync.TypedRecord::producewas made sync here (waspub async fn produce), then removed entirely in M15 — see Removed (breaking) below.aimdb-wasm-adapter:bindings::poll_synchelper deleted — no remaining callers now thatTypedRecord::produceis sync.- Dead
consumer.subscribe()error arms intransform/single.rsandtransform/join.rsremoved (theErrbranch was unreachable after M14).
-
Producer<T>/Consumer<T>drop the runtime parameterRand 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 (noHashMap<key>probe, noTypeIdcheck, no downcast), andsubscribe()collapses tobuffer.subscribe_boxed(). The internal mechanic is a new crate-privateWriteHandle<T>trait backed byRecordWriter<T>(inaimdb-core/src/buffer/writer.rs), pre-bound to the record'sArc<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) -> &stris removed. Capture the record key at the registration site instead.Producer::produce(value) -> ()andConsumer::subscribe() -> Box<dyn BufferReader<T> + Send>(v0.4 revision — see the sync/infallible bullet above for the rationale and migration). TheProducerTrait::produce_any/ConsumerTrait::subscribe_anytrait surfaces retainasync/Resultfor the type-erased downcast that can still fail.AimDb::producer<T>(key)/AimDb::consumer<T>(key)now returnDbResult<…>(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 asMissingConfigurationat build time (was a deferred subscribe-time error).TypedRecord::bufferfield isOption<Arc<dyn DynBuffer<T>>>(wasBox);TypedRecord::set_buffer(Box<…>)keeps its public signature and converts viaArc::from(box_)internally.TypedRecord::create_producer_trait(&self)no longer takesdb/record_key— it uses the newwriter_handle().ConnectorBuilder<R>cascade is zero-LOC: no connector struct carriedRafter M13. The outboundconsumer_factory/ inboundproducer_factorycallbacks now resolve the record once at link-startup time (viadb.inner().get_typed_record_by_key) and construct the new handles.- Codegen-emitted task scaffolds use
Producer<T>/Consumer<T>(no, TokioAdapter). data-contractslog_tapparameter isConsumer<T>.
-
Spawntrait 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 newAimDbRunner, then driven by a singleFuturesUnorderedfromrunner.run().await. No background work runs until the runner is polled.AimDb::spawn_taskis deleted. Migrate toon_start()(collected at build) or to a privateFuturesUnorderedinside your own future.- The
Runtimebundle no longer supertrait-requiresSpawn. Custom adapters dropimpl Spawn. R: Spawnbounds are gone everywhere inaimdb-core(Producer,Consumer,TypedRecord,TransformDescriptor,RecordRegistrar,RecordT,AnyRecordExt::as_typed, remote handler/supervisor,Database<A>) — replaced byR: RuntimeAdapter.RecordSpawner<T>renamed toRecordFutureCollector<T>; itsspawn_all_tasks→collect_all_futures. Internalspawn_consumer_tasks/spawn_producer_service/spawn_transform_taskonTypedRecordbecomecollect_consumer_futures/collect_producer_future/collect_transform_futures.- Join transforms now hoist their per-input forwarder construction to build time —
JoinPipeline::into_descriptor()returns aCollectedTransform { task_future, fanin_futures }and the lazyruntime.spawn(forwarder)insiderun_join_transformis gone. ConnectorBuilder::build()now returnsVec<BoxFuture<'static, ()>>instead ofArc<dyn Connector>(whichAimDbBuilderalready discarded).- Unsafe
impl Send/Syncblocks onProducer<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 baretokio::spawnunder#[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_startno_std bifurcation collapsed: a singleStartFnType<R>alias replaces the byte-identical std/no_std pair.
TypedRecord::produceremoved (M15, Design 031). The M14 step (above) made it sync; M15 removes it entirely. All writes now go throughWriteHandle::pushviaTypedRecord::writer_handle().AimDb::produceand AimXset_from_jsonroute through it; as a side effectset_from_jsonnow marks record metadata as updated (previously skipped on that path).WriteHandle/RecordWriterno longer carry the snapshot mutex.with_read_only_serialization()removed (M16, Design 032). ASerialize-only record can no longer be exposed read-only over remote access. Usewith_remote_access(), which additionally requiresDeserializeOwned. No in-tree callers existed.
1.1.0 - 2026-05-22
- 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 onno_std + alloctargets too.- New
profilingmodule exportingStageMetrics(atomiccall_count/total_time_ns/avg_time_ns/min_time_ns/max_time_nscounters),RecordProfilingMetricsper-record container, and serializableStageProfilingInfosnapshot. - Source-stage timing measures the interval between successive
Producer::produce()calls via a newProducerProfilingState. Tap- and link-stage timing wraps theBufferReaderreturned byConsumer::subscribe()in a newProfilingBufferReaderthat times the interval between successiverecv()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
StageKindenum (Source/Tap/Link/Transform);.transform()is stubbed for future instrumentation. RecordMetadatagains an optionalstage_profiling: Vec<StageProfilingInfo>field (feature-gated) attached automatically inTypedRecord::collect_metadata. New helperRecordMetadata::with_stage_profiling.AimDb::reset_stage_profiling()clears every record's counters. Newprofiling.resetAimX RPC method (write-permission gated) wired throughremote::handler.- New
RuntimeForProfilingmarker trait — blanket-implemented for everyRwhen the feature is off, requiresaimdb_executor::TimeOpswhen on. Surfaces onAimDbBuilder::run/buildandAimDb::build_with. Public API is unchanged when the feature is disabled. - New
Time::duration_as_nanosaccessor on the context (delegates toTimeOps). - Dependency:
portable-atomic(with thefallback+critical-sectionfeatures enabled by theprofilingfeature) for 64-bit-atomic emulation on targets without nativeAtomicU64(e.g.thumbv7em-none-eabihf).
- New
- 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 fromLinkFromBuilder::finish()(panic message includes the offending URL), with symmetric defense-in-depth checks added toTypedRecord::set_producer_service,set_transform, andadd_inbound_connector. Multiple.link_from()calls on the same record (fan-in) remain permitted. no_stdsupport for the full Transform API (Design 027):.transform()and.transform_join()are now available onno_std + alloctargets. Multi-input join fan-in is no longer hardcoded totokio::sync::mpsc; it uses the runtime-agnosticJoinFanInRuntimetraits fromaimdb-executor, implemented by Tokio, Embassy, and WASM adapters.JoinEventRx— type-erased trigger receiver passed to theon_triggershandler. Call.recv().awaitin a loop to consumeJoinTriggerevents from all input forwarders.transform_joinas an inherent method onRecordRegistrar(gatedfeature = "alloc",R: JoinFanInRuntime). Previously only exposed via theimpl_record_registrar_ext!macro underfeature = "std".- Context-Aware Deserializers (Design 026): Inbound connector deserializers can now receive a
RuntimeContext<R>for platform-independent timestamps and logging during deserialization- New
ContextDeserializerFntype alias for context-aware type-erased deserializer callbacks - New
DeserializerKindenum (Raw/Context) to enforce mutual exclusivity between plain and context-aware deserializers .with_deserializer(|ctx, bytes| ...)now accepts a context-aware closure receivingRuntimeContext<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
- New
- Context-Aware Serializers: Outbound connector serializers can now receive a
RuntimeContext<R>, symmetric with deserializers- New
ContextSerializerFntype alias for context-aware type-erased serializer callbacks - New
SerializerKindenum (Raw/Context) to enforce mutual exclusivity between plain and context-aware serializers .with_serializer(|ctx, value| ...)now accepts a context-aware closure receivingRuntimeContext<R>.with_serializer_raw(|value| ...)added for plain value-only serialization (no context needed)
- New
- Breaking — Join handler API redesign (Design 027 §Q4):
JoinBuilder::with_state(...).on_trigger(Fn(...) -> Pin<Box<dyn Future>>)replaced with task-modelJoinBuilder::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.awaitpoints. transform.rssplit intotransform/{mod,single,join}.rs— internal reorganization to keep thealloc-only join path separate from the runtime-agnostic single-input path.JoinBuilder,JoinPipeline,JoinTrigger,JoinEventRxare now re-exported fromtransform::join.transform_join_rawnow requiresR: JoinFanInRuntime(wasfeature = "std").ExecutorError::QueueClosedmapped toDbError::RuntimeErrorinFrom<ExecutorError>.- Breaking:
InboundConnectorLink::deserializerfield type changed fromDeserializerFntoDeserializerKind - Breaking:
InboundConnectorLink::new()now takesDeserializerKindinstead ofDeserializerFn - Breaking:
Router::route()signature changed to accept an additionalctxparameter - Breaking:
RouterBuilder::from_routes()andRouterBuilder::add_route()now takeDeserializerKindinstead ofDeserializerFn - Breaking:
ConnectorLink::serializerfield type changed fromOption<SerializerFn>toOption<SerializerKind> - Breaking:
.with_serializer()renamed to.with_serializer_raw()— old single-argument pattern - Breaking:
OutboundRoutetype alias updated to useSerializerKind - Breaking:
.with_deserializer()onInboundConnectorBuildernow expectsFn(RuntimeContext<R>, &[u8]) -> Result<T, String>instead ofFn(&[u8]) -> Result<T, String>— use.with_deserializer_raw()for the previous bytes-only signature AimDb::collect_inbound_routes()return type updated to useDeserializerKind
1.0.0 - 2026-03-11
ConnectorUrl::default_port()now handlesws://(→ 1883) andwss://(→ 8883) URL schemes for WebSocket connectorsConnectorUrl::is_secure()now includes thewssscheme- Documentation updated with WebSocket URL format examples
0.5.0 - 2026-02-21
- Transform API (Design 020): Reactive data transformations between records
- Single-Input Transforms:
transform_raw()method onRecordRegistrarfor 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 configurationJoinBuilder: Multi-input builder with.input::<T>(key)and.with_state().on_trigger()patternTransformDescriptor: Type-erased descriptor for storing transform configurationJoinTrigger: 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
- Single-Input Transforms:
- Graph Introspection API (Design 021): Dependency graph visualization and introspection
RecordOriginenum: Classifies record data sources (Source,Link,Transform,TransformJoin,Passive)GraphNodestruct: Node metadata including origin, buffer config, tap count, outbound statusGraphEdgestruct: Directed edge withfrom,to, and edge type classificationDependencyGraphstruct: Full graph with nodes, edges, and topological ordering- New
AnyRecordtrait methods:has_transform(),record_origin(),buffer_info(),transform_input_keys() RecordId::new()now acceptsRecordOriginparameter 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 returningOk(T),Err(BufferEmpty), orErr(BufferLagged)try_recv_json()on JsonBufferReader: JSON-serialized non-blocking receive for remote accessrecord.drainAimX 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 inext_macros.rsfor generating runtime adapter extension traits - Dynamic Topic/Destination Routing (Design 018): Complete support for dynamic topic resolution
- Outbound (
TopicProvidertrait): Dynamically determine MQTT topics or KNX group addresses based on data being published- New
TopicProvider<T>trait for type-safe topic determination - New
TopicProviderAnytrait for type-erased storage - New
TopicProviderWrapper<T, P>struct for type erasure - New
TopicProviderFntype alias for stored providers - New
topic_providerfield inConnectorLinkstruct - New
with_topic_provider()method onOutboundConnectorBuilder OutboundRoutetuple now includes optionalTopicProviderFn
- New
- Inbound (
TopicResolverFn): Late-binding topic resolution at connector startup- New
TopicResolverFntype alias for closure-based topic resolution - New
topic_resolverfield inInboundConnectorLinkstruct - New
with_topic_resolver()method onInboundConnectorBuilder - New
resolve_topic()method onInboundConnectorLink collect_inbound_routes()now resolves topics dynamically at route collection time
- New
- Enables runtime topic determination from smart contracts, service discovery, or configuration
- Works in both
stdandno_std + allocenvironments
- Outbound (
- Unit Tests: Added comprehensive tests for
TopicProviderandTopicResolverFn
- Renamed
.with_serialization()to.with_remote_access(): Clearer naming for JSON serialization configuration RecordIdconstructor: Now requiresRecordOriginparameter for dependency graph supportset_from_jsonprotection: Now also rejects writes on records with active transforms (in addition to sources)- Outbound Route Collection:
collect_outbound_routes()now returnsOutboundRoutetuples with optionalTopicProviderFn - Inbound Topic Resolution:
collect_inbound_routes()now callslink.resolve_topic()instead oflink.url.resource_id()directly, enabling dynamic topic resolution
0.4.0 - 2025-12-25
- RecordKey Trait (Issue #65):
RecordKeyis now a trait instead of a struct- Enables user-defined enum keys with
#[derive(RecordKey)]for compile-time safety as_str()method for string representationlink_address()method for connector metadata (MQTT topics, KNX addresses)Borrow<str>bound for O(1) HashMap lookups by string- Blanket implementation for
&'static str
- Enables user-defined enum keys with
- StringKey Type: New struct replacing old
RecordKeystructStatic(&'static str)variant for zero-allocation keysInterned(&'static str)variant usingBox::leakfor O(1) Copy/Clone- Implements
RecordKeytrait
- derive Feature: New feature flag enabling
#[derive(RecordKey)]macro viaaimdb-derive
- Breaking: RecordKey struct → trait: The
RecordKeystruct is now a trait. UseStringKeyfor string-based keys or define custom enum keys with#[derive(RecordKey)] - StringKey Memory Model: Dynamic keys now use
Box::leak(interning) instead ofArc<str>. This optimizes for O(1) cloning at the cost of never freeing dynamic key memory (acceptable for startup-time registration pattern)
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
- 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 accessStringKey: Hybrid&'static str/ interned withBorrow<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 keysubscribe::<T>(key): Subscribe to specific record by keyproducer::<T>(key): Get key-bound producerconsumer::<T>(key): Get key-bound consumer
- New Types:
Producer<T, R>andConsumer<T, R>for key-bound access with.key()accessor - Introspection Methods:
records_of_type::<T>(): Returns&[RecordId]for all records of type Tresolve_key(key): O(1) lookup returningOption<RecordId>
- New Error Variants:
RecordKeyNotFound: Key doesn't exist in registryInvalidRecordId: RecordId out of boundsTypeMismatch: Type assertion failed during downcastAmbiguousType: Multiple records of same type (use key-based API)DuplicateRecordKey: Key already registered
- RecordMetadata Extensions: Now includes
record_id: u32andrecord_key: Stringfields - Buffer Metrics API: New
BufferMetricstrait andBufferMetricsSnapshotstruct for buffer introspection (feature-gated behindmetrics)produced_count: Total items pushed to the bufferconsumed_count: Total items consumed across all readersdropped_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 toDynBuffertrait (returnsOption<BufferMetricsSnapshot>)
- Breaking: Legacy Type-Only Methods: Removed methods that accessed records by type alone:
get_typed_record<T>(): Useget_typed_record_by_key::<T>(key)insteadproduce<T>(value): Useproduce::<T>(key, value)insteadsubscribe<T>(): Usesubscribe::<T>(key)insteadproducer<T>(): Useproducer::<T>(key)insteadconsumer<T>(): Useconsumer::<T>(key)instead- This eliminates
AmbiguousTypeerrors - all record access now requires explicit keys
- 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 RecordIdHashMap<RecordKey, RecordId>for O(1) name lookupsHashMap<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:
ReadWritevariant now usesHashSet<String>for writable record keys - Dependencies: Added
hashbrownfor no_std-compatible HashMap withdefault-hasherfeature - Breaking: DynBuffer No Longer Has Blanket Impl: The blanket
impl<T, B: Buffer<T>> DynBuffer<T> for Bhas been removed. Each adapter now provides its own explicitDynBufferimplementation. This enables adapters to provide metrics support viametrics_snapshot(). CustomBuffer<T>implementations must now also implementDynBuffer<T>explicitly.
0.2.0 - 2025-11-20
- Bidirectional Connector Support: New
ConnectorBuildertrait enables connectors to support both outbound (AimDB → External) and inbound (External → AimDB) data flows - Type-Erased Router System: New
RouterandRouterBuilderinsrc/router.rsautomatically 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
ProducerTraitfor 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
ProducerFactoryFnandInboundConnectorLinktypes for creating producers dynamically at runtime - Consumer Trait for Outbound Routing: New
ConsumerTraitandAnyReadertraits insrc/connector.rsenable type-erased outbound message publishing, mirroringProducerTraitarchitecture - 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
ConsumerFactoryFntype alias and factory storage inOutboundConnectorLinkfor capturing types at configuration time - Type-Erased Consumer Adapter: Added
TypedAnyReaderinsrc/typed_api.rsimplementingAnyReadertrait forConsumer<T, R>
- 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 viadb.collect_inbound_routes() - Breaking: Sync Bound on Records: Added
Syncbound toAnyRecordtrait andTypedRecord<T, R>implementation. Record types must now beSend + Sync(previously onlySend). This enables safe concurrent access from multiple connector tasks in the bidirectional routing system. Types that wereSendbut notSyncmust be wrapped inArc<Mutex<_>>orArc<RwLock<_>>for interior mutability. - Breaking: Connector Naming Refactor: Renamed connector-related APIs to explicitly distinguish outbound (AimDB → External) from inbound (External → AimDB) flows:
TypedRecord::connectorsfield →outbound_connectorsTypedRecord::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_count→outbound_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:
OutboundConnectorLinknow storesconsumer_factory: Arc<ConsumerFactoryFn>instead of direct consumer - Changed: Connectors must now implement
spawn_outbound_publishers()and explicitly calldb.collect_outbound_routes() - Impact: All connectors require code changes to support outbound publishing via the new trait system
- Removed:
- Memory Management: Removed
async-traitdependency that leakedstdintono_stdbuilds, replaced with manualPin<Box<dyn Future>> - Type-Erased Routing: Fixed producer storage in collections by implementing
ProducerTraitwithBox<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.rsline 565 - changed from downcasting toArc<AimDb<R>>to downcasting toAimDb<R>then wrapping in Arc, resolving "Invalid db type in consumer factory" runtime panic
- 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 viaConsumerTraitandspawn_outbound_publishers()
0.1.0 - 2025-11-06
- 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_stdcompatibility for embedded targets- Error handling with comprehensive
DbResult<T>andDbErrortypes - 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