@@ -89,13 +89,77 @@ namespace hgraph {
8989 void ReduceNode::eval () {
9090 mark_evaluated ();
9191
92- auto &key_set = ts ()->key_set ();
92+ auto tsd = ts ();
93+ auto &key_set = tsd->key_set ();
9394
9495 // Process removals first, then additions
95- // Build vectors from Value-based iteration
9696 auto removed_keys = key_set.collect_removed ();
9797 remove_nodes_from_views (removed_keys);
9898
99+ // Check for stale REF values: when the upstream REF becomes empty,
100+ // the accessor doesn't explicitly remove keys (only modified_items are processed).
101+ // We need to detect keys bound to empty/invalid REF values and remove them.
102+ bool accessor_input_empty = false ;
103+ if (tsd->has_output () && tsd->output ()) {
104+ auto tsd_output = tsd->output ();
105+ if (tsd_output->has_owning_node ()) {
106+ auto accessor_node = tsd_output->owning_node ();
107+ auto accessor_input = accessor_node->input ();
108+ if (accessor_input) {
109+ for (size_t i = 0 ; i < accessor_input->size (); ++i) {
110+ auto input_item = (*accessor_input)[i];
111+ // Check if accessor input is a REF with empty value
112+ if (auto ref_input = dynamic_cast <TimeSeriesReferenceInput*>(input_item.get ())) {
113+ if (ref_input->value ().is_empty ()) {
114+ accessor_input_empty = true ;
115+ break ;
116+ }
117+ }
118+ // Check if accessor input is a TSD with no output binding
119+ if (auto tsd_input = dynamic_cast <TimeSeriesDictInput*>(input_item.get ())) {
120+ if (!tsd_input->has_output ()) {
121+ accessor_input_empty = true ;
122+ break ;
123+ }
124+ }
125+ }
126+ }
127+ }
128+ }
129+
130+ if (!bound_node_indexes_.empty ()) {
131+ std::vector<value::ConstValueView> stale_keys;
132+
133+ // If the TSD has no output OR accessor's input is empty/unbound, all keys are stale
134+ if (!tsd->has_output () || accessor_input_empty) {
135+ for (const auto & [key, ndx] : bound_node_indexes_) {
136+ stale_keys.push_back (key.const_view ());
137+ }
138+ } else {
139+ // Check individual keys for stale REF values
140+ for (const auto & [key, ndx] : bound_node_indexes_) {
141+ if (tsd->contains (key.const_view ())) {
142+ auto ts_ptr = (*tsd)[key.const_view ()];
143+ if (auto ref_input = dynamic_cast <TimeSeriesReferenceInput*>(ts_ptr.get ())) {
144+ auto ref_value = ref_input->value ();
145+ bool has_out = ref_value.has_output ();
146+ bool out_valid = has_out && ref_value.output ()->valid ();
147+ if (ref_value.is_empty () || (has_out && !out_valid)) {
148+ stale_keys.push_back (key.const_view ());
149+ }
150+ }
151+ } else {
152+ // Key in bound_node_indexes_ but not in TSD - was removed elsewhere
153+ stale_keys.push_back (key.const_view ());
154+ }
155+ }
156+ }
157+
158+ if (!stale_keys.empty ()) {
159+ remove_nodes_from_views (stale_keys);
160+ }
161+ }
162+
99163 auto added_keys = key_set.collect_added ();
100164 add_nodes_from_views (added_keys);
101165
0 commit comments