pub struct AppState {
pub nats: Option<Client>,
pub jetstream: Option<Context>,
pub formations: Arc<RwLock<BTreeMap<Uuid, FormationRecord>>>,
pub cells: Arc<RwLock<BTreeMap<String, CellRecord>>>,
pub api_token: ApiToken,
pub applied_cursor: Arc<AtomicU64>,
}Fields§
§nats: Option<Client>NATS connection used both by the WebSocket bridge and by future
projection replay. Option because tests can drive the router
without a live broker.
jetstream: Option<Context>JetStream context (ADR-0011, ADR-0015). Populated alongside
nats once the server confirms the CELLOS_EVENTS stream
exists. Option so router tests without a live broker still
build the state cleanly; the WS bridge falls through to a
“broker not configured” close if this is None.
formations: Arc<RwLock<BTreeMap<Uuid, FormationRecord>>>In-memory projection of formations. UUID-keyed for stable lookup.
cells: Arc<RwLock<BTreeMap<String, CellRecord>>>In-memory projection of cells.
api_token: ApiTokenBearer token required on every non-public route.
applied_cursor: Arc<AtomicU64>Highest JetStream stream-sequence the projection has applied
(ADR-0015 §D2). The snapshot endpoint reports this as cursor
so clients can open /ws/events?since=<cursor> and know they
will not miss any event after the snapshot.
The WebSocket bridge bumps this monotonically as it forwards
frames (see ws.rs). Until the bridge is migrated to a real
JetStream consumer, this counter is per-process and resets on
restart — that is acceptable for the MVP because every browser
reconnect repeats the snapshot fetch.
Implementations§
Source§impl AppState
impl AppState
Sourcepub fn new(nats: Option<NatsClient>, api_token: impl Into<String>) -> Self
pub fn new(nats: Option<NatsClient>, api_token: impl Into<String>) -> Self
Constructor used by both main.rs and the test harness.
Sourcepub fn with_jetstream(self, ctx: JsContext) -> Self
pub fn with_jetstream(self, ctx: JsContext) -> Self
Attach a JetStream context. Called from main.rs after
ensure_stream succeeds. Returns the same AppState for
builder-style chaining in tests.
Sourcepub fn cursor(&self) -> u64
pub fn cursor(&self) -> u64
Current snapshot cursor — the highest seq the server has applied.
Red-team wave 2 (LOW-W2A-1): Acquire is sufficient here. We need
to see writes that happen-before any successful bump_cursor
(i.e. the projection-apply write inside apply_event_payload);
the previous SeqCst enforced a global total order with every
other SeqCst operation in the process (counter fetch_adds in
sni_proxy, etc.) for no extra correctness gain on this read.
Sourcepub fn bump_cursor(&self, seq: u64)
pub fn bump_cursor(&self, seq: u64)
Bump the snapshot cursor to at least seq. Monotonic; out-of-order
values are ignored. Used by the WebSocket bridge as it forwards
frames.
Red-team wave 2 (LOW-W2A-1): downgraded from SeqCst to
AcqRel/Acquire. The CAS-loop structure (re-load on conflict,
strict seq > current gate) guarantees monotonicity independent
of the memory ordering chosen. AcqRel on success publishes the
new cursor to every subsequent cursor() reader (snapshot GET
handlers, primarily); Acquire on the reload picks up the
observed value from the winner of the conflict.
Sourcepub async fn apply_event_payload(&self, payload: &[u8]) -> Result<ApplyOutcome>
pub async fn apply_event_payload(&self, payload: &[u8]) -> Result<ApplyOutcome>
Apply a single CloudEvent payload (raw JSON bytes from JetStream) to the in-memory projection.
This is shared between the boot-time replay path
(jetstream::replay_projection) and the live WebSocket bridge
(ws.rs). Centralising the apply logic guarantees that what
the server reconstructs on startup is exactly what it
applies live — there is no “replay-only” code path that could
drift from the live one.
CloudEvent type discriminates the transition. Two event families
mutate state today:
formation.v1.*— server-emitted formation lifecycle, drives theformationsprojection (ADR-0010 admission gate output).cell.lifecycle.v1.*andcell.command.v1.completed— supervisor-emitted cell lifecycle (ARCH-001). Without this, the server replays cell events from JetStream but never updates thecellsmap andGET /v1/cellsreturns[]. The cell projector landed alongside the formation projector socellctl get cellsreflects what the supervisor actually ran.
Unknown event types still advance the JetStream cursor at the
caller (the apply contract is independent of whether the
projection mutated), so audit gaps surface as ApplyOutcome::Ignored
in the replay log rather than silent drops.