Skip to main content

graphrefly/
ctx.rs

1//! The fn-body context — the substrate↔user-fn boundary (D8).
2//!
3//! A node fn receives a [`Ctx`] and communicates **only** through it: no
4//! `actions.emit/forward/backward`, the whole surface unifies on `up`/`down`.
5//! Values cross erased as [`AnyValue`]; the typed `data`/`emit` helpers downcast.
6//!
7//! Slice scope: `down` (DATA/DIRTY/RESOLVED/INVALIDATE/terminal), typed `data`/
8//! `emit`, `state`, and the cleanup hooks (`on_deactivation` + `on_invalidate`).
9//! `up` validates control/demand-only (R-ctx-up) and self-handles PAUSE/RESUME/PULL; the
10//! INVALIDATE-at-depless-source terminus (D38, C-7) is deferred. See `CLEAN-SLATE.md`.
11
12use std::rc::Rc;
13
14use crate::node::{Core, Node, NodeOpts, RewireRequest};
15use crate::operators::Operator;
16use crate::protocol::{AnyValue, Message, PullDemand, Wave};
17
18/// A dep's terminal state, visible to the fn via [`Ctx::terminal`]
19/// (R-deps-terminal / C-15). `None` ⇔ the dep is live; `Some(..)` ⇔ it COMPLETEd
20/// or ERRORed. Read by `terminalAsRealInput` operators (rescue/reduce/*Map) that
21/// treat an inner terminal as a real input rather than an absorbed settle.
22///
23/// `Error` carries the dep's error **message** (an `Rc<str>`), not the original
24/// `GraphError`: `Box<dyn Error>` is not `Clone`, and a message crosses a dep
25/// subscription as a borrow (`&Message`), so the concrete error type cannot be
26/// preserved into this per-wave-cloneable record. Per-language (D24, D31
27/// error=unknown — the top type); the message survives, the downcast does not.
28#[derive(Clone)]
29pub enum DepTerminal {
30    /// The dep emitted COMPLETE.
31    Complete,
32    /// The dep emitted ERROR; carries its `Display` message (see type doc).
33    Error(Rc<str>),
34}
35
36/// One raw ctx wave-data projection item (R-ctx-wave-data / D77).
37#[derive(Clone)]
38pub enum WaveData {
39    /// A legal DATA payload.
40    Data(AnyValue),
41    /// The protocol SENTINEL marker used for INVALIDATE inside `ctx.wave_data`.
42    Sentinel,
43}
44
45impl WaveData {
46    /// Typed read of a DATA payload; returns `None` for SENTINEL or type mismatch.
47    pub fn data<U: 'static>(&self) -> Option<Rc<U>> {
48        match self {
49            Self::Data(v) => v.clone().downcast::<U>().ok(),
50            Self::Sentinel => None,
51        }
52    }
53
54    /// True iff this item is the protocol SENTINEL marker.
55    pub fn is_sentinel(&self) -> bool {
56        matches!(self, Self::Sentinel)
57    }
58}
59
60/// Internal per-dependency snapshot backing Ctx helpers. Raw ctx exposes only
61/// `wave_data` + `terminal` as public dep-input surfaces (R-fn-contract / D77);
62/// latest/prev helpers are derived reads over this snapshot and node-owned cache.
63#[derive(Clone)]
64pub(crate) struct DepRecord {
65    /// Per-upstream-wave projections delivered to this fn invocation.
66    pub(crate) wave_data: Vec<Vec<WaveData>>,
67    /// Last committed DATA before the current batch; `None` = SENTINEL (never emitted DATA) —
68    /// the canonical never-emitted detector (R-sentinel).
69    pub(crate) prev_data: Option<AnyValue>,
70    /// Latest DATA = last of `batch` if present, else `prev_data`.
71    pub(crate) latest: Option<AnyValue>,
72    /// The dep's terminal state (R-deps-terminal). `None` ⇔ live. A
73    /// `terminalAsRealInput` fn reads this to react to an inner COMPLETE/ERROR.
74    pub(crate) terminal: Option<DepTerminal>,
75}
76
77/// The single argument to a node fn. All emission is explicit via [`Ctx::down`] /
78/// [`Ctx::emit`]; there is no return-value framing (R-fn-contract / D8).
79pub struct Ctx {
80    pub(crate) node: Core,
81    dep_records: Vec<DepRecord>,
82    pull: Option<PullDemand>,
83}
84
85impl Ctx {
86    pub(crate) fn new(node: Core, dep_records: Vec<DepRecord>, pull: Option<PullDemand>) -> Self {
87        Self {
88            node,
89            dep_records,
90            pull,
91        }
92    }
93
94    pub(crate) fn dep_records(&self) -> &[DepRecord] {
95        &self.dep_records
96    }
97
98    /// Raw dep-value input surface (R-ctx-wave-data / D77): dep -> waves -> values/SENTINEL.
99    pub fn wave_data(&self) -> Vec<&[Vec<WaveData>]> {
100        self.dep_records
101            .iter()
102            .map(|record| record.wave_data.as_slice())
103            .collect()
104    }
105
106    /// Terminal metadata for dep `i`, separate from `wave_data` (R-fn-contract / D77).
107    pub fn terminal(&self, i: usize) -> Option<&DepTerminal> {
108        self.dep_records.get(i).and_then(|r| r.terminal.as_ref())
109    }
110
111    /// Number of declared deps in this fn invocation.
112    pub fn dep_len(&self) -> usize {
113        self.dep_records.len()
114    }
115
116    /// True iff no deps are declared.
117    pub fn deps_empty(&self) -> bool {
118        self.dep_records.is_empty()
119    }
120
121    /// Typed read of dep `i`'s latest DATA, downcast to `U` (SENTINEL → `None`).
122    /// Returns `Rc<U>` (shared, no value clone). Data flows through messages —
123    /// this reads the wave snapshot, never peeks the dep's `.cache` (R-data-not-peek).
124    pub fn data<U: 'static>(&self, i: usize) -> Option<Rc<U>> {
125        self.dep_records
126            .get(i)
127            .and_then(|r| r.latest.clone())
128            .and_then(|a| a.downcast::<U>().ok())
129    }
130
131    /// Typed read of dep `i`'s accumulated DATA grouped by upstream wave.
132    pub fn batches<U: 'static>(&self, i: usize) -> Vec<Vec<Rc<U>>> {
133        self.dep_records
134            .get(i)
135            .map(|r| {
136                r.wave_data
137                    .iter()
138                    .map(|wave| wave.iter().filter_map(WaveData::data::<U>).collect())
139                    .collect()
140            })
141            .unwrap_or_default()
142    }
143
144    /// Typed DATA payloads dep `i` delivered in this invocation, flattened across
145    /// upstream waves. This is a derived helper; raw occurrence shape lives in `wave_data`.
146    pub fn batch<U: 'static>(&self, i: usize) -> Vec<Rc<U>> {
147        self.batches(i).into_iter().flatten().collect()
148    }
149
150    /// Emit one typed DATA downstream — the value-level sugar (R-primary-api-clean):
151    /// the protocol DIRTY-before-DATA framing is synthesized by the substrate.
152    pub fn emit<T: 'static>(&self, v: T) {
153        self.node.down(vec![Message::Data(Rc::new(v))]);
154    }
155
156    /// Emit a raw wave downstream toward sinks (the ctx-level power surface, DR-1).
157    pub fn down(&self, msgs: Wave<AnyValue>) {
158        self.node.down(msgs);
159    }
160
161    /// Emit upstream toward deps — control tiers only (R-ctx-up). Validates the
162    /// kind; the terminus actions (PAUSE lockset / INVALIDATE honor) are deferred.
163    pub fn up(&self, msgs: Wave<AnyValue>) {
164        self.node.up(msgs, None);
165    }
166
167    /// Directed upstream control along one declared dep edge (R-up-routing).
168    pub fn up_toward(&self, toward_dep: usize, msgs: Wave<AnyValue>) {
169        self.node.up(msgs, Some(toward_dep));
170    }
171
172    /// Defer an upstream control wave to the committed wave boundary (R-rewire-deferred).
173    /// This is the self-demand path for pull nodes: `PULL({pullId, params?})` routes
174    /// after the current fn settles, avoiding D37 mid-wave re-entry.
175    pub fn up_next(&self, msgs: Wave<AnyValue>) {
176        self.node.request_up_next(msgs, None);
177    }
178
179    /// Directed form of [`Ctx::up_next`].
180    pub fn up_next_toward(&self, toward_dep: usize, msgs: Wave<AnyValue>) {
181        self.node.request_up_next(msgs, Some(toward_dep));
182    }
183
184    /// Read this node's private cross-wave state (R-ctx-state / D23), typed.
185    pub fn state_get<S: 'static>(&self) -> Option<Rc<S>> {
186        self.node.get_state().and_then(|a| a.downcast::<S>().ok())
187    }
188
189    /// Set this node's private cross-wave state (R-ctx-state).
190    pub fn state_set<S: 'static>(&self, v: S) {
191        self.node.set_state(Rc::new(v));
192    }
193
194    /// Keep `state` across the fresh-lifecycle wipe (R-ctx-state / D29).
195    pub fn state_persist(&self, on: bool) {
196        self.node.set_state_persist(on);
197    }
198
199    /// Holder-visible context for a PULL-caused invocation (D272). Absent for
200    /// normal dep-settle, activation, pause replay, terminal, and non-pull runs.
201    pub fn pull(&self) -> Option<&PullDemand> {
202        self.pull.as_ref()
203    }
204
205    /// Release external resources on deactivation (R-cleanup-hooks / D28). Fires once.
206    pub fn on_deactivation(&self, f: impl FnOnce() + 'static) {
207        self.node.register_on_deactivation(Box::new(f));
208    }
209
210    /// Flush external state on INVALIDATE (R-cleanup-hooks / D28). Re-callable: fires
211    /// once per INVALIDATE wave. INVALIDATE is lifecycle-continue (R-ctx-state / D29) —
212    /// it does NOT wipe `ctx.state`, so any internal reset must be done here.
213    pub fn on_invalidate(&self, f: impl Fn() + 'static) {
214        self.node.register_on_invalidate(Rc::new(f));
215    }
216
217    /// Graph-local async/time driver for source bodies (D111).
218    pub(crate) fn local_async_driver(
219        &self,
220    ) -> Option<Rc<dyn crate::async_driver::LocalAsyncDriver>> {
221        self.node.local_async_driver()
222    }
223
224    #[cfg(feature = "tokio-worker")]
225    pub(crate) fn dispatcher(&self) -> crate::dispatcher::Dispatcher {
226        self.node.dispatcher()
227    }
228
229    pub(crate) fn environment(&self) -> crate::environment::EnvironmentDrivers {
230        self.node.environment()
231    }
232
233    pub(crate) fn init_node_in_scope<T: 'static>(
234        &self,
235        op: Operator<T>,
236        deps: Vec<Core>,
237    ) -> Node<T> {
238        let node = crate::operators::init_node_in_arena_with_dispatcher(
239            op,
240            &self.node.arena(),
241            self.node.dispatcher(),
242            deps,
243            NodeOpts::default(),
244        );
245        node.erased().set_environment(self.node.environment());
246        node
247    }
248
249    /// Request a deferred self-rewire subscribe at the committed wave boundary (R-rewire-deferred /
250    /// D47): subscribe to `dep` (and swap the fn) AFTER the current wave settles — never in place, so
251    /// this is NOT the D37 mid-fn reject. Higher-order operators (*Map) use this to wire
252    /// runtime-created inner nodes as VISIBLE self-deps. `f` re-pairs the deps (SD-1 pairing).
253    pub fn rewire_next_subscribe_dep<F: Fn(&Ctx) + 'static>(&self, dep: Core, f: F) {
254        self.node
255            .request_rewire_next(RewireRequest::Add(dep, Rc::new(f)));
256    }
257
258    /// Request a deferred self-rewire unsubscribe (R-rewire-deferred): if `dep` is still live at the
259    /// boundary, unsubscribe from it + swap the fn. If the identity is already absent at apply time, the
260    /// request is a full no-op, including no fn swap, so stale duplicate unsubscribes cannot desync
261    /// the live dep/fn pairing. A successful unsubscribe DRAINS the dep's edge + tears down its source
262    /// on last-subscriber (onDeactivation) = the switchMap/abortInFlight cancellation + memory
263    /// bounding (D47 beta).
264    pub fn rewire_next_unsubscribe_dep<F: Fn(&Ctx) + 'static>(&self, dep: Core, f: F) {
265        self.node
266            .request_rewire_next(RewireRequest::remove(&dep, Rc::new(f)));
267    }
268
269    /// Request a deferred self-rewire replace (R-rewire-deferred): replace the whole dep set + swap
270    /// the fn at the boundary (the switch-variant — removes-before-adds keeps the tracked inner
271    /// list aligned across boundary waves).
272    pub fn rewire_next_replace_deps<F: Fn(&Ctx) + 'static>(&self, deps: Vec<Core>, f: F) {
273        self.node
274            .request_rewire_next(RewireRequest::Set(deps, Rc::new(f)));
275    }
276
277    /// Obtain an owned, `'static` deferred-emit handle for **async-pool** late emission
278    /// (R-sync-core / R-no-raw-async: async lives in the fn body; the emit serializes
279    /// back onto the single thread). The Rust analogue of the TS async fn capturing its
280    /// `ctx` — here `&Ctx` is a borrow that cannot outlive `invoke`, so an async fn must
281    /// stash this owned handle and call [`DeferredCtx::emit`] / [`DeferredCtx::down`]
282    /// later. Carries the wave's dep snapshot so a deferred read sees this wave's view
283    /// (matches the TS per-invocation async ctx snapshot).
284    pub fn defer(&self) -> DeferredCtx {
285        DeferredCtx {
286            node: self.node.clone(),
287            dep_records: self.dep_records.clone(),
288        }
289    }
290}
291
292/// An owned, `'static` late-emit handle obtained via [`Ctx::defer`] — the async-pool
293/// boundary. An async node fn stashes one and emits LATER (the deferred result of
294/// async work). The emit re-enters the wave engine as a FRESH external wave (its own
295/// wave-owner / D30 catch boundary; the leading DIRTY is synthesized like any external
296/// tier-3 emit, R-dirty-before-data). Holds the node handle ([`Core`] = an `Rc`) + the
297/// dep snapshot at defer time.
298pub struct DeferredCtx {
299    node: Core,
300    dep_records: Vec<DepRecord>,
301}
302
303impl DeferredCtx {
304    /// Raw dep-value input surface captured at defer time (R-ctx-wave-data / D77).
305    pub fn wave_data(&self) -> Vec<&[Vec<WaveData>]> {
306        self.dep_records
307            .iter()
308            .map(|record| record.wave_data.as_slice())
309            .collect()
310    }
311
312    /// Terminal metadata for dep `i`, separate from captured `wave_data`.
313    pub fn terminal(&self, i: usize) -> Option<&DepTerminal> {
314        self.dep_records.get(i).and_then(|r| r.terminal.as_ref())
315    }
316
317    /// Typed read of dep `i`'s latest DATA at defer time (SENTINEL → `None`).
318    pub fn data<U: 'static>(&self, i: usize) -> Option<Rc<U>> {
319        self.dep_records
320            .get(i)
321            .and_then(|r| r.latest.clone())
322            .and_then(|a| a.downcast::<U>().ok())
323    }
324
325    /// Typed read of dep `i`'s captured DATA grouped by upstream wave.
326    pub fn batches<U: 'static>(&self, i: usize) -> Vec<Vec<Rc<U>>> {
327        self.dep_records
328            .get(i)
329            .map(|r| {
330                r.wave_data
331                    .iter()
332                    .map(|wave| wave.iter().filter_map(WaveData::data::<U>).collect())
333                    .collect()
334            })
335            .unwrap_or_default()
336    }
337
338    /// Typed DATA payloads dep `i` delivered in the captured invocation, flattened
339    /// across upstream waves.
340    pub fn batch<U: 'static>(&self, i: usize) -> Vec<Rc<U>> {
341        self.batches(i).into_iter().flatten().collect()
342    }
343
344    /// Emit one typed DATA downstream as the deferred async result.
345    pub fn emit<T: 'static>(&self, v: T) {
346        self.node.owned_down(vec![Message::Data(Rc::new(v))]);
347    }
348
349    /// Emit a raw wave downstream as the deferred async result (the ctx-level power
350    /// surface, DR-1).
351    pub fn down(&self, msgs: Wave<AnyValue>) {
352        self.node.owned_down(msgs);
353    }
354
355    /// Emit upstream through the node's LIVE topology at emission time (R-rewire-async-live-edge).
356    pub fn up(&self, msgs: Wave<AnyValue>) {
357        self.node.owned_up(msgs, None);
358    }
359
360    /// Directed form of [`DeferredCtx::up`].
361    pub fn up_toward(&self, toward_dep: usize, msgs: Wave<AnyValue>) {
362        self.node.owned_up(msgs, Some(toward_dep));
363    }
364}