mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
214 lines
6.2 KiB
Rust
214 lines
6.2 KiB
Rust
use crate::{Attributes, NetworkId, NodeId, ResourceId, TimeStamp, attributes_value_equal};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::borrow::Cow;
|
|
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
pub struct Node {
|
|
pub(crate) implementation: Implementation,
|
|
pub(crate) inputs: Vec<InputSlot>,
|
|
pub(crate) attributes: Attributes,
|
|
pub(crate) network: NetworkId,
|
|
}
|
|
|
|
impl Node {
|
|
pub fn new(implementation: Implementation, inputs: Vec<InputSlot>, attributes: Attributes, network: NetworkId) -> Self {
|
|
Self {
|
|
implementation,
|
|
inputs,
|
|
attributes,
|
|
network,
|
|
}
|
|
}
|
|
|
|
pub fn implementation(&self) -> &Implementation {
|
|
&self.implementation
|
|
}
|
|
pub fn inputs(&self) -> &[InputSlot] {
|
|
&self.inputs
|
|
}
|
|
pub fn attributes(&self) -> &Attributes {
|
|
&self.attributes
|
|
}
|
|
pub fn network(&self) -> NetworkId {
|
|
self.network
|
|
}
|
|
|
|
// Mutable access for edits on a registry clone that is diffed back into deltas;
|
|
// live-session edits go through the delta paths instead.
|
|
pub fn implementation_mut(&mut self) -> &mut Implementation {
|
|
&mut self.implementation
|
|
}
|
|
pub fn inputs_mut(&mut self) -> &mut Vec<InputSlot> {
|
|
&mut self.inputs
|
|
}
|
|
pub fn attributes_mut(&mut self) -> &mut Attributes {
|
|
&mut self.attributes
|
|
}
|
|
pub fn set_network(&mut self, network: NetworkId) {
|
|
self.network = network;
|
|
}
|
|
|
|
/// True if both nodes agree on every value-bearing field, ignoring slot/attribute timestamps.
|
|
pub fn value_equal(&self, other: &Self) -> bool {
|
|
if self.implementation != other.implementation || self.network != other.network {
|
|
return false;
|
|
}
|
|
if self.inputs.len() != other.inputs.len() {
|
|
return false;
|
|
}
|
|
if !self
|
|
.inputs
|
|
.iter()
|
|
.zip(&other.inputs)
|
|
.all(|(a, b)| a.input == b.input && attributes_value_equal(&a.attributes, &b.attributes))
|
|
{
|
|
return false;
|
|
}
|
|
attributes_value_equal(&self.attributes, &other.attributes)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(crate) fn dummy() -> Self {
|
|
Self {
|
|
implementation: Implementation::ProtoNode(ResourceId::new()),
|
|
inputs: vec![],
|
|
attributes: Attributes::new(),
|
|
network: crate::ROOT_NETWORK,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// One positional input. The timestamp drives LWW on concurrent `ChangeNodeInput` ops targeting
|
|
/// the same `(node_id, input_idx)`.
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
pub struct InputSlot {
|
|
pub input: NodeInput,
|
|
pub timestamp: TimeStamp,
|
|
pub attributes: Attributes,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
pub enum NodeInput {
|
|
Node {
|
|
id: NodeId,
|
|
index: u32,
|
|
},
|
|
Value {
|
|
value: serde_json::Value,
|
|
exposed: bool,
|
|
},
|
|
Scope(Cow<'static, str>),
|
|
Import {
|
|
index: u32,
|
|
},
|
|
/// Marker; the `DocumentNodeMetadata` lives in `inputs_attributes`.
|
|
Reflection,
|
|
Other,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
pub enum Implementation {
|
|
/// References a proto-node declaration resource (see [`ProtoNode`]); the binding to content lives
|
|
/// in `Registry.resources` like any other resource.
|
|
ProtoNode(ResourceId),
|
|
Network(NetworkId),
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
|
pub struct Network {
|
|
pub exports: Vec<ExportSlot>,
|
|
/// Per-network `ui::*` state (navigation, previewing). Separate from `Node.attributes` so
|
|
/// view-state edits LWW independently.
|
|
pub attributes: Attributes,
|
|
}
|
|
|
|
impl Network {
|
|
/// True if both networks agree on every value-bearing field, ignoring slot/attribute timestamps.
|
|
pub fn value_equal(&self, other: &Self) -> bool {
|
|
// Compare slot targets index-by-index, treating out-of-range slots as `None`. A `SetExport(None)`
|
|
// truncation leaves a trailing empty slot (a tombstone in the CRDT state) that is value-equal to
|
|
// the slot being absent, so trailing `None`s must not count as drift. Mirrors `compute_deltas`
|
|
// (emits nothing for them) and `to_runtime` (drops them).
|
|
let max_len = self.exports.len().max(other.exports.len());
|
|
for slot_idx in 0..max_len {
|
|
let self_target = self.exports.get(slot_idx).and_then(|slot| slot.target.as_ref());
|
|
let other_target = other.exports.get(slot_idx).and_then(|slot| slot.target.as_ref());
|
|
if self_target != other_target {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
attributes_value_equal(&self.attributes, &other.attributes)
|
|
}
|
|
}
|
|
|
|
/// One positional export slot. `target == None` marks an empty/removed slot. Timestamp drives LWW
|
|
/// on concurrent `SetExport` ops.
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
pub struct ExportSlot {
|
|
pub target: Option<NodeInput>,
|
|
pub timestamp: TimeStamp,
|
|
}
|
|
|
|
/// Content of a proto-node declaration. Stored as a content-addressed resource (serialized bytes
|
|
/// keyed by `ResourceHash`, held by the `Gdd` byte store) and referenced from
|
|
/// `Implementation::ProtoNode(ResourceId)`. `graph-storage` itself only holds the reference.
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
pub struct ProtoNode {
|
|
pub identifier: String,
|
|
pub attributes: Attributes,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::TimeStamp;
|
|
|
|
fn target_slot(node_id: u64) -> ExportSlot {
|
|
ExportSlot {
|
|
target: Some(NodeInput::Node { id: NodeId(node_id), index: 0 }),
|
|
timestamp: TimeStamp::ORIGIN,
|
|
}
|
|
}
|
|
|
|
fn empty_slot() -> ExportSlot {
|
|
ExportSlot {
|
|
target: None,
|
|
timestamp: TimeStamp { counter: 5, peer: crate::PeerId(1) },
|
|
}
|
|
}
|
|
|
|
/// A `SetExport(None)` truncation leaves a trailing empty slot. Such a network is value-equal to
|
|
/// the same network without that slot, so the soak oracle doesn't false-report drift.
|
|
#[test]
|
|
fn trailing_empty_export_slot_is_value_equal() {
|
|
let compact = Network {
|
|
exports: vec![target_slot(1), target_slot(2)],
|
|
..Default::default()
|
|
};
|
|
let with_trailing_empty = Network {
|
|
exports: vec![target_slot(1), target_slot(2), empty_slot()],
|
|
..Default::default()
|
|
};
|
|
|
|
assert!(compact.value_equal(&with_trailing_empty));
|
|
assert!(with_trailing_empty.value_equal(&compact));
|
|
}
|
|
|
|
/// A `None` slot *between* live targets is a real value difference (a hole), not a trailing
|
|
/// tombstone, so it must still count as drift.
|
|
#[test]
|
|
fn interior_empty_export_slot_is_not_value_equal() {
|
|
let dense = Network {
|
|
exports: vec![target_slot(1), target_slot(2)],
|
|
..Default::default()
|
|
};
|
|
let with_hole = Network {
|
|
exports: vec![target_slot(1), empty_slot(), target_slot(2)],
|
|
..Default::default()
|
|
};
|
|
|
|
assert!(!dense.value_equal(&with_hole));
|
|
}
|
|
}
|