Skip to main content

lenso_kernel/
stream.rs

1use std::{any::Any, cell::Cell, fmt, marker::PhantomData, rc::Rc};
2
3use futures::{FutureExt, future::LocalBoxFuture};
4
5use super::{
6    DiagnosticEvent, DiagnosticOutcome, DiagnosticSource, InvocationContext, NativeAppRuntime,
7    NativeStreamEndpointBinding, RequestPermit, RuntimeFailure, diagnostics::diagnostic_operation,
8    schedule_plugin_supervision_after_failure,
9};
10
11/// Static identity and Rust value types generated for one stream Capability.
12pub trait StreamCapability: 'static {
13    /// Typed request used to open one stream session.
14    type OpenRequest: 'static;
15    /// Typed message exchanged in both directions after opening.
16    type Message: 'static;
17    /// Typed Capability-defined terminal or opening error value.
18    type DomainError: 'static;
19    /// Stable Capability series identity.
20    const ID: &'static str;
21    /// Exact generated Descriptor version.
22    const DESCRIPTOR_VERSION: &'static str;
23}
24
25/// One observable item received from a bidirectional stream.
26#[derive(Clone, Debug, PartialEq)]
27pub enum StreamEvent<M, E> {
28    /// One ordered message from the remote side.
29    Message(M),
30    /// The remote side closed only its sending direction.
31    PeerHalfClosed,
32    /// The stream's one terminal outcome. Runtime failures use the outer `Result`.
33    Terminal(Result<(), E>),
34}
35
36/// Type-erased stream item crossing the Kernel/Adapter seam.
37pub enum NativeStreamItem {
38    /// One generated message value.
39    Message(Box<dyn Any>),
40    /// The remote side closed its sending direction.
41    PeerHalfClosed,
42    /// The stream's one terminal success or Domain Error outcome.
43    Terminal(Result<(), Box<dyn Any>>),
44}
45
46impl fmt::Debug for NativeStreamItem {
47    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
48        match self {
49            Self::Message(_) => formatter.write_str("Message(<erased>)"),
50            Self::PeerHalfClosed => formatter.write_str("PeerHalfClosed"),
51            Self::Terminal(Ok(())) => formatter.write_str("Terminal(Ok(()))"),
52            Self::Terminal(Err(_)) => formatter.write_str("Terminal(Err(<erased>))"),
53        }
54    }
55}
56
57/// Adapter-owned bidirectional stream session.
58pub trait NativeStreamSession: fmt::Debug {
59    /// Sends one message, applying the Adapter's bounded admission policy.
60    fn send(&self, message: Box<dyn Any>) -> LocalBoxFuture<'static, Result<(), RuntimeFailure>>;
61    /// Receives one message, half-close marker, or terminal outcome.
62    fn receive(&self) -> LocalBoxFuture<'static, Result<NativeStreamItem, RuntimeFailure>>;
63    /// Closes this side's sending direction without terminating the peer receive direction.
64    fn close_send(&self) -> LocalBoxFuture<'static, Result<(), RuntimeFailure>>;
65    /// Cancels the session idempotently and prevents later delivery to application code.
66    fn cancel(&self);
67}
68
69/// Adapter-owned result of opening one type-erased native stream session.
70pub type NativeStreamOpenFuture = LocalBoxFuture<
71    'static,
72    Result<Result<Box<dyn NativeStreamSession>, Box<dyn Any>>, RuntimeFailure>,
73>;
74
75/// Adapter-facing endpoint for one or more bidirectional stream Operations.
76pub trait NativeStreamEndpoint: fmt::Debug {
77    /// Stable Capability series identity.
78    fn capability_id(&self) -> &'static str;
79    /// Exact Descriptor version implemented by this endpoint.
80    fn descriptor_version(&self) -> &'static str;
81    /// Exact stable stream Operation names implemented by this endpoint.
82    fn operations(&self) -> &'static [&'static str];
83    /// Opens one stream without serializing its typed Rust payload.
84    fn open(
85        &self,
86        operation: &str,
87        request: Box<dyn Any>,
88        context: InvocationContext,
89    ) -> NativeStreamOpenFuture;
90}
91
92/// Typed, immutable stream endpoints materialized before App boot completes.
93#[derive(Debug)]
94pub struct NativeStreamHandle<C: StreamCapability> {
95    endpoints: Vec<NativeStreamEndpointBinding>,
96    runtime: Rc<NativeAppRuntime>,
97    caller_instance: String,
98    allow_before_ready: bool,
99    capability: PhantomData<fn() -> C>,
100}
101
102impl<C: StreamCapability> NativeStreamHandle<C> {
103    pub(crate) fn from_endpoints(
104        endpoints: &[NativeStreamEndpointBinding],
105        runtime: Rc<NativeAppRuntime>,
106        caller_instance: &str,
107        allow_before_ready: bool,
108    ) -> Self {
109        Self {
110            endpoints: endpoints.to_vec(),
111            runtime,
112            caller_instance: caller_instance.to_owned(),
113            allow_before_ready,
114            capability: PhantomData,
115        }
116    }
117
118    /// Returns the number of provider endpoints captured by this handle.
119    pub fn binding_count(&self) -> usize {
120        self.endpoints.len()
121    }
122
123    /// Opens one stream with a fresh invocation context.
124    pub async fn open(
125        &self,
126        operation: &str,
127        request: C::OpenRequest,
128    ) -> Result<Result<NativeStream<C>, C::DomainError>, RuntimeFailure> {
129        let context = self.next_context();
130        self.open_with_context(operation, context, request).await
131    }
132
133    /// Opens one stream with an explicit propagated Invocation Context.
134    pub async fn open_with_context(
135        &self,
136        operation: &str,
137        context: InvocationContext,
138        request: C::OpenRequest,
139    ) -> Result<Result<NativeStream<C>, C::DomainError>, RuntimeFailure> {
140        let context = context
141            .for_caller(&self.caller_instance)
142            .for_target(C::ID, operation);
143        let started_at = (self.runtime.driver.now)();
144        let operation_name = self
145            .endpoints
146            .first()
147            .and_then(|endpoint| diagnostic_operation(endpoint.state.operations, operation));
148        let request_id = context.request_id();
149        self.runtime
150            .diagnostics
151            .emit(DiagnosticSource::Invocation, started_at, |_| {
152                DiagnosticEvent::InvocationStarted {
153                    requirement_id: self
154                        .endpoints
155                        .first()
156                        .map(|endpoint| endpoint.requirement_id.clone()),
157                    request_id,
158                    caller_instance: Some(self.caller_instance.clone()),
159                    provider_instance: self
160                        .endpoints
161                        .first()
162                        .map(|endpoint| endpoint.plugin_instance.clone()),
163                    capability: C::ID,
164                    operation: operation_name,
165                }
166            });
167        let result = self
168            .open_with_context_inner(operation, context, request)
169            .await;
170        let outcome = match &result {
171            Ok(Ok(_)) => DiagnosticOutcome::Succeeded,
172            Ok(Err(_)) => DiagnosticOutcome::DomainError,
173            Err(error) => DiagnosticOutcome::RuntimeFailure(error.into()),
174        };
175        self.runtime.diagnostics.emit(
176            DiagnosticSource::Invocation,
177            (self.runtime.driver.now)(),
178            |_| DiagnosticEvent::InvocationCompleted {
179                requirement_id: self
180                    .endpoints
181                    .first()
182                    .map(|endpoint| endpoint.requirement_id.clone()),
183                request_id,
184                caller_instance: Some(self.caller_instance.clone()),
185                provider_instance: self
186                    .endpoints
187                    .first()
188                    .map(|endpoint| endpoint.plugin_instance.clone()),
189                capability: C::ID,
190                operation: operation_name,
191                outcome,
192                elapsed: (self.runtime.driver.now)().saturating_sub(started_at),
193            },
194        );
195        if let Err(error) = &result {
196            self.runtime.diagnostics.emit_runtime_failure(
197                (self.runtime.driver.now)(),
198                self.endpoints
199                    .first()
200                    .map(|endpoint| endpoint.plugin_instance.as_str()),
201                error,
202            );
203        }
204        result
205    }
206
207    async fn open_with_context_inner(
208        &self,
209        operation: &str,
210        context: InvocationContext,
211        request: C::OpenRequest,
212    ) -> Result<Result<NativeStream<C>, C::DomainError>, RuntimeFailure> {
213        if self.runtime.shutdown_started.get()
214            || (!self.allow_before_ready && self.runtime.admission.is_closed())
215        {
216            return Err(RuntimeFailure::AdmissionClosed);
217        }
218        let endpoint = match self.endpoints.as_slice() {
219            [] => return Err(RuntimeFailure::Unavailable { capability: C::ID }),
220            [endpoint] => endpoint,
221            endpoints => {
222                return Err(RuntimeFailure::AmbiguousBinding {
223                    capability: C::ID,
224                    providers: endpoints.len(),
225                });
226            }
227        };
228        let snapshot = endpoint
229            .state
230            .snapshot()
231            .ok_or(RuntimeFailure::Unavailable { capability: C::ID })?;
232        let admission = endpoint
233            .admission(operation)
234            .ok_or_else(|| RuntimeFailure::UnknownOperation {
235                capability: C::ID,
236                operation: operation.to_owned(),
237            })?
238            .clone();
239        let permit = admission
240            .acquire(C::ID, operation, &context, &self.runtime.driver)
241            .await?;
242        if !endpoint.state.is_current(snapshot.generation) {
243            return Err(RuntimeFailure::Unavailable { capability: C::ID });
244        }
245        let generation_cancellation = snapshot.cancellation.clone();
246        let endpoint_impl = snapshot.endpoint.clone();
247        let operation_name = operation.to_owned();
248        let (outcome, permit) = super::settlement::operation(
249            &self.runtime,
250            &endpoint.plugin_instance,
251            &context,
252            snapshot.cancellation,
253            C::ID,
254            move |execution_context| {
255                async move {
256                    (
257                        endpoint_impl
258                            .open(&operation_name, Box::new(request), execution_context)
259                            .await,
260                        permit,
261                    )
262                }
263                .boxed_local()
264            },
265        )
266        .await
267        .map_err(|error| {
268            schedule_plugin_supervision_after_failure(
269                &self.runtime,
270                &endpoint.plugin_instance,
271                error,
272            )
273        })?;
274        let outcome = outcome.map_err(|error| {
275            schedule_plugin_supervision_after_failure(
276                &self.runtime,
277                &endpoint.plugin_instance,
278                error,
279            )
280        })?;
281        match outcome {
282            Ok(session) => Ok(Ok(NativeStream::new(
283                session,
284                self.runtime.clone(),
285                generation_cancellation,
286                endpoint.plugin_instance.clone(),
287                context,
288                permit,
289            ))),
290            Err(error) => Ok(Err(error
291                .downcast::<C::DomainError>()
292                .map(|error| *error)
293                .map_err(|_| RuntimeFailure::ProtocolViolation { capability: C::ID })?)),
294        }
295    }
296
297    fn next_context(&self) -> InvocationContext {
298        InvocationContext::new(
299            self.next_request_id(),
300            None,
301            super::CancellationToken::new(),
302        )
303        .with_caller_instance(self.caller_instance.clone())
304    }
305
306    fn next_request_id(&self) -> super::RequestId {
307        let request_id = self.runtime.request_ids.get();
308        self.runtime.request_ids.set(request_id.saturating_add(1));
309        request_id
310    }
311}
312
313/// One opened, typed bidirectional stream session.
314#[derive(Debug)]
315pub struct NativeStream<C: StreamCapability> {
316    inner: Rc<dyn NativeStreamSession>,
317    runtime: Rc<NativeAppRuntime>,
318    generation_cancellation: super::CancellationToken,
319    plugin_instance: String,
320    context: InvocationContext,
321    _permit: RequestPermit,
322    local_half_closed: Cell<bool>,
323    peer_half_closed: Cell<bool>,
324    terminal_seen: Cell<bool>,
325    cancelled: Cell<bool>,
326    capability: PhantomData<fn() -> C>,
327}
328
329impl<C: StreamCapability> NativeStream<C> {
330    fn new(
331        session: Box<dyn NativeStreamSession>,
332        runtime: Rc<NativeAppRuntime>,
333        generation_cancellation: super::CancellationToken,
334        plugin_instance: String,
335        context: InvocationContext,
336        permit: RequestPermit,
337    ) -> Self {
338        Self {
339            inner: Rc::from(session),
340            runtime,
341            generation_cancellation,
342            plugin_instance,
343            context,
344            _permit: permit,
345            local_half_closed: Cell::new(false),
346            peer_half_closed: Cell::new(false),
347            terminal_seen: Cell::new(false),
348            cancelled: Cell::new(false),
349            capability: PhantomData,
350        }
351    }
352
353    /// Sends one typed message to the remote side.
354    pub async fn send(&self, message: C::Message) -> Result<(), RuntimeFailure> {
355        if let Some(error) = self.cancelled_outcome() {
356            return Err(error);
357        }
358        if self.local_half_closed.get() || self.terminal_seen.get() {
359            return Err(Self::protocol_violation());
360        }
361        let inner = self.inner.clone();
362        super::settlement::operation(
363            &self.runtime,
364            &self.plugin_instance,
365            &self.context,
366            self.generation_cancellation.clone(),
367            C::ID,
368            move |_| inner.send(Box::new(message)),
369        )
370        .await
371        .map_err(|error| self.finish_with_error(error))?
372        .map_err(|error| self.finish_with_error(error))
373    }
374
375    /// Receives the next ordered event from the remote side.
376    pub async fn receive(&self) -> Result<StreamEvent<C::Message, C::DomainError>, RuntimeFailure> {
377        if let Some(error) = self.cancelled_outcome() {
378            return Err(error);
379        }
380        if self.terminal_seen.get() {
381            return Err(Self::protocol_violation());
382        }
383        let inner = self.inner.clone();
384        let item = super::settlement::operation(
385            &self.runtime,
386            &self.plugin_instance,
387            &self.context,
388            self.generation_cancellation.clone(),
389            C::ID,
390            move |_| inner.receive(),
391        )
392        .await
393        .map_err(|error| self.finish_with_error(error))?
394        .map_err(|error| self.finish_with_error(error))?;
395        match item {
396            super::NativeStreamItem::Message(message) => {
397                if self.peer_half_closed.get() {
398                    return Err(self.finish_with_error(Self::protocol_violation()));
399                }
400                message
401                    .downcast::<C::Message>()
402                    .map(|message| StreamEvent::Message(*message))
403                    .map_err(|_| self.finish_with_error(Self::protocol_violation()))
404            }
405            super::NativeStreamItem::PeerHalfClosed => {
406                if self.peer_half_closed.replace(true) {
407                    return Err(self.finish_with_error(Self::protocol_violation()));
408                }
409                Ok(StreamEvent::PeerHalfClosed)
410            }
411            super::NativeStreamItem::Terminal(outcome) => {
412                if self.terminal_seen.replace(true) {
413                    return Err(self.finish_with_error(Self::protocol_violation()));
414                }
415                let outcome = match outcome {
416                    Ok(()) => Ok(()),
417                    Err(error) => Err(error
418                        .downcast::<C::DomainError>()
419                        .map(|error| *error)
420                        .map_err(|_| self.finish_with_error(Self::protocol_violation()))?),
421                };
422                Ok(StreamEvent::Terminal(outcome))
423            }
424        }
425    }
426
427    /// Closes this side's sending direction while keeping receiving available.
428    pub async fn close_send(&self) -> Result<(), RuntimeFailure> {
429        if let Some(error) = self.cancelled_outcome() {
430            return Err(error);
431        }
432        if self.terminal_seen.get() || self.local_half_closed.replace(true) {
433            return Err(Self::protocol_violation());
434        }
435        let inner = self.inner.clone();
436        let result = super::settlement::operation(
437            &self.runtime,
438            &self.plugin_instance,
439            &self.context,
440            self.generation_cancellation.clone(),
441            C::ID,
442            move |_| inner.close_send(),
443        )
444        .await
445        .map_err(|error| self.finish_with_error(error))?
446        .map_err(|error| self.finish_with_error(error));
447        let resource_exhausted = result
448            .as_ref()
449            .err()
450            .is_some_and(|error| matches!(error, RuntimeFailure::ResourceExhausted { .. }));
451        if resource_exhausted {
452            self.local_half_closed.set(false);
453        }
454        result
455    }
456
457    /// Cancels the stream idempotently. No later frame is delivered to the caller.
458    pub fn cancel(&self) {
459        if !self.terminal_seen.get() && !self.cancelled.replace(true) {
460            self.context.cancellation().cancel();
461            self.inner.cancel();
462        }
463    }
464
465    /// Returns the propagated Kernel Request ID for this stream.
466    pub const fn request_id(&self) -> super::RequestId {
467        self.context.request_id()
468    }
469
470    fn protocol_violation() -> RuntimeFailure {
471        RuntimeFailure::ProtocolViolation { capability: C::ID }
472    }
473
474    fn cancelled_outcome(&self) -> Option<RuntimeFailure> {
475        if !self.cancelled.get() {
476            return None;
477        }
478        if self.terminal_seen.replace(true) {
479            Some(Self::protocol_violation())
480        } else {
481            Some(RuntimeFailure::Cancelled {
482                request_id: self.context.request_id(),
483            })
484        }
485    }
486
487    fn schedule_failure(&self, error: RuntimeFailure) -> RuntimeFailure {
488        schedule_plugin_supervision_after_failure(&self.runtime, &self.plugin_instance, error)
489    }
490
491    fn finish_with_error(&self, error: RuntimeFailure) -> RuntimeFailure {
492        let error = self.schedule_failure(error);
493        self.runtime.diagnostics.emit_runtime_failure(
494            (self.runtime.driver.now)(),
495            Some(&self.plugin_instance),
496            &error,
497        );
498        if !matches!(error, RuntimeFailure::ResourceExhausted { .. }) {
499            self.terminal_seen.set(true);
500            if !self.cancelled.replace(true) {
501                // A provider failure terminates this session, not the caller's shared context.
502                self.inner.cancel();
503            }
504        }
505        error
506    }
507}
508
509impl<C: StreamCapability> Drop for NativeStream<C> {
510    fn drop(&mut self) {
511        if !self.cancelled.replace(true) && !self.terminal_seen.get() {
512            self.inner.cancel();
513        }
514    }
515}
516
517/// Alias using the transport-neutral term used by the Capability model.
518pub type StreamSession<C> = NativeStream<C>;