From 8d8393100dfd509c21390d2f1d6996ee3ba51262 Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Sun, 1 Nov 2026 01:36:25 +0000 Subject: [PATCH] Mint a fresh peer when the user changes --- document/format/src/error.rs | 4 ++ document/format/src/lib.rs | 10 +-- document/format/src/persist.rs | 16 ++++- document/format/tests/deterministic_save.rs | 4 +- document/format/tests/history_metadata.rs | 2 +- document/format/tests/migration_costs.rs | 6 +- document/format/tests/migration_recovery.rs | 4 +- document/format/tests/migrations.rs | 4 +- document/format/tests/open_create.rs | 48 ++++++------- document/format/tests/staging_and_reopen.rs | 67 +++++++++++++++---- document/format/tests/staging_broadcast.rs | 12 +++- .../graph-storage/src/schema/session_state.rs | 2 +- document/graph-storage/src/session.rs | 13 ++++ .../graph-storage/src/tests/bookkeeping.rs | 51 +++++++++++++- .../storage_tests/round_trip_tests.rs | 14 ++-- .../document/storage_tests/test_support.rs | 4 +- .../messages/portfolio/document_storage_io.rs | 7 +- node-graph/graphene-cli/src/lib.rs | 1 - 18 files changed, 200 insertions(+), 69 deletions(-) diff --git a/document/format/src/error.rs b/document/format/src/error.rs index af0c3d69fc4..cca37d89066 100644 --- a/document/format/src/error.rs +++ b/document/format/src/error.rs @@ -58,4 +58,8 @@ pub enum Error { #[cfg(feature = "network")] #[error("network error: {0}")] Network(#[from] peer_transport::PacketError), + /// The room knows this device by its peer, so the user can't change. + #[cfg(feature = "network")] + #[error("the user cannot change while the document is connected")] + UserChangeWhileConnected, } diff --git a/document/format/src/lib.rs b/document/format/src/lib.rs index 8b69aea6632..8b7596ab479 100644 --- a/document/format/src/lib.rs +++ b/document/format/src/lib.rs @@ -186,10 +186,10 @@ impl Gdd { /// Create a fresh, empty working copy at `path` bound to `peer`. Writes a default manifest /// and session state; the caller fills in editor metadata via [`Gdd::update_manifest`]. - pub fn create(path: &Path, peer: PeerId, document_uuid: u64, editor_version: String, stdlib_version: String) -> Result { + pub fn create(path: &Path, peer: PeerId, user: UserId, document_uuid: u64, editor_version: String, stdlib_version: String) -> Result { let working = AnyContainer::Folder(FolderBackend::create(path)?); let layout = L::default(); - Self::create_in(working, layout, peer, document_uuid, editor_version, stdlib_version) + Self::create_in(working, layout, peer, user, document_uuid, editor_version, stdlib_version) } } @@ -344,18 +344,18 @@ impl Gdd { /// Backend-agnostic create. Records the working-copy default codecs (see `DEFAULT_*_CODEC`) in /// the manifest and writes each payload with its recorded codec. - pub fn create_in(working: AnyContainer, layout: L, peer: PeerId, document_uuid: u64, editor_version: String, stdlib_version: String) -> Result { + pub fn create_in(working: AnyContainer, layout: L, peer: PeerId, user: UserId, document_uuid: u64, editor_version: String, stdlib_version: String) -> Result { let manifest = Manifest::new(document_uuid, editor_version, stdlib_version); let codecs = manifest.codecs; io::write_single(&working, layout.manifest_basename(), MANIFEST_CODEC, &manifest)?; let session_state = SessionState { peer_id: peer, - user_id: UserId(peer.0), + user_id: user, ..Default::default() }; io::write_single(&working, layout.session_basename(), codecs.session, &Sorted(&session_state))?; - let session = Session::with_peer(peer); + let session = Session::with_identity(peer, user); io::write_single(&working, layout.registry_basename(), codecs.registry, &Sorted(session.registry()))?; Ok(Self::assemble(session, working, layout, manifest, HistoryMetadata::default())) diff --git a/document/format/src/persist.rs b/document/format/src/persist.rs index 1359a729a44..7aa35c55d85 100644 --- a/document/format/src/persist.rs +++ b/document/format/src/persist.rs @@ -6,7 +6,7 @@ use document_container::{AnyContainer, AsyncContainer}; #[cfg(feature = "conversion")] use document_graph_storage::NodeMetadataSource; -use document_graph_storage::{HistoryMetadata, HotOp, MetadataFact, RegistryDelta, Rev, Sorted, Subject, TimeStamp, Value, WallStamp}; +use document_graph_storage::{HistoryMetadata, HotOp, MetadataFact, PeerId, RegistryDelta, Rev, Sorted, Subject, TimeStamp, UserId, Value, WallStamp}; #[cfg(feature = "conversion")] use graphene_resource::ResourceStorage; @@ -65,6 +65,20 @@ impl Gdd { Ok(rev) } + /// Set the person using this device. `peer` must be unused, so the previous user keeps their steps. Refused while connected. + pub fn set_user(&mut self, user: UserId, peer: PeerId) -> Result<(), Error> { + if self.session.user() == user { + return Ok(()); + } + #[cfg(feature = "network")] + if self.network.is_some() { + return Err(Error::UserChangeWhileConnected); + } + let closing = self.session.set_user(user, peer)?; + self.persist_staged(&closing)?; + self.persist_session_state() + } + /// Edit the cached manifest and persist it. Always JSON, synchronous. pub fn update_manifest(&mut self, edit: impl FnOnce(&mut Manifest)) -> Result<(), Error> { edit(&mut self.manifest); diff --git a/document/format/tests/deterministic_save.rs b/document/format/tests/deterministic_save.rs index 5cd5ba590b3..19490a5406c 100644 --- a/document/format/tests/deterministic_save.rs +++ b/document/format/tests/deterministic_save.rs @@ -4,7 +4,7 @@ use document_container::AnyContainer; use document_container::backends::memory::MemoryBackend; use document_format::{ExportFormat, ExportOptions, GddV1, GddV1Layout}; -use document_graph_storage::{Implementation, Network, NetworkId, Node, NodeId, PeerId, Priority, RegistryDelta, ResourceId, SourceKey, TimeStamp, to_value}; +use document_graph_storage::{Implementation, Network, NetworkId, Node, NodeId, PeerId, Priority, RegistryDelta, ResourceId, SourceKey, TimeStamp, UserId, to_value}; use graph_craft::application_io::resource::{HashMapResourceStorage, ResourceStorage}; use graphene_resource::DataSource; use std::collections::BTreeMap; @@ -16,7 +16,7 @@ const LATEST: TimeStamp = TimeStamp { }; fn document(peer: u64) -> GddV1 { - GddV1::create_in(AnyContainer::Memory(MemoryBackend::new()), GddV1Layout, PeerId(peer), 1, "editor".into(), "stdlib".into()).expect("create") + GddV1::create_in(AnyContainer::Memory(MemoryBackend::new()), GddV1Layout, PeerId(peer), UserId(peer), 1, "editor".into(), "stdlib".into()).expect("create") } /// Networks and nodes, some of each removed again, plus link-only resources whose bytes sit in the byte store. diff --git a/document/format/tests/history_metadata.rs b/document/format/tests/history_metadata.rs index cc13db4c6ce..026013a71f0 100644 --- a/document/format/tests/history_metadata.rs +++ b/document/format/tests/history_metadata.rs @@ -7,7 +7,7 @@ const NOW_MS: f64 = 1_700_000_000_000.; const ADA: Subject = Subject::User(UserId(10)); fn document(peer: u64) -> GddV1 { - GddV1::create_in(AnyContainer::Memory(MemoryBackend::new()), GddV1Layout, PeerId(peer), 1, "editor".into(), "stdlib".into()).expect("create") + GddV1::create_in(AnyContainer::Memory(MemoryBackend::new()), GddV1Layout, PeerId(peer), UserId(peer), 1, "editor".into(), "stdlib".into()).expect("create") } /// Let the room settle through greetings, the sync handshake and whatever was broadcast. diff --git a/document/format/tests/migration_costs.rs b/document/format/tests/migration_costs.rs index 2d73fae7baa..9c689687d1f 100644 --- a/document/format/tests/migration_costs.rs +++ b/document/format/tests/migration_costs.rs @@ -4,7 +4,7 @@ use document_container::AnyContainer; use document_container::backends::memory::MemoryBackend; use document_format::{GddV1, GddV1Layout}; -use document_graph_storage::{EncodedDeclaration, Implementation, NoMetadata, PeerId, Registry}; +use document_graph_storage::{EncodedDeclaration, Implementation, NoMetadata, PeerId, Registry, UserId}; use futures::executor::block_on; use graph_craft::application_io::resource::HashMapResourceStorage; use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeNetwork}; @@ -91,7 +91,7 @@ fn stored(nodes: &[(u64, &'static str)]) -> AnyContainer { /// A document whose declaration bytes went to `store`, as the editor sends them to its global cache. fn stored_with_bytes_in(nodes: &[(u64, &'static str)], store: &HashMapResourceStorage) -> AnyContainer { - let mut gdd = GddV1::create_in(AnyContainer::Memory(MemoryBackend::new()), GddV1Layout, PeerId(7), 1, "editor".into(), "std".into()).unwrap(); + let mut gdd = GddV1::create_in(AnyContainer::Memory(MemoryBackend::new()), GddV1Layout, PeerId(7), UserId(7), 1, "editor".into(), "std".into()).unwrap(); gdd.commit_from_runtime(&network_of(nodes), &NoMetadata, &ResourceRegistry::new(), store).unwrap(); gdd.into_storage().0 } @@ -143,7 +143,7 @@ fn declarations_come_from_the_container_when_the_cache_lacks_them() { let nodes = [(1, "test_nodes::CopyNode"), (2, "test_nodes::OtherNode")]; // An archive carries its declarations in the container, which a device's cache hasn't seen yet - let mut gdd = GddV1::create_in(AnyContainer::Memory(MemoryBackend::new()), GddV1Layout, PeerId(7), 1, "editor".into(), "std".into()).unwrap(); + let mut gdd = GddV1::create_in(AnyContainer::Memory(MemoryBackend::new()), GddV1Layout, PeerId(7), UserId(7), 1, "editor".into(), "std".into()).unwrap(); gdd.commit_from_runtime(&network_of(&nodes), &NoMetadata, &ResourceRegistry::new(), &HashMapResourceStorage::new()) .unwrap(); for (_, identifier) in nodes { diff --git a/document/format/tests/migration_recovery.rs b/document/format/tests/migration_recovery.rs index baa17b41f56..84f3c587296 100644 --- a/document/format/tests/migration_recovery.rs +++ b/document/format/tests/migration_recovery.rs @@ -31,7 +31,7 @@ fn network_of(nodes: &[(u64, &'static str)]) -> NodeNetwork { } fn create() -> GddV1 { - GddV1::create_in(AnyContainer::Memory(MemoryBackend::new()), GddV1Layout, PeerId(7), 1, "editor".into(), "std".into()).unwrap() + GddV1::create_in(AnyContainer::Memory(MemoryBackend::new()), GddV1Layout, PeerId(7), UserId(7), 1, "editor".into(), "std".into()).unwrap() } fn commit(gdd: &mut GddV1, nodes: &[(u64, &'static str)], store: &HashMapResourceStorage) { @@ -79,7 +79,7 @@ fn a_migration_on_disk_leaves_no_staging_behind() { block_on(async { let directory = tempfile::tempdir().unwrap(); let path = directory.path().join("document"); - let mut gdd = GddV1::create(&path, PeerId(7), 1, "editor".into(), "std".into()).unwrap(); + let mut gdd = GddV1::create(&path, PeerId(7), UserId(7), 1, "editor".into(), "std".into()).unwrap(); commit(&mut gdd, &[(1, FADE)], &HashMapResourceStorage::new()); drop(gdd); diff --git a/document/format/tests/migrations.rs b/document/format/tests/migrations.rs index 0e65f27c52a..fb4950fefeb 100644 --- a/document/format/tests/migrations.rs +++ b/document/format/tests/migrations.rs @@ -4,7 +4,7 @@ use document_container::AnyContainer; use document_container::backends::memory::MemoryBackend; use document_format::{Error, GddV1, GddV1Layout, manifest}; -use document_graph_storage::{EncodedDeclaration, Implementation, NoMetadata, PeerId}; +use document_graph_storage::{EncodedDeclaration, Implementation, NoMetadata, PeerId, UserId}; use futures::executor::block_on; use graph_craft::application_io::resource::HashMapResourceStorage; use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeNetwork}; @@ -28,7 +28,7 @@ fn network_of(nodes: &[(u64, &'static str)]) -> NodeNetwork { } fn create() -> GddV1 { - GddV1::create_in(AnyContainer::Memory(MemoryBackend::new()), GddV1Layout, PeerId(7), 1, "editor".into(), "std".into()).unwrap() + GddV1::create_in(AnyContainer::Memory(MemoryBackend::new()), GddV1Layout, PeerId(7), UserId(7), 1, "editor".into(), "std".into()).unwrap() } fn implementations(gdd: &GddV1) -> Vec { diff --git a/document/format/tests/open_create.rs b/document/format/tests/open_create.rs index d47fd78e1bd..4a0a163d3e8 100644 --- a/document/format/tests/open_create.rs +++ b/document/format/tests/open_create.rs @@ -5,7 +5,7 @@ use document_container::AnyContainer; use document_container::backends::memory::MemoryBackend; use document_format::{Codec, Error, GddV1, GddV1Layout, Layout, Manifest, io, manifest}; -use document_graph_storage::{HotOp, HotSequence, Network, NetworkId, PeerId, ROOT_NETWORK, RegistryDelta, TimeStamp}; +use document_graph_storage::{HotOp, HotSequence, Network, NetworkId, PeerId, ROOT_NETWORK, RegistryDelta, TimeStamp, UserId}; fn empty_container() -> AnyContainer { AnyContainer::Memory(MemoryBackend::new()) @@ -44,7 +44,7 @@ fn create_in_round_trips_empty_document() { futures::executor::block_on(async { let container = empty_container(); - let created = match GddV1::create_in(container, GddV1Layout, PeerId(7), 0xFEED, "editor-x".into(), "stdlib-x".into()) { + let created = match GddV1::create_in(container, GddV1Layout, PeerId(7), UserId(7), 0xFEED, "editor-x".into(), "stdlib-x".into()) { Ok(gdd) => gdd, Err(error) => panic!("create_in failed: {error:?}"), }; @@ -82,7 +82,7 @@ fn open_in_rejects_wrong_format_magic() { #[test] fn manifest_returns_what_create_in_wrote() { futures::executor::block_on(async { - let gdd = GddV1::create_in(empty_container(), GddV1Layout, PeerId(13), 0xC0FFEE, "ed-1.2".into(), "std-0.7".into()).unwrap_or_else(|error| panic!("create_in failed: {error:?}")); + let gdd = GddV1::create_in(empty_container(), GddV1Layout, PeerId(13), UserId(13), 0xC0FFEE, "ed-1.2".into(), "std-0.7".into()).unwrap_or_else(|error| panic!("create_in failed: {error:?}")); assert_eq!(gdd.session().peer(), PeerId(13)); @@ -99,7 +99,7 @@ fn an_empty_registry_file_is_corrupt_rather_than_absent() { use document_container::AsyncContainer; futures::executor::block_on(async { - let gdd = GddV1::create_in(empty_container(), GddV1Layout, PeerId(1), 0xAB, "ed".into(), "std".into()).unwrap_or_else(|error| panic!("create_in failed: {error:?}")); + let gdd = GddV1::create_in(empty_container(), GddV1Layout, PeerId(1), UserId(1), 0xAB, "ed".into(), "std".into()).unwrap_or_else(|error| panic!("create_in failed: {error:?}")); let codec = gdd.manifest().codecs.registry; let (working, layout) = gdd.into_storage(); @@ -114,7 +114,7 @@ fn opening_a_registry_without_history_keeps_the_node_counter() { use document_container::AsyncContainer; futures::executor::block_on(async { - let gdd = GddV1::create_in(empty_container(), GddV1Layout, PeerId(1), 0xAB, "ed".into(), "std".into()).unwrap_or_else(|error| panic!("create_in failed: {error:?}")); + let gdd = GddV1::create_in(empty_container(), GddV1Layout, PeerId(1), UserId(1), 0xAB, "ed".into(), "std".into()).unwrap_or_else(|error| panic!("create_in failed: {error:?}")); let codecs = gdd.manifest().codecs; let (working, layout) = gdd.into_storage(); @@ -135,7 +135,7 @@ fn opening_a_registry_without_history_keeps_the_node_counter() { #[test] fn update_manifest_changes_visible_after_reopen() { futures::executor::block_on(async { - let mut gdd = GddV1::create_in(empty_container(), GddV1Layout, PeerId(1), 0xAB, "ed".into(), "std".into()).unwrap_or_else(|error| panic!("create_in failed: {error:?}")); + let mut gdd = GddV1::create_in(empty_container(), GddV1Layout, PeerId(1), UserId(1), 0xAB, "ed".into(), "std".into()).unwrap_or_else(|error| panic!("create_in failed: {error:?}")); gdd.update_manifest(|m| m.editor_version = "ed-NEW".into()) .unwrap_or_else(|error| panic!("update_manifest failed: {error:?}")); @@ -150,7 +150,7 @@ fn update_manifest_changes_visible_after_reopen() { #[test] fn apply_hot_op_persists_to_hot_log_and_survives_reopen() { futures::executor::block_on(async { - let mut gdd = GddV1::create_in(empty_container(), GddV1Layout, PeerId(5), 0xDEAD, "ed".into(), "std".into()).unwrap_or_else(|error| panic!("create_in failed: {error:?}")); + let mut gdd = GddV1::create_in(empty_container(), GddV1Layout, PeerId(5), UserId(5), 0xDEAD, "ed".into(), "std".into()).unwrap_or_else(|error| panic!("create_in failed: {error:?}")); // AddNetwork on the root network. Idempotent at apply, so two hot ops applied in sequence // produces one network in the registry. @@ -177,7 +177,7 @@ fn apply_hot_op_persists_to_hot_log_and_survives_reopen() { #[test] fn retire_moves_eligible_hot_ops_to_history_and_keeps_rest() { futures::executor::block_on(async { - let mut gdd = GddV1::create_in(empty_container(), GddV1Layout, PeerId(5), 0xDEAD, "ed".into(), "std".into()).unwrap_or_else(|error| panic!("create_in failed: {error:?}")); + let mut gdd = GddV1::create_in(empty_container(), GddV1Layout, PeerId(5), UserId(5), 0xDEAD, "ed".into(), "std".into()).unwrap_or_else(|error| panic!("create_in failed: {error:?}")); // Two hot ops: one with low timestamp (will retire), one with high (will stay). let early = HotOp { @@ -225,7 +225,7 @@ fn retire_moves_eligible_hot_ops_to_history_and_keeps_rest() { #[test] fn last_broadcast_rev_persists_across_reopen() { futures::executor::block_on(async { - let mut gdd = GddV1::create_in(empty_container(), GddV1Layout, PeerId(5), 0xDEAD, "ed".into(), "std".into()).unwrap_or_else(|error| panic!("create_in failed: {error:?}")); + let mut gdd = GddV1::create_in(empty_container(), GddV1Layout, PeerId(5), UserId(5), 0xDEAD, "ed".into(), "std".into()).unwrap_or_else(|error| panic!("create_in failed: {error:?}")); // Retire one op so there is a real retired rev to mark as published. let op = HotOp { @@ -256,7 +256,7 @@ fn export_folder_round_trips_through_open() { use document_format::{ExportFormat, ExportOptions}; futures::executor::block_on(async { - let gdd = GddV1::create_in(empty_container(), GddV1Layout, PeerId(3), 0xAB, "ed".into(), "std".into()).unwrap_or_else(|error| panic!("create_in failed: {error:?}")); + let gdd = GddV1::create_in(empty_container(), GddV1Layout, PeerId(3), UserId(3), 0xAB, "ed".into(), "std".into()).unwrap_or_else(|error| panic!("create_in failed: {error:?}")); let dir = tempfile::tempdir().unwrap(); let dest = dir.path().join("export"); @@ -284,7 +284,7 @@ fn export_zip_round_trips_via_deserialize() { use document_format::{ExportFormat, ExportOptions}; futures::executor::block_on(async { - let gdd = GddV1::create_in(empty_container(), GddV1Layout, PeerId(4), 0xCD, "ed".into(), "std".into()).unwrap_or_else(|error| panic!("create_in failed: {error:?}")); + let gdd = GddV1::create_in(empty_container(), GddV1Layout, PeerId(4), UserId(4), 0xCD, "ed".into(), "std".into()).unwrap_or_else(|error| panic!("create_in failed: {error:?}")); let dir = tempfile::tempdir().unwrap(); let dest = dir.path().join("doc.gdd.zip"); @@ -309,7 +309,7 @@ fn export_rejects_invalid_options() { use document_format::{ExportFormat, ExportOptions}; futures::executor::block_on(async { - let gdd = GddV1::create_in(empty_container(), GddV1Layout, PeerId(1), 0xEF, "ed".into(), "std".into()).unwrap_or_else(|error| panic!("create_in failed: {error:?}")); + let gdd = GddV1::create_in(empty_container(), GddV1Layout, PeerId(1), UserId(1), 0xEF, "ed".into(), "std".into()).unwrap_or_else(|error| panic!("create_in failed: {error:?}")); let dir = tempfile::tempdir().unwrap(); let dest = dir.path().join("nope"); @@ -332,7 +332,7 @@ fn resource_round_trip_add_read_remove() { use graphene_resource::{ResourceHash, ResourceId}; futures::executor::block_on(async { - let mut gdd = GddV1::create_in(empty_container(), GddV1Layout, PeerId(99), 0xCAFE, "ed".into(), "std".into()).unwrap_or_else(|error| panic!("create_in failed: {error:?}")); + let mut gdd = GddV1::create_in(empty_container(), GddV1Layout, PeerId(99), UserId(99), 0xCAFE, "ed".into(), "std".into()).unwrap_or_else(|error| panic!("create_in failed: {error:?}")); let payload = b"deadbeef cafe babe"; let hash = ResourceHash::from(&payload[..]); @@ -362,7 +362,7 @@ fn resource_survives_reopen() { use graphene_resource::{ResourceHash, ResourceId}; futures::executor::block_on(async { - let mut gdd = GddV1::create_in(empty_container(), GddV1Layout, PeerId(7), 0xC0DE, "ed".into(), "std".into()).unwrap_or_else(|error| panic!("create_in failed: {error:?}")); + let mut gdd = GddV1::create_in(empty_container(), GddV1Layout, PeerId(7), UserId(7), 0xC0DE, "ed".into(), "std".into()).unwrap_or_else(|error| panic!("create_in failed: {error:?}")); let payload = b"persistent bytes"; let hash = ResourceHash::from(&payload[..]); @@ -391,7 +391,7 @@ fn resource_from_path_uses_fs_copy_on_folder_backend() { // Need a folder-backed working copy to exercise the fs::copy path. let working_dir = tempfile::tempdir().unwrap(); let working = AnyContainer::Folder(FolderBackend::create(working_dir.path()).unwrap()); - let mut gdd = GddV1::create_in(working, GddV1Layout, PeerId(1), 0xAB, "ed".into(), "std".into()).unwrap_or_else(|error| panic!("create_in failed: {error:?}")); + let mut gdd = GddV1::create_in(working, GddV1Layout, PeerId(1), UserId(1), 0xAB, "ed".into(), "std".into()).unwrap_or_else(|error| panic!("create_in failed: {error:?}")); // Source file outside the working copy. let payload = b"external resource bytes"; @@ -415,7 +415,7 @@ fn export_carries_resources() { use graphene_resource::{ResourceHash, ResourceId}; futures::executor::block_on(async { - let mut gdd = GddV1::create_in(empty_container(), GddV1Layout, PeerId(2), 0xBC, "ed".into(), "std".into()).unwrap_or_else(|error| panic!("create_in failed: {error:?}")); + let mut gdd = GddV1::create_in(empty_container(), GddV1Layout, PeerId(2), UserId(2), 0xBC, "ed".into(), "std".into()).unwrap_or_else(|error| panic!("create_in failed: {error:?}")); let payload = b"exported resource"; let hash = ResourceHash::from(&payload[..]); @@ -443,7 +443,7 @@ fn embed_all_resources_materializes_link_only_resource() { use graphene_resource::{DataSource, ResourceHash, ResourceId, ResourceRegistry}; futures::executor::block_on(async { - let mut gdd = GddV1::create_in(empty_container(), GddV1Layout, PeerId(8), 0xF00D, "ed".into(), "std".into()).unwrap_or_else(|error| panic!("create_in failed: {error:?}")); + let mut gdd = GddV1::create_in(empty_container(), GddV1Layout, PeerId(8), UserId(8), 0xF00D, "ed".into(), "std".into()).unwrap_or_else(|error| panic!("create_in failed: {error:?}")); // A resource whose only source is a URL, resolved to a hash. The bytes live solely in the // byte store; the working copy never holds them. @@ -508,7 +508,7 @@ fn export_materializes_embedded_resource_from_byte_store() { use graphene_resource::{DataSource, ResourceHash, ResourceId, ResourceRegistry}; futures::executor::block_on(async { - let mut gdd = GddV1::create_in(empty_container(), GddV1Layout, PeerId(9), 0xBEEF, "ed".into(), "std".into()).unwrap_or_else(|error| panic!("create_in failed: {error:?}")); + let mut gdd = GddV1::create_in(empty_container(), GddV1Layout, PeerId(9), UserId(9), 0xBEEF, "ed".into(), "std".into()).unwrap_or_else(|error| panic!("create_in failed: {error:?}")); // An Embedded resource whose bytes live only in the byte store, not the working copy. let payload = b"embedded bytes in the cache"; @@ -549,7 +549,7 @@ fn export_round_trips_unretired_hot_ops() { use graphene_resource::{DataSource, ResourceId, ResourceRegistry}; futures::executor::block_on(async { - let mut gdd = GddV1::create_in(empty_container(), GddV1Layout, PeerId(3), 0xF15E, "ed".into(), "std".into()).unwrap_or_else(|error| panic!("create_in failed: {error:?}")); + let mut gdd = GddV1::create_in(empty_container(), GddV1Layout, PeerId(3), UserId(3), 0xF15E, "ed".into(), "std".into()).unwrap_or_else(|error| panic!("create_in failed: {error:?}")); let payload = b"hot resource bytes"; let hash = graphene_resource::ResourceHash::from(&payload[..]); @@ -602,7 +602,7 @@ fn open_in_rejects_future_format_version() { #[test] fn create_in_records_default_codecs_in_manifest() { futures::executor::block_on(async { - let gdd = GddV1::create_in(empty_container(), GddV1Layout, PeerId(1), 0xAB, "ed".into(), "std".into()).unwrap_or_else(|error| panic!("create_in failed: {error:?}")); + let gdd = GddV1::create_in(empty_container(), GddV1Layout, PeerId(1), UserId(1), 0xAB, "ed".into(), "std".into()).unwrap_or_else(|error| panic!("create_in failed: {error:?}")); let codecs = gdd.manifest().codecs; assert_eq!(codecs.registry, Codec::Postcard); @@ -623,7 +623,7 @@ fn first_commit_registers_peer_and_survives_reopen() { use graphene_resource::ResourceRegistry; futures::executor::block_on(async { - let mut gdd = GddV1::create_in(empty_container(), GddV1Layout, PeerId(21), 0xAB, "ed".into(), "std".into()).unwrap_or_else(|error| panic!("create_in failed: {error:?}")); + let mut gdd = GddV1::create_in(empty_container(), GddV1Layout, PeerId(21), UserId(21), 0xAB, "ed".into(), "std".into()).unwrap_or_else(|error| panic!("create_in failed: {error:?}")); let network = NodeNetwork { exports: vec![NodeInput::node(core_types::uuid::NodeId(0), 0)], @@ -665,7 +665,7 @@ fn persist_path_writes_at_manifest_declared_codec_paths() { futures::executor::block_on(async { use document_container::AsyncContainer; - let mut gdd = GddV1::create_in(empty_container(), GddV1Layout, PeerId(5), 0xDEAD, "ed".into(), "std".into()).unwrap_or_else(|error| panic!("create_in failed: {error:?}")); + let mut gdd = GddV1::create_in(empty_container(), GddV1Layout, PeerId(5), UserId(5), 0xDEAD, "ed".into(), "std".into()).unwrap_or_else(|error| panic!("create_in failed: {error:?}")); let hot_op = HotOp { op: RegistryDelta::AddNetwork { @@ -718,7 +718,7 @@ fn declarations_round_trip_through_byte_store() { ..Default::default() }; - let mut gdd = GddV1::create_in(empty_container(), GddV1Layout, PeerId(1), 0xAB, "ed".into(), "std".into()).unwrap_or_else(|error| panic!("create_in failed: {error:?}")); + let mut gdd = GddV1::create_in(empty_container(), GddV1Layout, PeerId(1), UserId(1), 0xAB, "ed".into(), "std".into()).unwrap_or_else(|error| panic!("create_in failed: {error:?}")); // Commit: declaration bytes flow into the byte store, not the Gdd container. let byte_store = HashMapResourceStorage::new(); @@ -746,7 +746,7 @@ fn an_export_without_history_drops_tombstones() { use document_graph_storage::{Network, NetworkId, RegistryDelta}; futures::executor::block_on(async { - let mut gdd = GddV1::create_in(empty_container(), GddV1Layout, PeerId(5), 0xAC, "ed".into(), "std".into()).unwrap_or_else(|error| panic!("create_in failed: {error:?}")); + let mut gdd = GddV1::create_in(empty_container(), GddV1Layout, PeerId(5), UserId(5), 0xAC, "ed".into(), "std".into()).unwrap_or_else(|error| panic!("create_in failed: {error:?}")); let id = NetworkId(7); gdd.stage_constructed_ops(vec![RegistryDelta::AddNetwork { id, network: Network::default() }], &Default::default(), &empty_byte_store()) .unwrap(); diff --git a/document/format/tests/staging_and_reopen.rs b/document/format/tests/staging_and_reopen.rs index 87dce9bcae6..5c1c766f3df 100644 --- a/document/format/tests/staging_and_reopen.rs +++ b/document/format/tests/staging_and_reopen.rs @@ -4,7 +4,7 @@ use document_container::AnyContainer; use document_container::backends::memory::MemoryBackend; use document_format::{GddV1, GddV1Layout}; -use document_graph_storage::{NoMetadata, PeerId, Registry}; +use document_graph_storage::{NoMetadata, PeerId, Registry, UserId}; use graph_craft::application_io::resource::HashMapResourceStorage; use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeNetwork}; use graph_craft::{ProtoNodeIdentifier, concrete}; @@ -25,7 +25,7 @@ fn network_with_one_node() -> NodeNetwork { /// A whole-document stage after a recorded batch stages nothing new: restating the batch would fail on the node it added. #[test] fn restaging_the_whole_document_after_a_batch_stages_nothing() { - let mut gdd = GddV1::create_in(AnyContainer::Memory(MemoryBackend::new()), GddV1Layout, PeerId(21), 1, "ed".into(), "std".into()).unwrap(); + let mut gdd = GddV1::create_in(AnyContainer::Memory(MemoryBackend::new()), GddV1Layout, PeerId(21), UserId(21), 1, "ed".into(), "std".into()).unwrap(); let store = HashMapResourceStorage::new(); let resources = ResourceRegistry::new(); gdd.stage_runtime_snapshot(&NodeNetwork::default(), &NoMetadata, &resources, &store) @@ -57,7 +57,7 @@ fn a_reopen_keeps_unretired_hot_ops_out_of_the_retired_snapshot() { attributes: Default::default(), }; futures::executor::block_on(async { - let mut gdd = GddV1::create_in(AnyContainer::Memory(MemoryBackend::new()), GddV1Layout, PeerId(22), 1, "ed".into(), "std".into()).unwrap(); + let mut gdd = GddV1::create_in(AnyContainer::Memory(MemoryBackend::new()), GddV1Layout, PeerId(22), UserId(22), 1, "ed".into(), "std".into()).unwrap(); gdd.apply_hot_op(set(1, 1)).unwrap(); gdd.apply_hot_op(set(2, 2)).unwrap(); gdd.retire(TimeStamp { counter: 1, peer: PeerId(22) }).unwrap(); @@ -80,28 +80,69 @@ fn a_reopen_continues_the_hot_op_sequence() { value: Some(Value::from(serde_json::json!(value))), }, }; - fn stage(gdd: &mut GddV1, op: RegistryDelta) -> u64 { - gdd.stage_constructed_ops(vec![op], &Default::default(), &HashMapResourceStorage::new()).unwrap(); - gdd.session().hot_log().last().unwrap().sequence.0 - } futures::executor::block_on(async { - let mut gdd = GddV1::create_in(AnyContainer::Memory(MemoryBackend::new()), GddV1Layout, PeerId(23), 1, "ed".into(), "std".into()).unwrap(); - stage(&mut gdd, set(1)); + let mut gdd = GddV1::create_in(AnyContainer::Memory(MemoryBackend::new()), GddV1Layout, PeerId(23), UserId(23), 1, "ed".into(), "std".into()).unwrap(); + gdd.stage_ops([set(1)]).unwrap(); let up_to = gdd.session().hot_log().last().unwrap().timestamp; gdd.retire(up_to).unwrap(); - let last = stage(&mut gdd, set(2)); + let last = gdd.stage_ops([set(2)]).unwrap().last().unwrap().sequence; let (working, layout) = gdd.into_storage(); let mut reopened = GddV1::open_in(working, layout).await.unwrap(); - assert_eq!(stage(&mut reopened, set(3)), last + 1); + assert_eq!(reopened.stage_ops([set(3)]).unwrap()[0].sequence.0, last.0 + 1); let up_to = reopened.session().hot_log().last().unwrap().timestamp; reopened.retire(up_to).unwrap(); - let last = reopened.session().last_hot_sequence().0; + let last = reopened.session().last_hot_sequence(); let clock = reopened.session().clock_counter(); let (working, layout) = reopened.into_storage(); let mut reopened = GddV1::open_in(working, layout).await.unwrap(); assert_eq!(reopened.session().clock_counter(), clock, "the clock carries over"); - assert_eq!(stage(&mut reopened, set(4)), last + 1); + assert_eq!(reopened.stage_ops([set(4)]).unwrap()[0].sequence.0, last.0 + 1); + }); +} + +#[test] +fn a_user_change_survives_a_reopen() { + use document_graph_storage::{AttributeDelta, RegistryDelta, Value}; + let set = |key: &str| RegistryDelta::ChangeDocumentAttribute { + delta: AttributeDelta { + key: key.into(), + value: Some(Value::from(serde_json::json!(1))), + }, + }; + futures::executor::block_on(async { + let mut gdd = GddV1::create_in(AnyContainer::Memory(MemoryBackend::new()), GddV1Layout, PeerId(24), UserId(240), 1, "ed".into(), "std".into()).unwrap(); + gdd.stage_ops([set("a")]).unwrap(); + gdd.retire_pending_interaction().unwrap(); + gdd.stage_ops([set("b")]).unwrap(); + + gdd.set_user(UserId(250), PeerId(25)).unwrap(); + let clock = gdd.session().clock_counter(); + let (working, layout) = gdd.into_storage(); + let mut reopened = GddV1::open_in(working, layout).await.unwrap(); + assert_eq!(reopened.session().peer(), PeerId(25)); + assert_eq!(reopened.session().user(), UserId(250)); + assert_eq!(reopened.session().clock_counter(), clock); + + let staged = reopened.stage_ops([set("c")]).unwrap(); + assert!(staged.iter().all(|hot_op| hot_op.timestamp.peer == PeerId(25) && hot_op.timestamp.counter > clock)); + assert_eq!(staged[0].sequence.0, 1, "the new peer's run starts over"); + reopened.retire_pending_interaction().unwrap(); + + let (working, layout) = reopened.into_storage(); + let reopened = GddV1::open_in(working, layout).await.unwrap(); + let session = reopened.session(); + assert!(session.hot_log().is_empty(), "the previous user's open step closed and retired"); + assert_eq!(session.user_of(PeerId(24)), Some(UserId(240))); + assert_eq!(session.user_of(PeerId(25)), Some(UserId(250))); + assert!(!session.is_mine(PeerId(24))); + assert!(session.is_mine(PeerId(25))); + let authors: Vec = session + .history() + .filter(|delta| matches!(delta.kind, RegistryDelta::ChangeDocumentAttribute { .. })) + .map(|delta| delta.author) + .collect(); + assert_eq!(authors, [PeerId(24), PeerId(24), PeerId(25)]); }); } diff --git a/document/format/tests/staging_broadcast.rs b/document/format/tests/staging_broadcast.rs index 2a0b43727d7..9ee531ea2a3 100644 --- a/document/format/tests/staging_broadcast.rs +++ b/document/format/tests/staging_broadcast.rs @@ -9,7 +9,7 @@ use peer_transport::mock::MockNetwork; use peer_transport::{Event, SyncTarget}; fn document(peer: u64) -> GddV1 { - GddV1::create_in(AnyContainer::Memory(MemoryBackend::new()), GddV1Layout, PeerId(peer), 1, "editor".into(), "stdlib".into()).expect("create") + GddV1::create_in(AnyContainer::Memory(MemoryBackend::new()), GddV1Layout, PeerId(peer), UserId(peer), 1, "editor".into(), "stdlib".into()).expect("create") } fn settle(network: &mut MockNetwork, peers: &mut [&mut GddV1]) { @@ -110,3 +110,13 @@ fn an_interaction_past_a_gap_waits_to_retire() { assert!(!host.retire_pending_interaction().expect("retire").is_empty()); assert!(host.session().hot_log().is_empty()); } + +#[test] +fn the_user_cannot_change_while_connected() { + let mut host = document(1); + let mut network = MockNetwork::new(1); + host.share(network.endpoint(), UserId(1)).expect("share"); + assert!(matches!(host.set_user(UserId(2), PeerId(2)), Err(document_format::Error::UserChangeWhileConnected))); + assert_eq!(host.session().peer(), PeerId(1)); + assert_eq!(host.session().user(), UserId(1)); +} diff --git a/document/graph-storage/src/schema/session_state.rs b/document/graph-storage/src/schema/session_state.rs index dc2c951065b..e76a7182954 100644 --- a/document/graph-storage/src/schema/session_state.rs +++ b/document/graph-storage/src/schema/session_state.rs @@ -10,7 +10,7 @@ use std::collections::BTreeMap; #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct SessionState { - /// This peer's identity for the document, stable per (device, document). Per-peer rather than + /// This peer's identity for the document, stable per (device, document, user). Per-peer rather than /// document identity, so it lives with the cursor here, not in the manifest. Used for CRDT /// tiebreaking and minting peer-scoped IDs. #[serde(default)] diff --git a/document/graph-storage/src/session.rs b/document/graph-storage/src/session.rs index 6fdd8cf53a2..bdb37ff26be 100644 --- a/document/graph-storage/src/session.rs +++ b/document/graph-storage/src/session.rs @@ -55,6 +55,19 @@ impl Session { self.document.user } + /// Hand this device to `user` under `peer`, an unused id, so the old peer's ops keep their author. + pub fn set_user(&mut self, user: UserId, peer: PeerId) -> Result, CrdtError> { + let closing = self.end_interaction()?; + let mut clock = LamportClock::new(peer); + clock.counter = self.document.clock.counter; + self.document.clock = clock; + self.document.peer = peer; + self.document.user = user; + self.document.last_hot_sequence = HotSequence::NONE; + self.document.redo_stack.clear(); + Ok(closing) + } + pub fn user_of(&self, peer: PeerId) -> Option { let registered = || self.document.working_registry.peer_users.get(&peer).map(|registration| registration.user); (peer == self.document.peer).then_some(self.document.user).or_else(registered) diff --git a/document/graph-storage/src/tests/bookkeeping.rs b/document/graph-storage/src/tests/bookkeeping.rs index f29cc2cbf25..a082f437a4b 100644 --- a/document/graph-storage/src/tests/bookkeeping.rs +++ b/document/graph-storage/src/tests/bookkeeping.rs @@ -281,7 +281,6 @@ fn newest_peer_registration_wins_in_any_order() { stage_and_retire(&mut device, set_attribute("first", 1)); assert_eq!(device.registry().peer_users[&PeerId(1)].user, UserId(10)); - // The person at the device changes, so the next batch registers again. device.document.user = UserId(20); stage_and_retire(&mut device, set_attribute("second", 2)); assert_eq!(device.registry().peer_users[&PeerId(1)].user, UserId(20)); @@ -296,6 +295,56 @@ fn newest_peer_registration_wins_in_any_order() { assert_eq!(other.user_of(PeerId(3)), None, "a peer never registered has no user"); } +#[test] +fn a_new_user_takes_a_fresh_peer_and_leaves_the_old_steps_to_their_maker() { + fn interaction(session: &mut Session, key: &str) { + session.stage_ops([set_attribute(key, 1)]).expect("stage"); + session.end_interaction().expect("close"); + for closed in session.closed_interactions() { + session.retire_interaction(&closed).expect("retire"); + } + } + let mut device = Session::with_identity(PeerId(1), UserId(10)); + for key in ["a", "b", "c"] { + interaction(&mut device, key); + } + device.undo().expect("undo c"); + assert!(device.can_redo()); + device.stage_ops([set_attribute("d", 1)]).expect("stage d, left open"); + let clock_before = device.clock_counter(); + + let closing = device.set_user(UserId(20), PeerId(2)).expect("switch"); + assert!(closing.iter().all(|hot_op| hot_op.timestamp.peer == PeerId(1)), "the old peer closes its own interaction"); + assert!(closing.last().is_some_and(crate::HotOp::ends_interaction)); + assert_eq!(device.peer(), PeerId(2)); + assert!(device.clock_counter() >= clock_before); + assert!(!device.can_undo(), "the previous user's steps are not this person's to undo"); + assert!(!device.can_redo()); + assert_eq!(device.user_of(PeerId(1)), Some(UserId(10))); + assert!(!device.is_mine(PeerId(1))); + + device.stage_ops([set_attribute("e", 1)]).expect("stage e"); + device.end_interaction().expect("close e"); + let new_ops: Vec<_> = device.hot_log().iter().filter(|hot_op| hot_op.timestamp.peer == PeerId(2)).collect(); + assert_eq!(new_ops.first().map(|hot_op| hot_op.sequence), Some(crate::HotSequence(1)), "the new peer's run starts over"); + assert!(new_ops.iter().all(|hot_op| hot_op.timestamp.counter > clock_before)); + for closed in device.closed_interactions() { + device.retire_interaction(&closed).expect("retire"); + } + + assert_eq!(device.registry().peer_users[&PeerId(1)].user, UserId(10), "the old registration stands"); + assert_eq!(device.user_of(PeerId(2)), Some(UserId(20))); + assert!(device.is_mine(PeerId(2))); + for delta in device.history().filter(|delta| !matches!(delta.kind, RegistryDelta::RegisterPeer { .. })) { + let expected = if delta.author == PeerId(1) { UserId(10) } else { UserId(20) }; + assert_eq!(device.user_of(delta.author), Some(expected)); + } + + assert!(device.can_undo(), "the new user's own step undoes"); + device.undo().expect("undo e"); + assert!(!device.can_undo(), "but not past it into the previous user's"); +} + #[test] fn a_batch_past_the_last_sequence_is_refused_whole() { let mut session = Session::with_peer(PeerId(1)); diff --git a/editor/src/messages/portfolio/document/storage_tests/round_trip_tests.rs b/editor/src/messages/portfolio/document/storage_tests/round_trip_tests.rs index 11fe0e36ebb..6930c4c6253 100644 --- a/editor/src/messages/portfolio/document/storage_tests/round_trip_tests.rs +++ b/editor/src/messages/portfolio/document/storage_tests/round_trip_tests.rs @@ -7,7 +7,7 @@ use document_container::AnyContainer; use document_container::backends::memory::MemoryBackend; use document_format::{GddV1, GddV1Layout}; -use document_graph_storage::{NodeMetadataSource, PeerId, to_value}; +use document_graph_storage::{NodeMetadataSource, PeerId, UserId, to_value}; use graph_craft::application_io::resource::HashMapResourceStorage; use super::test_support::{RoundTrip, node_locations, node_paths, round_trip_through_gdd}; @@ -160,7 +160,7 @@ async fn edit_after_open_commits_cleanly() { // reopened registry: the editor's .gdd-open path. let byte_store = HashMapResourceStorage::new(); let source = editor.active_document(); - let mut gdd = GddV1::create_in(AnyContainer::Memory(MemoryBackend::new()), GddV1Layout, PeerId(1), 0xABCD, "test".into(), "test".into()).expect("create_in"); + let mut gdd = GddV1::create_in(AnyContainer::Memory(MemoryBackend::new()), GddV1Layout, PeerId(1), UserId(1), 0xABCD, "test".into(), "test".into()).expect("create_in"); let source_network = source.network_interface.document_network().clone(); let source_view = StorageMetadataView::new(&source.network_interface); gdd.commit_from_runtime(&source_network, &source_view, &source.resources.registry, &byte_store) @@ -210,7 +210,7 @@ async fn edit_after_open_commits_cleanly() { #[tokio::test] async fn gdd_undo_redo_walks_interactions() { let byte_store = HashMapResourceStorage::new(); - let mut gdd = GddV1::create_in(AnyContainer::Memory(MemoryBackend::new()), GddV1Layout, PeerId(1), 0xABCD, "test".into(), "test".into()).expect("create_in"); + let mut gdd = GddV1::create_in(AnyContainer::Memory(MemoryBackend::new()), GddV1Layout, PeerId(1), UserId(1), 0xABCD, "test".into(), "test".into()).expect("create_in"); // Commit the active document's current runtime state as one interaction. async fn commit_interaction(gdd: &mut GddV1, document: &DocumentMessageHandler, byte_store: &HashMapResourceStorage) { @@ -261,7 +261,7 @@ async fn gdd_undo_redo_walks_interactions() { #[tokio::test] async fn reopen_after_undo_restores_consistent_registry() { let byte_store = HashMapResourceStorage::new(); - let mut gdd = GddV1::create_in(AnyContainer::Memory(MemoryBackend::new()), GddV1Layout, PeerId(1), 0xABCD, "test".into(), "test".into()).expect("create_in"); + let mut gdd = GddV1::create_in(AnyContainer::Memory(MemoryBackend::new()), GddV1Layout, PeerId(1), UserId(1), 0xABCD, "test".into(), "test".into()).expect("create_in"); async fn commit_interaction(gdd: &mut GddV1, document: &DocumentMessageHandler, byte_store: &HashMapResourceStorage) { let network = document.network_interface.document_network().clone(); @@ -383,7 +383,7 @@ async fn gdd_archive_round_trips_view_settings() { use document_graph_storage::attr::session::doc; let byte_store = HashMapResourceStorage::new(); - let mut gdd = GddV1::create_in(AnyContainer::Memory(MemoryBackend::new()), GddV1Layout, PeerId(1), 0xABCD, "test".into(), "test".into()).expect("create_in"); + let mut gdd = GddV1::create_in(AnyContainer::Memory(MemoryBackend::new()), GddV1Layout, PeerId(1), UserId(1), 0xABCD, "test".into(), "test".into()).expect("create_in"); // Stage a distinctive PTZ into the working copy's `view_settings`, as `commit_storage_snapshot` does. let mut ptz = crate::messages::portfolio::document::utility_types::misc::PTZ::default(); @@ -431,7 +431,7 @@ async fn per_network_navigation_round_trips_via_session_not_registry() { let expected_pan = editor.active_document().network_interface.node_graph_ptz(&[]).unwrap().pan; // Commit the document into a fresh `Gdd`, collecting the per-network view state the editor persists. - let mut gdd = GddV1::create_in(AnyContainer::Memory(MemoryBackend::new()), GddV1Layout, PeerId(1), 0xABCD, "test".into(), "test".into()).expect("create_in"); + let mut gdd = GddV1::create_in(AnyContainer::Memory(MemoryBackend::new()), GddV1Layout, PeerId(1), UserId(1), 0xABCD, "test".into(), "test".into()).expect("create_in"); let document = editor.active_document(); let network = document.network_interface.document_network().clone(); let view = StorageMetadataView::new(&document.network_interface); @@ -665,7 +665,7 @@ fn assert_cursor_matches_runtime(document: &DocumentMessageHandler, at: &str) { /// Mount a fresh in-memory `Gdd` onto the active document so `commit_storage_snapshot` (the real /// autosave path) runs against it. Returns the byte store the document's resources resolve through. fn mount_in_memory_storage(editor: &mut EditorTestUtils) -> HashMapResourceStorage { - let gdd = GddV1::create_in(AnyContainer::Memory(MemoryBackend::new()), GddV1Layout, PeerId(1), 0x5EED, "test".into(), "test".into()).expect("create_in"); + let gdd = GddV1::create_in(AnyContainer::Memory(MemoryBackend::new()), GddV1Layout, PeerId(1), UserId(1), 0x5EED, "test".into(), "test".into()).expect("create_in"); editor.active_document_mut().set_storage(gdd, Default::default()); HashMapResourceStorage::new() } diff --git a/editor/src/messages/portfolio/document/storage_tests/test_support.rs b/editor/src/messages/portfolio/document/storage_tests/test_support.rs index 0c9bf419695..63254dd27f3 100644 --- a/editor/src/messages/portfolio/document/storage_tests/test_support.rs +++ b/editor/src/messages/portfolio/document/storage_tests/test_support.rs @@ -5,7 +5,7 @@ use document_container::AnyContainer; use document_container::backends::memory::MemoryBackend; use document_format::{GddV1, GddV1Layout}; -use document_graph_storage::PeerId; +use document_graph_storage::{PeerId, UserId}; use graph_craft::application_io::resource::HashMapResourceStorage; use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NetworkLocation, NodeId, NodeNetwork}; @@ -110,7 +110,7 @@ pub struct RoundTrip { pub async fn round_trip_through_gdd(document: &DocumentMessageHandler) -> RoundTrip { let byte_store = HashMapResourceStorage::new(); - let mut gdd = GddV1::create_in(AnyContainer::Memory(MemoryBackend::new()), GddV1Layout, PeerId(1), 0xABCD, "test".into(), "test".into()).expect("create_in"); + let mut gdd = GddV1::create_in(AnyContainer::Memory(MemoryBackend::new()), GddV1Layout, PeerId(1), UserId(1), 0xABCD, "test".into(), "test".into()).expect("create_in"); let network = document.network_interface.document_network().clone(); let view = StorageMetadataView::new(&document.network_interface); diff --git a/editor/src/messages/portfolio/document_storage_io.rs b/editor/src/messages/portfolio/document_storage_io.rs index a0df780d914..dc8238d1114 100644 --- a/editor/src/messages/portfolio/document_storage_io.rs +++ b/editor/src/messages/portfolio/document_storage_io.rs @@ -17,7 +17,7 @@ use crate::messages::resource_storage::ResourcesHandle; use document_container::store::{DocumentKey, DocumentStore, MemoryStore}; use document_container::{AnyContainer, AsyncContainer}; use document_format::{Error as DocumentFormatError, GddV1, GddV1Layout, Layout, ReadError}; -use document_graph_storage::{Declarations, PeerId}; +use document_graph_storage::{Declarations, PeerId, UserId}; use graph_craft::application_io::resource::LoadResource; use graph_craft::application_io::resource::{DataSource, ResourceHash, ResourceStorage}; use std::collections::HashSet; @@ -274,7 +274,8 @@ async fn open_storage_or_fallback_to_legacy(container: AnyContainer, document_id container.remove(&path).await?; } } - GddV1::create_in(container, GddV1Layout, PeerId(generate_uuid()), document_id.0, version.clone(), version) + let peer = PeerId(generate_uuid()); + GddV1::create_in(container, GddV1Layout, peer, UserId(peer.0), document_id.0, version.clone(), version) } .await; match created { @@ -428,7 +429,7 @@ mod tests { #[tokio::test] async fn a_failed_migration_keeps_the_stored_document() { let container = AnyContainer::Memory(MemoryBackend::new()); - let mut gdd = GddV1::create_in(container.clone(), GddV1Layout, PeerId(1), 1, "editor".into(), "std".into()).unwrap(); + let mut gdd = GddV1::create_in(container.clone(), GddV1Layout, PeerId(1), UserId(1), 1, "editor".into(), "std".into()).unwrap(); // No format step starts from version 0, so the migration fails gdd.update_manifest(|manifest| manifest.format_version = 0).unwrap(); diff --git a/node-graph/graphene-cli/src/lib.rs b/node-graph/graphene-cli/src/lib.rs index eb3a9d5d6f9..59e12d07bdc 100644 --- a/node-graph/graphene-cli/src/lib.rs +++ b/node-graph/graphene-cli/src/lib.rs @@ -95,7 +95,6 @@ pub struct CompiledDocument { spawner: Arc, } -/// Compile `document` against a fresh GPU context, resolving its resources from its `.gdd`. pub async fn compile_document(document: OpenedDocument) -> Result> { log::info!("Creating GPU context"); let mut application_io = PlatformApplicationIo::new().await;