1//! The three documents every device shares and the household's hub holds the master copy of: what 2//! Whiskers remembers, the grown-ups' choices with the day's time, and the one running chat. 3//! 4//! A hub is **not** get/put. A device does not fetch the master copy, change it and write it back; 5//! it hands over what it knows and is given what the hub now knows. That one verb, `merge`, is the 6//! whole write side, and the adapter must make it atomic in whatever way its platform allows (a mutex 7//! and an atomic file replace natively; a Durable Object's single thread of execution in a Worker). 8//! A get/put pair would make atomicity every caller's problem and every adapter's silent gap: two 9//! devices syncing at once would each read, merge locally and write, and one would lose. 10//! 11//! The merge *rule* is not the adapter's either. It is a pure function of the two documents that 12//! lives in `whiskers-core` (`MemoryDoc::merge`, `Household::merge`, `ChatState::merge`), so every 13//! backend converges to the same document and the adapter only decides where the result is kept. 14 15use whiskers_core::{ChatState, Household, MemorySnapshot}; 16 17use crate::error::StoreError; 18 19/// What a merge did. 20#[derive(Clone, Debug, PartialEq)] 21pub struct Merged<D> { 22 /// The hub's document once `theirs` is in: what the caller should now hold. 23 pub document: D, 24 /// How much of the hub's document changed because of this merge. Zero means the hub already held 25 /// everything the caller sent, so repeating the same merge must always report zero. 26 pub changed: usize, 27} 28 29/// A convergent document held by the household's hub. 30/// 31/// Contract, for every adapter (the conformance suite in `whiskers-conformance` asserts it): 32/// 33/// - **Atomic.** Two merges, however they overlap, behave as if one ran entirely before the other. 34/// - **Convergent.** Merging the same set of documents in any order gives the same document (up to 35/// the hub's own numbering, which is not part of the document's meaning), and merging one in a 36/// second time changes nothing. 37/// - **Durable once it returns.** A merge that reported `Ok` survives the hub restarting. A merge 38/// that reported `Err` left the stored document as it was, and `current` reports that document too: 39/// an adapter keeps a copy before it adopts it, never the other way round. 40#[expect(async_fn_in_trait, reason = "a Worker's futures hold JavaScript values and cannot be Send, so no Send bound may be required here")] 41pub trait Hub { 42 type Document; 43 44 /// Takes `theirs` in and returns the hub's document as it now stands. 45 async fn merge(&self, theirs: Self::Document) -> Result<Merged<Self::Document>, StoreError>; 46 47 /// The hub's document as it stands, changing nothing. For decisions that read a choice (the 48 /// token window, the voice allowance) and never for a read-modify-write: that is `merge`. 49 async fn current(&self) -> Result<Self::Document, StoreError>; 50} 51 52/// What Whiskers remembers. The document on the wire is a snapshot: facts and the identities of those 53/// the parents forgot (a forgotten fact wins over its own copy on any device). 54pub trait MemoryHub: Hub<Document = MemorySnapshot> {} 55impl<T: Hub<Document = MemorySnapshot>> MemoryHub for T {} 56 57/// The grown-ups' choices and the day's time. 58pub trait HouseholdHub: Hub<Document = Household> {} 59impl<T: Hub<Document = Household>> HouseholdHub for T {} 60 61/// The one running session: the newest copy wins whole. 62pub trait ChatHub: Hub<Document = ChatState> {} 63impl<T: Hub<Document = ChatState>> ChatHub for T {}