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> Clone for NativeStreamHandle<C> {
103    fn clone(&self) -> Self {
104        Self {
105            endpoints: self.endpoints.clone(),
106            runtime: self.runtime.clone(),
107            caller_instance: self.caller_instance.clone(),
108            allow_before_ready: self.allow_before_ready,
109            capability: PhantomData,
110        }
111    }
112}
113
114impl<C: StreamCapability> NativeStreamHandle<C> {
115    pub(crate) fn from_endpoints(
116        endpoints: &[NativeStreamEndpointBinding],
117        runtime: Rc<NativeAppRuntime>,
118        caller_instance: &str,
119        allow_before_ready: bool,
120    ) -> Self {
121        Self {
122            endpoints: endpoints.to_vec(),
123            runtime,
124            caller_instance: caller_instance.to_owned(),
125            allow_before_ready,
126            capability: PhantomData,
127        }
128    }
129
130    /// Returns the number of provider endpoints captured by this handle.
131    pub fn binding_count(&self) -> usize {
132        self.endpoints.len()
133    }
134
135    /// Opens one stream with a fresh invocation context.
136    pub async fn open(
137        &self,
138        operation: &str,
139        request: C::OpenRequest,
140    ) -> Result<Result<NativeStream<C>, C::DomainError>, RuntimeFailure> {
141        let context = self.next_context();
142        self.open_with_context(operation, context, request).await
143    }
144
145    /// Opens one stream with an explicit propagated Invocation Context.
146    pub async fn open_with_context(
147        &self,
148        operation: &str,
149        context: InvocationContext,
150        request: C::OpenRequest,
151    ) -> Result<Result<NativeStream<C>, C::DomainError>, RuntimeFailure> {
152        let context = context
153            .for_caller(&self.caller_instance)
154            .for_target(C::ID, operation);
155        let started_at = (self.runtime.driver.now)();
156        let operation_name = self
157            .endpoints
158            .first()
159            .and_then(|endpoint| diagnostic_operation(endpoint.state.operations, operation));
160        let request_id = context.request_id();
161        self.runtime
162            .diagnostics
163            .emit(DiagnosticSource::Invocation, started_at, |_| {
164                DiagnosticEvent::InvocationStarted {
165                    requirement_id: self
166                        .endpoints
167                        .first()
168                        .map(|endpoint| endpoint.requirement_id.clone()),
169                    request_id,
170                    caller_instance: Some(self.caller_instance.clone()),
171                    provider_instance: self
172                        .endpoints
173                        .first()
174                        .map(|endpoint| endpoint.plugin_instance.clone()),
175                    capability: C::ID,
176                    operation: operation_name,
177                }
178            });
179        let result = self
180            .open_with_context_inner(operation, context, request)
181            .await;
182        let outcome = match &result {
183            Ok(Ok(_)) => DiagnosticOutcome::Succeeded,
184            Ok(Err(_)) => DiagnosticOutcome::DomainError,
185            Err(error) => DiagnosticOutcome::RuntimeFailure(error.into()),
186        };
187        self.runtime.diagnostics.emit(
188            DiagnosticSource::Invocation,
189            (self.runtime.driver.now)(),
190            |_| DiagnosticEvent::InvocationCompleted {
191                requirement_id: self
192                    .endpoints
193                    .first()
194                    .map(|endpoint| endpoint.requirement_id.clone()),
195                request_id,
196                caller_instance: Some(self.caller_instance.clone()),
197                provider_instance: self
198                    .endpoints
199                    .first()
200                    .map(|endpoint| endpoint.plugin_instance.clone()),
201                capability: C::ID,
202                operation: operation_name,
203                outcome,
204                elapsed: (self.runtime.driver.now)().saturating_sub(started_at),
205            },
206        );
207        if let Err(error) = &result {
208            self.runtime.diagnostics.emit_runtime_failure(
209                (self.runtime.driver.now)(),
210                self.endpoints
211                    .first()
212                    .map(|endpoint| endpoint.plugin_instance.as_str()),
213                error,
214            );
215        }
216        result
217    }
218
219    async fn open_with_context_inner(
220        &self,
221        operation: &str,
222        context: InvocationContext,
223        request: C::OpenRequest,
224    ) -> Result<Result<NativeStream<C>, C::DomainError>, RuntimeFailure> {
225        if self.runtime.shutdown_started.get()
226            || (!self.allow_before_ready && self.runtime.admission.is_closed())
227        {
228            return Err(RuntimeFailure::AdmissionClosed);
229        }
230        let endpoint = match self.endpoints.as_slice() {
231            [] => return Err(RuntimeFailure::Unavailable { capability: C::ID }),
232            [endpoint] => endpoint,
233            endpoints => {
234                return Err(RuntimeFailure::AmbiguousBinding {
235                    capability: C::ID,
236                    providers: endpoints.len(),
237                });
238            }
239        };
240        let snapshot = endpoint
241            .state
242            .snapshot()
243            .ok_or(RuntimeFailure::Unavailable { capability: C::ID })?;
244        let admission = endpoint
245            .admission(operation)
246            .ok_or_else(|| RuntimeFailure::UnknownOperation {
247                capability: C::ID,
248                operation: operation.to_owned(),
249            })?
250            .clone();
251        let permit = admission
252            .acquire(C::ID, operation, &context, &self.runtime.driver)
253            .await?;
254        if !endpoint.state.is_current(snapshot.generation) {
255            return Err(RuntimeFailure::Unavailable { capability: C::ID });
256        }
257        let generation_cancellation = snapshot.cancellation.clone();
258        let endpoint_impl = snapshot.endpoint.clone();
259        let operation_name = operation.to_owned();
260        let (outcome, permit) = super::settlement::operation(
261            &self.runtime,
262            &endpoint.plugin_instance,
263            &context,
264            snapshot.cancellation,
265            C::ID,
266            move |execution_context| {
267                async move {
268                    (
269                        endpoint_impl
270                            .open(&operation_name, Box::new(request), execution_context)
271                            .await,
272                        permit,
273                    )
274                }
275                .boxed_local()
276            },
277        )
278        .await
279        .map_err(|error| {
280            schedule_plugin_supervision_after_failure(
281                &self.runtime,
282                &endpoint.plugin_instance,
283                error,
284            )
285        })?;
286        let outcome = outcome.map_err(|error| {
287            schedule_plugin_supervision_after_failure(
288                &self.runtime,
289                &endpoint.plugin_instance,
290                error,
291            )
292        })?;
293        match outcome {
294            Ok(session) => Ok(Ok(NativeStream::new(
295                session,
296                self.runtime.clone(),
297                generation_cancellation,
298                endpoint.plugin_instance.clone(),
299                context,
300                permit,
301            ))),
302            Err(error) => Ok(Err(error
303                .downcast::<C::DomainError>()
304                .map(|error| *error)
305                .map_err(|_| RuntimeFailure::ProtocolViolation { capability: C::ID })?)),
306        }
307    }
308
309    fn next_context(&self) -> InvocationContext {
310        InvocationContext::new(
311            self.next_request_id(),
312            None,
313            super::CancellationToken::new(),
314        )
315        .with_caller_instance(self.caller_instance.clone())
316    }
317
318    fn next_request_id(&self) -> super::RequestId {
319        let request_id = self.runtime.request_ids.get();
320        self.runtime.request_ids.set(request_id.saturating_add(1));
321        request_id
322    }
323}
324
325/// One opened, typed bidirectional stream session.
326#[derive(Debug)]
327pub struct NativeStream<C: StreamCapability> {
328    inner: Rc<dyn NativeStreamSession>,
329    runtime: Rc<NativeAppRuntime>,
330    generation_cancellation: super::CancellationToken,
331    plugin_instance: String,
332    context: InvocationContext,
333    _permit: RequestPermit,
334    local_half_closed: Cell<bool>,
335    peer_half_closed: Cell<bool>,
336    terminal_seen: Cell<bool>,
337    cancelled: Cell<bool>,
338    capability: PhantomData<fn() -> C>,
339}
340
341impl<C: StreamCapability> NativeStream<C> {
342    fn new(
343        session: Box<dyn NativeStreamSession>,
344        runtime: Rc<NativeAppRuntime>,
345        generation_cancellation: super::CancellationToken,
346        plugin_instance: String,
347        context: InvocationContext,
348        permit: RequestPermit,
349    ) -> Self {
350        Self {
351            inner: Rc::from(session),
352            runtime,
353            generation_cancellation,
354            plugin_instance,
355            context,
356            _permit: permit,
357            local_half_closed: Cell::new(false),
358            peer_half_closed: Cell::new(false),
359            terminal_seen: Cell::new(false),
360            cancelled: Cell::new(false),
361            capability: PhantomData,
362        }
363    }
364
365    /// Sends one typed message to the remote side.
366    pub async fn send(&self, message: C::Message) -> Result<(), RuntimeFailure> {
367        if let Some(error) = self.cancelled_outcome() {
368            return Err(error);
369        }
370        if self.local_half_closed.get() || self.terminal_seen.get() {
371            return Err(Self::protocol_violation());
372        }
373        let inner = self.inner.clone();
374        super::settlement::operation(
375            &self.runtime,
376            &self.plugin_instance,
377            &self.context,
378            self.generation_cancellation.clone(),
379            C::ID,
380            move |_| inner.send(Box::new(message)),
381        )
382        .await
383        .map_err(|error| self.finish_with_error(error))?
384        .map_err(|error| self.finish_with_error(error))
385    }
386
387    /// Receives the next ordered event from the remote side.
388    pub async fn receive(&self) -> Result<StreamEvent<C::Message, C::DomainError>, RuntimeFailure> {
389        if let Some(error) = self.cancelled_outcome() {
390            return Err(error);
391        }
392        if self.terminal_seen.get() {
393            return Err(Self::protocol_violation());
394        }
395        let inner = self.inner.clone();
396        let item = super::settlement::operation(
397            &self.runtime,
398            &self.plugin_instance,
399            &self.context,
400            self.generation_cancellation.clone(),
401            C::ID,
402            move |_| inner.receive(),
403        )
404        .await
405        .map_err(|error| self.finish_with_error(error))?
406        .map_err(|error| self.finish_with_error(error))?;
407        match item {
408            super::NativeStreamItem::Message(message) => {
409                if self.peer_half_closed.get() {
410                    return Err(self.finish_with_error(Self::protocol_violation()));
411                }
412                message
413                    .downcast::<C::Message>()
414                    .map(|message| StreamEvent::Message(*message))
415                    .map_err(|_| self.finish_with_error(Self::protocol_violation()))
416            }
417            super::NativeStreamItem::PeerHalfClosed => {
418                if self.peer_half_closed.replace(true) {
419                    return Err(self.finish_with_error(Self::protocol_violation()));
420                }
421                Ok(StreamEvent::PeerHalfClosed)
422            }
423            super::NativeStreamItem::Terminal(outcome) => {
424                if self.terminal_seen.replace(true) {
425                    return Err(self.finish_with_error(Self::protocol_violation()));
426                }
427                let outcome = match outcome {
428                    Ok(()) => Ok(()),
429                    Err(error) => Err(error
430                        .downcast::<C::DomainError>()
431                        .map(|error| *error)
432                        .map_err(|_| self.finish_with_error(Self::protocol_violation()))?),
433                };
434                Ok(StreamEvent::Terminal(outcome))
435            }
436        }
437    }
438
439    /// Closes this side's sending direction while keeping receiving available.
440    pub async fn close_send(&self) -> Result<(), RuntimeFailure> {
441        if let Some(error) = self.cancelled_outcome() {
442            return Err(error);
443        }
444        if self.terminal_seen.get() || self.local_half_closed.replace(true) {
445            return Err(Self::protocol_violation());
446        }
447        let inner = self.inner.clone();
448        let result = super::settlement::operation(
449            &self.runtime,
450            &self.plugin_instance,
451            &self.context,
452            self.generation_cancellation.clone(),
453            C::ID,
454            move |_| inner.close_send(),
455        )
456        .await
457        .map_err(|error| self.finish_with_error(error))?
458        .map_err(|error| self.finish_with_error(error));
459        let resource_exhausted = result
460            .as_ref()
461            .err()
462            .is_some_and(|error| matches!(error, RuntimeFailure::ResourceExhausted { .. }));
463        if resource_exhausted {
464            self.local_half_closed.set(false);
465        }
466        result
467    }
468
469    /// Cancels the stream idempotently. No later frame is delivered to the caller.
470    pub fn cancel(&self) {
471        if !self.terminal_seen.get() && !self.cancelled.replace(true) {
472            self.context.cancellation().cancel();
473            self.inner.cancel();
474        }
475    }
476
477    /// Returns the propagated Kernel Request ID for this stream.
478    pub const fn request_id(&self) -> super::RequestId {
479        self.context.request_id()
480    }
481
482    fn protocol_violation() -> RuntimeFailure {
483        RuntimeFailure::ProtocolViolation { capability: C::ID }
484    }
485
486    fn cancelled_outcome(&self) -> Option<RuntimeFailure> {
487        if !self.cancelled.get() {
488            return None;
489        }
490        if self.terminal_seen.replace(true) {
491            Some(Self::protocol_violation())
492        } else {
493            Some(RuntimeFailure::Cancelled {
494                request_id: self.context.request_id(),
495            })
496        }
497    }
498
499    fn schedule_failure(&self, error: RuntimeFailure) -> RuntimeFailure {
500        schedule_plugin_supervision_after_failure(&self.runtime, &self.plugin_instance, error)
501    }
502
503    fn finish_with_error(&self, error: RuntimeFailure) -> RuntimeFailure {
504        let error = self.schedule_failure(error);
505        self.runtime.diagnostics.emit_runtime_failure(
506            (self.runtime.driver.now)(),
507            Some(&self.plugin_instance),
508            &error,
509        );
510        if !matches!(error, RuntimeFailure::ResourceExhausted { .. }) {
511            self.terminal_seen.set(true);
512            if !self.cancelled.replace(true) {
513                // A provider failure terminates this session, not the caller's shared context.
514                self.inner.cancel();
515            }
516        }
517        error
518    }
519}
520
521impl<C: StreamCapability> Drop for NativeStream<C> {
522    fn drop(&mut self) {
523        if !self.cancelled.replace(true) && !self.terminal_seen.get() {
524            self.inner.cancel();
525        }
526    }
527}
528
529/// Alias using the transport-neutral term used by the Capability model.
530pub type StreamSession<C> = NativeStream<C>;