//! Persistent notes in `/state/notes/notes.json`. //! Each call has its own process. The add method locks the file before its read-modify-write operation. //! Atomic replacement lets readers observe a complete list without a lock. use sausage_plugin::serde_json; use sausage_plugin::{Result, lock, rpc, schema, state_dir, write_atomic}; /// The stateless notes plugin reads its file on each call. struct Notes; /// Parameters for a new note. #[schema] struct AddParams { /// The complete note text, including any newlines. text: String, } /// The result after a note enters the list. #[schema] struct Added { /// The number of notes, including the new note. count: usize, } #[rpc] impl Notes { /// Add a note and return the new count. async fn add(&self, AddParams { text }: AddParams) -> Result { let path = state_dir().join("notes.json"); let _guard = lock(&path).await?; let mut notes = load()?; notes.push(text); std::fs::create_dir_all(state_dir())?; write_atomic(path, serde_json::to_vec(¬es)?)?; Ok(Added { count: notes.len() }) } /// List all notes, oldest first. fn list(&self) -> Result> { load() } } /// Read the list, or return an empty list when the file does not exist. fn load() -> Result> { match std::fs::read(state_dir().join("notes.json")) { Ok(bytes) => Ok(serde_json::from_slice(&bytes)?), Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(vec![]), Err(e) => Err(e.into()), } } /// Start the SDK transport for notes. fn main() { sausage_plugin::run(Notes) } /// Tests run in fresh worlds under the WASI test runner. #[cfg(test)] mod tests { use super::*; /// The core lock protects two sequential additions and their counts. #[sausage_plugin::test] async fn add_appends_and_counts() { assert_eq!( Notes .add(AddParams { text: "milk".into() }) .await .unwrap() .count, 1 ); assert_eq!( Notes .add(AddParams { text: "eggs".into() }) .await .unwrap() .count, 2 ); assert_eq!(Notes.list().unwrap(), ["milk", "eggs"]); } /// A fresh test world has no notes file. #[test] fn list_is_empty_without_a_file() { assert!(Notes.list().unwrap().is_empty()); } }