hub.rsannotatedhub.rssource63 lines · 3.5 KB · raw
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 {}