lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit e211b80133a563ab5838a5a0675b612a7c92ff41
parent 4367ce9d28abf8aeeb46b9bb0e2da0db41a90238
Author: triesap <tyson@radroots.org>
Date:   Thu, 11 Jun 2026 16:54:44 -0700

events: add farm crdt change model

- add the Field CRDT change envelope and schema constants
- add typed document kind, backend, and semantic kind enums
- cover required envelope fields and serde content shape with unit tests

Diffstat:
Acrates/events/src/farm_crdt.rs | 132+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/events/src/lib.rs | 1+
2 files changed, 133 insertions(+), 0 deletions(-)

diff --git a/crates/events/src/farm_crdt.rs b/crates/events/src/farm_crdt.rs @@ -0,0 +1,132 @@ +#![forbid(unsafe_code)] + +use crate::farm_workspace::RadrootsFarmWorkspaceRef; +use crate::kinds::KIND_FARM_CRDT_CHANGE as KIND_FARM_CRDT_CHANGE_EVENT; + +#[cfg(not(feature = "std"))] +use alloc::{string::String, vec::Vec}; + +pub const KIND_FARM_CRDT_CHANGE: u32 = KIND_FARM_CRDT_CHANGE_EVENT; +pub const RADROOTS_FARM_CRDT_CHANGE_SCHEMA: &str = "radroots.farm.crdt.change.v1"; +pub const RADROOTS_FARM_CRDT_TAG: &str = "radroots:farm:crdt"; + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsFarmCrdtChange { + pub schema: String, + pub workspace: RadrootsFarmWorkspaceRef, + pub farm_group_id: String, + pub document_id: String, + pub document_kind: RadrootsFarmCrdtDocumentKind, + pub crdt_backend: RadrootsCrdtBackend, + pub crdt_backend_version: Option<String>, + pub actor_id: String, + pub change_hash: String, + pub dependencies: Vec<String>, + pub encoded_change: String, + pub semantic_kind: RadrootsFarmSemanticKind, + pub business_time_ms: u64, + pub author_member_id: Option<String>, + pub app_version: Option<String>, +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RadrootsFarmCrdtDocumentKind { + FarmTask, + FarmWorkSession, + FarmHarvestRecord, + FarmInventoryItem, + FarmMediaAsset, + FarmObservation, +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RadrootsCrdtBackend { + Automerge, + Yjs, + Loro, +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RadrootsFarmSemanticKind { + FarmTaskCreate, + FarmTaskUpdate, + FarmTaskComplete, + FarmWorkSessionStart, + FarmWorkSessionUpdate, + FarmWorkSessionEnd, + FarmHarvestRecordCreate, + FarmHarvestRecordUpdate, + FarmInventoryItemUpdate, + FarmMediaAssetAttach, + FarmObservationCreate, + FarmWorkspaceUpdate, +} + +#[cfg(all(test, feature = "serde"))] +mod tests { + use super::*; + + #[test] + fn crdt_change_kind_uses_custom_app_data_kind() { + assert_eq!(KIND_FARM_CRDT_CHANGE, 78); + } + + #[test] + fn crdt_change_represents_required_envelope_fields() { + let change = sample_change(); + + assert_eq!(change.schema, RADROOTS_FARM_CRDT_CHANGE_SCHEMA); + assert_eq!(change.workspace.pubkey, "workspace_pubkey"); + assert_eq!(change.farm_group_id, "BCDEFGHIJKLMNOPQRSTUVW"); + assert_eq!(change.document_id, "DEFGHIJKLMNOPQRSTUVWXY"); + assert_eq!(change.document_kind, RadrootsFarmCrdtDocumentKind::FarmTask); + assert_eq!(change.crdt_backend, RadrootsCrdtBackend::Automerge); + assert_eq!(change.dependencies, Vec::<String>::new()); + assert_eq!( + change.semantic_kind, + RadrootsFarmSemanticKind::FarmTaskCreate + ); + assert_eq!(change.business_time_ms, 1_780_000_000_000); + assert_eq!(change.author_member_id.as_deref(), Some("member_abc")); + assert_eq!(change.app_version.as_deref(), Some("0.1.0")); + } + + #[test] + fn crdt_change_serializes_stable_content_shape() { + let value = serde_json::to_value(sample_change()).unwrap(); + + assert_eq!(value["schema"], RADROOTS_FARM_CRDT_CHANGE_SCHEMA); + assert_eq!(value["workspace"]["d_tag"], "ABCDEFGHIJKLMNOPQRSTUV"); + assert_eq!(value["document_kind"], "FarmTask"); + assert_eq!(value["crdt_backend"], "Automerge"); + assert_eq!(value["semantic_kind"], "FarmTaskCreate"); + assert_eq!(value["business_time_ms"], 1_780_000_000_000_u64); + } + + fn sample_change() -> RadrootsFarmCrdtChange { + RadrootsFarmCrdtChange { + schema: RADROOTS_FARM_CRDT_CHANGE_SCHEMA.to_string(), + workspace: RadrootsFarmWorkspaceRef { + pubkey: "workspace_pubkey".to_string(), + d_tag: "ABCDEFGHIJKLMNOPQRSTUV".to_string(), + }, + farm_group_id: "BCDEFGHIJKLMNOPQRSTUVW".to_string(), + document_id: "DEFGHIJKLMNOPQRSTUVWXY".to_string(), + document_kind: RadrootsFarmCrdtDocumentKind::FarmTask, + crdt_backend: RadrootsCrdtBackend::Automerge, + crdt_backend_version: Some("0.x".to_string()), + actor_id: "actor_abc".to_string(), + change_hash: "crdt_hash_abc".to_string(), + dependencies: Vec::new(), + encoded_change: "base64url-encoded-change".to_string(), + semantic_kind: RadrootsFarmSemanticKind::FarmTaskCreate, + business_time_ms: 1_780_000_000_000, + author_member_id: Some("member_abc".to_string()), + app_version: Some("0.1.0".to_string()), + } + } +} diff --git a/crates/events/src/lib.rs b/crates/events/src/lib.rs @@ -12,6 +12,7 @@ pub mod comment; pub mod coop; pub mod document; pub mod farm; +pub mod farm_crdt; pub mod farm_workspace; pub mod follow; pub mod geochat;