Skip to content
This repository was archived by the owner on Feb 6, 2026. It is now read-only.

Commit 22d1106

Browse files
si-bors-ng[bot]jhelwigjobelenus
authored
merge: #3468
3468: Update Components to reflect (un)deletion of Components r=jhelwig a=jhelwig # Update Component inputs/outputs to reflect when a Component is (un)marked as to_delete. Data should be able to flow between components following the rules below: | From Component `to_delete` | To Component `to_delete` | Data Propagates | | -------------------------- | ------------------------ | --------------- | | False | False | Yes | | False | True | Yes | | True | False | No | | True | True | Yes | Whenever a Component is marked (or un-marked) as `to_delete`, we need to make sure that both its inputs, and all downstream Components' inputs are updated to reflect the rules above. This allows us to both show the effect of "deleting" a Component on the rest of the workspace, and keep data that may be necessary to perform the delete action in Components that are marked for deletion. # Enable delete_connection API endpoint Removing an AttributePrototypeArgument will now cause the AttributeValue(s) that use that Prototype to automatically be updated to reflect the new function inputs. Co-authored-by: Jacob Helwig <jacob@technosorcery.net> Co-authored-by: John Obelenus <jobelenus@systeminit.com>
2 parents abcaed7 + f7fab90 commit 22d1106

12 files changed

Lines changed: 1037 additions & 221 deletions

File tree

app/web/src/components/ModelingView/ModelingRightClickMenu.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ const rightClickMenuItems = computed(() => {
8585
});
8686
8787
// single selected component
88-
if (selectedComponent.value.changeStatus === "deleted") {
88+
if (selectedComponent.value.toDelete) {
8989
items.push({
9090
label: `Restore ${typeDisplayName()} "${
9191
selectedComponent.value.displayName

app/web/src/store/components.store.ts

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1096,25 +1096,6 @@ export const useComponentsStore = (forceChangeSetId?: ChangeSetId) => {
10961096
},
10971097
});
10981098
},
1099-
async RESTORE_COMPONENT(componentId: ComponentId) {
1100-
if (changeSetsStore.creatingChangeSet)
1101-
throw new Error("race, wait until the change set is created");
1102-
if (changeSetId === changeSetsStore.headChangeSetId)
1103-
changeSetsStore.creatingChangeSet = true;
1104-
1105-
return new ApiRequest({
1106-
method: "post",
1107-
url: "diagram/restore_component",
1108-
keyRequestStatusBy: componentId,
1109-
params: {
1110-
componentId,
1111-
...visibilityParams,
1112-
},
1113-
onSuccess: (response) => {
1114-
// this.componentDiffsById[componentId] = response.componentDiff;
1115-
},
1116-
});
1117-
},
11181099

11191100
async PASTE_COMPONENTS(
11201101
componentIds: ComponentId[],
@@ -1214,7 +1195,7 @@ export const useComponentsStore = (forceChangeSetId?: ChangeSetId) => {
12141195

12151196
return new ApiRequest({
12161197
method: "post",
1217-
url: "diagram/restore_components",
1198+
url: "diagram/remove_delete_intent",
12181199
keyRequestStatusBy: componentIds,
12191200
params: {
12201201
componentIds,

lib/dal/src/attribute/prototype.rs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
1212
use std::sync::Arc;
1313

14+
use content_node_weight::ContentNodeWeight;
1415
use petgraph::Direction;
1516
use serde::{Deserialize, Serialize};
1617
use si_events::ContentHash;
@@ -27,7 +28,7 @@ use crate::workspace_snapshot::edge_weight::{
2728
EdgeWeight, EdgeWeightError, EdgeWeightKind, EdgeWeightKindDiscriminants,
2829
};
2930
use crate::workspace_snapshot::node_weight::{
30-
NodeWeight, NodeWeightDiscriminants, NodeWeightError,
31+
content_node_weight, NodeWeight, NodeWeightDiscriminants, NodeWeightError,
3132
};
3233
use crate::workspace_snapshot::WorkspaceSnapshotError;
3334
use crate::{
@@ -50,8 +51,12 @@ pub enum AttributePrototypeError {
5051
LayerDb(#[from] LayerDbError),
5152
#[error("attribute prototype {0} is missing a function edge")]
5253
MissingFunction(AttributePrototypeId),
54+
#[error("No attribute values for: {0}")]
55+
NoAttributeValues(AttributePrototypeId),
5356
#[error("node weight error: {0}")]
5457
NodeWeight(#[from] NodeWeightError),
58+
#[error("Attribute Prototype not found: {0}")]
59+
NotFound(AttributePrototypeId),
5560
#[error("transactions error: {0}")]
5661
Transactions(#[from] TransactionsError),
5762
#[error("could not acquire lock: {0}")]
@@ -220,6 +225,40 @@ impl AttributePrototype {
220225
Ok(None)
221226
}
222227

228+
pub async fn get_by_id(
229+
ctx: &DalContext,
230+
prototype_id: AttributePrototypeId,
231+
) -> AttributePrototypeResult<Option<Self>> {
232+
let (_node_weight, content) = Self::get_node_weight_and_content(ctx, prototype_id).await?;
233+
Ok(Some(Self::assemble(prototype_id, &content)))
234+
}
235+
236+
async fn get_node_weight_and_content(
237+
ctx: &DalContext,
238+
prototype_id: AttributePrototypeId,
239+
) -> AttributePrototypeResult<(ContentNodeWeight, AttributePrototypeContentV1)> {
240+
let content_weight = ctx
241+
.workspace_snapshot()?
242+
.get_node_weight_by_id(prototype_id)
243+
.await?;
244+
let prototype_node_weight = content_weight
245+
.get_content_node_weight_of_kind(ContentAddressDiscriminants::AttributePrototype)?;
246+
247+
let content: AttributePrototypeContent = ctx
248+
.layer_db()
249+
.cas()
250+
.try_read_as(&prototype_node_weight.content_hash())
251+
.await?
252+
.ok_or(WorkspaceSnapshotError::MissingContentFromStore(
253+
prototype_id.into(),
254+
))?;
255+
256+
// Do "upgrading" of the storage format from old versions to the latest here.
257+
let AttributePrototypeContent::V1(inner) = content;
258+
259+
Ok((prototype_node_weight, inner))
260+
}
261+
223262
pub async fn update_func_by_id(
224263
ctx: &DalContext,
225264
attribute_prototype_id: AttributePrototypeId,

lib/dal/src/attribute/prototype/argument.rs

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
//! value. It defines source of the value for the function argument in the
44
//! context of the prototype.
55
6+
use std::collections::HashSet;
7+
68
use serde::{Deserialize, Serialize};
79
use telemetry::prelude::*;
810
use thiserror::Error;
@@ -22,8 +24,8 @@ use crate::{
2224
},
2325
WorkspaceSnapshotError,
2426
},
25-
AttributePrototype, AttributePrototypeId, ComponentId, DalContext, OutputSocketId, PropId,
26-
Timestamp, TransactionsError,
27+
AttributePrototype, AttributePrototypeId, AttributeValue, ComponentId, DalContext,
28+
OutputSocketId, PropId, Timestamp, TransactionsError,
2729
};
2830

2931
use self::{
@@ -45,6 +47,8 @@ pk!(AttributePrototypeArgumentId);
4547
pub enum AttributePrototypeArgumentError {
4648
#[error("attribute prototype error: {0}")]
4749
AttributePrototype(#[from] AttributePrototypeError),
50+
#[error("attribute value error: {0}")]
51+
AttributeValue(String),
4852
#[error("change set error: {0}")]
4953
ChangeSet(#[from] ChangeSetPointerError),
5054
#[error("edge weight error: {0}")]
@@ -59,8 +63,12 @@ pub enum AttributePrototypeArgumentError {
5963
LayerDb(#[from] si_layer_cache::LayerDbError),
6064
#[error("attribute prototype argument {0} has no func argument")]
6165
MissingFuncArgument(AttributePrototypeArgumentId),
66+
#[error("attribute prototype argument {0} has no value source")]
67+
MissingSource(AttributePrototypeArgumentId),
6268
#[error("node weight error: {0}")]
6369
NodeWeight(#[from] NodeWeightError),
70+
#[error("no targets for prototype argument: {0}")]
71+
NoTargets(AttributePrototypeArgumentId),
6472
#[error("serde json error: {0}")]
6573
Serde(#[from] serde_json::Error),
6674
#[error("transactions error: {0}")]
@@ -525,9 +533,37 @@ impl AttributePrototypeArgument {
525533
ctx: &DalContext,
526534
apa_id: AttributePrototypeArgumentId,
527535
) -> AttributePrototypeArgumentResult<()> {
536+
let apa = Self::get_by_id(ctx, apa_id).await?;
537+
let prototype_id = apa.prototype_id(ctx).await?;
538+
// Find all of the "destination" attribute values.
539+
let mut avs_to_update = AttributePrototype::attribute_value_ids(ctx, prototype_id).await?;
540+
// If the argument has targets, then we only care about AVs that are for the same
541+
// destination component.
542+
if let Some(targets) = apa.targets() {
543+
let mut av_ids_to_keep = HashSet::new();
544+
for av_id in &avs_to_update {
545+
let component_id = AttributeValue::component_id(ctx, *av_id)
546+
.await
547+
.map_err(|e| AttributePrototypeArgumentError::AttributeValue(e.to_string()))?;
548+
if component_id == targets.destination_component_id {
549+
av_ids_to_keep.insert(*av_id);
550+
}
551+
}
552+
avs_to_update.retain(|av_id| av_ids_to_keep.contains(av_id));
553+
}
554+
555+
// Remove the argument
528556
ctx.workspace_snapshot()?
529557
.remove_node_by_id(ctx.change_set_pointer()?, apa_id)
530558
.await?;
559+
// Update the destination attribute values
560+
for av_id_to_update in &avs_to_update {
561+
AttributeValue::update_from_prototype_function(ctx, *av_id_to_update)
562+
.await
563+
.map_err(|e| AttributePrototypeArgumentError::AttributeValue(e.to_string()))?;
564+
}
565+
// Enqueue a dependent values update with the destination attribute values
566+
ctx.enqueue_dependent_values_update(avs_to_update).await?;
531567

532568
Ok(())
533569
}

lib/dal/src/component.rs

Lines changed: 103 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1199,30 +1199,33 @@ impl Component {
11991199
where
12001200
L: FnOnce(&mut Self) -> ComponentResult<()>,
12011201
{
1202+
let original_component = self.clone();
12021203
let mut component = self;
12031204

12041205
let before = ComponentContentV1::from(component.clone());
12051206
lambda(&mut component)?;
12061207

1207-
let workspace_snapshot = ctx.workspace_snapshot()?;
1208-
let component_idx = workspace_snapshot
1209-
.get_node_index_by_id(component.id())
1210-
.await?;
1211-
let component_node_weight = workspace_snapshot
1212-
.get_node_weight(component_idx)
1213-
.await?
1214-
.get_component_node_weight()?;
1215-
12161208
// The `to_delete` lives on the node itself, not in the content, so we need to be a little
12171209
// more manual when updating that field.
1218-
if component.to_delete != component_node_weight.to_delete() {
1210+
if component.to_delete != original_component.to_delete {
1211+
let component_idx = ctx
1212+
.workspace_snapshot()?
1213+
.get_node_index_by_id(original_component.id)
1214+
.await?;
1215+
let component_node_weight = ctx
1216+
.workspace_snapshot()?
1217+
.get_node_weight(component_idx)
1218+
.await?
1219+
.get_component_node_weight()?;
12191220
let mut new_component_node_weight = component_node_weight
12201221
.new_with_incremented_vector_clock(ctx.change_set_pointer()?)?;
12211222
new_component_node_weight.set_to_delete(component.to_delete);
1222-
workspace_snapshot
1223+
ctx.workspace_snapshot()?
12231224
.add_node(NodeWeight::Component(new_component_node_weight))
12241225
.await?;
1225-
workspace_snapshot.replace_references(component_idx).await?;
1226+
ctx.workspace_snapshot()?
1227+
.replace_references(component_idx)
1228+
.await?;
12261229
}
12271230

12281231
let updated = ComponentContentV1::from(component.clone());
@@ -1237,31 +1240,105 @@ impl Component {
12371240
ctx.events_actor(),
12381241
)
12391242
.await?;
1240-
workspace_snapshot
1243+
ctx.workspace_snapshot()?
12411244
.update_content(ctx.change_set_pointer()?, component.id.into(), hash)
12421245
.await?;
12431246
}
12441247

1248+
let component_node_weight = ctx
1249+
.workspace_snapshot()?
1250+
.get_node_weight_by_id(original_component.id)
1251+
.await?
1252+
.get_component_node_weight()?;
1253+
12451254
Ok(Component::assemble(&component_node_weight, updated))
12461255
}
12471256

12481257
pub async fn delete(self, ctx: &DalContext) -> ComponentResult<Self> {
1249-
self.modify(ctx, |component| {
1250-
component.to_delete = true;
1251-
Ok(())
1252-
})
1253-
.await
1254-
1255-
// TODO: Trigger DependentValuesUpdate for all Components that get data from this
1256-
// Component.
1258+
self.set_to_delete(ctx, true).await
12571259
}
12581260

12591261
pub async fn set_to_delete(self, ctx: &DalContext, to_delete: bool) -> ComponentResult<Self> {
1260-
self.modify(ctx, |component| {
1261-
component.to_delete = to_delete;
1262-
Ok(())
1263-
})
1264-
.await
1262+
let modified = self
1263+
.modify(ctx, |component| {
1264+
component.to_delete = to_delete;
1265+
Ok(())
1266+
})
1267+
.await?;
1268+
1269+
// If we're clearing the `to_delete` flag, we need to make sure our inputs are updated
1270+
// appropriately, as we may have an input connected to a still `to_delete` component, and
1271+
// we should not be using it for input as long as it's still marked `to_delete`.
1272+
//
1273+
// If we're setting the `to_delete` flag, then we may need to pick up inputs from other
1274+
// `to_delete` Components that we were ignoring before.
1275+
//
1276+
// This will update more than is strictly necessary, but it will ensure that everything is
1277+
// correct.
1278+
let input_av_ids: Vec<AttributeValueId> = modified
1279+
.input_socket_attribute_values(ctx)
1280+
.await?
1281+
.values()
1282+
.copied()
1283+
.collect();
1284+
for av_id in &input_av_ids {
1285+
AttributeValue::update_from_prototype_function(ctx, *av_id).await?;
1286+
}
1287+
ctx.enqueue_dependent_values_update(input_av_ids).await?;
1288+
1289+
// We always want to make sure that everything "downstream" of us reacts appropriately
1290+
// regardless of whether we're setting, or clearing the `to_delete` flag.
1291+
//
1292+
// We can't use self.output_socket_attribute_values here, and just enqueue a dependent
1293+
// values update for those IDs, as the DVU explicitly *does not* update a not-to_delete AV,
1294+
// using a source from a to_delete AV, and we want the not-to_delete AVs to be updated to
1295+
// reflect that they're not getting data from this to_delete Component any more.
1296+
let downstream_av_ids = modified.downstream_attribute_value_ids(ctx).await?;
1297+
for av_id in &downstream_av_ids {
1298+
AttributeValue::update_from_prototype_function(ctx, *av_id).await?;
1299+
}
1300+
ctx.enqueue_dependent_values_update(downstream_av_ids)
1301+
.await?;
1302+
1303+
Ok(modified)
1304+
}
1305+
1306+
/// `AttributeValueId`s of all input sockets connected to any output socket of this component.
1307+
async fn downstream_attribute_value_ids(
1308+
&self,
1309+
ctx: &DalContext,
1310+
) -> ComponentResult<Vec<AttributeValueId>> {
1311+
let mut results = Vec::new();
1312+
1313+
let output_sockets: Vec<OutputSocketId> = self
1314+
.output_socket_attribute_values(ctx)
1315+
.await?
1316+
.keys()
1317+
.copied()
1318+
.collect();
1319+
for output_socket_id in output_sockets {
1320+
let output_socket = OutputSocket::get_by_id(ctx, output_socket_id).await?;
1321+
for argument_using_id in output_socket.prototype_arguments_using(ctx).await? {
1322+
let argument_using =
1323+
AttributePrototypeArgument::get_by_id(ctx, argument_using_id).await?;
1324+
if let Some(targets) = argument_using.targets() {
1325+
if targets.source_component_id == self.id() {
1326+
let prototype_id = argument_using.prototype_id(ctx).await?;
1327+
for maybe_downstream_av_id in
1328+
AttributePrototype::attribute_value_ids(ctx, prototype_id).await?
1329+
{
1330+
if AttributeValue::component_id(ctx, maybe_downstream_av_id).await?
1331+
== targets.destination_component_id
1332+
{
1333+
results.push(maybe_downstream_av_id);
1334+
}
1335+
}
1336+
}
1337+
}
1338+
}
1339+
}
1340+
1341+
Ok(results)
12651342
}
12661343
}
12671344

0 commit comments

Comments
 (0)