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}