Skip to main content

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}