liminal_server/server/connection/worker_front_door.rs
1//! Capability-scoped worker front door services (D2).
2//!
3//! [`WorkerFrontDoorServices`] is the [`ConnectionServices`] adapter for
4//! worker-only ("worker front door") deployments — aion-style hosts that use the
5//! bus for worker registration, correlated server push/reply, and
6//! notifier-consumed reserved publishes, and nothing else. It constructs no
7//! channel supervisor, conversation supervisor, haematite database/store, dedup
8//! cache, or temporary directory: it is a stateless adapter, so the only runtime
9//! the worker-front-door construction path spins up is the connection supervisor's
10//! own scheduler (owned by [`super::supervisor`], shared by both profiles).
11//!
12//! Worker registration, correlated push/reply, and the reserved observability tap
13//! do NOT flow through this adapter at all — they are served by the connection
14//! supervisor and the [`ConnectionNotifier`](super::notifier::ConnectionNotifier)
15//! independently of which `ConnectionServices` is installed, so they behave exactly
16//! as in full mode. This adapter carries only the channel/conversation operations,
17//! every one of which it rejects with a typed error frame.
18
19use liminal::protocol::{MessageEnvelope, SchemaId as ProtocolSchemaId};
20
21use super::conversation::ConnectionConversation;
22use super::services::{ConnectionServices, ConnectionSubscription, PublishOutcome};
23use crate::ServerError;
24use crate::config::types::ServiceProfile;
25
26/// [`ConnectionServices`] for the worker front door: registration, correlated
27/// push/reply, and notifier-consumed reserved publishes only.
28///
29/// The type holds no state and constructs nothing, so [`Self::new`] is an
30/// infallible `const fn` — a structural signal that this path starts no scheduler
31/// and opens no store (both of which are fallible in full mode). Ordinary
32/// publish/subscribe/conversation operations return a typed [`ServerError`], which
33/// [`super::apply`] renders as the matching typed error frame
34/// (`PublishError`/`SubscribeError`/`ConversationError`); the connection stays
35/// healthy across the rejection.
36#[derive(Debug, Default, Clone, Copy)]
37pub struct WorkerFrontDoorServices;
38
39impl WorkerFrontDoorServices {
40 /// Creates the worker-front-door adapter. Constructs nothing.
41 #[must_use]
42 pub const fn new() -> Self {
43 Self
44 }
45
46 /// Builds the typed rejection returned for an unsupported channel/conversation
47 /// operation. Rendered by [`super::apply`] as the operation's typed error frame.
48 fn unsupported(operation: &str) -> ServerError {
49 ServerError::UnsupportedOperation {
50 operation: operation.to_owned(),
51 profile: ServiceProfile::WORKER_FRONT_DOOR,
52 }
53 }
54}
55
56impl ConnectionServices for WorkerFrontDoorServices {
57 fn publish(
58 &self,
59 channel: &str,
60 _envelope: &MessageEnvelope,
61 _idempotency_key: Option<&str>,
62 ) -> Result<PublishOutcome, ServerError> {
63 // Reserved observability publishes never reach here: `apply_frame` offers
64 // every publish to the notifier's observability tap FIRST and only falls
65 // through to `services.publish` when the tap did not consume it. So an
66 // ordinary (untapped) publish is the only thing that lands here, and the
67 // front door serves no channels — reject it explicitly.
68 Err(Self::unsupported(&format!(
69 "publish to channel '{channel}'"
70 )))
71 }
72
73 fn subscribe(
74 &self,
75 channel: &str,
76 _accepted_schemas: &[ProtocolSchemaId],
77 _install: Option<liminal::channel::InboxInstall>,
78 ) -> Result<ConnectionSubscription, ServerError> {
79 Err(Self::unsupported(&format!(
80 "subscribe to channel '{channel}'"
81 )))
82 }
83
84 fn unsubscribe(&self, subscription: ConnectionSubscription) -> Result<(), ServerError> {
85 // Unreachable in practice: `subscribe` never yields a `ConnectionSubscription`,
86 // so no owned subscription can exist to pass here, and `apply_frame` rejects
87 // an `Unsubscribe` frame before this method via `supports_channel_operations`.
88 // Release the resource and succeed rather than invent a second rejection
89 // surface for a value that cannot be constructed in this profile.
90 subscription.unsubscribe()
91 }
92
93 fn open_conversation(
94 &self,
95 _conversation_id: u64,
96 subject: &str,
97 ) -> Result<ConnectionConversation, ServerError> {
98 Err(Self::unsupported(&format!(
99 "opening conversation '{subject}'"
100 )))
101 }
102
103 fn conversation_message(
104 &self,
105 _conversation: &ConnectionConversation,
106 _envelope: &MessageEnvelope,
107 ) -> Result<(), ServerError> {
108 // Unreachable: no `ConnectionConversation` can exist because
109 // `open_conversation` always rejects. `apply_frame` rejects a
110 // `ConversationMessage` frame before this method via
111 // `supports_channel_operations`.
112 Err(Self::unsupported("conversation message"))
113 }
114
115 fn close_conversation(&self, conversation: ConnectionConversation) -> Result<(), ServerError> {
116 // Unreachable for the same reason as `unsubscribe`; release and succeed.
117 conversation.close()
118 }
119
120 fn flush_durable_state(&self) -> Result<(), ServerError> {
121 // No durable channels exist, so a graceful-shutdown flush is a total no-op —
122 // never an error, so shutting a worker-front-door server down stays clean.
123 Ok(())
124 }
125
126 fn supports_channel_operations(&self) -> bool {
127 false
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::WorkerFrontDoorServices;
134 use crate::server::connection::services::ConnectionServices;
135
136 /// Structural adjunct to the §9 D2 gate: the front-door adapter constructs
137 /// nothing, so its constructor is infallible and starts no scheduler. A
138 /// `Scheduler::new` (channel/conversation/haematite) is fallible and returns
139 /// `Result`; this returning a bare `Self` is the in-language signal that no
140 /// beamr scheduler is created by the adapter itself.
141 ///
142 /// The thread half of the §9 gate is the record-by-construction SEAM CENSUS,
143 /// not this test: the profile-aware construction path reaches every
144 /// scheduler-owning subsystem only through `services::SubsystemFactory`, whose
145 /// recording test implementation cannot skip a record without also failing to
146 /// construct (asserted with a full-profile positive control in
147 /// `services::durable_store_tests` and `supervisor::tests`). A true OS-level
148 /// thread census still belongs to the beamr composition lane's upcoming
149 /// scheduler-inventory API; the census assertions upgrade to it when it lands.
150 /// It is deliberately NOT faked with a platform-specific OS thread count.
151 #[test]
152 fn construction_is_infallible_and_starts_no_scheduler() {
153 let services = WorkerFrontDoorServices::new();
154 // The type carries no channel/conversation/store/dedup fields — that absence
155 // is compile-time enforced by the struct definition. This adapter reports it
156 // serves no channel/conversation operations.
157 assert!(
158 !services.supports_channel_operations(),
159 "the worker front door must report that it serves no channel operations"
160 );
161 }
162}