pub struct Core { /* private fields */ }Expand description
The handle-protocol Core dispatcher.
Holds an Arc to the BindingBoundary and all dispatch state. Cheap
to clone (the inner Arc<Mutex<CoreState>> is shared); pass Core by
value to threads.
Implementations§
Source§impl Core
impl Core
Sourcepub fn batch<F>(&self, f: F)where
F: FnOnce(),
pub fn batch<F>(&self, f: F)where
F: FnOnce(),
Coalesce multiple emissions into a single wave. Every emit /
complete / error / teardown / invalidate call inside f
queues its downstream work; the wave drains when f returns.
R1.3.6.a — DIRTY still propagates immediately (tier 1 isn’t
deferred); only tier-3+ delivery is held until scope exit. R1.3.6.b
— repeated emits on the same node coalesce into a single multi-message
delivery (one Message::Dirty for the wave + one Message::Data
per emit, all delivered together in the per-node phase-2 pass).
Nested batch() calls share the outer wave; only the outermost call
drives the drain. Re-entrant calls from inside an emit/fn (the wave
engine’s own in_tick re-entrance) compose with this method
transparently — they observe in_tick = true and skip drain just
like nested batch().
On panic inside f, the BatchGuard returned by the internal
begin_batch call drops normally and discards pending tier-3+ work
(subscribers do not observe the half-built wave). See
Core::begin_batch for the RAII variant if you need explicit control
over the scope boundary.
Sourcepub fn begin_batch(&self) -> BatchGuard<'_>
pub fn begin_batch(&self) -> BatchGuard<'_>
RAII batch handle — opens a wave when constructed, drains on drop.
Mirrors the closure-based Self::batch but exposes the scope
boundary so callers can compose batches with non-FnOnce control
flow (e.g. async-state-machine code paths, or splitting setup and
drain across helper functions).
use graphrefly_core::{Core, BindingBoundary, NodeRegistration, NodeOpts,
HandleId, NodeId, FnId, FnResult, DepBatch};
use std::sync::Arc;
struct Stub;
impl BindingBoundary for Stub {
fn invoke_fn(&self, _: NodeId, _: FnId, _: &[DepBatch]) -> FnResult {
FnResult::Noop { tracked: None }
}
fn custom_equals(&self, _: FnId, _: HandleId, _: HandleId) -> bool { false }
fn release_handle(&self, _: HandleId) {}
}
let core = Core::new(Arc::new(Stub) as Arc<dyn BindingBoundary>);
let state_a = core.register(NodeRegistration {
deps: vec![], fn_or_op: None,
opts: NodeOpts { initial: HandleId::new(1), ..Default::default() },
}).unwrap();
let state_b = core.register(NodeRegistration {
deps: vec![], fn_or_op: None,
opts: NodeOpts { initial: HandleId::new(2), ..Default::default() },
}).unwrap();
let g = core.begin_batch();
core.emit(state_a, HandleId::new(10));
core.emit(state_b, HandleId::new(20));
drop(g); // wave drains hereLike the closure form, nested begin_batch calls share the outer
wave (only the outermost guard drains).
Sourcepub fn begin_batch_for(&self, seed: NodeId) -> BatchGuard<'_>
pub fn begin_batch_for(&self, seed: NodeId) -> BatchGuard<'_>
Begin a batch for a wave seeded at seed. Historically this
acquired the per-partition wave_owner ReentrantMutexes for
every partition transitively touched from seed (the Slice Y1
parallelism win). S2c/D248 deleted that machinery: Core is
single-owner !Send + !Sync, so a wave is one uninterrupted
owner-side drain with nothing to lock. This now delegates to the
infallible Self::try_begin_batch_for, which acquires nothing
(the all-None single-owner floor); the seed parameter is
retained for the declared-group identity routing + D211
minimal-churn (callers compile unchanged). Cross-Core
parallelism is host-native via independent per-worker Cores
(actor model), not per-partition locks.
§Panics
Panics only if Self::try_begin_batch_for returns Err —
unreachable: it is now always Ok (scheduling groups are
static identity; no PartitionOrderViolation, no epoch retry).
Slice Y1 / Phase E (2026-05-08); infallible-since S2c (D246).
Source§impl Core
impl Core
Sourcepub fn new(binding: Arc<dyn BindingBoundary>) -> Self
pub fn new(binding: Arc<dyn BindingBoundary>) -> Self
Construct a fresh Core wired to the given binding. Pause buffer
cap defaults to unbounded; set via Self::set_pause_buffer_cap.
Sourcepub fn same_dispatcher(&self, other: &Core) -> bool
pub fn same_dispatcher(&self, other: &Core) -> bool
Whether self and other are the same dispatcher instance.
S2b (D223): Core is owned by value (no Arc<C>, no Clone), so
identity is the unique per-Core Self::generation counter
(assigned once from [CORE_GENERATION] at construction), not
Arc::ptr_eq. Two independently Core::new-constructed instances
have distinct generations even with the same binding; there is no
longer any way to produce two handles to one Core (no Clone),
so this is true only for genuine self-vs-self comparison.
Used by graphrefly-graph’s mount to enforce the single-Core
v1 invariant — cross-Core mount is post-M6.
Sourcepub fn drain_mailbox(&self)
pub fn drain_mailbox(&self)
Owner-side mailbox drain (D227/D230). Pops every
crate::mailbox::CoreMailbox-queued timer Emit request in
FIFO order and applies it via the synchronous Self::emit — so
autonomous timer tasks (which can no longer hold &Core/Weak<C>
under D223) get their fires delivered with no async in Core.
Called from the embedder’s existing advance/pump site (test
harness TestRuntime advance helper, napi pump). Timer tasks
already require the host runtime to be advanced before they fire,
so draining here is behaviour-identical to the deleted autonomous
WeakCore::upgrade → core.emit path. Idempotent on an empty
mailbox. Re-entrant-safe: emit may cascade and a concurrent
timer task may post again — the next drain picks it up (the
runnable bit is re-set on every post).
§Panics
Panics if the id-mailbox / defer-queue keep mutually re-posting
past the configured max_batch_drain_iterations cap — a livelock
signal that a Defer/Emit pair is consuming its own output.
Sourcepub fn post_defer(&self, f: DeferFn) -> bool
pub fn post_defer(&self, f: DeferFn) -> bool
Owner-side enqueue of a deferred closure (D233/D249). Runs at
the next Self::drain_mailbox / in-wave BatchGuard drain
with the object-safe full-Core surface. Returns false iff
the owning Core is gone (closure dropped unrun). The
owner-only Rc<DeferQueue> (also reachable via
Self::defer_queue for capture into !Send
Sink/TopologySink closures that cannot hold &Core).
Sourcepub fn defer_queue(&self) -> Rc<DeferQueue>
pub fn defer_queue(&self) -> Rc<DeferQueue>
Shared handle to this Core‘s owner-side defer queue (D249).
graphrefly-operators’ ProducerEmitter + reactive
describe/observe topo sinks hold this Rc to enqueue Defer
work from inside a !Send sink closure (which carries no
&Core). Owner-thread-only; never sent across threads.
Sourcepub fn mailbox(&self) -> Arc<CoreMailbox> ⓘ
pub fn mailbox(&self) -> Arc<CoreMailbox> ⓘ
Shared handle to this Core’s crate::mailbox::CoreMailbox.
Handed to autonomous async producers (timer tasks via
crate::timer::spawn_timer_task) so they can post Emit
requests without holding &Core/Weak<C> (D223/D227/D230).
Sourcepub fn binding(&self) -> Arc<dyn BindingBoundary> ⓘ
pub fn binding(&self) -> Arc<dyn BindingBoundary> ⓘ
Shared handle to this Core‘s binding. S2b/D231/A′: producer
build closures obtain the binding here (via ctx.core()) for
their spawned sinks’ retain_handle/pack_tuple/release_handle
— instead of the deleted Weak<dyn ProducerBinding> cycle-break.
No cycle: the registry-stored build closure captures nothing
strong; the Arc<dyn BindingBoundary> a sink clones lives in
Core’s subscriber map and drops on unsubscribe.
Sourcepub fn push_deferred_producer_op(&self, op: DeferredProducerOp)
pub fn push_deferred_producer_op(&self, op: DeferredProducerOp)
Execute a producer-pattern op. §7: the union-find ascending-order
constraint that motivated deferring these is deleted (groups
are static identity only — S2c removed the group-lock machinery,
so there is no PartitionOrderViolation to dodge). The op runs
immediately — re-entrant execution on the single owner thread
is absorbed by the in-flight wave drain (one-Core-per-OS-thread
IN_TICK_OWNED slot, D252), exactly as the deferred drain used
to run it at wave-end. The deferred_producer_ops queue +
drain_deferred_producer_ops are deleted; this shim is retained
so graphrefly-operators compiles unchanged (D211 minimal-churn).
For Emit/Error the caller retained the handle before
pushing (mirroring the old drain contract); we release it after
firing so refcount discipline is unchanged.
Source§impl Core
impl Core
Sourcepub fn set_pause_buffer_cap(&self, cap: Option<usize>)
pub fn set_pause_buffer_cap(&self, cap: Option<usize>)
Configure the Core-global cap on pause replay buffer length. When set,
any per-node pause buffer that would exceed cap drops the oldest
message(s) from the front; the dropped count is reported back via the
resume callback (see ResumeReport). None (default) means
unbounded; messages buffer indefinitely until the lockset clears.
Sourcepub fn set_replay_buffer_cap(&self, node_id: NodeId, cap: Option<usize>)
pub fn set_replay_buffer_cap(&self, node_id: NodeId, cap: Option<usize>)
Configure the replay buffer cap on node_id (R2.6.5 / Lock 6.G —
Slice E1, 2026-05-07). None disables the buffer. Some(N) keeps
the last N DATA emissions in a circular buffer; late subscribers
receive them as part of the per-tier handshake (between START and
any terminal). Switching from a larger cap to a smaller cap evicts
the front of the buffer to fit; switching to None drains the
buffer entirely. Each evicted/drained handle’s retain is released
back to the binding.
§Panics
Panics if node_id is not registered.
Sourcepub fn set_pausable_mode(
&self,
node_id: NodeId,
mode: PausableMode,
) -> Result<(), SetPausableModeError>
pub fn set_pausable_mode( &self, node_id: NodeId, mode: PausableMode, ) -> Result<(), SetPausableModeError>
Reconfigure the pause mode for node_id (canonical §2.6 — Slice F
audit close, 2026-05-07). Default for new nodes is
PausableMode::Default; switch to PausableMode::ResumeAll
for nodes whose pause-window emit history must be observable
verbatim, or PausableMode::Off for nodes intrinsically
pause-immune.
§Errors
SetPausableModeError::UnknownNode—node_idis not registered.SetPausableModeError::WhilePaused— the node currently holds at least one pause lock. Changing mode mid-pause would lose buffered content or strand apending_waveflag — resume all locks first.
Sourcepub fn set_max_batch_drain_iterations(&self, cap: u32)
pub fn set_max_batch_drain_iterations(&self, cap: u32)
Configure the wave-drain iteration cap (R4.3 / Lock 2.F′). The wave
engine aborts a drain after cap iterations with a diagnostic panic.
Default is 10_000 — high enough to avoid false positives on legitimate
fan-in cascades, low enough to surface runtime cycles within seconds.
Lower this only when running adversarial / property-based tests that want fast cycle detection. Raise it only with concrete evidence that a legitimate workload needs more iterations than the default — and even then, prefer to tune the workload (per-subgraph batching, etc.) over raising the cap.
§Panics
Panics if cap == 0 — a zero cap would abort every wave on the very
first iteration, deadlocking any subsequent dispatcher work.
Sourcepub fn up(&self, node_id: NodeId, message: Message) -> Result<(), UpError>
pub fn up(&self, node_id: NodeId, message: Message) -> Result<(), UpError>
Send a message UPSTREAM from node_id to each of its declared deps
(canonical R1.4.1 — Slice F audit, F2 / 2026-05-07).
The dispatcher rejects tier-3 (DATA / RESOLVED) and tier-5 (COMPLETE / ERROR) per R1.4.1: value and terminal-lifecycle planes are downstream-only. All other tiers (0 START, 1 DIRTY, 2 PAUSE / RESUME, 4 INVALIDATE, 6 TEARDOWN) pass.
§Routing per tier
- Tier 0 (
Message::Start): no-op. START is a per-subscription handshake, not a routable wire signal — sending it upstream has no well-defined target. - Tier 1 (
Message::Dirty): no-op. The dep’s “something changed” notification is its ownSelf::emit/ commit responsibility; ignoring upstream DIRTY hints is safe. - Tier 2 (
Message::Pause/Message::Resume): translates toSelf::pause/Self::resumeon each dep. Lock id is forwarded verbatim. Errors from individual deps are accumulated in thedep_errorsfield of the returned report. - Tier 4 (
Message::Invalidate): translates toSelf::invalidateon each dep. Note: canonical R1.4.2 distinguishes “downstream INVALIDATE” (cache clear + cascade) from “upstream INVALIDATE” (plain forward, no self-process). The Rust port v1 SIMPLIFICATION delegates to the sameCore::invalidatepath — upstream INVALIDATE here DOES clear dep caches and cascade. If a “plain forward” mode surfaces as a real consumer need, addup_with_options. - Tier 6 (
Message::Teardown): translates toSelf::teardownon each dep. Cascades per the standard teardown path.
§Errors
UpError::UnknownNode—node_idis not registered.UpError::TierForbidden— tier 3 or tier 5.
Sourcepub fn alloc_lock_id(&self) -> LockId
pub fn alloc_lock_id(&self) -> LockId
Allocate a unique LockId for use with Self::pause /
Self::resume. Convenience for callers that don’t already have an
id-allocation scheme; user-supplied ids work too.
Sourcepub fn binding_ptr(&self) -> &Arc<dyn BindingBoundary> ⓘ
pub fn binding_ptr(&self) -> &Arc<dyn BindingBoundary> ⓘ
Access the binding boundary for this Core.
Used by graphrefly-graph for snapshot serialization (M4.E1 / D166):
Graph::snapshot() calls binding.serialize_handle(cache) to
project each node’s cached value into portable JSON.
Sourcepub fn register(&self, reg: NodeRegistration) -> Result<NodeId, RegisterError>
pub fn register(&self, reg: NodeRegistration) -> Result<NodeId, RegisterError>
Unified node registration (D030).
reg describes the node’s identity (deps + closure-form fn id OR
typed-op + per-kind opts). The kind is derived from the field
shape, not stored — see NodeKind.
Sugar wrappers below (Self::register_state,
Self::register_producer, Self::register_derived,
Self::register_dynamic, Self::register_operator) build the
registration for the common kinds and delegate here. Direct callers
that need uncommon combinations (e.g., a partial-true derived) can
invoke this method directly.
§Errors
Errors are returned in evaluation order — earlier phases short-circuit later ones, so a single registration produces at most one variant.
Phase 1 — lock-released, side-effect-free validation:
RegisterError::OperatorWithoutDeps—regcarries an op butdepsis empty. Operator nodes need at least one dep — for subscription-managed combinators with no declared deps, useSelf::register_producerinstead.RegisterError::InitialOnlyForStateNodes—reg.opts.initialis non-sentinel for a non-state shape (deps non-empty, or fn_or_op present). State nodes are the only kind with an initial cache.
Phase 2 — operator scratch construction (lock-released):
RegisterError::OperatorSeedSentinel—regcarriesOp(Scan)/Op(Reduce)with aNO_HANDLEseed. R2.5.3 — stateful folders must have a real seed.
Phase 3 — state-lock validation (folded with insertion under a
single lock acquisition per /qa F1 to prevent TOCTOU between
validation and nodes.insert):
RegisterError::UnknownDep— any element ofreg.depsis not a registered node id.RegisterError::TerminalDep— a dep is terminal (COMPLETE / ERROR) AND not resubscribable — would create a permanent wedge.
All errors are construction-time invariants — the dispatcher
rejects the registration before any reactive state is created.
On Err, no node has been added and any handle retains taken on
the way in (operator scratch seed retains via
BindingBoundary::retain_handle) have been released
lock-released — see [ScratchReleaseGuard] for the RAII
discipline that covers both early-return AND unwind paths.
Last { default } retains its default handle on the same
release path.
Sourcepub fn register_state(
&self,
initial: HandleId,
partial: bool,
) -> Result<NodeId, RegisterError>
pub fn register_state( &self, initial: HandleId, partial: bool, ) -> Result<NodeId, RegisterError>
Sugar over Self::register — register a state node. initial
may be NO_HANDLE to start sentinel.
partial is accepted for surface consistency (D019); for state
nodes it has no effect (state nodes don’t fire fn).
§Errors
State registration is structurally simple — no deps, no op — so
the only reachable variant is none in practice. Returns
Result for surface consistency with Self::register.
Sourcepub fn register_producer(&self, fn_id: FnId) -> Result<NodeId, RegisterError>
pub fn register_producer(&self, fn_id: FnId) -> Result<NodeId, RegisterError>
Sugar over Self::register — register a producer node (D031,
Slice D). No deps; fn fires once on first subscribe; cleanup runs
via BindingBoundary::producer_deactivate when the last
subscriber unsubscribes.
The fn body uses the binding’s ProducerCtx-equivalent helper
(see graphrefly-operators::producer) to subscribe to other Core
nodes — the zip / concat / race / takeUntil pattern.
§Errors
Producer registration has no user-supplied deps, so structurally
none of RegisterError’s variants are reachable. Returns
Result for surface consistency with Self::register.
Sourcepub fn register_derived(
&self,
deps: &[NodeId],
fn_id: FnId,
equals: EqualsMode,
partial: bool,
) -> Result<NodeId, RegisterError>
pub fn register_derived( &self, deps: &[NodeId], fn_id: FnId, equals: EqualsMode, partial: bool, ) -> Result<NodeId, RegisterError>
Sugar over Self::register — register a derived (static) node.
partial controls the R2.5.3 first-run gate (D011).
§Errors
RegisterError::UnknownDep— any element ofdepsis not registered.RegisterError::TerminalDep— a dep is terminal and not resubscribable.
Sourcepub fn register_dynamic(
&self,
deps: &[NodeId],
fn_id: FnId,
equals: EqualsMode,
partial: bool,
) -> Result<NodeId, RegisterError>
pub fn register_dynamic( &self, deps: &[NodeId], fn_id: FnId, equals: EqualsMode, partial: bool, ) -> Result<NodeId, RegisterError>
Sugar over Self::register — register a dynamic node (fn
declares its actually-tracked dep indices per fire). partial
controls the R2.5.3 first-run gate (D011).
§Errors
RegisterError::UnknownDep— any element ofdepsis not registered.RegisterError::TerminalDep— a dep is terminal and not resubscribable.
Sourcepub fn register_operator(
&self,
deps: &[NodeId],
op: OperatorOp,
opts: OperatorOpts,
) -> Result<NodeId, RegisterError>
pub fn register_operator( &self, deps: &[NodeId], op: OperatorOp, opts: OperatorOpts, ) -> Result<NodeId, RegisterError>
Sugar over Self::register — register a built-in operator node
(Slice C-1, D009; D026 generic scratch). The operator dispatch path
lives in fire_operator; op selects which per-operator FFI
method on BindingBoundary gets called per fire.
For stateful operators (OperatorOp::Scan / [Reduce] /
[Last] with a default), the seed/default handle is captured
into the appropriate
OperatorScratch struct
stored at [NodeRecord::op_scratch], and Core takes one retain
share via BindingBoundary::retain_handle.
§Errors
RegisterError::OperatorWithoutDeps—depsis empty (useSelf::register_producerinstead).RegisterError::OperatorSeedSentinel—opisOperatorOp::Scan/OperatorOp::Reducewith aNO_HANDLEseed.RegisterError::UnknownDep— any element ofdepsis not registered.RegisterError::TerminalDep— a dep is terminal and not resubscribable.
Sourcepub fn subscribe(&self, node_id: NodeId, sink: Sink) -> SubscriptionId
pub fn subscribe(&self, node_id: NodeId, sink: Sink) -> SubscriptionId
Subscribe a sink to a node. Returns a [Subscription] handle —
dropping the handle unsubscribes the sink. Per §10.12, no manual
unsubscribe(node, id) call is required.
Push-on-subscribe (R1.2.3, R2.2.3 step 4): the sink is registered AFTER the START handshake fires. The handshake contents depend on node state:
- Sentinel cache + live (non-terminal):
[START] - Cached + live:
[START, DATA(handle)]
Subscribe-after-terminal semantics (canonical R2.2.7.a / R2.2.7.b, D118 2026-05-10):
- Resubscribable + terminal (any TEARDOWN state): the subscribe
call first resets the node — clears
terminal,has_fired_once,has_received_teardown, alldep_handlestoNO_HANDLE, alldep_terminalstoNone, drains the pause lockset, clears the replay buffer. The new subscriber receives a fresh[START](cache survives for state nodes per R2.2.8; sentinel for compute). Thewipe_ctxcleanup hook fires lock-released so binding-sidectx.storestarts fresh. - Non-resubscribable + terminal (any TEARDOWN state): the
subscribe is rejected —
try_subscribereturnsSubscribeError::TornDown; this method (the panic-on-error variant) panics with the diagnostic.
Activation (R2.2.3 step 5): if this is the first subscriber and the
node is a derived/dynamic compute, recursively activate deps so their
cached handles fill our dep_handles.
§Returns
Returns the SubscriptionId. Pair it with the node_id you
passed here and call Self::unsubscribe to deregister (S2b /
D225: core-level RAII retired — a binding-layer RAII wrapper over
unsubscribe provides drop-convenience where the holder co-owns
the Core on its affinity worker).
§Panics
Panics if:
- Subscribing would violate the Phase H+ ascending partition-order
invariant (
SubscribeError::PartitionOrderViolation). - The node is non-resubscribable AND has terminated
(
SubscribeError::TornDown, R2.2.7.b).
Sourcepub fn unsubscribe(&self, node_id: NodeId, sub_id: SubscriptionId)
pub fn unsubscribe(&self, node_id: NodeId, sub_id: SubscriptionId)
Synchronous owner-invoked unsubscribe (D225 refined A2). Symmetric
with Core::subscribe (the caller has &Core, exactly like
Core::emit). Runs the full deregister + Phase G / lifecycle
cleanup chain (OnDeactivation → producer_deactivate → wipe_ctx
→ Core cache-clear), behaviour-identical to the legacy
Subscription::Drop. Idempotent: a sub_id already gone (node
destroyed / double-unsub) is a silent no-op. D225 S2b retires the
core-level RAII Subscription in favour of a binding-layer RAII
wrapper over this method (the binding holds its Core on its
affinity worker, so its wrapper’s Drop calls this synchronously).
S2b promotes this (and SubscriptionId) to pub as the
binding-facing surface that replaces the retired core-level RAII
Subscription. Callers (binding-layer RAII wrapper, producer
producer_deactivate per D229, tests) pair the node_id they
subscribed with the returned sub_id.
Sourcepub fn try_subscribe(
&self,
node_id: NodeId,
sink: Sink,
) -> Result<SubscriptionId, SubscribeError>
pub fn try_subscribe( &self, node_id: NodeId, sink: Sink, ) -> Result<SubscriptionId, SubscribeError>
Fallible subscribe. Returns Err on:
- Partition order violation (Phase H+ STRICT, D115) — caller defers.
- Non-resubscribable terminal node (R2.2.7.b, D118) — caller skips.
Used by subscribe (unwraps both errors as panics) and producer-
pattern operator sinks (match on variant).
Sourcepub fn set_resubscribable(&self, node_id: NodeId, resubscribable: bool)
pub fn set_resubscribable(&self, node_id: NodeId, resubscribable: bool)
Mark node_id as resubscribable per R2.2.7. Resubscribable nodes
reset their terminal-lifecycle state on a fresh subscribe — see
Self::subscribe.
Configuration call — must be made before the node has any active subscribers, since changing the policy mid-flight would surprise existing observers.
§Panics
Panics if the node has subscribers (the policy is observable behavior; changing it after the fact would change semantics for existing sinks).
Sourcepub fn emit(&self, node_id: NodeId, new_handle: HandleId)
pub fn emit(&self, node_id: NodeId, new_handle: HandleId)
Set a state node’s value. Triggers a wave (DIRTY → DATA/RESOLVED → fn fires for downstream).
Silent no-op if the node has already terminated (R1.3.4). The handle passed in is still released by the caller’s binding-side intern path — no implicit retain is consumed when the call short-circuits.
§Panics
Panics if node_id is not a state node, or if new_handle is
NO_HANDLE (per R1.2.4, sentinel is not a valid DATA payload).
Sourcepub fn emit_or_defer(&self, node_id: NodeId, new_handle: HandleId)
pub fn emit_or_defer(&self, node_id: NodeId, new_handle: HandleId)
Emit or defer to wave-end on partition order violation.
For producer-pattern operator sinks. Retains handle on defer;
the drain releases it after firing (or on discard).
Sourcepub fn cache_of(&self, node_id: NodeId) -> HandleId
pub fn cache_of(&self, node_id: NodeId) -> HandleId
Read a node’s current cache. Returns NO_HANDLE if sentinel.
Sourcepub fn has_fired_once(&self, node_id: NodeId) -> bool
pub fn has_fired_once(&self, node_id: NodeId) -> bool
Whether the node’s fn has fired at least once (compute) OR it has had a non-sentinel value (state).
Sourcepub fn node_ids(&self) -> Vec<NodeId>
pub fn node_ids(&self) -> Vec<NodeId>
Snapshot of every registered NodeId in unspecified order. The
order matches HashMap iteration over the internal node table —
callers that need stable ordering should track names at the
Graph layer (canonical spec §3.5 namespace).
Sourcepub fn node_count(&self) -> usize
pub fn node_count(&self) -> usize
Total number of nodes registered in this Core.
Sourcepub fn kind_of(&self, node_id: NodeId) -> Option<NodeKind>
pub fn kind_of(&self, node_id: NodeId) -> Option<NodeKind>
Returns Some(kind) for known nodes, None for unknown. Kind is
derived from the field shape per D030 — see NodeKind.
Sourcepub fn deps_of(&self, node_id: NodeId) -> Vec<NodeId>
pub fn deps_of(&self, node_id: NodeId) -> Vec<NodeId>
Snapshot of the node’s deps in declaration order. Empty for unknown nodes or for state nodes (which have no deps).
Sourcepub fn is_terminal(&self, node_id: NodeId) -> Option<TerminalKind>
pub fn is_terminal(&self, node_id: NodeId) -> Option<TerminalKind>
Returns Some(kind) if the node has terminated (R1.3.4) — the
pair Some(Complete) / Some(Error(h)) mirrors the wire message
the node emitted. None for live nodes or unknown ids.
Sourcepub fn is_dirty(&self, node_id: NodeId) -> bool
pub fn is_dirty(&self, node_id: NodeId) -> bool
Whether the node has wave-scoped DIRTY pending (a tier-1 message
queued but the matching tier-3 settle has not yet flushed).
false for unknown ids. Mostly useful for describe() status
classification (R3.6.1 "dirty").
Sourcepub fn meta_companions_of(&self, parent: NodeId) -> Vec<NodeId>
pub fn meta_companions_of(&self, parent: NodeId) -> Vec<NodeId>
Snapshot of parent’s meta companion list (R1.3.9.d / R2.3.3 —
the companions added via Self::add_meta_companion). Empty
for unknown ids or for nodes with no companions registered.
Used by the graph layer’s signal_invalidate to filter meta
children out of the broadcast (canonical R3.7.2 — meta caches
are preserved across graph-wide INVALIDATE).
Source§impl Core
impl Core
Sourcepub fn complete(&self, node_id: NodeId)
pub fn complete(&self, node_id: NodeId)
Emit [COMPLETE] (R1.3.4) on node_id, marking it terminal. After
this call:
- Subsequent
Core::emiton this node is a silent no-op (idempotent termination). - The node’s fn no longer fires.
- The node’s cache is preserved (last value still observable via
cache_of). - Children receive
[COMPLETE](tier 5 — bypasses pause buffer). - Auto-cascade gating (Lock 2.B): each child that has all of its
deps in a terminal state auto-emits its own
[COMPLETE]. ERROR dominates COMPLETE — if any of a child’s deps emitted ERROR, the child auto-cascades that ERROR instead.
Idempotent: calling complete on an already-terminal node is a no-op.
§Panics
Panics if node_id is unknown.
Sourcepub fn complete_or_defer(&self, node_id: NodeId)
pub fn complete_or_defer(&self, node_id: NodeId)
Complete or defer to wave-end on partition order violation. For producer-pattern operator sinks.
Sourcepub fn error(&self, node_id: NodeId, error_handle: HandleId)
pub fn error(&self, node_id: NodeId, error_handle: HandleId)
Emit [ERROR, error_handle] (R1.3.4) on node_id. error_handle
must resolve to a non-sentinel value (R1.2.5) — the binding side has
already interned the error value before this call. Same lifecycle
effects as Self::complete; ERROR dominates COMPLETE in auto-
cascade gating.
§Panics
Panics if node_id is unknown or error_handle == NO_HANDLE.
Sourcepub fn error_or_defer(&self, node_id: NodeId, error_handle: HandleId)
pub fn error_or_defer(&self, node_id: NodeId, error_handle: HandleId)
Error or defer to wave-end on partition order violation.
For producer-pattern operator sinks. Retains handle on defer;
the drain releases it after firing (or on discard).
Source§impl Core
impl Core
Sourcepub fn teardown(&self, node_id: NodeId)
pub fn teardown(&self, node_id: NodeId)
Tear node_id down. Per R2.6.4 / Lock 6.F:
- Auto-prepend COMPLETE. If the node has not yet emitted a
terminal (
COMPLETE/ERROR),terminate_nodeis called withCompletefirst so subscribers see[COMPLETE, TEARDOWN], not bare[TEARDOWN]. This guarantees a clean end-of-stream signal to async iterators and other consumers that wait on terminal delivery. - Idempotent on duplicate delivery. The per-node
has_received_teardownflag is set on the first call; subsequentteardowncalls (or cascade visits from other paths) are silent no-ops — no second[COMPLETE, TEARDOWN]pair to subscribers. - Cascade downstream. Each child is recursively torn down. The
child’s own COMPLETE auto-cascades from
terminate_node’s logic (Lock 2.B); its TEARDOWN comes from this cascade.
§Panics
Panics if node_id is unknown.
Sourcepub fn teardown_or_defer(&self, node_id: NodeId)
pub fn teardown_or_defer(&self, node_id: NodeId)
Teardown or defer to wave-end on partition order violation. For producer-pattern operator sinks.
Sourcepub fn add_meta_companion(&self, parent: NodeId, companion: NodeId)
pub fn add_meta_companion(&self, parent: NodeId, companion: NodeId)
Attach companion as a meta companion of parent per R1.3.9.d.
Meta companions are nodes whose lifecycle is bound to the parent’s
in TEARDOWN ordering: when parent tears down, companion tears
down first.
Use this for inspection / audit / sidecar nodes that subscribe to parent state — without the ordering, the companion could observe the parent mid-destruction and emit garbage.
Idempotent on duplicate registration of the same companion.
§Lifecycle constraint
Intended for setup-time wiring — call this before parent or
companion enters a wave. Mid-wave registration (especially during
a teardown cascade in flight) is implementation-defined: the new
edge takes effect on the next wave. Adding a companion to a
torn-down parent silently no-ops (the parent will not tear down
again). For dynamic companion attachment with deterministic
ordering, prefer constructing the wiring before subscribers exist.
§Panics
Panics if either node id is unknown, or if parent == companion
(a node cannot be its own meta companion — would loop on TEARDOWN).
Source§impl Core
impl Core
Sourcepub fn invalidate(&self, node_id: NodeId)
pub fn invalidate(&self, node_id: NodeId)
Clear node_id’s cache and cascade [INVALIDATE] to downstream
dependents per canonical spec §1.4.
Semantics:
- Never-populated case (R1.4 line 197): if
cache == NO_HANDLE, the call is a no-op — no cache to clear, no INVALIDATE emitted. This naturally provides idempotency within a wave: once a node has been invalidated this wave (cache = NO_HANDLE), a second invalidate on the same node does nothing. - Cache clear (immediate): the node’s cached handle is dropped
(refcount released),
cachebecomesNO_HANDLE. State nodes keephas_fired_onceper spec — INVALIDATE is not a re-gating event (the next emission to a previously-fired state still does not re-trigger the first-run gate; that’s a resubscribable-terminal lifecycle concern, separate slice). - Wire emission (tier 4):
[INVALIDATE]is queued via the normal pause-aware notify path. Buffers while paused, flushes immediately otherwise. - Downstream cascade: for each child of this node, the child’s
dep_handles[idx_of_node]is reset toNO_HANDLE(its previous value referenced a now-released handle). The child is then recursively invalidated (no-op if its cache was alreadyNO_HANDLE). This re-closes the child’s first-run gate — fn won’t fire again until the upstream re-emits a value.
Wraps in a fresh wave when called from outside a wave, so notifications flush at the natural wave boundary.
§Panics
Panics if node_id is unknown, consistent with emit / pause.
Sourcepub fn invalidate_or_defer(&self, node_id: NodeId)
pub fn invalidate_or_defer(&self, node_id: NodeId)
Invalidate or defer to wave-end on partition order violation. For producer-pattern operator sinks.
§Panics
Panics if node_id is not registered in this Core.
Source§impl Core
impl Core
Sourcepub fn pause(&self, node_id: NodeId, lock_id: LockId) -> Result<(), PauseError>
pub fn pause(&self, node_id: NodeId, lock_id: LockId) -> Result<(), PauseError>
Acquire a pause lock on node_id. The first lock transitions the
node from Active to Paused; further locks add to the lockset.
While paused, tier-3 (DATA/RESOLVED) and tier-4 (INVALIDATE) outgoing
messages buffer in the node’s pause buffer; other tiers flush
immediately.
Re-acquiring the same lock_id is an idempotent no-op (matches TS
convention, R1.2.6 silent on the case).
Sourcepub fn resume(
&self,
node_id: NodeId,
lock_id: LockId,
) -> Result<Option<ResumeReport>, PauseError>
pub fn resume( &self, node_id: NodeId, lock_id: LockId, ) -> Result<Option<ResumeReport>, PauseError>
Release a pause lock on node_id. If the lockset becomes empty, the
node transitions back to Active and the buffered messages are
dispatched to subscribers in arrival order. Returns a ResumeReport
when the final lock released; None if the lockset is still
non-empty (further locks held).
Releasing an unknown lock_id (or releasing on an already-Active
node) is an idempotent no-op returning None.
Sourcepub fn is_paused(&self, node_id: NodeId) -> bool
pub fn is_paused(&self, node_id: NodeId) -> bool
True if the node currently holds at least one pause lock.
Sourcepub fn pause_lock_count(&self, node_id: NodeId) -> usize
pub fn pause_lock_count(&self, node_id: NodeId) -> usize
Number of pause locks currently held on node_id. 0 if Active.
Sourcepub fn holds_pause_lock(&self, node_id: NodeId, lock_id: LockId) -> bool
pub fn holds_pause_lock(&self, node_id: NodeId, lock_id: LockId) -> bool
Test helper: whether node_id currently holds the given lock_id.
Source§impl Core
impl Core
Sourcepub fn set_deps(
&self,
n: NodeId,
new_deps: &[NodeId],
) -> Result<(), SetDepsError>
pub fn set_deps( &self, n: NodeId, new_deps: &[NodeId], ) -> Result<(), SetDepsError>
Atomic dep mutation — change a node’s upstream deps without TEARDOWN cascading and without losing cache.
Per the TLA+-verified design at
~/src/graphrefly-ts/docs/research/wave_protocol_rewire.tla
(35,950 distinct states, all 7 invariants clean):
- Removed deps: clear dirtyMask bit, drain pending queue, drop DepRecord.
- Added deps: SENTINEL prevData; push-on-subscribe if added dep has cached DATA.
- Preserved:
firstRunPassed,pauseLocks,pauseBuffer,cache(ROM/RAM). - Status auto-settles if dirtyMask becomes empty.
- Idempotent on
new_deps == current deps. - Self-rewire
n ∈ new_depsrejected (SelfDependency). - Cycles rejected (
WouldCreateCycle). - Allowed mid-wave + while paused.
- Phase 13.8 Q1: terminal
nrejected (TerminalNode); newly-added terminal non-resubscribable deps rejected (TerminalDep).
The body is a single atomic dep-mutation transaction with several discrete validation stages. Splitting would require passing a partially-mutable CoreState across helpers, and the transaction’s locality is what makes the F1 refcount-leak collection work.
Source§impl Core
impl Core
Sourcepub fn subscribe_topology(&self, sink: TopologySink) -> TopologySubscriptionId
pub fn subscribe_topology(&self, sink: TopologySink) -> TopologySubscriptionId
Subscribe to topology changes. The sink fires synchronously
from the registration / teardown / set_deps call site, under
no Core lock (the state lock is dropped before firing). Sinks
MAY re-enter Core (register_*, teardown, set_deps, etc.)
— the lock-released discipline (Slice A close) makes this safe.
Returns a TopologySubscriptionId; pass it to
Self::unsubscribe_topology to deregister (S2b / D225: core
RAII retired — binding-layer RAII wraps unsubscribe_topology).
§Event semantics
NodeRegistered(id)fires fromregister_state/register_computed. The Core has finished installing the node record but a Graph-layer namespace name (if any) is NOT yet in place — the sink runs while the caller (Graph::add) is still between Core insert and namespace insert. Sinks callinggraph.name_of(id)from this event will seeNone. Use the Graph-levelcrate::node::Core-paired namespace-change hook (graphrefly-graph) for namespace-aware reactivity.NodeTornDown(id)fires for the root teardown AND for every meta companion + downstream consumer that auto-cascades. OneCore::teardown(root)call may produce many events.DepsChanged { ... }fires only whenset_depsactually rewires deps. The idempotent fast-path (deps unchanged as a set) returns without firing.
Sourcepub fn unsubscribe_topology(&self, id: TopologySubscriptionId)
pub fn unsubscribe_topology(&self, id: TopologySubscriptionId)
Synchronous owner-invoked topology unsubscribe (D225 refined A2).
Symmetric with subscribe_topology; the binding-layer / embedder
RAII wrapper calls this on Drop (it holds the Core on its
affinity worker). Idempotent — removing an absent id is a no-op.
Trait Implementations§
Source§impl CoreFull for Core
impl CoreFull for Core
Source§fn register_state(
&self,
initial: HandleId,
partial: bool,
) -> Result<NodeId, RegisterError>
fn register_state( &self, initial: HandleId, partial: bool, ) -> Result<NodeId, RegisterError>
Core::register_state.Source§fn register_producer(&self, fn_id: FnId) -> Result<NodeId, RegisterError>
fn register_producer(&self, fn_id: FnId) -> Result<NodeId, RegisterError>
Source§fn subscribe(&self, node_id: NodeId, sink: Sink) -> SubscriptionId
fn subscribe(&self, node_id: NodeId, sink: Sink) -> SubscriptionId
Core::subscribe.Source§fn try_subscribe(
&self,
node_id: NodeId,
sink: Sink,
) -> Result<SubscriptionId, SubscribeError>
fn try_subscribe( &self, node_id: NodeId, sink: Sink, ) -> Result<SubscriptionId, SubscribeError>
Core::try_subscribe.Source§fn unsubscribe(&self, node_id: NodeId, sub_id: SubscriptionId)
fn unsubscribe(&self, node_id: NodeId, sub_id: SubscriptionId)
Core::unsubscribe.Source§fn complete(&self, node_id: NodeId)
fn complete(&self, node_id: NodeId)
Core::complete.Source§fn teardown(&self, node_id: NodeId)
fn teardown(&self, node_id: NodeId)
Core::teardown. Sink-side terminal forwards (e.g.
stratify TEARDOWN passthrough) route through em.defer — rare,
so Defer rather than a 5th MailboxOp fast-path variant.Source§fn invalidate(&self, node_id: NodeId)
fn invalidate(&self, node_id: NodeId)
Core::invalidate. Sink-side INVALIDATE forwards
(higher-order build_inner_sink) route through em.defer.Source§fn has_fired_once(&self, node_id: NodeId) -> bool
fn has_fired_once(&self, node_id: NodeId) -> bool
Core::has_fired_once.Source§fn is_terminal(&self, node_id: NodeId) -> Option<TerminalKind>
fn is_terminal(&self, node_id: NodeId) -> Option<TerminalKind>
Core::is_terminal.Source§fn serialize_handle(&self, handle: HandleId) -> Option<Value>
fn serialize_handle(&self, handle: HandleId) -> Option<Value>
HandleId via the binding (S2b/β
D244). Delegates to binding_ptr().serialize_handle — needed so
an in-wave MailboxOp::Defer closure (storage snapshot-on-
observe) can run graphrefly_graph snapshot through &dyn CoreFull. Pure binding-delegating read; no C/T surfaced.Source§fn mailbox(&self) -> Arc<CoreMailbox> ⓘ
fn mailbox(&self) -> Arc<CoreMailbox> ⓘ
CoreMailbox] (D246 rule 5: the one facade is
mutation + inspection + serialize + mailbox). Lets a holder
of &dyn CoreFull post deferred ops without naming the cell
type — folds D245’s per-binding “how do I reach the mailbox”.Source§fn defer_queue(&self) -> Rc<DeferQueue>
fn defer_queue(&self) -> Rc<DeferQueue>
crate::mailbox::DeferQueue (D249/S2c — the
!Send Defer split off CoreMailbox). Lets a holder of
&dyn CoreFull post owner-side deferred closures (ProducerCtx
build path) without naming the cell type.Source§fn binding(&self) -> Arc<dyn BindingBoundary> ⓘ
fn binding(&self) -> Arc<dyn BindingBoundary> ⓘ
Core::binding.Source§fn emit_or_defer(&self, node_id: NodeId, new_handle: HandleId)
fn emit_or_defer(&self, node_id: NodeId, new_handle: HandleId)
Core::emit_or_defer.Source§fn complete_or_defer(&self, node_id: NodeId)
fn complete_or_defer(&self, node_id: NodeId)
Source§fn error_or_defer(&self, node_id: NodeId, error_handle: HandleId)
fn error_or_defer(&self, node_id: NodeId, error_handle: HandleId)
Core::error_or_defer.