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}