Skip to main content

dig_service/
traits.rs

1//! The three lifecycle / API traits DIG binaries implement to plug into
2//! [`crate::Service`].
3//!
4//! - [`NodeLifecycle`]: the business-logic core. Required for every service.
5//! - [`PeerApi`]: the peer-protocol dispatcher. Optional — `()` impls no-op.
6//! - [`RpcApi`]: the JSON-RPC dispatcher. Optional — `()` impls no-op.
7//!
8//! All three are trait objects — consumers can plug arbitrary concrete
9//! types. We use `async_trait::async_trait` for v0.1 so the returned
10//! futures are `Box<dyn Future>`; this is tolerable at the lifecycle layer
11//! (one call per hook) but will migrate to `async fn` in traits when MSRV
12//! reaches 1.75+.
13
14use async_trait::async_trait;
15
16use crate::shutdown::{ExitReason, ShutdownToken};
17use crate::tasks::TaskRegistry;
18
19/// A stable peer identifier — typically `SHA256(remote cert pubkey)`.
20pub type PeerId = [u8; 32];
21
22/// Peer metadata populated at connect time.
23#[derive(Debug, Clone)]
24pub struct PeerInfo {
25    /// Peer identifier.
26    pub peer_id: PeerId,
27    /// Remote `"ip:port"` string.
28    pub remote_addr: String,
29    /// Peer-declared node type (for debug / logs).
30    pub node_type: String,
31}
32
33/// Why a peer disconnected.
34#[non_exhaustive]
35#[derive(Debug, Clone)]
36pub enum DisconnectReason {
37    /// Peer closed gracefully.
38    Graceful,
39    /// Local side closed (shutdown, eviction).
40    LocalClose,
41    /// Transport error.
42    Transport(String),
43    /// Protocol violation — peer was banned.
44    ProtocolViolation(String),
45}
46
47/// A raw inbound peer-protocol message.
48///
49/// The shape is intentionally opaque at this layer; concrete binaries
50/// decode opcodes via `dig-protocol` and route to typed handlers.
51#[derive(Debug, Clone)]
52pub struct InboundMessage {
53    /// The opcode byte / u8.
54    pub opcode: u8,
55    /// Raw payload bytes.
56    pub payload: Vec<u8>,
57}
58
59/// Context passed to `pre_start` / `on_start`.
60pub struct StartContext<'a> {
61    /// Service name (equal to `N::NAME`).
62    pub name: &'static str,
63    /// Shutdown token. Capture `.clone()` into background tasks.
64    pub shutdown: ShutdownToken,
65    /// Task registry. Spawn background loops here so they join cleanly.
66    pub tasks: &'a TaskRegistry,
67}
68
69/// Context passed to `run`. Differs from `StartContext` only in that it
70/// owns a `TaskRegistry` (spawning from inside `run` is the common case).
71pub struct RunContext {
72    /// Shutdown token. Await `.cancelled()` to notice shutdown.
73    pub shutdown: ShutdownToken,
74    /// Task registry.
75    pub tasks: TaskRegistry,
76}
77
78/// Context passed to `on_stop` / `post_stop`.
79pub struct StopContext<'a> {
80    /// Shutdown token.
81    pub shutdown: ShutdownToken,
82    /// Task registry (for inspection).
83    pub tasks: &'a TaskRegistry,
84    /// The reason the service is exiting.
85    pub exit_reason: ExitReason,
86}
87
88/// The business-logic core of a service.
89///
90/// Implementors own all in-memory state (stores, pools, caches) and expose
91/// it through the five lifecycle hooks. See crate-level docs for ordering.
92///
93/// # Associated constants
94///
95/// - `NAME` — an optional static identifier surfaced via `ServiceHandle::name`
96///   and used in tracing spans. Set to `Some("validator")` etc.
97#[async_trait]
98pub trait NodeLifecycle: Send + Sync + 'static {
99    /// Optional static name. Used in logs; may be `None`.
100    const NAME: Option<&'static str> = None;
101
102    /// Called before peer / RPC servers bind. Typical work:
103    /// open stores, replay journal, restore state.
104    async fn pre_start(&self, ctx: &StartContext<'_>) -> anyhow::Result<()>;
105
106    /// Called after peer / RPC servers bind but before `run` begins. Typical
107    /// work: announce capabilities, warm caches.
108    async fn on_start(&self, ctx: &StartContext<'_>) -> anyhow::Result<()>;
109
110    /// Main run loop. Returns on:
111    /// - `ctx.shutdown.cancelled()` firing (graceful), OR
112    /// - a fatal internal error (`Err`).
113    async fn run(&self, ctx: RunContext) -> anyhow::Result<()>;
114
115    /// Called after `run` returns. Typical work: flush stores,
116    /// write snapshots. Errors here are reported but do not stop `post_stop`
117    /// from also running.
118    async fn on_stop(&self, ctx: &StopContext<'_>) -> anyhow::Result<()>;
119
120    /// Final hook. Typical work: close stores, release file locks.
121    async fn post_stop(&self, ctx: &StopContext<'_>) -> anyhow::Result<()>;
122}
123
124/// Peer-protocol dispatcher.
125///
126/// `()` is blanket-implemented to no-op every method; binaries that don't
127/// serve a peer surface (daemon, wallet) instantiate
128/// `Service<N, (), R>`.
129#[async_trait]
130pub trait PeerApi: Send + Sync + 'static {
131    /// Called for every inbound message after handshake succeeds.
132    async fn on_message(&self, _peer: PeerId, _msg: InboundMessage) -> anyhow::Result<()> {
133        Ok(())
134    }
135
136    /// Called when a peer connects (post-handshake).
137    async fn on_peer_connected(&self, _peer: PeerId, _info: PeerInfo) {}
138
139    /// Called when a peer disconnects.
140    async fn on_peer_disconnected(&self, _peer: PeerId, _reason: DisconnectReason) {}
141}
142
143#[async_trait]
144impl PeerApi for () {}
145
146/// JSON-RPC dispatcher.
147///
148/// `()` is blanket-implemented to route every method to `MethodNotFound`;
149/// binaries that don't serve RPC (introducer, relay) instantiate
150/// `Service<N, A, ()>`.
151#[async_trait]
152pub trait RpcApi: Send + Sync + 'static {
153    /// Called for every inbound RPC request. `method` is already parsed;
154    /// `params` is the method-specific JSON payload.
155    async fn dispatch(
156        &self,
157        method: &str,
158        params: serde_json::Value,
159    ) -> std::result::Result<serde_json::Value, dig_rpc_types::envelope::JsonRpcError>;
160
161    /// Optional health probe. Default: `Ok(())`.
162    async fn healthz(&self) -> std::result::Result<(), dig_rpc_types::envelope::JsonRpcError> {
163        Ok(())
164    }
165}
166
167#[async_trait]
168impl RpcApi for () {
169    async fn dispatch(
170        &self,
171        method: &str,
172        _params: serde_json::Value,
173    ) -> std::result::Result<serde_json::Value, dig_rpc_types::envelope::JsonRpcError> {
174        Err(dig_rpc_types::envelope::JsonRpcError {
175            code: dig_rpc_types::errors::ErrorCode::MethodNotFound,
176            message: format!("RpcApi not configured; method {method:?} rejected"),
177            data: None,
178        })
179    }
180}