Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 7 additions & 15 deletions document/format/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -292,24 +292,16 @@ impl<L: Layout> Gdd<L> {
(working, self.layout)
}

/// Resolve the proto-node declarations referenced by the registry into a [`document_graph_storage::Declarations`]
/// map, loading each `ProtoNode`'s bytes from `byte_store` (the global cache in the editor, the
/// working-copy container for standalone). Only resources referenced by `Implementation::ProtoNode`
/// are visited, so image/font resources are skipped. Cold-path (open / `to_runtime`); async
/// because resource loads are.
/// Resolve every proto-node declaration referenced by the registry or its history into a
/// [`document_graph_storage::Declarations`] map, loading each `ProtoNode`'s bytes from `byte_store`
/// (the global cache in the editor, the working-copy container for standalone). Covers history so
/// the map can serve any undo/redo target. Cold-path (mount / open); async because resource loads are.
#[cfg(feature = "conversion")]
pub async fn declarations(&self, byte_store: &dyn LoadResource) -> document_graph_storage::Declarations {
use document_graph_storage::Implementation;

let registry = self.session.registry();
let mut declarations = document_graph_storage::Declarations::new();

for node in registry.node_instances.values() {
let Implementation::ProtoNode(id) = node.implementation() else { continue };
if declarations.contains_key(id) {
continue;
}
let Some(hash) = registry.resources.get(id).and_then(|entry| entry.hash) else {
for (id, hash) in self.session.all_declaration_resources() {
let Some(hash) = hash else {
log::error!("Declaration resource {id} has no resolved hash; cannot load ProtoNode");
continue;
};
Expand All @@ -319,7 +311,7 @@ impl<L: Layout> Gdd<L> {
};
match document_graph_storage::decode_declaration(resource.as_ref()) {
Ok(proto) => {
declarations.insert(*id, proto);
declarations.insert(id, proto);
}
Err(error) => log::error!("Failed to deserialize ProtoNode for {id}: {error}"),
}
Expand Down
10 changes: 5 additions & 5 deletions document/format/src/persist.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ impl<L: Layout> Gdd<L> {
/// declaration bytes. The working registry reflects the edit immediately, but nothing enters durable
/// retired history until [`retire_pending_interaction`](Self::retire_pending_interaction). Staging on
/// every edit while retiring only at interaction boundaries lets several edits coalesce into one retired
/// interaction.
/// interaction. Returns the decoded declarations the snapshot references, for the caller's cache.
///
/// # Errors
/// [`Error::Commit`] if the runtime diff is rejected by the session. On an [`Error::Container`] /
Expand All @@ -60,8 +60,8 @@ impl<L: Layout> Gdd<L> {
metadata: &M,
resources: &graphene_resource::ResourceRegistry,
byte_store: &dyn ResourceStorage,
) -> Result<(), Error> {
let (hot_ops, declaration_bytes) = self.session.stage_from_runtime(network, metadata, resources)?;
) -> Result<document_graph_storage::Declarations, Error> {
let (hot_ops, conversion) = self.session.stage_from_runtime(network, metadata, resources)?;

for hot_op in &hot_ops {
self.append_hot_frame(hot_op)?;
Expand All @@ -70,10 +70,10 @@ impl<L: Layout> Gdd<L> {
// Persist proto-node declaration content to the byte store (the global cache in the editor,
// the working-copy container for standalone export). Content-addressed, so re-storing
// identical bytes on every commit is an idempotent no-op.
for bytes in declaration_bytes.values() {
for bytes in conversion.declaration_bytes.values() {
byte_store.store(bytes);
}
Ok(())
Ok(conversion.declarations)
}

/// Retire every pending hot op into durable history as a single interaction (marking the batch's last
Expand Down
27 changes: 8 additions & 19 deletions document/graph-storage/src/from_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,33 +89,17 @@ impl TryFrom<&NodeNetwork> for Registry {
pub type DeclarationBytes = HashMap<ResourceHash, Vec<u8>>;

/// A `from_runtime` conversion result: the reference-only [`Registry`] plus the proto-node
/// declaration *bytes* it extracted, keyed by content hash. `document-graph-storage` doesn't own a byte
/// store, so the caller (the `Gdd`) persists these into its content store; the registry only holds
/// the `ResourceId`/`ResourceHash` references.
/// declarations it extracted, as bytes keyed by content hash (for the caller's byte store) and as
/// decoded [`ProtoNode`]s keyed by id (for the caller's declaration cache).
pub struct RuntimeConversion {
pub registry: Registry,
pub declaration_bytes: DeclarationBytes,
pub declarations: crate::Declarations,
/// Each network's runtime `metadata_path` mapped to its stable storage `NetworkId`, for associating
/// per-network, per-peer view state (`session.json`) without re-deriving ids.
pub network_ids: HashMap<Vec<RuntimeNodeId>, NetworkId>,
}

impl RuntimeConversion {
/// Rebuild the [`Declarations`](crate::Declarations) map (`ResourceId` → [`ProtoNode`]) from the
/// extracted bytes, for callers that keep the bytes in hand instead of routing them through a
/// byte store (tests, the round-trip CLI). Editor/`Gdd` paths persist the bytes and resolve via
/// their byte store instead.
pub fn declarations(&self) -> Result<crate::Declarations, ConversionError> {
self.declaration_bytes
.iter()
.map(|(hash, bytes)| {
let proto = decode_declaration(bytes).map_err(|error| ConversionError::SerializationError(format!("declaration {hash}: {error}")))?;
Ok((ResourceId::from_hash(hash), proto))
})
.collect()
}
}

/// Encode a [`ProtoNode`] declaration to its content-addressed bytes: through a self-describing
/// `serde_json::Value` (so serde aliases keep working and the on-disk shape stays migratable), then
/// rmp-serialized (which encodes the intermediate `Value` compactly). Paired with [`decode_declaration`].
Expand Down Expand Up @@ -149,6 +133,7 @@ impl Registry {
let mut ctx = ConversionContext {
declaration_ids: HashMap::new(),
declaration_bytes: HashMap::new(),
declarations: HashMap::new(),
network_ids: HashMap::new(),
metadata,
peer,
Expand All @@ -167,6 +152,7 @@ impl Registry {
Ok(RuntimeConversion {
registry,
declaration_bytes: ctx.declaration_bytes,
declarations: ctx.declarations,
network_ids: ctx.network_ids,
})
}
Expand Down Expand Up @@ -254,6 +240,8 @@ struct ConversionContext<'m, M: NodeMetadataSource + ?Sized> {
declaration_ids: HashMap<String, ResourceId>,
/// Extracted declaration content keyed by hash, handed back for the caller's byte store.
declaration_bytes: DeclarationBytes,
/// The same declarations decoded, keyed by id, handed back for the caller's declaration cache.
declarations: crate::Declarations,
/// Maps each network's runtime `metadata_path` to its stable storage `NetworkId`, so the caller can
/// associate per-network, per-peer view state (in `session.json`) with networks without re-deriving ids.
network_ids: HashMap<Vec<RuntimeNodeId>, NetworkId>,
Expand Down Expand Up @@ -555,6 +543,7 @@ fn convert_implementation<M: NodeMetadataSource + ?Sized>(

register_declaration_resource(registry, id, hash, ctx.peer);
ctx.declaration_bytes.insert(hash, bytes);
ctx.declarations.insert(id, proto);
ctx.declaration_ids.insert(identifier_str, id);

Implementation::ProtoNode(id)
Expand Down
43 changes: 38 additions & 5 deletions document/graph-storage/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
use crate::NodeMetadataSource;
#[cfg(any(feature = "conversion", test))]
use crate::from_runtime;
use crate::{ApplyMode, Delta, Document, History, LamportClock, NetworkId, NodeId, PeerId, Registry, RegistryDelta, RegistryTarget, ResourceEntry, Rev, TimeStamp, UserId};
use crate::{ApplyMode, Delta, Document, History, Implementation, LamportClock, NetworkId, NodeId, PeerId, Registry, RegistryDelta, RegistryTarget, ResourceEntry, Rev, TimeStamp, UserId};
use graphene_resource::{ResourceHash, ResourceId};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
Expand Down Expand Up @@ -64,8 +64,7 @@ impl Session {
/// Diff the current registry against a fresh conversion of `network`, then commit each emitted
/// op as its own `Delta` on the local chain. One `clock.tick()` per op (strictly causal within
/// a commit). Returns the new `Rev`s in commit order (empty if nothing changed) plus the
/// proto-node declaration bytes the conversion extracted, keyed by content hash, for the caller
/// to persist into its byte store (`document-graph-storage` itself is byte-unaware).
/// conversion, whose extracted declarations the caller persists and caches.
///
/// Stages the diff as hot ops rather than retired deltas: each op is applied to the registry and
/// pushed onto the hot log. The caller persists the returned hot frames and then calls `retire`
Expand All @@ -76,11 +75,11 @@ impl Session {
network: &graph_craft::document::NodeNetwork,
metadata: &M,
resources: &graphene_resource::ResourceRegistry,
) -> Result<(Vec<HotOp>, from_runtime::DeclarationBytes), CommitError> {
) -> Result<(Vec<HotOp>, from_runtime::RuntimeConversion), CommitError> {
let conversion = Registry::convert_from_runtime(network, metadata, resources, self.document.peer)?;
let ops = crate::delta::compute_deltas(&self.document.working_registry, &conversion.registry);
let hot_ops = self.stage_ops(ops)?;
Ok((hot_ops, conversion.declaration_bytes))
Ok((hot_ops, conversion))
}

/// Resolve each runtime `network_path` to its stable [`NetworkId`] for this document's peer, so the
Expand Down Expand Up @@ -473,6 +472,40 @@ impl Session {
hashes
}

/// Every proto-node declaration resource referenced by the current registry or anywhere in history,
/// with its content hash, or `None` where the hash never resolved.
pub fn all_declaration_resources(&self) -> HashMap<ResourceId, Option<ResourceHash>> {
let registry = &self.document.working_registry;

let mut hashes: HashMap<ResourceId, ResourceHash> = registry.resources.iter().filter_map(|(id, entry)| Some((*id, entry.hash?))).collect();
for delta in self.document.history.iter() {
match &delta.kind {
RegistryDelta::AddResource { id, entry } => hashes.extend(entry.hash.map(|hash| (*id, hash))),
RegistryDelta::RemoveResource { id, snapshot } => hashes.extend(snapshot.hash.map(|hash| (*id, hash))),
RegistryDelta::SetResourceHash { id, hash: Some(hash) } => {
hashes.insert(*id, *hash);
}
_ => {}
}
}

let current_nodes = registry.node_instances.values();
let historic_nodes = self.document.history.iter().filter_map(|delta| match &delta.kind {
RegistryDelta::AddNode { node, .. } => Some(node),
RegistryDelta::RemoveNode { snapshot, .. } => Some(snapshot),
_ => None,
});

current_nodes
.chain(historic_nodes)
.filter_map(|node| match node.implementation() {
Implementation::ProtoNode(id) => Some(*id),
Implementation::Network(_) => None,
})
.map(|id| (id, hashes.get(&id).copied()))
.collect()
}

pub fn hot_log(&self) -> &[HotOp] {
&self.document.hot_log
}
Expand Down
8 changes: 3 additions & 5 deletions document/graph-storage/src/tests/round_trip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,10 @@ fn verify_network_compiles(network: &NodeNetwork) -> Result<(), String> {

/// Convert a runtime network to a storage `Registry`, returning the declarations alongside it.
/// Proto-node declaration content is no longer stored in the registry (it lives in a byte store);
/// these tests have no byte store, so they keep the extracted bytes in hand and rebuild a
/// `Declarations` map for the back-conversion.
/// these tests have no byte store, so they keep the extracted `Declarations` in hand for the back-conversion.
fn to_registry(network: &NodeNetwork) -> (Registry, crate::Declarations) {
let conversion = Registry::convert_from_runtime(network, &crate::NoMetadata, &Default::default(), PeerId(0)).expect("Failed to convert NodeNetwork to Registry");
let declarations = conversion.declarations().expect("rebuild declarations");
(conversion.registry, declarations)
(conversion.registry, conversion.declarations)
}

/// A one-node network whose single node references `id` via a `TaggedValue::Resource` input, so
Expand Down Expand Up @@ -477,7 +475,7 @@ fn test_ui_metadata_round_trip() {
);

let conversion = Registry::convert_from_runtime(&network, &metadata, &Default::default(), PeerId(0)).expect("Failed to convert to Registry with metadata");
let declarations = conversion.declarations().expect("rebuild declarations");
let declarations = conversion.declarations;
let registry = conversion.registry;

let (converted, entries) = registry.to_runtime_with_metadata(&declarations).expect("Failed to convert Registry back with metadata");
Expand Down
78 changes: 53 additions & 25 deletions editor/src/messages/portfolio/document/document_history.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use std::collections::VecDeque;
use std::collections::{BTreeMap, HashSet};

use document_graph_storage::Registry;
use document_graph_storage::{Declarations, Registry};
use graph_craft::application_io::resource::{ResourceId, ResourceRegistry, ResourceStorage};

use super::utility_types::network_interface::NodeNetworkInterface;
use super::utility_types::network_interface::storage_metadata::{StorageMetadataView, collect_network_view_settings};
use super::utility_types::network_interface::storage_metadata::{StorageMetadataView, build_interface_from_storage, collect_network_view_settings};

/// Per-document undo/redo state: the legacy snapshot stacks plus the `Gdd` working-copy cursor that is
/// becoming the authoritative history. Owns the dual-stack bookkeeping push/pop/clear and the cursor's stage/retire/move/verify
Expand All @@ -24,6 +24,19 @@ pub struct DocumentHistory {
/// future built by `load_document` resolves.
#[derivative(Debug = "ignore")]
storage: Option<document_format::GddV1>,
/// Decoded proto-node declarations for every registry state the cursor can reach, filled at mount
/// and extended on each staging, so a cursor rebuild never touches the byte store.
#[derivative(Debug = "ignore")]
declarations: Declarations,
}

/// Why [`DocumentHistory::move_cursor`] produced no interface.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CursorMoveError {
/// Nothing to move to, unmounted, or the move itself failed. The cursor did not move.
NotMoved,
/// The cursor moved but the rebuild from it failed. It stays moved unless the caller reverts it.
RebuildFailed,
}

impl DocumentHistory {
Expand Down Expand Up @@ -79,9 +92,10 @@ impl DocumentHistory {
self.storage.as_mut()
}

/// Attach (or clear) the `Gdd` working copy once the mount future resolves.
pub fn set_storage(&mut self, storage: Option<document_format::GddV1>) {
self.storage = storage;
/// Attach the `Gdd` working copy once the mount future resolves, with the declarations it references.
pub fn set_storage(&mut self, storage: document_format::GddV1, declarations: Declarations) {
self.storage = Some(storage);
self.declarations = declarations;
}

/// Retire the pending staged hot ops into durable Gdd history as one undo unit. Called at each undo-step
Expand Down Expand Up @@ -119,9 +133,12 @@ impl DocumentHistory {

// Stage without retiring: a tool drag fires several `CommitTransaction`s but is one legacy undo
// step, so the deltas accumulate as hot ops and coalesce at the next undo-step boundary.
if let Err(error) = storage.stage_runtime_snapshot(network, &metadata_view, registry, byte_store) {
log::error!("Storage snapshot staging failed: {error}");
return;
match storage.stage_runtime_snapshot(network, &metadata_view, registry, byte_store) {
Ok(declarations) => self.declarations.extend(declarations),
Comment thread
TrueDoctor marked this conversation as resolved.
Err(error) => {
log::error!("Storage snapshot staging failed: {error}");
return;
}
}

if let Err(error) = storage.set_view_settings(view_settings) {
Expand All @@ -142,31 +159,48 @@ impl DocumentHistory {
}

/// Move the `Gdd` undo/redo cursor along the retired interaction chain, flushing any open interaction
/// first. Returns a clone of the post-move `Gdd` (`Arc`-shared) so a `'static` rebuild future can read
/// the rewound state while the live document keeps its cursor. `None` when there is nothing to move to,
/// unmounted, or the move failed.
pub fn move_cursor(&mut self, undo: bool) -> Option<document_format::GddV1> {
/// first, and rebuild the interface from the rewound registry using the declaration cache.
pub fn move_cursor(&mut self, undo: bool) -> Result<NodeNetworkInterface, CursorMoveError> {
self.retire_storage_interaction();

let storage = self.storage.as_mut()?;
let storage = self.storage.as_mut().ok_or(CursorMoveError::NotMoved)?;

let moved = if undo {
if !storage.can_undo() {
return None;
return Err(CursorMoveError::NotMoved);
}
storage.undo().map(|_| ())
} else {
if !storage.can_redo() {
return None;
return Err(CursorMoveError::NotMoved);
}
storage.redo().map(|_| ())
};
if let Err(error) = moved {
log::error!("Storage undo/redo cursor move failed: {error}");
return None;
return Err(CursorMoveError::NotMoved);
}

Some(storage.clone())
storage
.registry()
.to_runtime_with_full_metadata(&self.declarations)
.map_err(|error| error.to_string())
.and_then(|(network, node_entries, network_entries)| build_interface_from_storage(network, node_entries, network_entries).map_err(|error| error.to_string()))
.map_err(|error| {
log::error!("Storage undo/redo rebuild failed: {error}");
CursorMoveError::RebuildFailed
})
}

/// Step the cursor back the other way, undoing a [`move_cursor`](Self::move_cursor) in the `undo`
/// direction whose rebuild failed.
pub fn revert_cursor(&mut self, undo: bool) {
let Some(storage) = self.storage.as_mut() else { return };

let reverted = if undo { storage.redo() } else { storage.undo() };
if let Err(error) = reverted {
log::error!("Storage undo/redo cursor revert failed: {error}");
}
}

// Soak round-trip verification (runtime-gated by `validate_storage_round_trip`)
Expand All @@ -193,13 +227,7 @@ impl DocumentHistory {
}
};
let target = &conversion.registry;
let declarations = match conversion.declarations() {
Ok(declarations) => declarations,
Err(error) => {
log::error!("storage round-trip: declaration rebuild failed: {error}");
return;
}
};
let declarations = &conversion.declarations;

let stored = storage.registry();
if !stored.value_equal(target) {
Expand All @@ -213,7 +241,7 @@ impl DocumentHistory {
panic!("storage round-trip: timestamp order inconsistent between stored and target");
}

let (round_tripped, _entries) = match stored.to_runtime_with_metadata(&declarations) {
let (round_tripped, _entries) = match stored.to_runtime_with_metadata(declarations) {
Ok(result) => result,
Err(error) => {
log::error!("storage round-trip: to_runtime failed: {error}");
Expand Down
Loading
Loading