@@ -332,7 +332,7 @@ class PluginManager {
332332 infos[result.info_idx ].deps });
333333 rebuild_name_index ();
334334 wire_instance_tracked (plugins_.back (), config_map);
335- instances_dirty_ = true ;
335+
336336 }
337337 }
338338 }
@@ -377,26 +377,30 @@ class PluginManager {
377377 }
378378
379379 // --- Dependency + version checks (D6) ---
380+ // add_plugin is permissive about missing deps (provider may be added
381+ // later — the reverse version check on line ~407 catches mismatches
382+ // when the provider arrives). Version constraints are still validated
383+ // when the provider is already loaded.
380384 std::vector<std::string> deps;
381385 auto * dep_aware = dynamic_cast <IDependencyAware*>(instance.get ());
382386 if (dep_aware) {
383387 for (const auto & raw_dep : dep_aware->dependencies ()) {
384388 auto parsed = Dependency::parse (raw_dep);
385389 deps.push_back (parsed.type );
386390
387- if (parsed. op != Dependency::Op::any) {
388- // Find the provider and check version constraint.
389- for ( const auto & lp : plugins_ ) {
390- if (lp. entry . type == parsed. type ) {
391+ // Check version constraint only if the provider is already loaded.
392+ for ( const auto & lp : plugins_) {
393+ if (lp. entry . type == parsed. type ) {
394+ if (parsed. op != Dependency::Op::any ) {
391395 auto provider_version = SemVer::parse (lp.entry .version );
392396 if (!parsed.satisfied_by (provider_version)) {
393397 throw std::runtime_error (
394398 " Plugin '" + entry.name + " ' requires " + raw_dep +
395399 " but '" + lp.entry .name + " ' provides version " +
396400 lp.entry .version );
397401 }
398- break ;
399402 }
403+ break ;
400404 }
401405 }
402406 }
@@ -448,7 +452,7 @@ class PluginManager {
448452 rebuild_name_index ();
449453 wire_instance_tracked (plugins_.back (), config_map, /* skip_validation=*/ true );
450454 notify_loaded (entry.name , entry.type );
451- instances_dirty_ = true ;
455+
452456 }
453457
454458 // --- Per-plugin unload (A1) ---
@@ -495,7 +499,7 @@ class PluginManager {
495499 plugins_ = std::move (remaining);
496500 rebuild_name_index ();
497501 rebuild_locator ();
498- instances_dirty_ = true ;
502+
499503 }
500504
501505 // --- Per-plugin reload (A1) ---
@@ -568,7 +572,7 @@ class PluginManager {
568572 }
569573
570574 notify_reloaded (name, target_type);
571- instances_dirty_ = true ;
575+
572576 }
573577
574578 // --- Health checks (B1) ---
@@ -793,7 +797,7 @@ class PluginManager {
793797 locator_.clear ();
794798 event_bus_.clear ();
795799 load_errors_.clear ();
796- instances_dirty_ = true ;
800+
797801 }
798802
799803 [[nodiscard]] ServiceLocator& locator () { return locator_; }
@@ -802,17 +806,13 @@ class PluginManager {
802806 [[nodiscard]] EventBus& event_bus () { return event_bus_; }
803807 [[nodiscard]] const EventBus& event_bus () const { return event_bus_; }
804808
805- [[nodiscard]] const std::vector<std::shared_ptr<IPlugin>>& instances ()
806- const {
807- if (instances_dirty_) {
808- instances_cache_.clear ();
809- instances_cache_.reserve (plugins_.size ());
810- for (const auto & lp : plugins_) {
811- instances_cache_.push_back (lp.instance );
812- }
813- instances_dirty_ = false ;
809+ [[nodiscard]] std::vector<std::shared_ptr<IPlugin>> instances () const {
810+ std::vector<std::shared_ptr<IPlugin>> result;
811+ result.reserve (plugins_.size ());
812+ for (const auto & lp : plugins_) {
813+ result.push_back (lp.instance );
814814 }
815- return instances_cache_ ;
815+ return result ;
816816 }
817817
818818 // Get a loaded plugin by name. Returns nullptr if not found.
@@ -859,8 +859,6 @@ class PluginManager {
859859 EventBus event_bus_;
860860 std::vector<LoadedPlugin> plugins_;
861861 std::unordered_map<std::string, std::size_t > name_index_;
862- mutable std::vector<std::shared_ptr<IPlugin>> instances_cache_;
863- mutable bool instances_dirty_ = true ;
864862 std::vector<ErrorRecord> load_errors_;
865863 std::vector<PluginObserver*> observers_;
866864
@@ -1148,7 +1146,7 @@ class PluginManager {
11481146 rebuild_name_index ();
11491147 wire_instance_tracked (plugins_.back (), config_map);
11501148 notify_loaded (entry.name , entry.type );
1151- instances_dirty_ = true ;
1149+
11521150 }
11531151
11541152 // Wire all opt-in mixins on an instance.
@@ -1218,18 +1216,43 @@ class PluginManager {
12181216 }
12191217
12201218 // Wire + track EventBus subscription IDs (for enable/disable).
1219+ //
1220+ // Relies on EventBus::next_id_ being monotonically increasing with no
1221+ // reuse: all IDs allocated between [before, after) belong to this plugin.
1222+ // This holds because PluginManager is single-threaded (or exclusively
1223+ // locked via ThreadSafe), and wire_instance may call on_init() /
1224+ // set_event_bus() which subscribe — those subscriptions correctly belong
1225+ // to this plugin. Unsubscribes during wiring don't break the invariant
1226+ // because IDs are never recycled.
12211227 void wire_instance_tracked (LoadedPlugin& lp, const ConfigMap& config_map,
12221228 bool skip_validation = false ) {
12231229 auto before = event_bus_.next_subscription_id ();
12241230 wire_instance (lp.instance , lp.entry , config_map, skip_validation);
12251231 auto after = event_bus_.next_subscription_id ();
12261232
1227- // Record all subscription IDs created during wiring
12281233 lp.subscription_ids .clear ();
12291234 for (auto id = before; id < after; ++id) {
12301235 lp.subscription_ids .push_back (id);
12311236 }
12321237 }
12331238};
12341239
1240+ // --- ObserverGuard inline implementation ---
1241+ // Defined here because it needs PluginManager to be complete.
1242+
1243+ inline ObserverGuard::ObserverGuard (PluginManager& manager,
1244+ PluginObserver* observer)
1245+ : manager_(&manager), observer_(observer) {
1246+ if (observer_) manager_->add_observer (observer_);
1247+ }
1248+
1249+ inline ObserverGuard::~ObserverGuard () { release (); }
1250+
1251+ inline void ObserverGuard::release () {
1252+ if (observer_) {
1253+ manager_->remove_observer (observer_);
1254+ observer_ = nullptr ;
1255+ }
1256+ }
1257+
12351258} // namespace plugin_arch
0 commit comments