polydat_core/kernel/subcontext/pull.rs
1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! [`PullConsumer`] — wrapper / dispenser registration shape.
5//!
6//! ## Cross-crate boundary
7//!
8//! The runtime fixture / pull-plan machinery (SRD-32) lives in
9//! `nbrs-runtime`. This crate (`polydat`) cannot depend on
10//! `nbrs-runtime` — the dependency runs the other way. The
11//! [`PullConsumer`] trait below carries only the *intent*: a list
12//! of names the consumer wants to pull at cycle time. The
13//! activity-side `ScopeFixture::register_consumer` adapter walks
14//! these names and seals them into a `PullPlan` against the
15//! spawned kernel's program. SRD-32's pull-plan format and the
16//! per-consumer contract stay as 32 specifies; only the
17//! accumulator surface unifies under
18//! [`crate::kernel::subcontext::SubcontextBuilder::register_pull`].
19//!
20//! The host reads the registrations back through
21//! [`crate::kernel::subcontext::ScopeKernel::consumers`] on the
22//! spawned kernel (design doc §2.4).
23
24use std::sync::Arc;
25
26/// Anything that wants to pull Polydat values at cycle time.
27///
28/// Implementors expose the names they intend to read; the spawn
29/// path makes those names available on the spawned kernel for
30/// the activity-side fixture to seal into a `PullPlan`.
31///
32/// This trait is `Send + Sync` because consumers are stored on
33/// the artifact and may flow across threads with the spawned
34/// kernel.
35pub trait PullConsumer: Send + Sync {
36 /// The Polydat names this consumer will pull at cycle time.
37 /// Returned in registration order so handle indices match the
38 /// caller's expected layout.
39 fn names(&self) -> &[String];
40
41 /// Diagnostic label — appears in error messages when a
42 /// registered name fails to resolve against the kernel
43 /// program at seal time. Default: `"<unnamed-consumer>"`.
44 fn label(&self) -> &str {
45 "<unnamed-consumer>"
46 }
47}
48
49/// A consumer registration that has been recorded in a
50/// [`crate::kernel::subcontext::SubcontextBuilder`].
51///
52/// Wraps an `Arc<dyn PullConsumer>` so consumers can be cheaply
53/// shared between the artifact and the spawned kernel without
54/// trait-object cloning. The host's fixture adapter owns the seal
55/// step; this type only records the consumer for later
56/// inspection.
57#[derive(Clone)]
58pub struct RegisteredPullConsumer {
59 inner: Arc<dyn PullConsumer>,
60}
61
62impl RegisteredPullConsumer {
63 /// A registered consumer wrapping `consumer`.
64 pub fn new(consumer: Arc<dyn PullConsumer>) -> Self {
65 Self { inner: consumer }
66 }
67
68 /// Inspect the names this consumer requested. Used by the
69 /// activity-side adapter at seal time and by Phase 1 tests
70 /// to verify the registration round-trip.
71 pub fn names(&self) -> &[String] {
72 self.inner.names()
73 }
74
75 /// The consumer's diagnostic label.
76 pub fn label(&self) -> &str {
77 self.inner.label()
78 }
79
80 /// Borrow the underlying trait object — the activity-side
81 /// adapter uses this to dispatch to consumer-specific
82 /// configuration once it has the kernel program in hand.
83 pub fn as_dyn(&self) -> &dyn PullConsumer {
84 self.inner.as_ref()
85 }
86}
87
88impl std::fmt::Debug for RegisteredPullConsumer {
89 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90 f.debug_struct("RegisteredPullConsumer")
91 .field("label", &self.label())
92 .field("names", &self.names())
93 .finish()
94 }
95}
96
97/// Test-only / scratch consumer: holds a fixed list of names.
98/// Production consumers (validation / conditional / throttle /
99/// …) implement [`PullConsumer`] themselves on the activity
100/// side. This shape exists so Phase 1 tests can exercise the
101/// builder + spawn path without importing the activity crate.
102#[derive(Debug)]
103pub struct NamedPullConsumer {
104 label: String,
105 names: Vec<String>,
106}
107
108impl NamedPullConsumer {
109 /// A consumer with a label and the output names it pulls.
110 pub fn new(label: impl Into<String>, names: impl IntoIterator<Item = String>) -> Self {
111 Self {
112 label: label.into(),
113 names: names.into_iter().collect(),
114 }
115 }
116}
117
118impl PullConsumer for NamedPullConsumer {
119 fn names(&self) -> &[String] {
120 &self.names
121 }
122
123 fn label(&self) -> &str {
124 &self.label
125 }
126}