aviso 2.0.0-rc.2

Core client library for aviso-server, ECMWF's notification service.
Documentation
// (C) Copyright 2024- ECMWF and individual contributors.
//
// This software is licensed under the terms of the Apache Licence Version 2.0
// which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
// In applying this licence, ECMWF does not waive the privileges and immunities
// granted to it by virtue of its status as an intergovernmental organisation nor
// does it submit to any jurisdiction.

//! Watch state machine.
//!
//! This module contains the pure-logic state machine that drives a
//! watch session, as specified by ADRs D2 and D15. The state is the
//! orthogonal product of two axes,
//! [`ReplayPhase`] x [`ConnectionStatus`], and is advanced through a
//! single reducer. The reducer owns no I/O, no async runtime, and no
//! checkpoint state; downstream watch supervisors wire it to the
//! HTTP/SSE transport, the auth provider, and the [`crate::state`]
//! store.
//!
//! The public surface is accessible as `aviso::watch::*`. The
//! [`crate::AvisoClient::watch`] method returns a [`NotificationStream`]
//! whose supervisor task drives this reducer; the same supervisor also
//! backs [`crate::AvisoClient::watch_with_handler`].
//!
//! # Concepts
//!
//! - [`ReplayPhase`]: where the stream is in its replay-or-live
//!   lifecycle. Starts in `Replaying { start, replay_completed: false }`
//!   when constructed with a resume position, or in `Live` when
//!   constructed without one.
//! - [`ConnectionStatus`]: the transport-level state of the SSE
//!   connection (connected, reconnecting, waiting on backoff, refreshing
//!   auth).
//! - [`WatchMode`]: whether the session is `Watch` (historical-then-live;
//!   reconnects on `end_of_stream`) or `ReplayOnly` (terminates on
//!   `end_of_stream` once the server's `replay_completed` event has been
//!   received).
//! - [`WatchEvent`]: the input vocabulary the supervisor uses to drive
//!   the reducer.
//! - [`WatchOutcome`]: the reducer's reply, telling the supervisor what
//!   to do next (continue, reconnect with a [`ReconnectPolicy`], refresh
//!   auth, surface a gap, or stop).
//!
//! # Transition summary
//!
//! The full transition table (one row per event x precondition pair)
//! lives in the commit message that introduced
//! `crates/aviso/src/watch/state.rs`. The summary below is the
//! reader-friendly view; the row numbers in `state.rs`'s match arms
//! refer to the same table.
//!
//! - `ConnectionEstablished`: `connection_status` -> `Connected`.
//! - `ConnectionLost { .. }` or `HeartbeatStarvation`: reconnect with
//!   [`ReconnectPolicy::ExponentialBackoff`].
//! - `ServerClose { reason }`: dispatches on the reason.
//!   `MaxDurationReached` reconnects immediately;
//!   `ServerShutdown` applies a short backoff;
//!   `EndOfStream` in [`WatchMode::Watch`] reconnects immediately;
//!   `EndOfStream` in [`WatchMode::ReplayOnly`] terminates iff
//!   `replay_completed` was already true, otherwise reconnects.
//! - `BackoffStarted(d)` records the duration in
//!   [`ConnectionStatus::BackoffWait`]; `BackoffElapsed` returns the
//!   reducer to `Reconnecting` only if currently waiting.
//! - `AuthRejected`: `connection_status` -> `RefreshingAuth`; outcome
//!   [`WatchOutcome::RefreshAuth`].
//!   `AuthRefreshCompleted { success: true }`: `connection_status` ->
//!   `Reconnecting`.
//!   `AuthRefreshCompleted { success: false }`: terminate with
//!   [`FatalKind::AuthenticationRejectedAfterRefresh`].
//! - `HeartbeatReceived`, `NotificationReceived { .. }`: pure
//!   observations, no state change.
//! - `ReplayCompleted`: in [`WatchMode::Watch`] from `Replaying` moves
//!   to `Live`. In [`WatchMode::ReplayOnly`] from `Replaying { rc:
//!   false }` flips `replay_completed` to true. Idempotent in every
//!   other non-terminal phase.
//! - `GapDetected(reason)`: phase -> `GapDetected { reason }`; outcome
//!   [`WatchOutcome::Gap`] (dedicated, not folded into `Continue`).
//! - `Fatal(kind)`: phase -> `Closed { Fatal { kind } }`; outcome
//!   `Stop`.
//! - `Stop`: phase -> `Closed { UserRequested }`; outcome `Stop`.
//! - `Closed` is sticky: every subsequent event is a no-op returning
//!   `Continue`.
//!
//! # Usage
//!
//! ```
//! use aviso::watch::{
//!     ReconnectPolicy, ServerCloseReason, WatchEvent, WatchOutcome,
//!     WatchState,
//! };
//!
//! // A live-only watch (no replay backlog) starts directly in
//! // `Live` with `Reconnecting` while the transport opens.
//! let mut state = WatchState::watch(None);
//!
//! // Supervisor establishes the transport.
//! let _ = state.transition(WatchEvent::ConnectionEstablished);
//!
//! // Server hits its `connection_max_duration_sec`. Routine close;
//! // reconnect immediately, no backoff.
//! let outcome = state.transition(WatchEvent::ServerClose {
//!     reason: ServerCloseReason::MaxDurationReached,
//! });
//! assert_eq!(
//!     outcome,
//!     WatchOutcome::Reconnect {
//!         policy: ReconnectPolicy::Immediate,
//!     }
//! );
//!
//! // User asks the watch to stop.
//! let _ = state.transition(WatchEvent::Stop);
//! assert!(state.is_terminal());
//! ```
//!
//! # Cross-references
//!
//! - D2 (`plans/decisions.md`): reconnect-as-norm,
//!   at-least-once delivery, state-machine sketch, reconnect classifier.
//! - D15: state machine is the orthogonal product `ReplayPhase x
//!   ConnectionStatus`, single reducer, fields private.
//! - D9: a malformed `CloudEvent` id is terminal; surfaces here through
//!   [`WatchEvent::Fatal`] with [`FatalKind::MalformedEvent`].
//! - D17: `from_date` is bootstrap-only and converts to a sequence
//!   cursor after the first commit. The reducer does not own that
//!   bookkeeping; the supervisor advances its own checkpoint state in
//!   response to [`WatchEvent::NotificationReceived`].

mod backoff;
mod connection;
mod event;
mod mode;
mod outcome;
mod phase;
mod request;
mod retry_after;
mod state;
mod stream;
mod supervisor;
mod trigger;
mod wire;

pub use connection::{ConnectionLossReason, ConnectionStatus};
pub use event::{ServerCloseReason, WatchEvent};
pub use mode::WatchMode;
pub use outcome::{ReconnectPolicy, WatchOutcome};
pub use phase::{CloseReason, FatalKind, GapReason, ReplayPhase, ResumeStart};
pub use request::WatchRequest;
pub use state::WatchState;
pub use stream::NotificationStream;
#[cfg(unix)]
pub use trigger::CommandTriggerConfig;
pub use trigger::{
    DEFAULT_WEBHOOK_TIMEOUT, EchoConfig, HttpMethod, LogConfig, TemplateErrorKind, Trigger,
    TriggerConfig, TriggerError, TriggerKindLabel, WebhookTriggerConfig,
};

pub(crate) use supervisor::{CHANNEL_CAPACITY, run_supervisor};
pub(crate) use wire::WireWatchRequest;