Files
Graphite/document/document-format/src/io.rs
Dennis Kobert de11d29d8e 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
2026-06-21 12:35:47 +00:00

61 lines
2.5 KiB
Rust

//! Bridge between [`crate::Codec`] and [`document_container::AnyContainer`]. Each payload's codec
//! is known up front (the manifest is always JSON; every other payload's codec is recorded in the
//! manifest), so reads and writes address a fixed `{basename}.{ext}` path without probing.
use document_container::{AnyContainer, AsyncContainer};
use serde::Serialize;
use serde::de::DeserializeOwned;
use crate::{Codec, CodecError};
/// Compose a container path from `basename` and `codec.extension()`.
pub fn path_for(basename: &str, codec: Codec) -> String {
format!("{basename}.{}", codec.extension())
}
#[derive(Debug, thiserror::Error)]
pub enum ReadError {
#[error("file not found for basename {basename:?} with codec {codec:?}")]
NotFound { basename: String, codec: Codec },
#[error("container error: {0}")]
Container(#[from] document_container::ContainerError),
#[error("codec error: {0}")]
Codec(#[from] CodecError),
}
/// Read `{basename}.{ext}` and decode the single value it contains.
pub async fn read_single<T: DeserializeOwned>(container: &AnyContainer, basename: &str, codec: Codec) -> Result<T, ReadError> {
let bytes = read_bytes(container, basename, codec).await?;
Ok(codec.read_single::<T>(bytes.as_slice())?)
}
/// Same as [`read_single`] but yields every value when `codec` is a stream codec.
pub async fn iter<T: DeserializeOwned>(container: &AnyContainer, basename: &str, codec: Codec) -> Result<Vec<T>, ReadError> {
let bytes = read_bytes(container, basename, codec).await?;
Ok(codec.iter::<T>(bytes.as_slice()).collect::<Result<Vec<_>, _>>()?)
}
/// Whether `{basename}.{ext}` exists for the given codec.
pub async fn exists(container: &AnyContainer, basename: &str, codec: Codec) -> bool {
container.exists(&path_for(basename, codec)).await
}
/// Encode `value` with `codec` and write to `{basename}.{ext}`. Synchronous: the write goes through
/// the container's sync write surface (durable on folder/memory, enqueued on OPFS).
pub fn write_single<T: Serialize>(container: &AnyContainer, basename: &str, codec: Codec, value: &T) -> Result<(), crate::Error> {
let bytes = codec.write_single(value)?;
container.write_non_blocking(&path_for(basename, codec), &bytes)?;
Ok(())
}
async fn read_bytes(container: &AnyContainer, basename: &str, codec: Codec) -> Result<document_container::ByteHolder, ReadError> {
let path = path_for(basename, codec);
if !container.exists(&path).await {
return Err(ReadError::NotFound {
basename: basename.to_string(),
codec,
});
}
Ok(container.read(&path).await?)
}