Skip to content

Commit 6155803

Browse files
rsthorntonclaude
andcommitted
fix(save): persist internal N-network flows via deferred processing
Internal flows (interface→subsystem, subsystem↔subsystem) previously caused silent panics or were silently dropped because save.rs processed flows before subsystems were registered in entity_to_id. Solution: Deferred flow pattern - Add DeferredFlow struct to capture flows with unregistered targets - Early-exit check in flow loop detects unregistered subsystem targets - process_deferred_flows() runs after all subsystems registered - Reuses existing build_interaction() for consistent JSON output The fix is additive - doesn't restructure existing phases, preserving the implicit contract between save.rs and load.rs. Fixes: interface→subsystem flow save crash Fixes: direct subsystem↔subsystem flow persistence 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 6af61a0 commit 6155803

1 file changed

Lines changed: 83 additions & 0 deletions

File tree

src/bevy_app/data_model/save.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ use tauri_sys::core::invoke;
1212
use wasm_bindgen::JsCast;
1313
use web_sys::{Blob, HtmlAnchorElement, Url};
1414

15+
/// Flow deferred because target subsystem wasn't registered yet
16+
struct DeferredFlow {
17+
flow_entity: Entity,
18+
system_entity: Entity, // The system context we were processing
19+
}
20+
1521
/// Context for bookkeeping while we traverse the ECS and build the data model that is serialized.
1622
struct Context {
1723
/// Remember how many objects of the given type and the index list of the parent have been created.
@@ -23,6 +29,8 @@ struct Context {
2329
interactions: Vec<Interaction>,
2430
/// Map bevy entities to their index in `interactions`.
2531
entity_to_interaction_idx: HashMap<Entity, usize>,
32+
/// Flows deferred because target subsystem wasn't registered yet
33+
deferred_flows: Vec<DeferredFlow>,
2634
}
2735

2836
impl Context {
@@ -32,6 +40,7 @@ impl Context {
3240
entity_to_id: HashMap::new(),
3341
interactions: vec![],
3442
entity_to_interaction_idx: HashMap::new(),
43+
deferred_flows: Vec::new(),
3544
}
3645
}
3746

@@ -354,6 +363,14 @@ pub fn serialize_world(
354363
&mut entity_to_system,
355364
);
356365

366+
// Phase 4: Process deferred internal flows (subsystems now registered)
367+
process_deferred_flows(
368+
&mut ctx,
369+
&entity_to_system,
370+
&name_and_description_query,
371+
&flow_query,
372+
);
373+
357374
// Add the source and sink interface connections to all interactions after all
358375
// interfaces and interactions have been created.
359376
for (flow_entity, _, _, _, flow_start_interface_connection, flow_end_interface_connection) in
@@ -551,6 +568,52 @@ fn build_subsystems(
551568
}
552569
}
553570

571+
/// Process flows that were deferred because their target subsystem wasn't registered yet.
572+
/// Called after all subsystems are built, so all entity IDs are now available.
573+
fn process_deferred_flows(
574+
ctx: &mut Context,
575+
entity_to_system: &HashMap<Entity, System>,
576+
name_and_description_query: &Query<(&Name, &ElementDescription)>,
577+
flow_query: &Query<(
578+
Entity,
579+
&Flow,
580+
&FlowStartConnection,
581+
&FlowEndConnection,
582+
Option<&FlowStartInterfaceConnection>,
583+
Option<&FlowEndInterfaceConnection>,
584+
)>,
585+
) {
586+
let deferred = std::mem::take(&mut ctx.deferred_flows);
587+
588+
for DeferredFlow {
589+
flow_entity,
590+
system_entity,
591+
} in deferred
592+
{
593+
if let Ok((_, flow, flow_start_connection, flow_end_connection, _, _)) =
594+
flow_query.get(flow_entity)
595+
{
596+
// Now all subsystems are registered - get the IDs
597+
let source_id = ctx.entity_to_id[&flow_start_connection.target].clone();
598+
let sink_id = ctx.entity_to_id[&flow_end_connection.target].clone();
599+
600+
// Find the parent system for this interaction (the system we were processing)
601+
if let Some(parent_system) = entity_to_system.get(&system_entity) {
602+
// Reuse existing build_interaction function
603+
build_interaction(
604+
ctx,
605+
flow_entity,
606+
flow,
607+
parent_system,
608+
source_id,
609+
sink_id,
610+
name_and_description_query,
611+
);
612+
}
613+
}
614+
}
615+
}
616+
554617
/// Create all interfaces that are part of the given system together with connected
555618
/// interactions and external entities.
556619
fn build_interfaces_interaction_and_external_entities<P: HasInfo + HasSourcesAndSinks>(
@@ -584,6 +647,26 @@ fn build_interfaces_interaction_and_external_entities<P: HasInfo + HasSourcesAnd
584647
flow_end_interface_connection,
585648
) in flow_query
586649
{
650+
// Check if this flow connects to an unregistered subsystem - defer if so
651+
// This handles internal flows where the other end is a subsystem not yet registered
652+
let start_is_unregistered_subsystem =
653+
matches!(flow_start_connection.target_type, StartTargetType::System)
654+
&& flow_start_connection.target != system_entity
655+
&& !ctx.entity_to_id.contains_key(&flow_start_connection.target);
656+
657+
let end_is_unregistered_subsystem =
658+
matches!(flow_end_connection.target_type, EndTargetType::System)
659+
&& flow_end_connection.target != system_entity
660+
&& !ctx.entity_to_id.contains_key(&flow_end_connection.target);
661+
662+
if start_is_unregistered_subsystem || end_is_unregistered_subsystem {
663+
ctx.deferred_flows.push(DeferredFlow {
664+
flow_entity,
665+
system_entity,
666+
});
667+
continue;
668+
}
669+
587670
// if it's connected at the start to this system ...
588671
if flow_start_connection.target == system_entity {
589672
// ... first we build the start interface ...

0 commit comments

Comments
 (0)