Skip to content

Core: the Account owns its requests and responses; the reducer only carries what follows - #70

Merged
JacobStephens2 merged 2 commits into
mainfrom
issue-69
Oct 4, 2026
Merged

JacobStephens2 merged 2 commits into
mainfrom
issue-69

Conversation

@JacobStephens2

Copy link
Copy Markdown
Owner

Summary

The Account now applies its own rules. The reducer only carries the AccountChange it returns through to listening and storage, in one place:

 Command::ServerResponse { id, status, body }
-  account.settle(id) -> PendingRequest
-    account_response(): 401 rule, status, session, persists   # in reducer
-  listening.awaits(id)
-    sync_response(): parse body, sync_succeeded/sync_failed, 401 rule
+  if account.awaits(id)
+    account.settle(class, body) -> Option<AccountChange>
+      apply_account_change()          # the one place write order is decided
+  else if listening.awaits(id)
+    listening.settle(class, body) -> Synced | Failed | Unauthorized | Dropped
+      Synced       -> persist listening
+      Unauthorized -> apply_account_change(account.expire())
crates/cascade-core/src/
├── account.rs    # private session/pending/status; intents, busy guard, settle, expire, snapshot
├── listening.rs  # ledger builds + sends its own push; settle -> Settled
├── server.rs     # + RequestIds: the one id source; into_effect is private
├── state.rs      # reducer: delegates intents; apply_account_change (~130 lines of account rules gone)
└── snapshot.rs   # account section comes from Account::snapshot()

The id peek and its debug_assert are gone. Each module asks RequestIds for an id only when it sends.

Within one update, the listening write now always comes before the account write. Sign-out and a 401 used to write the account first. ADR 0001 decision 6 gains the sentence that records this.

Evidence

  • Before: the new test a_401_writes_listening_before_the_account failed on the old reducer:
    assertion failed: sync
      left: ["account", "listening"]
     right: ["listening", "account"]
    
    After: it passes, along with an_account_delete_writes_listening_before_the_account. The diff to existing tests is additions only, and every existing core test passes unchanged through Core::dispatch.
  • cargo test --workspace, cargo clippy --workspace --all-targets and cargo fmt --check all pass. cascade-wasm (wasm32) and cascade-uniffi build.

Merge Danger

Door: two-way

No command, effect, snapshot field or wire shape changes. The one thing a shell can observe is the order of PersistListening / PersistAccount on sign-out and on a 401. Each shell runs every effect it is handed, so none depends on that order.

Blast Radius: core-internal

A regression here would show up in account or sync behaviour on every shell. The existing account, listening-sync, restore and property suites pin that behaviour.

Unaddressed findings

Standards

  • The ADR sentence should be a new dated amendment citing Core: the Account owns its requests and responses; the reducer only carries what follows #69, with the Status line updated. Skipped: the spec asks for exactly one sentence in decision 6 and says "Nothing else in the ADR changes."
  • ADR decision 5 says outcome routing lives in server.rs, but it now lives in Account::settle / ListeningLedger::settle. Skipped: the spec says nothing else in the ADR changes. Endpoints, bodies and body parsing still live in server.rs. Worth a follow-up amendment.
  • Duplicated shape in delete_listening / delete_account, and a busy check in four methods. Skipped: the spec decided on one method per intent. The shared step is already begin, so a further helper would hide each intent's guard.
  • Data clump: ids and effects travel together. Skipped: the spec gives the ledger "the session token and the id source". An outbox type would be a separate design change.
  • Failed and Dropped are handled the same way by the reducer. Skipped: the spec names these four outcomes.
  • Primitive obsession: new_device_id: String. Skipped: inherited from before this change, and out of scope.
  • Add Account::is_signed_in() for the restore check. Skipped: the spec fixes the Account's interface, and session_token() already covers this check.
  • Middle man in the reducer's intent arms. Skipped: the delegation is intentional.

Spec

  • ADR decision 5's phrase about where outcome routing lives is slightly stale. Same as above: skipped because the spec says nothing else in the ADR changes.

Closes #69

🤖 Generated with Claude Code

JacobStephens2 and others added 2 commits October 3, 2026 20:19
…arries what follows

The Account's session, pending request and status are private; it answers
each account intent and response itself and returns an AccountChange. The
listening ledger sends and settles its own push. Request ids come from one
RequestIds source in core state. apply_account_change is the one place the
write order is decided: listening before account, now for sign-out and 401
too, pinned by a new account test. ADR 0001 decision 6 records the order.

Closes #69

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@JacobStephens2
JacobStephens2 merged commit 24cb778 into main Oct 4, 2026
3 checks passed
@JacobStephens2
JacobStephens2 deleted the issue-69 branch October 4, 2026 00:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Core: the Account owns its requests and responses; the reducer only carries what follows

1 participant