Expand description
Config-file driven plugin loader for cordis-rs.
This crate is the assembly half of porting upstream Cordis’ loader: it
connects cordis-include’s entry trees to cordis fibers. Everything a
config-driven cordis application needs is re-exported here — depend on
cordis-loader alone.
§How it works
- Static registry (
PluginRegistry) replaces upstream’s dynamicimport(name): register plugins at startup, entries resolve by name. Thegroupbuiltin is pre-registered. With thedynamicfeature, names can also resolve to plugins compiled as dynamic libraries (see the [dynamic] module). - Startup:
Loader::openreads the entry file (writinginitialwhen missing), builds theEntryTree, and starts every enabled entry — group entries ascordis_group::Groupfibers, children beneath their parent group’s context, so disposing a group cascades. - Config: entries carry a
cordis_include::Nodeconfig (never()); loader plugins read it viaconfig.downcast::<Node>().${{ env.X }}templates expand at hand-off time; the file keeps the raw text. - Reload (
Loader::reload, wired to thewatchfeature): re-read the file, diff the tree, and reconcile fibers — created entries start, removed subtrees stop, moved entries restart under their new parent, entries whose plugin name / inject declaration / enabled flag changed stop and restart with their new options, and config-only changes patch in place viaFiber::update_value. A corrupt or unreadable main file fails the operation instead of silently booting an empty tree (import files keep a tolerant record-and-skip path). - Document sources (
LoaderConfig::with_document,Loader::update): compose from an in-memory document instead of the entry file. Boot then never reads or writes the file — concurrent boots on one shared draft cannot race, and the directory may stay read-only — while reloads recompose from the stored document (import files are still read).updatereconciles a fresh composition with the same diff → stop → patch → start machinery but no write-back, making it the HMR primitive for layer-based composition: a watcher recomposes and hands the result over. - Inject: an entry’s
injectlist is merged into the plugin’s own declaration, so the core fiber machinery reconciles entries when services come and go — “hot-swapped service restarts its dependents” for free. - Self-kill vs. removal: a fiber that reaches
Disposedoutside loader operation was killed by its own plugin; the loader persistsdisabled: truefor that entry shortly after, deferred off the dying fiber’s transition lock. Removing an entry from the file just stops it. - Write-back:
Loader::update_configis the runtime entry point — it updates the fiber and persists the config. Reloads apply their patches without writing them back; only newly generated ids are persisted. For a document-backed loader the entry file is a pure write-back draft: rows materialized into it never re-enter the composition.reload,update,update_config, anddisposeserialize through one operation lock (reentrant from event listeners), so a watch-thread reload cannot interleave with a plugin-threadupdate_config.
§Example
use cordis_loader::{Loader, LoaderConfig, PluginRegistry};
let root = cordis::Context::new();
let mut registry = PluginRegistry::new();
// registry.register_plugin(my_plugin); // your plugins, by name
let config = LoaderConfig::new("cordis.yml").with_registry(registry);
let loader = Loader::open(&root, config)?;Register the plugins first (the group builtin is pre-registered), then
open; plugins registered later via Loader::register_plugin are picked
up by the next Loader::reload.
§Imports
An entry with name: import and config: { url: "…" } mounts another
config file as its subtree. Reloads compose every involved file into
one tree (so diffs and id reuse work across files), while write-back
decomposes: mounted children are persisted to the file they came from,
never to the importing file. Import cycles are reported through
Loader::last_error instead of recursing. With the watch feature,
import files are watched like the main file.
§Events and write coalescing
Lifecycle transitions are observable through the events module’s
event names on the root context’s bus; listener failures are recorded,
never propagated. Write-backs can be debounced via
LoaderConfig::with_write_debounce or
Loader::set_write_debounce: rapid successive writes coalesce into
one physical write after the quiet window.
§Dynamic library plugins
With the dynamic feature, plugins can be compiled as cdylib
libraries and resolved from a directory instead of being registered
statically:
let registry = PluginRegistry::new().with_dynamic_dirs(["./plugins"]);A plugin library exports its implementation through
[dynamic::export_plugin!] and must be built by the exact same
toolchain, target, panic strategy, and cordis-rs version as the loading
process — the loader verifies a build fingerprint before accepting the
library. Libraries are never unloaded within a process; reloading a
changed library is the worker-restart HMR flow implemented by
cordis-cli.
§Not in scope yet
Isolate/service migration is future work.
Re-exports§
pub use error::LoaderError;pub use error::Result;pub use loader::Loader;pub use loader::LoaderConfig;pub use loader::LoaderHandle;pub use registry::PluginRegistry;
Modules§
- error
- Error type combining core and include failures.
- events
- Names of the events the loader emits on the root context’s event bus.
- loader
- The loader: entry tree ⇄ fiber lifecycle, file reloads, write-back.
- registry
- Static plugin registry — the Rust replacement for upstream’s dynamic
import(name).
Structs§
- Document
- The parsed content of one config file: the entry list plus any unknown top-level keys, which are preserved on write-back.
- Entry
- A live entry in an
crate::EntryTree. - Entry
Options - One entry in a config file: a plugin instance plus its group position.
- Entry
Tree - An in-memory tree of
Entrys mirroring one config file. - Group
- Nesting marker plugin for group entries.
- Loader
File - A handle to one config file on disk.
Enums§
- Node
- A dynamically typed value that round-trips through YAML and JSON while preserving object key order.