Add document-format crate (#4234)

* Add document-format crate

* Adapt document-format to merged graph-storage and consolidate errors

* Fix resource write ordering, codec error masking, and export docs

* Remove document-format RFC; tracked in a dedicated PR

* Restructure document-format into gdd/persist/export/resource modules

Split the 963-line lib.rs by responsibility: persist path into persist.rs, resource I/O into resource.rs, export engine into export.rs. Rename the GddV1 layout struct to GddV1Layout and add a GddV1 = Gdd<GddV1Layout> alias.

* Feature-split document-format and graph-storage for minimal builds

* Address PR review: O(1) history-delta lookup and no-default-features test build

* Address PR review: propagate apply_hot_op persist error and assert resource hash

* Make decompression from archive streaming

* Share archive export body between export and export_to_bytes

* Review cleanup

* Export documents with un-retired hot ops losslessly

* Persist last_broadcast_rev in session.json, drop unused last_retired_at

* Move peer_id from manifest to session.json

* Rename gesture to interaction in document-format storage layer
This commit is contained in:
Dennis Kobert
2026-06-21 14:35:47 +02:00
committed by GitHub
parent 194c5b2222
commit de11d29d8e
23 changed files with 2569 additions and 58 deletions

View File

@@ -777,20 +777,33 @@ fn no_op_commit_preserves_redo_stack() {
assert!(session.can_redo(), "a no-op commit must not clear the redo stack");
}
/// `embed_resource_sources` overwrites the working registry with the snapshot, valid only when no
/// unretired hot ops are present. Called with a non-empty hot log it must error rather than silently
/// drop the hot-zone edits.
/// `embed_resource_sources` commits its `AddSource` deltas as retired, then mirrors them onto the
/// working registry. With unretired hot ops present it must keep the hot-zone edits (export of a
/// mid-interaction document is lossless) rather than clobbering the working registry with the snapshot.
#[test]
fn embed_resource_sources_rejects_unretired_hot_ops() {
fn embed_resource_sources_preserves_unretired_hot_ops() {
let mut session = Session::with_peer(PeerId(1));
let resources = graphene_resource::ResourceRegistry::new();
// Stage without retiring, leaving hot ops in the log.
session.stage_from_runtime(&tiny_network(), &NoMetadata, &resources).expect("stage");
assert!(!session.hot_log().is_empty(), "staging should leave unretired hot ops");
// Retire a base so the network's nodes live in the retired snapshot.
session.stage_from_runtime(&tiny_network(), &NoMetadata, &resources).expect("stage base");
let base_up_to = session.hot_log().last().expect("staged base").timestamp;
session.retire(base_up_to).expect("retire base");
let result = session.embed_resource_sources(std::iter::empty::<ResourceId>());
assert!(matches!(result, Err(crate::CrdtError::HotLogNotEmpty)), "expected HotLogNotEmpty, got {result:?}");
// Stage an embedded resource without retiring, leaving it in the hot log (the working registry now
// holds it, the retired snapshot does not).
let hash = ResourceHash::from(&b"hot-resource"[..]);
let id = ResourceId::new();
session.stage_embedded_resource(id, hash).expect("stage resource");
assert!(!session.hot_log().is_empty(), "staging should leave unretired hot ops");
assert!(session.registry().resources.contains_key(&id), "working registry should hold the hot resource");
session.embed_resource_sources(std::iter::empty::<ResourceId>()).expect("embed tolerates a non-empty hot log");
// The hot-zone resource survives in the working registry (not reset to the snapshot), and the hot log
// is untouched so a later retire still promotes it.
assert!(session.registry().resources.contains_key(&id), "hot resource must survive the embed");
assert!(!session.hot_log().is_empty(), "embed must not drain the hot log");
}
/// A delta's `Rev` is content-addressed, so two byte-equal deltas must hash identically regardless