aimdb_core/builder.rs
1//! Database builder with type-safe record registration
2//!
3//! Provides `AimDb` and `AimDbBuilder` for constructing databases with
4//! type-safe, self-registering records using the producer-consumer pattern.
5
6use core::any::TypeId;
7use core::fmt::Debug;
8
9use alloc::{
10 boxed::Box,
11 string::{String, ToString},
12 sync::Arc,
13 vec::Vec,
14};
15use hashbrown::HashMap;
16
17use crate::extensions::Extensions;
18use crate::graph::DependencyGraph;
19
20/// Shorthand for a heap-pinned, `Send`, `'static` future — the unit of work
21/// the `AimDbRunner` drives. Canonical definition lives in `crate::executor`.
22pub type BoxFuture = crate::executor::BoxFuture;
23
24/// `on_start` task stored in `AimDbBuilder::start_fns`, invoked at `build()`.
25type StartFnType = Box<dyn FnOnce(crate::RuntimeContext) -> BoxFuture + Send>;
26
27/// Per-record future collector stored in `AimDbBuilder::spawn_fns`.
28///
29/// At `build()` time each is invoked in topological order; the returned
30/// `Vec<BoxFuture>` is appended to the runner's accumulator.
31type SpawnFnType = Box<dyn FnOnce(&Arc<AimDb>, RecordId) -> DbResult<Vec<BoxFuture>> + Send>;
32use crate::record_id::{RecordId, RecordKey, StringKey};
33use crate::typed_api::RecordRegistrar;
34use crate::typed_record::{AnyRecord, AnyRecordExt, RecordFutureCollector, TypedRecord};
35use crate::{DbError, DbResult};
36
37/// One outbound route returned by [`AimDb::collect_outbound_routes`]
38pub struct OutboundRoute {
39 /// Default topic/destination from the URL path; used when the source
40 /// yields no per-value destination.
41 pub topic: String,
42 /// Fused wire-level source: its readers yield destination + serialized
43 /// payload directly (subscribe → recv → resolve topic → serialize, all
44 /// typed inside — no `Box<dyn Any>` per message).
45 pub source: Box<dyn crate::connector::SerializedSource>,
46 /// Configuration options from the URL query
47 pub config: Vec<(String, String)>,
48}
49
50/// One registered record: its key, concrete type, and type-erased storage.
51struct RecordEntry {
52 key: StringKey,
53 type_id: TypeId,
54 record: Box<dyn AnyRecord>,
55}
56
57/// Internal database state
58///
59/// The registry is immutable after `build()` and holds tens of records, so
60/// it needs exactly two structures: the entries in registration order
61/// (indexed by `RecordId`) and a key → id map for name resolution. The hot
62/// path (produce/consume) uses pre-resolved handles and never touches the
63/// registry; everything else here is control-plane/introspection.
64pub struct AimDbInner {
65 /// Record entries, indexed by `RecordId`. Order matches registration
66 /// order. Immutable after build().
67 storages: Vec<RecordEntry>,
68
69 /// Name → RecordId lookup (control plane)
70 ///
71 /// Used by remote access, CLI, MCP for name resolution.
72 by_key: HashMap<StringKey, RecordId>,
73
74 /// Dependency graph (immutable after build)
75 ///
76 /// Contains all record nodes, edges, and topological order.
77 /// Used for introspection, spawn ordering, and graph queries.
78 dependency_graph: crate::graph::DependencyGraph,
79
80 /// Generic extension storage (frozen after build)
81 ///
82 /// External crates (e.g. `aimdb-persistence`) store typed state here
83 /// during builder configuration and retrieve it at query time via
84 /// `AimDb::extensions()`.
85 pub(crate) extensions: Extensions,
86}
87
88impl AimDbInner {
89 /// Resolve RecordKey to RecordId (control plane - O(1) average)
90 #[inline]
91 pub fn resolve<K: RecordKey>(&self, key: &K) -> Option<RecordId> {
92 self.by_key.get(key.as_str()).copied()
93 }
94
95 /// Resolve string to RecordId (convenience for remote access)
96 ///
97 /// O(1) average thanks to `Borrow<str>` implementation on `RecordKey`.
98 #[inline]
99 pub fn resolve_str(&self, name: &str) -> Option<RecordId> {
100 self.by_key.get(name).copied()
101 }
102
103 /// Get storage by RecordId (hot path - O(1))
104 #[inline]
105 pub fn storage(&self, id: RecordId) -> Option<&dyn AnyRecord> {
106 self.storages.get(id.index()).map(|e| e.record.as_ref())
107 }
108
109 /// Get the StringKey for a given RecordId
110 #[inline]
111 pub fn key_for(&self, id: RecordId) -> Option<&StringKey> {
112 self.storages.get(id.index()).map(|e| &e.key)
113 }
114
115 /// Get all RecordIds for a type (introspection)
116 ///
117 /// Linear scan — the registry is immutable after `build()`, holds tens of
118 /// records, and this is called from introspection paths only.
119 pub fn records_of_type<T: 'static>(&self) -> Vec<RecordId> {
120 let wanted = TypeId::of::<T>();
121 self.storages
122 .iter()
123 .enumerate()
124 .filter(|(_, e)| e.type_id == wanted)
125 .map(|(i, _)| RecordId::new(i as u32))
126 .collect()
127 }
128
129 /// Get the number of registered records
130 #[inline]
131 pub fn record_count(&self) -> usize {
132 self.storages.len()
133 }
134
135 /// Get the dependency graph (immutable reference)
136 ///
137 /// The graph is constructed once during `build()` and never changes.
138 /// Contains all record nodes, edges, and topological ordering.
139 #[inline]
140 pub fn dependency_graph(&self) -> &crate::graph::DependencyGraph {
141 &self.dependency_graph
142 }
143
144 /// Helper to get a typed record by RecordKey
145 ///
146 /// This encapsulates the common pattern of:
147 /// 1. Resolving key to RecordId
148 /// 2. Validating TypeId matches
149 /// 3. Downcasting to the typed record
150 pub fn get_typed_record_by_key<T>(&self, key: impl AsRef<str>) -> DbResult<&TypedRecord<T>>
151 where
152 T: Send + 'static + Debug + Clone,
153 {
154 let key_str = key.as_ref();
155
156 // Resolve key to RecordId
157 let id = self
158 .resolve_str(key_str)
159 .ok_or_else(|| DbError::record_key_not_found(key_str))?;
160
161 self.get_typed_record_by_id::<T>(id)
162 }
163
164 /// Helper to get a typed record by RecordId with type validation
165 pub fn get_typed_record_by_id<T>(&self, id: RecordId) -> DbResult<&TypedRecord<T>>
166 where
167 T: Send + 'static + Debug + Clone,
168 {
169 use crate::typed_record::AnyRecordExt;
170
171 let entry = self
172 .storages
173 .get(id.index())
174 .ok_or(DbError::InvalidRecordId { id: id.raw() })?;
175
176 // The downcast is the type check; keep the descriptive error on failure.
177 entry
178 .record
179 .as_typed::<T>()
180 .ok_or_else(|| DbError::TypeMismatch {
181 record_id: id.raw(),
182 expected_type: core::any::type_name::<T>().to_string(),
183 })
184 }
185
186 /// Collects metadata for all registered records
187 ///
188 /// Returns a vector of `RecordMetadata` for remote introspection.
189 /// Available only when the `remote` feature is enabled.
190 #[cfg(feature = "remote")]
191 pub fn list_records(&self) -> Vec<crate::remote::RecordMetadata> {
192 self.storages
193 .iter()
194 .enumerate()
195 .map(|(i, e)| {
196 let id = RecordId::new(i as u32);
197 e.record.collect_metadata(e.type_id, e.key, id)
198 })
199 .collect()
200 }
201
202 /// Try to get record's latest value as JSON by record key
203 ///
204 /// O(1) lookup using the key-based index.
205 ///
206 /// # Arguments
207 /// * `record_key` - The record key (e.g., "sensors.temperature")
208 ///
209 /// # Returns
210 /// `Some(JsonValue)` with the current record value, or `None`
211 #[cfg(feature = "remote")]
212 pub fn try_latest_as_json(&self, record_key: &str) -> Option<serde_json::Value> {
213 let id = self.resolve_str(record_key)?;
214 self.storages
215 .get(id.index())?
216 .record
217 .json_access()?
218 .latest_json()
219 }
220
221 /// Try to get a record's latest value as owned JSON bytes.
222 ///
223 /// This is the byte-oriented sibling of
224 /// [`try_latest_as_json`](Self::try_latest_as_json). The built-in Serde
225 /// codec serializes the typed record directly; compatibility codecs may
226 /// inherit a `Value`-based fallback.
227 #[cfg(feature = "remote")]
228 pub fn try_latest_as_json_bytes(&self, record_key: &str) -> Option<Vec<u8>> {
229 let id = self.resolve_str(record_key)?;
230 self.storages
231 .get(id.index())?
232 .record
233 .json_access()?
234 .latest_json_bytes()
235 }
236
237 /// Sets a record value from JSON (remote access API)
238 ///
239 /// Deserializes the JSON value and writes it to the record's buffer.
240 ///
241 /// **SAFETY:** Enforces the "No Producer Override" rule:
242 /// - Only works for records with `producer_count == 0`
243 /// - Returns error if the record has active producers
244 ///
245 /// # Arguments
246 /// * `record_key` - The record key (e.g., "config.app")
247 /// * `json_value` - JSON representation of the value
248 ///
249 /// # Returns
250 /// - `Ok(())` - Successfully set the value
251 /// - `Err(DbError)` - If record not found, has producers, or deserialization fails
252 #[cfg(feature = "remote")]
253 pub fn set_record_from_json(
254 &self,
255 record_key: &str,
256 json_value: serde_json::Value,
257 ) -> DbResult<()> {
258 let id = self
259 .resolve_str(record_key)
260 .ok_or_else(|| DbError::record_key_not_found(record_key.to_string()))?;
261
262 self.storages[id.index()]
263 .record
264 .json_access()
265 .ok_or_else(|| {
266 DbError::runtime_error(alloc::format!(
267 "Record '{record_key}' does not support JSON remote access"
268 ))
269 })?
270 .set_from_json(json_value)
271 }
272}
273
274/// Database builder for producer-consumer pattern
275///
276/// Provides a fluent API for constructing databases with type-safe record registration.
277/// Set the runtime via `.runtime()`; a missing runtime is reported by `build()`
278/// like any other configuration mistake.
279pub struct AimDbBuilder {
280 /// Registered records with their keys (order matters for RecordId assignment)
281 records: Vec<(StringKey, TypeId, Box<dyn AnyRecord>)>,
282
283 /// Key → index into `records`, so repeated `configure()` calls resolve a
284 /// key without scanning the vec.
285 record_index: HashMap<StringKey, usize>,
286
287 /// Runtime capabilities, held as a value
288 runtime: Option<Arc<dyn crate::executor::RuntimeOps>>,
289
290 /// Connector builders that will be invoked during build()
291 connector_builders: Vec<Box<dyn crate::connector::ConnectorBuilder>>,
292
293 /// Per-record future collectors with their keys.
294 spawn_fns: Vec<(StringKey, SpawnFnType)>,
295
296 /// Startup tasks registered via on_start() — spawned after build() completes.
297 start_fns: Vec<StartFnType>,
298
299 /// Generic extension storage for external crates (e.g., persistence, metrics).
300 /// Moved into AimDbInner during build() so it can be read on the live AimDb handle.
301 extensions: Extensions,
302
303 /// Builder-level configuration mistakes (e.g. re-registering a key with a
304 /// different type), recorded instead of panicking and reported — together
305 /// with the per-record errors — by `build()` via
306 /// [`DbError::InvalidConfiguration`].
307 config_errors: Vec<crate::error::ConfigError>,
308}
309
310impl AimDbBuilder {
311 /// Creates a new database builder without a runtime
312 ///
313 /// Call `.runtime()` to set the runtime adapter.
314 pub fn new() -> Self {
315 Self {
316 records: Vec::new(),
317 record_index: HashMap::new(),
318 runtime: None,
319 connector_builders: Vec::new(),
320 spawn_fns: Vec::new(),
321 start_fns: Vec::new(),
322 extensions: Extensions::new(),
323 config_errors: Vec::new(),
324 }
325 }
326
327 /// Sets the runtime adapter.
328 ///
329 /// Accepts any adapter implementing the dyn-safe
330 /// [`RuntimeOps`](crate::executor::RuntimeOps) capability surface
331 /// (`TokioAdapter`, `EmbassyAdapter`, `WasmAdapter`); the builder stores it
332 /// as a value — no runtime type parameter.
333 pub fn runtime(mut self, rt: Arc<impl crate::executor::RuntimeOps + 'static>) -> Self {
334 self.runtime = Some(rt);
335 self
336 }
337 /// Returns a shared reference to the extension storage.
338 ///
339 /// External crates use this to retrieve state stored via `extensions_mut()`.
340 pub fn extensions(&self) -> &Extensions {
341 &self.extensions
342 }
343
344 /// Returns a mutable reference to the extension storage.
345 ///
346 /// Use this inside `AimDbBuilderPersistExt` (or similar) to store typed state
347 /// that `.persist()` and `AimDbQueryExt` will later retrieve.
348 pub fn extensions_mut(&mut self) -> &mut Extensions {
349 &mut self.extensions
350 }
351
352 /// Registers a task to be spawned after `build()` completes.
353 ///
354 /// The closure receives the [`RuntimeContext`](crate::RuntimeContext) and
355 /// must return a future that runs for as long as needed (e.g. an infinite
356 /// cleanup loop). Tasks are spawned in registration order, after all
357 /// record tasks and connectors have been started.
358 pub fn on_start<F, Fut>(&mut self, f: F) -> &mut Self
359 where
360 F: FnOnce(crate::RuntimeContext) -> Fut + Send + 'static,
361 Fut: core::future::Future<Output = ()> + Send + 'static,
362 {
363 self.start_fns.push(Box::new(move |ctx| Box::pin(f(ctx))));
364 self
365 }
366
367 /// Registers a connector builder, invoked during [`build`](Self::build).
368 ///
369 /// This single entry point registers **two kinds** of connector — both
370 /// implement [`ConnectorBuilder`](crate::connector::ConnectorBuilder) and are
371 /// driven the same way:
372 ///
373 /// 1. **Data-plane links** (MQTT / KNX / WebSocket): a record opts in with
374 /// `link_to("<scheme>://<topic>")` / `link_from(...)`, and the connector
375 /// mirrors that record to/from the external topic. The connector's
376 /// [`scheme`](crate::connector::ConnectorBuilder::scheme) is what those
377 /// links match against.
378 /// 2. **Remote-access session connectors** (UDS / serial / TCP): these expose
379 /// AimDB itself over a transport so peers can introspect/subscribe/write.
380 /// - The **client** half (e.g. `UdsClient`) dials a peer and *does* use
381 /// `link_to`/`link_from` under its scheme — just like (1), the scheme is
382 /// `"uds"` by default instead of `"mqtt"`.
383 /// - The **server** half (e.g. `UdsServer`) *accepts* connections and takes
384 /// **no links** — registering it is how a server stands up remote access
385 /// (this replaces the old `with_remote_access(config)`).
386 ///
387 /// # Examples
388 ///
389 /// Illustrative (not compiled: the connector types live in downstream
390 /// crates `aimdb-core` cannot depend on):
391 ///
392 /// ```rust,ignore
393 /// // (1) data-plane link to an MQTT topic
394 /// let mut b = AimDbBuilder::new().runtime(rt)
395 /// .with_connector(MqttConnector::new("mqtt://broker.local:1883"));
396 /// b.configure::<Temperature>("commands.temp", |r| {
397 /// r.link_from("mqtt://commands/temp").with_deserializer(parse).finish();
398 /// });
399 /// b.build().await?;
400 ///
401 /// // (2a) remote-access SERVER — no links, just expose this db over UDS
402 /// AimDbBuilder::new().runtime(rt)
403 /// .with_connector(UdsServer::from_config(remote_config))
404 /// .build().await?;
405 ///
406 /// // (2b) remote-access CLIENT — mirror a record to a peer over UDS
407 /// let mut b = AimDbBuilder::new().runtime(rt)
408 /// .with_connector(UdsClient::new("/run/aimdb.sock"));
409 /// b.configure::<Temp>("temp", |r| { r.with_remote_access().link_to("uds://temp"); });
410 /// b.build().await?;
411 /// ```
412 pub fn with_connector(
413 mut self,
414 builder: impl crate::connector::ConnectorBuilder + 'static,
415 ) -> Self {
416 self.connector_builders.push(Box::new(builder));
417 self
418 }
419
420 // NOTE: a remote-access **server** is registered like any other connector —
421 // there is no dedicated builder method:
422 //
423 // .with_connector(aimdb_uds_connector::UdsServer::from_config(config))
424 //
425 // This rides the `with_connector` spine (see its docs) and lets the transport
426 // be swapped (UDS / serial / TCP) without touching the builder. The per-record
427 // `TypedRecord::with_remote_access()` is unrelated.
428
429 /// Configures a record type manually with a unique key
430 ///
431 /// The key uniquely identifies this record instance. Multiple records of the same
432 /// type can exist with different keys (e.g., "sensor.temperature.room1" and
433 /// "sensor.temperature.room2").
434 ///
435 /// # Arguments
436 /// * `key` - A unique identifier for this record. Can be a string literal, `StringKey`,
437 /// or any type implementing `RecordKey` (including user-defined enum keys).
438 /// * `f` - Configuration closure
439 pub fn configure<T>(
440 &mut self,
441 key: impl RecordKey,
442 f: impl FnOnce(&mut RecordRegistrar<'_, T>),
443 ) -> &mut Self
444 where
445 T: Send + Sync + 'static + Debug + Clone,
446 {
447 // Convert any RecordKey to StringKey for internal storage
448 let record_key: StringKey = StringKey::from_dynamic(key.as_str());
449 let type_id = TypeId::of::<T>();
450
451 // Find existing record with this key, or create new one
452 let record_index = self.record_index.get(&record_key).copied();
453
454 // Reporting these instead would need a second mutable borrow of `self`
455 // while the first is live.
456 #[allow(
457 clippy::expect_used,
458 clippy::unwrap_used,
459 reason = "unreachable given the TypeId check above"
460 )]
461 let (rec, is_new_record) = match record_index {
462 Some(idx) => {
463 // Use existing record. A key re-registered with a different
464 // type is a user mistake: record it (reported from build())
465 // and skip the closure — a registrar for the wrong `T`
466 // cannot even be constructed.
467 let (_, existing_type, record) = &mut self.records[idx];
468 if *existing_type != type_id {
469 self.config_errors.push(crate::error::ConfigError::new(
470 record_key.as_str(),
471 None,
472 "key already registered with a different type",
473 ));
474 return self;
475 }
476 (
477 record.as_typed_mut::<T>().expect(
478 "record registry type mismatch despite TypeId check — \
479 this is a bug in aimdb-core",
480 ),
481 false,
482 )
483 }
484 None => {
485 // Create new record
486 self.record_index.insert(record_key, self.records.len());
487 self.records
488 .push((record_key, type_id, Box::new(TypedRecord::<T>::new())));
489 let (_, _, record) = self.records.last_mut().unwrap();
490 (
491 record.as_typed_mut::<T>().expect(
492 "record registry type mismatch despite TypeId check — \
493 this is a bug in aimdb-core",
494 ),
495 true,
496 )
497 }
498 };
499
500 let mut reg = RecordRegistrar {
501 rec,
502 connector_builders: &self.connector_builders,
503 record_key: record_key.as_str().to_string(),
504 extensions: &self.extensions,
505 last_stage: None,
506 };
507 f(&mut reg);
508
509 // Only store spawn function for new records to avoid duplicates
510 if is_new_record {
511 let spawn_key = record_key;
512
513 let spawn_fn: SpawnFnType = Box::new(move |db: &Arc<AimDb>, id: RecordId| {
514 let typed_record = db.inner().get_typed_record_by_id::<T>(id)?;
515 // Resolve the record's key for key-based Producer/Consumer construction.
516 let key = db
517 .inner()
518 .key_for(id)
519 .map(|k| k.as_str().to_string())
520 .unwrap_or_else(|| alloc::format!("__record_{}", id.index()));
521 RecordFutureCollector::<T>::collect_all_futures(typed_record, db, key.as_str())
522 });
523
524 self.spawn_fns.push((spawn_key, spawn_fn));
525 }
526
527 self
528 }
529
530 /// Builds the database and drives every collected future to completion.
531 ///
532 /// Convenience wrapper for the common case: call `build()`, then immediately
533 /// `runner.run().await`. The database handle is dropped on exit.
534 ///
535 /// For programmatic access to the handle (manual subscriptions, holding the
536 /// `AimDb` for other uses), prefer `build()` directly.
537 ///
538 /// # Returns
539 /// `DbResult<()>` — Ok once the database starts; the call then blocks until
540 /// every future the runner is driving has completed (typically forever).
541 pub async fn run(self) -> DbResult<()> {
542 log_info!("Building database and spawning background tasks...");
543
544 let (_db, runner) = self.build().await?;
545
546 log_info!("Database running, runner driving collected futures.");
547
548 runner.run().await;
549
550 Ok(())
551 }
552
553 /// Builds the database and returns a `(handle, runner)` pair.
554 ///
555 /// `build()` collects every future the database needs to drive (producer
556 /// services, consumer taps, transforms with fan-in forwarders, connectors,
557 /// remote-access supervisor, `on_start` tasks) into the [`AimDbRunner`]
558 /// returned alongside the clone-able [`AimDb`] handle. **No background
559 /// work runs until `runner.run().await` is awaited.**
560 ///
561 /// Use this when you need programmatic access to the handle for manual
562 /// subscriptions or production. For typical services, use `.run().await`
563 /// which calls `build()` and `runner.run()` for you.
564 ///
565 /// **Connector Setup:** Connectors are constructed during `build()` from
566 /// the `ConnectorBuilder`s previously registered via `.with_connector()`.
567 /// Their driving futures are appended to the runner.
568 ///
569 /// # Returns
570 /// `DbResult<(AimDb, AimDbRunner)>` — the handle (cloneable) and the
571 /// non-`Clone` runner that owns the collected futures.
572 ///
573 /// # Errors
574 ///
575 /// `build()` is the single surface for configuration mistakes: everything
576 /// recorded during configuration (conflicting `.source()`/`.transform()`/
577 /// `.link_from()`, missing serializers, unregistered schemes, …) plus the
578 /// build-time checks here (record validation, linked records without a
579 /// buffer, duplicate keys, dependency-graph cycles) is collected and
580 /// returned as one [`DbError::InvalidConfiguration`] carrying **all**
581 /// findings — one run surfaces every mistake.
582 pub async fn build(mut self) -> DbResult<(AimDb, AimDbRunner)> {
583 use crate::error::ConfigError;
584
585 // ── Validation pass: collect every configuration mistake before any
586 // effectful phase runs. Nothing returns early here.
587 let mut errors = core::mem::take(&mut self.config_errors);
588
589 for (key, _, record) in self.records.iter_mut() {
590 // Mistakes recorded during configuration — fill in the record key
591 // the setters didn't have.
592 for mut e in record.drain_config_errors() {
593 if e.record_key.is_empty() {
594 e.record_key = key.as_str().to_string();
595 }
596 errors.push(e);
597 }
598
599 // Writer exclusivity: .source(), .transform(), and .link_from()
600 // all write into the record's buffer and would race as
601 // last-writer-wins — at most one origin kind per record. Validated
602 // once here (the setters only catch same-stage duplicates).
603 let mut writers: Vec<&str> = Vec::new();
604 if record.has_producer() {
605 writers.push(".source()");
606 }
607 if record.has_transform() {
608 writers.push(".transform()");
609 }
610 if !record.inbound_connectors().is_empty() {
611 writers.push(".link_from()");
612 }
613 if writers.len() > 1 {
614 let url = record
615 .inbound_connectors()
616 .first()
617 .map(|l| l.url.to_string());
618 errors.push(ConfigError::new(
619 key.as_str(),
620 url,
621 alloc::format!(
622 "conflicting writers: record has {}; these are mutually \
623 exclusive (all write the same buffer)",
624 writers.join(" + ")
625 ),
626 ));
627 }
628
629 // Connector links subscribe to / produce into the record's buffer;
630 // a linked record without one only surfaced at spawn time before.
631 let has_links =
632 !record.outbound_connectors().is_empty() || !record.inbound_connectors().is_empty();
633 if has_links && !record.has_buffer() {
634 let url = record
635 .outbound_connectors()
636 .first()
637 .map(|l| l.url.to_string())
638 .or_else(|| {
639 record
640 .inbound_connectors()
641 .first()
642 .map(|l| l.url.to_string())
643 });
644 errors.push(ConfigError::new(
645 key.as_str(),
646 url,
647 "linked record requires a buffer (call .buffer(...))",
648 ));
649 }
650 }
651
652 // Ensure runtime is set — a missing runtime is a configuration
653 // mistake, collected like every other so it never hides the rest of
654 // the findings. The runtime is bound at the final check.
655 if self.runtime.is_none() {
656 errors.push(ConfigError::new(
657 "",
658 None,
659 "runtime not set (use .runtime())",
660 ));
661 }
662
663 // Build the registry
664 let record_count = self.records.len();
665 let mut storages: Vec<RecordEntry> = Vec::with_capacity(record_count);
666 let mut by_key: HashMap<StringKey, RecordId> = HashMap::with_capacity(record_count);
667
668 for (key, type_id, record) in self.records.into_iter() {
669 // Duplicate keys (should not happen if configure() is used
670 // correctly): collect and skip so every mistake is reported.
671 if by_key.contains_key(&key) {
672 errors.push(ConfigError::new(key.as_str(), None, "duplicate record key"));
673 continue;
674 }
675
676 let id = RecordId::new(storages.len() as u32);
677 storages.push(RecordEntry {
678 key,
679 type_id,
680 record,
681 });
682 by_key.insert(key, id);
683 }
684
685 // Build dependency graph nodes straight from the records.
686 let graph_nodes: Vec<crate::graph::GraphNode> = storages
687 .iter()
688 .map(|e| {
689 let record = &e.record;
690 let (buffer_type, buffer_capacity) = record.buffer_info();
691
692 crate::graph::GraphNode {
693 key: e.key.as_str().to_string(),
694 origin: record.record_origin(),
695 buffer_type,
696 buffer_capacity,
697 tap_count: record.consumer_count(),
698 has_outbound_link: !record.outbound_connectors().is_empty(),
699 }
700 })
701 .collect();
702
703 // Build and validate the dependency graph; fold its findings (cycles,
704 // unregistered transform inputs) into the collected errors.
705 let dependency_graph = match DependencyGraph::build_and_validate(graph_nodes) {
706 Ok(graph) => graph,
707 Err(e) => {
708 errors.push(ConfigError::new("", None, alloc::format!("{e}")));
709 return Err(DbError::InvalidConfiguration { errors });
710 }
711 };
712
713 // All validation done — report every collected mistake at once. The
714 // runtime binding lives here so a missing one is reported alongside
715 // every other finding instead of short-circuiting them.
716 let runtime = match (self.runtime.take(), errors.is_empty()) {
717 (Some(rt), true) => rt,
718 _ => return Err(DbError::InvalidConfiguration { errors }),
719 };
720
721 log_debug!(
722 "Dependency graph built successfully ({} nodes, {} edges, topo order: {:?})",
723 dependency_graph.nodes.len(),
724 dependency_graph.edges.len(),
725 dependency_graph.topo_order
726 );
727
728 let inner = Arc::new(AimDbInner {
729 storages,
730 by_key,
731 dependency_graph,
732 extensions: self.extensions,
733 });
734
735 #[cfg(feature = "observability")]
736 let profiling_clock = crate::profiling::make_clock(runtime.clone());
737
738 let db = Arc::new(AimDb {
739 inner: inner.clone(),
740 runtime: runtime.clone(),
741 #[cfg(feature = "observability")]
742 profiling_clock,
743 });
744
745 // Accumulator for every future the runner will drive.
746 let mut futures_acc: Vec<BoxFuture> = Vec::new();
747
748 log_info!("Collecting futures for {} records", self.spawn_fns.len());
749
750 // Build a lookup map from spawn_fns for topological ordering
751 let mut spawn_fn_map: HashMap<StringKey, SpawnFnType> =
752 self.spawn_fns.into_iter().collect();
753
754 // Execute collectors in topological order — transforms collect after their inputs.
755 // `StringKey: Borrow<str>` with content-based Hash, so the map lookup by
756 // `&str` is O(1) and yields both the interned key and the RecordId.
757 for key_str in inner.dependency_graph.topo_order() {
758 let Some((&key, &id)) = inner.by_key.get_key_value(key_str.as_str()) else {
759 continue;
760 };
761
762 let Some(spawn_fn) = spawn_fn_map.remove(&key) else {
763 continue;
764 };
765
766 futures_acc.extend(spawn_fn(&db, id)?);
767 }
768
769 log_info!("Record future collection complete");
770
771 // AimX remote-access servers are no longer stood up here: register a
772 // session connector (`UdsServer::from_config(...)`) via `with_connector`
773 // instead — it collects below like any other connector, binds its
774 // transport, applies the security policy's writable marking, and drives
775 // the shared session engine. See `with_connector`'s docs.
776
777 // A connector that claims every route for its scheme must be the only
778 // one registered under it (see `ConnectorBuilder::owns_scheme`).
779 // Checked before any `build`, so the error names the misconfiguration
780 // instead of surfacing later as duplicated or misdirected traffic.
781 // Keyless `ConfigError`s: the mistake is the db's, not a record's.
782 let mut duplicate_schemes: Vec<crate::error::ConfigError> = Vec::new();
783 for (i, builder) in self.connector_builders.iter().enumerate() {
784 if !builder.owns_scheme() {
785 continue;
786 }
787 let first = self.connector_builders[..i]
788 .iter()
789 .position(|earlier| earlier.scheme() == builder.scheme());
790 // Only the second registration reports; a third would repeat it.
791 if first.is_some()
792 && !duplicate_schemes
793 .iter()
794 .any(|e| e.url.as_deref() == Some(builder.scheme()))
795 {
796 duplicate_schemes.push(crate::error::ConfigError::new(
797 "",
798 Some(builder.scheme().into()),
799 alloc::format!(
800 "More than one connector registered for scheme '{}'. Routes are \
801 collected by scheme alone, so each connector would claim all of \
802 them: every `link_to` would publish twice, and the routes cannot \
803 be divided between the two. Register one connector for this \
804 scheme, or give one of them a distinct scheme.",
805 builder.scheme()
806 ),
807 ));
808 }
809 }
810 if !duplicate_schemes.is_empty() {
811 return Err(DbError::InvalidConfiguration {
812 errors: duplicate_schemes,
813 });
814 }
815
816 // Collect connector futures. Connector builders return a
817 // `Vec<BoxFuture>` for the runner to drive — there is no connector
818 // object to keep.
819 for builder in self.connector_builders {
820 log_debug!("Building connector for scheme: {}", builder.scheme());
821
822 let connector_futures = builder.build(&db).await?;
823 let n_futures = connector_futures.len();
824 futures_acc.extend(connector_futures);
825
826 log_info!(
827 "Connector '{}' contributed {} future(s)",
828 builder.scheme(),
829 n_futures
830 );
831 }
832
833 // Collect on_start futures (registered by external crates like aimdb-persistence).
834 if !self.start_fns.is_empty() {
835 log_debug!("Collecting {} on_start future(s)", self.start_fns.len());
836
837 for start_fn in self.start_fns {
838 futures_acc.push(start_fn(crate::RuntimeContext::new(runtime.clone())));
839 }
840 }
841
842 let db_owned = (*db).clone();
843
844 Ok((db_owned, AimDbRunner::new(futures_acc)))
845 }
846}
847
848impl Default for AimDbBuilder {
849 fn default() -> Self {
850 Self::new()
851 }
852}
853
854/// Producer-consumer database
855///
856/// A database instance with type-safe record registration and cross-record
857/// communication via the Emitter pattern. The runtime is held as a value
858/// (`Arc<dyn RuntimeOps>`) — records are the only generic surface.
859///
860/// See `examples/` for usage.
861///
862/// # Examples
863///
864/// Illustrative (not compiled: the runtime adapter lives in a downstream
865/// crate `aimdb-core` cannot depend on):
866///
867/// ```rust,ignore
868/// use aimdb_tokio_adapter::TokioAdapter;
869///
870/// let mut builder = AimDbBuilder::new().runtime(Arc::new(TokioAdapter::new()?));
871/// builder.register_record::<Temperature>(&TemperatureConfig);
872/// let (db, runner) = builder.build().await?;
873/// ```
874#[derive(Clone)]
875pub struct AimDb {
876 /// Internal state
877 inner: Arc<AimDbInner>,
878
879 /// Runtime capabilities, held as a value
880 runtime: Arc<dyn crate::executor::RuntimeOps>,
881
882 /// Shared wall clock for stage profiling, built from the runtime at `build()` time.
883 #[cfg(feature = "observability")]
884 profiling_clock: crate::profiling::Clock,
885}
886
887// ============================================================================
888// AimDbRunner — drives every future the builder collected
889// ============================================================================
890
891/// Non-`Clone` runner returned alongside [`AimDb`] from
892/// [`AimDbBuilder::build()`].
893///
894/// Owns the complete set of futures that drive the database (producer
895/// services, consumer taps, transforms with fan-in forwarders, connectors,
896/// remote-access supervisor, `on_start` tasks). `run()` consumes `self`,
897/// drives every future cooperatively via a `FuturesUnordered`, and returns
898/// only when **every** future has completed — typically never, since
899/// producer/consumer loops run for the application's lifetime.
900///
901/// Cancellation is the application's responsibility — wrap `runner.run()`
902/// in `tokio::select!` with a shutdown signal, or use a `CancellationToken`
903/// inside each registered service.
904pub struct AimDbRunner {
905 futures: Vec<BoxFuture>,
906}
907
908impl AimDbRunner {
909 pub(crate) fn new(futures: Vec<BoxFuture>) -> Self {
910 Self { futures }
911 }
912
913 /// Returns the number of futures the runner will drive. Useful for tests
914 /// and diagnostic logging.
915 pub fn future_count(&self) -> usize {
916 self.futures.len()
917 }
918
919 /// Drives every collected future to completion.
920 ///
921 /// Returns when all futures complete (normally: never, while producer and
922 /// consumer loops are alive). To cancel, wrap this call in `tokio::select!`
923 /// (or the analogous primitive on Embassy / WASM) with a shutdown signal —
924 /// when the `select!` arm fires, this future is dropped and every
925 /// `FuturesUnordered` entry is cancelled with it.
926 pub async fn run(self) {
927 use futures_util::stream::{FuturesUnordered, StreamExt};
928
929 if self.futures.is_empty() {
930 return;
931 }
932
933 let mut set: FuturesUnordered<BoxFuture> = self.futures.into_iter().collect();
934 while set.next().await.is_some() {}
935 }
936}
937
938impl AimDb {
939 /// Internal accessor for the inner state
940 ///
941 /// Used by adapter crates and internal spawning logic.
942 #[doc(hidden)]
943 pub fn inner(&self) -> &Arc<AimDbInner> {
944 &self.inner
945 }
946
947 /// Returns the extension storage frozen at `build()` time.
948 ///
949 /// External crates (e.g. `aimdb-persistence`) retrieve their typed state here
950 /// to service query calls. The extensions are read-only on the live handle.
951 pub fn extensions(&self) -> &Extensions {
952 &self.inner.extensions
953 }
954
955 /// Shared wall clock used by stage profiling (nanoseconds since an arbitrary epoch).
956 #[cfg(feature = "observability")]
957 pub(crate) fn profiling_clock(&self) -> &crate::profiling::Clock {
958 &self.profiling_clock
959 }
960
961 /// Builds a database with a closure-based builder pattern
962 pub async fn build_with(
963 rt: Arc<impl crate::executor::RuntimeOps + 'static>,
964 f: impl FnOnce(&mut AimDbBuilder),
965 ) -> DbResult<()> {
966 let mut b = AimDbBuilder::new().runtime(rt);
967 f(&mut b);
968 b.run().await
969 }
970
971 /// Produces a value to a specific record by key.
972 ///
973 /// Uses O(1) key-based lookup to find the correct record. Sync + fallible —
974 /// only the lookup can fail; the buffer push itself is infallible.
975 ///
976 /// For hot paths, prefer `db.producer::<T>(key)?` once and reuse the
977 /// returned handle.
978 ///
979 /// # Arguments
980 /// * `key` - The record key (e.g., "sensor.temperature")
981 /// * `value` - The value to produce
982 pub fn produce<T>(&self, key: impl AsRef<str>, value: T) -> DbResult<()>
983 where
984 T: Send + 'static + Debug + Clone,
985 {
986 // Single write path via WriteHandle. For hot paths,
987 // prefer `db.producer::<T>(key)` once and reuse the returned handle.
988 let typed_rec = self.inner.get_typed_record_by_key::<T>(key)?;
989 typed_rec.writer_handle().push(value);
990 Ok(())
991 }
992
993 /// Subscribes to a specific record by key
994 ///
995 /// Uses O(1) key-based lookup to find the correct record.
996 ///
997 /// # Arguments
998 /// * `key` - The record key (e.g., "sensor.temperature")
999 pub fn subscribe<T>(&self, key: impl AsRef<str>) -> DbResult<crate::buffer::Reader<T>>
1000 where
1001 T: Send + Sync + 'static + Debug + Clone,
1002 {
1003 let typed_rec = self.inner.get_typed_record_by_key::<T>(key)?;
1004 typed_rec.subscribe()
1005 }
1006
1007 /// Creates a type-safe producer for a specific record by key
1008 ///
1009 /// Returns a `Producer<T>` bound to a specific record key.
1010 ///
1011 /// # Arguments
1012 /// * `key` - The record key (e.g., "sensor.temperature")
1013 pub fn producer<T>(
1014 &self,
1015 key: impl Into<alloc::string::String>,
1016 ) -> DbResult<crate::typed_api::Producer<T>>
1017 where
1018 T: Send + 'static + Debug + Clone,
1019 {
1020 // Pre-resolve the typed record so the returned Producer holds a write
1021 // handle to the record's buffer/snapshot/metadata directly
1022 let key_str: alloc::string::String = key.into();
1023 let typed_rec = self.inner.get_typed_record_by_key::<T>(&key_str)?;
1024 Ok(crate::typed_api::Producer::new(typed_rec.writer_handle()))
1025 }
1026
1027 /// Creates a type-safe consumer for a specific record by key
1028 ///
1029 /// Returns a `Consumer<T>` bound to a specific record key.
1030 ///
1031 /// # Arguments
1032 /// * `key` - The record key (e.g., "sensor.temperature")
1033 pub fn consumer<T>(
1034 &self,
1035 key: impl Into<alloc::string::String>,
1036 ) -> DbResult<crate::typed_api::Consumer<T>>
1037 where
1038 T: Send + Sync + 'static + Debug + Clone,
1039 {
1040 // Pre-resolve the buffer Arc so the returned Consumer subscribes
1041 // through a direct virtual call. A `.consumer()` without
1042 // a configured buffer surfaces as `MissingConfiguration` here rather
1043 // than panicking later inside `subscribe()`.
1044 let key_str: alloc::string::String = key.into();
1045 let typed_rec = self.inner.get_typed_record_by_key::<T>(&key_str)?;
1046 let buffer = typed_rec.buffer_handle().ok_or_else(|| {
1047 DbError::missing_configuration(alloc::format!("buffer for record '{}'", key_str))
1048 })?;
1049 Ok(crate::typed_api::Consumer::new(buffer))
1050 }
1051
1052 /// Resolve a record key to its RecordId
1053 ///
1054 /// Useful for checking if a record exists before operations.
1055 pub fn resolve_key(&self, key: &str) -> Option<crate::record_id::RecordId> {
1056 self.inner.resolve_str(key)
1057 }
1058
1059 /// Get all record IDs for a specific type
1060 ///
1061 /// Returns a slice of RecordIds for all records of type T.
1062 /// Useful for introspection when multiple records of the same type exist.
1063 pub fn records_of_type<T: 'static>(&self) -> Vec<crate::record_id::RecordId> {
1064 self.inner.records_of_type::<T>()
1065 }
1066
1067 /// Returns an owned `Arc` handle to the runtime capabilities.
1068 ///
1069 /// Connectors that hand the runtime to a `'static` engine future (e.g. the
1070 /// session client engine, which needs the clock for reconnect
1071 /// backoff/keepalive) clone it through here.
1072 pub fn runtime_ops(&self) -> Arc<dyn crate::executor::RuntimeOps> {
1073 self.runtime.clone()
1074 }
1075
1076 /// Returns a [`RuntimeContext`](crate::RuntimeContext) over this
1077 /// database's runtime — the value handed to services and context-aware
1078 /// (de)serializers.
1079 pub fn runtime_ctx(&self) -> crate::RuntimeContext {
1080 crate::RuntimeContext::new(self.runtime.clone())
1081 }
1082
1083 /// Lists all registered records (std only)
1084 ///
1085 /// Returns metadata for all registered records, useful for remote access introspection.
1086 /// Available only when the `std` feature is enabled.
1087 #[cfg(feature = "remote")]
1088 pub fn list_records(&self) -> Vec<crate::remote::RecordMetadata> {
1089 self.inner.list_records()
1090 }
1091
1092 /// Resets stage profiling counters for every record (feature `observability`).
1093 #[cfg(feature = "observability")]
1094 pub fn reset_stage_profiling(&self) {
1095 for entry in &self.inner.storages {
1096 entry.record.reset_profiling();
1097 }
1098 }
1099
1100 /// Resets buffer introspection counters for every record (feature `observability`).
1101 #[cfg(feature = "observability")]
1102 pub fn reset_buffer_metrics(&self) {
1103 for entry in &self.inner.storages {
1104 entry.record.reset_buffer_metrics();
1105 }
1106 }
1107
1108 /// Try to get record's latest value as JSON by name
1109 ///
1110 /// Convenience wrapper around `AimDbInner::try_latest_as_json()`.
1111 ///
1112 /// # Arguments
1113 /// * `record_name` - The full Rust type name (e.g., "server::Temperature")
1114 ///
1115 /// # Returns
1116 /// `Some(JsonValue)` with the current value, or `None` if the record has no
1117 /// value yet, no buffer, or a buffer with **no canonical latest** — i.e.
1118 /// [`SpmcRing`](crate::buffer::BufferCfg::SpmcRing). A ring is a stream/backlog
1119 /// with no single "current value"; read it via a subscriber or `record.drain`,
1120 /// not a peek (`record.get`). Use [`SingleLatest`](crate::buffer::BufferCfg::SingleLatest)
1121 /// for state you want to read latest-value style.
1122 #[cfg(feature = "remote")]
1123 pub fn try_latest_as_json(&self, record_name: &str) -> Option<serde_json::Value> {
1124 self.inner.try_latest_as_json(record_name)
1125 }
1126
1127 /// Try to get a record's latest value as owned JSON bytes.
1128 ///
1129 /// Semantics match [`try_latest_as_json`](Self::try_latest_as_json):
1130 /// buffers without a canonical latest value (notably `SpmcRing`) return
1131 /// `None`. AimX `record.get` retains its per-connection drain fallback for
1132 /// those records.
1133 #[cfg(feature = "remote")]
1134 pub fn try_latest_as_json_bytes(&self, record_name: &str) -> Option<Vec<u8>> {
1135 self.inner.try_latest_as_json_bytes(record_name)
1136 }
1137
1138 /// Sets a record value from JSON (remote access API)
1139 ///
1140 /// Deserializes JSON and produces the value to the record's buffer.
1141 ///
1142 /// **SAFETY:** Enforces "No Producer Override" rule - only works for configuration
1143 /// records without active producers.
1144 ///
1145 /// # Arguments
1146 /// * `record_name` - Full Rust type name
1147 /// * `json_value` - JSON value to set
1148 ///
1149 /// # Returns
1150 /// `Ok(())` on success, error if record not found, has producers, or deserialization fails
1151 #[cfg(feature = "remote")]
1152 pub fn set_record_from_json(
1153 &self,
1154 record_name: &str,
1155 json_value: serde_json::Value,
1156 ) -> DbResult<()> {
1157 self.inner.set_record_from_json(record_name, json_value)
1158 }
1159
1160 /// Collects inbound connector routes for automatic router construction (std only)
1161 ///
1162 /// Iterates all records, filters their inbound_connectors by scheme,
1163 /// and returns routes with fused ingest callbacks (deserialize + produce
1164 /// in one typed closure — no `Box<dyn Any>` per message).
1165 ///
1166 /// # Arguments
1167 /// * `scheme` - URL scheme to filter by (e.g., "mqtt", "kafka")
1168 ///
1169 /// # Returns
1170 /// Vector of tuples: (topic, ingest)
1171 ///
1172 /// The topic is resolved dynamically if a `TopicResolverFn` is configured,
1173 /// otherwise the static topic from the URL is used.
1174 pub fn collect_inbound_routes(
1175 &self,
1176 scheme: &str,
1177 ) -> Vec<(String, crate::connector::IngestFn)> {
1178 let mut routes = Vec::new();
1179
1180 for entry in &self.inner.storages {
1181 let inbound_links = entry.record.inbound_connectors();
1182
1183 for link in inbound_links {
1184 // Filter by scheme
1185 if link.url.scheme() != scheme {
1186 continue;
1187 }
1188
1189 // Resolve topic: dynamic (from resolver) or static (from URL)
1190 let topic = link.resolve_topic();
1191
1192 // Create the fused ingest callback using the stored factory
1193 routes.push((topic, link.create_ingest(self)));
1194 }
1195 }
1196
1197 if !routes.is_empty() {
1198 log_debug!(
1199 "Collected {} inbound routes for scheme '{}'",
1200 routes.len(),
1201 scheme
1202 );
1203 }
1204
1205 routes
1206 }
1207
1208 /// Collects outbound routes for a specific protocol scheme
1209 ///
1210 /// Mirrors `collect_inbound_routes()` for symmetry. Iterates all records,
1211 /// filters their outbound_connectors by scheme, and returns
1212 /// [`OutboundRoute`]s carrying fused serialized sources (subscribe →
1213 /// recv → resolve topic → serialize, all typed inside — no
1214 /// `Box<dyn Any>` per message).
1215 ///
1216 /// This method is called by connectors during their `build()` phase to
1217 /// collect all configured outbound routes and spawn publisher tasks
1218 /// (usually via `pump_sink`).
1219 ///
1220 /// # Arguments
1221 /// * `scheme` - URL scheme to filter by (e.g., "mqtt", "kafka")
1222 pub fn collect_outbound_routes(&self, scheme: &str) -> Vec<OutboundRoute> {
1223 let mut routes = Vec::new();
1224
1225 for entry in &self.inner.storages {
1226 let outbound_links = entry.record.outbound_connectors();
1227
1228 for link in outbound_links {
1229 // Filter by scheme
1230 if link.url.scheme() != scheme {
1231 continue;
1232 }
1233
1234 // Create the fused source using the stored factory
1235 routes.push(OutboundRoute {
1236 topic: link.url.resource_id().to_string(),
1237 source: link.create_source(self),
1238 config: link.config.clone(),
1239 });
1240 }
1241 }
1242
1243 if !routes.is_empty() {
1244 log_debug!(
1245 "Collected {} outbound routes for scheme '{}'",
1246 routes.len(),
1247 scheme
1248 );
1249 }
1250
1251 routes
1252 }
1253}