1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
//! Config-file driven plugin loader for
//! [cordis-rs](https://crates.io/crates/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 dynamic
//! `import(name)`: register plugins at startup, entries resolve by name.
//! The `group` builtin is pre-registered. With the `dynamic` feature,
//! names can also resolve to plugins compiled as dynamic libraries (see
//! the [`dynamic`] module).
//! - **Startup**: [`Loader::open`] reads the entry file (writing
//! `initial` when missing), builds the [`EntryTree`], and starts every
//! enabled entry — group entries as `cordis_group::Group` fibers, children
//! beneath their parent group's context, so disposing a group cascades.
//! - **Config**: entries carry a `cordis_include::Node` config (never `()`);
//! loader plugins read it via `config.downcast::<Node>()`. `${{ env.X }}`
//! templates expand at hand-off time; the file keeps the raw text.
//! - **Reload** ([`Loader::reload`], wired to the `watch` feature):
//! 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 via `Fiber::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). `update` reconciles 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 `inject` list 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 `Disposed` outside
//! loader operation was killed by its own plugin; the loader persists
//! `disabled: true` for 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_config`] is 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`, and `dispose`
//! serialize through one operation lock (reentrant from event listeners),
//! so a watch-thread reload cannot interleave with a plugin-thread
//! `update_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)?;
//! # assert!(loader.tree().entries().is_empty());
//! # Ok::<(), cordis_loader::LoaderError>(())
//! ```
//!
//! 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:
//!
//! ```rust,ignore
//! 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.
// `deny` instead of `forbid` because the `dynamic` feature wraps
// libloading's unsafe primitives; every unsafe operation lives in that one
// module, item-scoped behind `#[allow(unsafe_code)]` with SAFETY notes
// (the same pattern cordis-cli uses for dotenv).
pub use Group;
pub use ;
pub use ;
pub use ;
pub use PluginRegistry;
/// Lock a mutex tolerantly, treating a poisoned lock as unlocked.
pub