Skip to main content

liminal_server/server/connection/
notifier.rs

1//! Connection-keyed notifier hook for worker registration lifecycle.
2//!
3//! This is the application seam for self-describing worker registration. When a
4//! worker sends a [`Frame::WorkerRegister`](liminal::protocol::Frame) over its
5//! established connection, the server associates the registration with the
6//! connection's beamr process id and invokes the configured
7//! [`ConnectionNotifier`]; on connection close it invokes the matching
8//! deregistration. The notifier is connection-keyed (by pid), which is distinct
9//! from the subject-keyed responder registry in [`super::services`].
10//!
11//! Keeping the hook a `liminal-server` trait — rather than a liminal-core
12//! concern — preserves liminal's generality: liminal still runs standalone with
13//! no notifier configured, and the application (aion, in Stage 2) plugs its
14//! registry in without liminal depending on it.
15//!
16//! The same trait carries the presence seam for pass-stamped connections: a
17//! connection whose `Connect` verified a registry pass (a browser or agent
18//! seat, which sends no `WorkerRegister`) announces itself once on attach and
19//! once on detach through [`ConnectionNotifier::on_pass_attached`] and
20//! [`ConnectionNotifier::on_pass_detached`], so an application derives
21//! presence from those two events and never polls for it.
22
23use liminal::protocol::WorkerRegistration;
24
25use crate::ServerError;
26use crate::auth_pass::PassPrincipal;
27
28/// Application hook invoked when a worker registers or unregisters on a
29/// connection.
30///
31/// Implementations associate the connection's beamr process id (`pid`) with the
32/// worker's declared [`WorkerRegistration`] so the application can route work to
33/// it, and release that association on disconnect. The hook is synchronous: a
34/// registration is acknowledged to the worker only after
35/// [`on_worker_registered`](Self::on_worker_registered) returns, so a rejecting
36/// application surfaces a `Rejected` ack instead of leaving the worker silently
37/// connected but never dispatched-to.
38pub trait ConnectionNotifier: std::fmt::Debug + Send + Sync {
39    /// Called when a worker registers on the connection identified by `pid`.
40    ///
41    /// Returning `Ok(())` accepts the registration (the worker receives an
42    /// `Accepted` ack). Returning [`ServerError`] rejects it (the worker receives
43    /// a `Rejected` ack carrying the error text), so a failed association never
44    /// leaves the worker believing it is registered.
45    ///
46    /// # Errors
47    /// Returns [`ServerError`] when the application declines the registration.
48    fn on_worker_registered(
49        &self,
50        pid: u64,
51        registration: &WorkerRegistration,
52    ) -> Result<(), ServerError>;
53
54    /// Called when the connection identified by `pid` — which had a stored
55    /// registration — closes, so the application can release the association.
56    ///
57    /// Deregistration is best-effort and infallible from the connection's
58    /// perspective: it runs on the close path where there is no peer to report an
59    /// error to.
60    fn on_worker_unregistered(&self, pid: u64);
61
62    /// Called when the connection identified by `pid` publishes to `channel`,
63    /// carrying the opaque envelope `payload`, BEFORE the normal channel fan-out.
64    ///
65    /// Returns `true` when the application CONSUMED the publish out-of-band (an
66    /// observability-drain tap): the connection process then does NOT route it to the
67    /// channel-fan-out cluster and answers with no wire response, so a tapped channel
68    /// need not be a declared fan-out channel. Returns `false` (the default) to let
69    /// the publish flow through the normal channel machinery unchanged.
70    ///
71    /// This is the observability-drain hook: a worker publishing an agent transcript
72    /// event to the reserved observability channel is consumed here — the hosting
73    /// application (aion) persists and live-fans-out the event without a second
74    /// connection. It is fire-and-forget: a publish is a one-way notification, so
75    /// there is no reply and a failed persist is the application's concern to log.
76    ///
77    /// The default returns `false`, so liminal still runs standalone: with no
78    /// notifier, or a notifier that does not recognise the channel, every publish
79    /// routes to the normal fan-out exactly as before.
80    fn on_channel_publish(&self, pid: u64, channel: &str, payload: &[u8]) -> bool {
81        let _ = (pid, channel, payload);
82        false
83    }
84
85    /// Called exactly once when a `Connect` carrying a registry pass has
86    /// SUCCEEDED on the connection identified by `pid` — after the pass
87    /// verified, version negotiation passed, and the handshake was admitted —
88    /// carrying the principal the pass stamped: participant, public key, live
89    /// prefix, conversation scope, `may_enroll`. `pid` is the connection's own
90    /// id, the same one every other hook on this trait is keyed by, so the
91    /// matching [`on_pass_detached`](Self::on_pass_detached) can be paired.
92    ///
93    /// A `Connect` that presents a pass and is refused (or a bearer connection,
94    /// which stamps no principal) never reaches this hook. The default does
95    /// nothing, so existing implementors compile unchanged and liminal still
96    /// runs standalone.
97    fn on_pass_attached(&self, pid: u64, principal: &PassPrincipal) {
98        let _ = (pid, principal);
99    }
100
101    /// Called exactly once when a connection that announced
102    /// [`on_pass_attached`](Self::on_pass_attached) closes — for any reason: a
103    /// clean `Disconnect`, the peer ending the transport, a transport error, a
104    /// process crash, or supervisor shutdown — carrying the same `pid` and the
105    /// principal the attach carried. Best-effort and infallible like
106    /// [`on_worker_unregistered`](Self::on_worker_unregistered): it runs on the
107    /// close path where there is no peer to report to. The default does
108    /// nothing.
109    fn on_pass_detached(&self, pid: u64, principal: &PassPrincipal) {
110        let _ = (pid, principal);
111    }
112}