hub.rsannotatedhub.rssource63 lines · 3.5 KB · raw

The three documents every device shares and the household's hub holds the master copy of: what Whiskers remembers, the grown-ups' choices with the day's time, and the one running chat.

A hub is not get/put. A device does not fetch the master copy, change it and write it back; it hands over what it knows and is given what the hub now knows. That one verb, merge, is the whole write side, and the adapter must make it atomic in whatever way its platform allows (a mutex and an atomic file replace natively; a Durable Object's single thread of execution in a Worker). A get/put pair would make atomicity every caller's problem and every adapter's silent gap: two devices syncing at once would each read, merge locally and write, and one would lose.

The merge rule is not the adapter's either. It is a pure function of the two documents that lives in whiskers-core (MemoryDoc::merge, Household::merge, ChatState::merge), so every backend converges to the same document and the adapter only decides where the result is kept.

15use whiskers_core::{ChatState, Household, MemorySnapshot};
17use crate::error::StoreError;

What a merge did.

20#[derive(Clone, Debug, PartialEq)]
21pub struct Merged<D> {

The hub's document once theirs is in: what the caller should now hold.

23    pub document: D,

How much of the hub's document changed because of this merge. Zero means the hub already held everything the caller sent, so repeating the same merge must always report zero.

26    pub changed: usize,
27}

A convergent document held by the household's hub.

Contract, for every adapter (the conformance suite in whiskers-conformance asserts it):

  • Atomic. Two merges, however they overlap, behave as if one ran entirely before the other.
  • Convergent. Merging the same set of documents in any order gives the same document (up to the hub's own numbering, which is not part of the document's meaning), and merging one in a second time changes nothing.
  • Durable once it returns. A merge that reported Ok survives the hub restarting. A merge that reported Err left the stored document as it was, and current reports that document too: 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;

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>;

The hub's document as it stands, changing nothing. For decisions that read a choice (the 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}

What Whiskers remembers. The document on the wire is a snapshot: facts and the identities of those 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 {}

The grown-ups' choices and the day's time.

58pub trait HouseholdHub: Hub<Document = Household> {}
59impl<T: Hub<Document = Household>> HouseholdHub for T {}

The one running session: the newest copy wins whole.

62pub trait ChatHub: Hub<Document = ChatState> {}
63impl<T: Hub<Document = ChatState>> ChatHub for T {}