Skip to main content

rill_patchbay/
module_factory.rs

1//! Module factory — type-registry for modular rack module construction.
2//!
3//! `ModuleFactory` is the single creation point for all rack modules:
4//! Servo, Sensor, Graph, and Custom. Each archetype registers a
5//! `ModuleConstructor` that receives a `ModuleDef` descriptor
6//! and returns an `ActorRef<CommandEnum>` for the rack actor fan-out.
7
8use std::collections::HashMap;
9use std::fmt;
10use std::sync::Arc;
11
12use rill_core::queues::CommandEnum;
13use rill_core::traits::ParamValue;
14use rill_core_actor::{ActorRef, ActorSystem};
15
16use crate::module_def::{AutomatonDef, ModuleDef};
17
18#[cfg(feature = "debug")]
19use crate::debug::PatchbayInspector;
20
21/// Errors returned by module construction via the type-registry.
22#[derive(Debug, Clone)]
23pub enum ModuleError {
24    /// The requested module type name was not registered.
25    UnknownType(String),
26    /// Construction failed with a user-readable reason.
27    ConstructionFailed(String),
28}
29
30impl fmt::Display for ModuleError {
31    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32        match self {
33            Self::UnknownType(t) => write!(f, "unknown module type: {t}"),
34            Self::ConstructionFailed(e) => write!(f, "module construction failed: {e}"),
35        }
36    }
37}
38
39/// Constructs rack modules from a [`ModuleDef`] descriptor.
40///
41/// Each constructor receives the full module descriptor plus the list of
42/// automaton definitions needed by `Servo` modules. Custom and registered
43/// constructors return an [`ActorRef<CommandEnum>`] that the rack actor
44/// uses for fan-out.
45pub trait ModuleConstructor: Send + Sync {
46    /// Returns the string key used to register this constructor.
47    fn type_name(&self) -> &'static str;
48
49    /// Build the module and return its actor handle.
50    ///
51    /// `automaton_defs` provides the automaton definitions referenced
52    /// by `ModuleDef::Servo`. Other module types ignore this parameter.
53    #[cfg_attr(feature = "debug", allow(unused_variables))]
54    fn construct(
55        &self,
56        module: &ModuleDef,
57        automaton_defs: &[AutomatonDef],
58        system: &Arc<ActorSystem>,
59        graph_ref: &ActorRef<CommandEnum>,
60        #[cfg(feature = "debug")] inspector: Option<&PatchbayInspector>,
61    ) -> Result<ActorRef<CommandEnum>, ModuleError>;
62
63    /// Returns a heap-allocated clone of this constructor.
64    fn clone_box(&self) -> Box<dyn ModuleConstructor>;
65}
66
67/// How the actor's drain loop is spawned.
68#[derive(Debug, Clone, Copy)]
69pub enum Drain {
70    /// OS thread with periodic drain (handler: !Send).
71    OsThread {
72        /// Drain interval in milliseconds.
73        interval_ms: u64,
74    },
75    /// Tokio task with periodic drain (handler: Send).
76    TokioTask {
77        /// Drain interval in milliseconds.
78        interval_ms: u64,
79    },
80    /// I/O callback drain — handler drained inline in the backend callback.
81    /// Factory spawns the I/O thread, construction closures run inside it.
82    IoCallback,
83}
84/// Registry that maps module type names to constructors.
85pub struct ModuleFactory {
86    entries: HashMap<String, Box<dyn ModuleConstructor>>,
87}
88
89impl ModuleFactory {
90    /// Creates an empty module factory.
91    pub fn new() -> Self {
92        Self {
93            entries: HashMap::new(),
94        }
95    }
96    /// Adds a typed constructor to the registry, keyed by its type name.
97    pub fn register(&mut self, ctor: impl ModuleConstructor + 'static) {
98        self.entries
99            .insert(ctor.type_name().to_string(), Box::new(ctor));
100    }
101
102    /// Register a closure-based constructor (handler: !Send).
103    ///
104    /// `make_handler` receives module params and the graph handle, and returns
105    /// the message handler closure. The factory calls it **inside the drain thread**,
106    /// so the handler does not need `Send`.
107    #[allow(dead_code)]
108    pub fn register_fn(
109        &mut self,
110        type_name: impl Into<String>,
111        drain: Drain,
112        make_handler: impl Fn(
113                &str,
114                &HashMap<String, ParamValue>,
115                &ActorRef<CommandEnum>,
116            ) -> Box<dyn FnMut(CommandEnum) + 'static>
117            + Send
118            + Sync
119            + 'static,
120    ) {
121        self.entries.insert(
122            type_name.into(),
123            Box::new(ClosureCtor::new_erased(drain, make_handler)),
124        );
125    }
126
127    /// Register a closure-based constructor (handler: `Send`, for [`Drain::TokioTask`]).
128    ///
129    /// The returned handler must be `Send` so it can be stored in a tokio future.
130    #[allow(dead_code)]
131    pub fn register_fn_send(
132        &mut self,
133        type_name: impl Into<String>,
134        drain: Drain,
135        make_handler: impl Fn(
136                &str,
137                &HashMap<String, ParamValue>,
138                &ActorRef<CommandEnum>,
139            ) -> Box<dyn FnMut(CommandEnum) + Send + 'static>
140            + Send
141            + Sync
142            + 'static,
143    ) {
144        self.entries.insert(
145            type_name.into(),
146            Box::new(ClosureCtor::new_send(drain, make_handler)),
147        );
148    }
149
150    /// Looks up a type name and constructs the corresponding module actor.
151    pub fn construct(
152        &self,
153        module: &ModuleDef,
154        automaton_defs: &[AutomatonDef],
155        system: &Arc<ActorSystem>,
156        graph_ref: &ActorRef<CommandEnum>,
157        #[cfg(feature = "debug")] inspector: Option<&PatchbayInspector>,
158    ) -> Result<ActorRef<CommandEnum>, ModuleError> {
159        let type_name = module.type_name();
160        self.entries
161            .get(type_name)
162            .ok_or_else(|| ModuleError::UnknownType(type_name.to_string()))
163            .and_then(|ctor| {
164                ctor.construct(
165                    module,
166                    automaton_defs,
167                    system,
168                    graph_ref,
169                    #[cfg(feature = "debug")]
170                    inspector,
171                )
172            })
173    }
174
175    /// Checks whether a type name is registered.
176    pub fn contains(&self, type_name: &str) -> bool {
177        self.entries.contains_key(type_name)
178    }
179    /// Returns the number of registered module types.
180    pub fn len(&self) -> usize {
181        self.entries.len()
182    }
183    /// Returns true if no module types are registered.
184    pub fn is_empty(&self) -> bool {
185        self.entries.is_empty()
186    }
187}
188
189impl Default for ModuleFactory {
190    fn default() -> Self {
191        Self::new()
192    }
193}
194
195// ============================================================================
196// ClosureCtor
197// ============================================================================
198
199type ErasedCtorFn = Arc<
200    dyn Fn(
201            &str,
202            &HashMap<String, ParamValue>,
203            &ActorRef<CommandEnum>,
204        ) -> Box<dyn FnMut(CommandEnum) + 'static>
205        + Send
206        + Sync,
207>;
208
209type SendCtorFn = Arc<
210    dyn Fn(
211            &str,
212            &HashMap<String, ParamValue>,
213            &ActorRef<CommandEnum>,
214        ) -> Box<dyn FnMut(CommandEnum) + Send + 'static>
215        + Send
216        + Sync,
217>;
218
219enum ClosureCtorKind {
220    Erased { f: ErasedCtorFn },
221    Send { f: SendCtorFn },
222}
223
224struct ClosureCtor {
225    drain: Drain,
226    kind: ClosureCtorKind,
227}
228
229impl ClosureCtor {
230    fn new_erased(
231        drain: Drain,
232        f: impl Fn(
233                &str,
234                &HashMap<String, ParamValue>,
235                &ActorRef<CommandEnum>,
236            ) -> Box<dyn FnMut(CommandEnum) + 'static>
237            + Send
238            + Sync
239            + 'static,
240    ) -> Self {
241        Self {
242            drain,
243            kind: ClosureCtorKind::Erased { f: Arc::new(f) },
244        }
245    }
246
247    fn new_send(
248        drain: Drain,
249        f: impl Fn(
250                &str,
251                &HashMap<String, ParamValue>,
252                &ActorRef<CommandEnum>,
253            ) -> Box<dyn FnMut(CommandEnum) + Send + 'static>
254            + Send
255            + Sync
256            + 'static,
257    ) -> Self {
258        Self {
259            drain,
260            kind: ClosureCtorKind::Send { f: Arc::new(f) },
261        }
262    }
263}
264
265impl ModuleConstructor for ClosureCtor {
266    fn type_name(&self) -> &'static str {
267        ""
268    }
269    fn construct(
270        &self,
271        module: &ModuleDef,
272        _automaton_defs: &[AutomatonDef],
273        system: &Arc<ActorSystem>,
274        graph_ref: &ActorRef<CommandEnum>,
275        #[cfg(feature = "debug")] _inspector: Option<&PatchbayInspector>,
276    ) -> Result<ActorRef<CommandEnum>, ModuleError> {
277        let ModuleDef::Custom {
278            type_name: _,
279            params,
280        } = module
281        else {
282            return Err(ModuleError::ConstructionFailed(
283                "ClosureCtor only supports Custom modules".into(),
284            ));
285        };
286
287        let id_owned = String::new(); // Custom modules use type_name as id
288        let name = "custom".to_string();
289        let graph_ref = graph_ref.clone();
290        let params = params.clone();
291
292        match (&self.kind, self.drain) {
293            (ClosureCtorKind::Erased { f }, Drain::OsThread { interval_ms }) => {
294                let f = f.clone();
295                let actor_ref = system.spawn_detached(
296                    &name,
297                    move || f(&id_owned, &params, &graph_ref),
298                    interval_ms,
299                );
300                Ok(actor_ref)
301            }
302            (ClosureCtorKind::Send { f }, Drain::OsThread { interval_ms }) => {
303                let f = f.clone();
304                let actor_ref = system.spawn_detached(
305                    &name,
306                    move || f(&id_owned, &params, &graph_ref),
307                    interval_ms,
308                );
309                Ok(actor_ref)
310            }
311            (ClosureCtorKind::Send { f }, Drain::TokioTask { interval_ms }) => {
312                let f = f.clone();
313                let actor_ref = system.spawn_detached_tokio(
314                    &name,
315                    move || f(&id_owned, &params, &graph_ref),
316                    interval_ms,
317                );
318                Ok(actor_ref)
319            }
320            (ClosureCtorKind::Erased { .. }, Drain::TokioTask { .. }) => {
321                Err(ModuleError::ConstructionFailed(
322                    "TokioTask drain requires a Send handler; use register_fn_send()".into(),
323                ))
324            }
325            (ClosureCtorKind::Erased { .. }, Drain::IoCallback) => {
326                Err(ModuleError::ConstructionFailed(
327                    "IoCallback drain not supported via register_fn(); use Graph constructor directly".into(),
328                ))
329            }
330            (ClosureCtorKind::Send { .. }, Drain::IoCallback) => {
331                Err(ModuleError::ConstructionFailed(
332                    "IoCallback drain not supported via register_fn_send(); use Graph constructor directly".into(),
333                ))
334            }
335        }
336    }
337    fn clone_box(&self) -> Box<dyn ModuleConstructor> {
338        match &self.kind {
339            ClosureCtorKind::Erased { f } => Box::new(ClosureCtor {
340                drain: self.drain,
341                kind: ClosureCtorKind::Erased { f: f.clone() },
342            }),
343            ClosureCtorKind::Send { f } => Box::new(ClosureCtor {
344                drain: self.drain,
345                kind: ClosureCtorKind::Send { f: f.clone() },
346            }),
347        }
348    }
349}