Skip to main content

Core

Struct Core 

Source
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

Source

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.

Source

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 here

Like the closure form, nested begin_batch calls share the outer wave (only the outermost guard drains).

Source

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 Errunreachable: 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

Source

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.

Source

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.

Source

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.

Source

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).

Source

pub fn defer_queue(&self) -> Rc<DeferQueue>

Shared handle to this Core‘s owner-side defer queue (D249). graphrefly-operatorsProducerEmitter + 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.

Source

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).

Source

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.

Source

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

Source

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.

Source

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.

Source

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
Source

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.

Source

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 own Self::emit / commit responsibility; ignoring upstream DIRTY hints is safe.
  • Tier 2 (Message::Pause / Message::Resume): translates to Self::pause / Self::resume on each dep. Lock id is forwarded verbatim. Errors from individual deps are accumulated in the dep_errors field of the returned report.
  • Tier 4 (Message::Invalidate): translates to Self::invalidate on 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 same Core::invalidate path — upstream INVALIDATE here DOES clear dep caches and cascade. If a “plain forward” mode surfaces as a real consumer need, add up_with_options.
  • Tier 6 (Message::Teardown): translates to Self::teardown on each dep. Cascades per the standard teardown path.
§Errors
Source

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.

Source

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.

Source

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:

Phase 2 — operator scratch construction (lock-released):

Phase 3 — state-lock validation (folded with insertion under a single lock acquisition per /qa F1 to prevent TOCTOU between validation and nodes.insert):

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.

Source

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.

Source

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.

Source

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
Source

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
Source

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
Source

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, all dep_handles to NO_HANDLE, all dep_terminals to None, 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). The wipe_ctx cleanup hook fires lock-released so binding-side ctx.store starts fresh.
  • Non-resubscribable + terminal (any TEARDOWN state): the subscribe is rejected — try_subscribe returns SubscribeError::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:

Source

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_deactivatewipe_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.

Source

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).

Source

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).

Source

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).

Source

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).

Source

pub fn cache_of(&self, node_id: NodeId) -> HandleId

Read a node’s current cache. Returns NO_HANDLE if sentinel.

Source

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).

Source

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).

Source

pub fn node_count(&self) -> usize

Total number of nodes registered in this Core.

Source

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.

Source

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).

Source

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.

Source

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").

Source

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

Source

pub fn complete(&self, node_id: NodeId)

Emit [COMPLETE] (R1.3.4) on node_id, marking it terminal. After this call:

  • Subsequent Core::emit on 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.

Source

pub fn complete_or_defer(&self, node_id: NodeId)

Complete or defer to wave-end on partition order violation. For producer-pattern operator sinks.

Source

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.

Source

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

Source

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_node is called with Complete first 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_teardown flag is set on the first call; subsequent teardown calls (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.

Source

pub fn teardown_or_defer(&self, node_id: NodeId)

Teardown or defer to wave-end on partition order violation. For producer-pattern operator sinks.

Source

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

Source

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), cache becomes NO_HANDLE. State nodes keep has_fired_once per 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 to NO_HANDLE (its previous value referenced a now-released handle). The child is then recursively invalidated (no-op if its cache was already NO_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.

Source

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

Source

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).

Source

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.

Source

pub fn is_paused(&self, node_id: NodeId) -> bool

True if the node currently holds at least one pause lock.

Source

pub fn pause_lock_count(&self, node_id: NodeId) -> usize

Number of pause locks currently held on node_id. 0 if Active.

Source

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

Source

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_deps rejected (SelfDependency).
  • Cycles rejected (WouldCreateCycle).
  • Allowed mid-wave + while paused.
  • Phase 13.8 Q1: terminal n rejected (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

Source

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 from register_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 calling graph.name_of(id) from this event will see None. Use the Graph-level crate::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. One Core::teardown(root) call may produce many events.
  • DepsChanged { ... } fires only when set_deps actually rewires deps. The idempotent fast-path (deps unchanged as a set) returns without firing.
Source

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

Source§

fn register_state( &self, initial: HandleId, partial: bool, ) -> Result<NodeId, RegisterError>

Source§

fn register_producer(&self, fn_id: FnId) -> Result<NodeId, RegisterError>

Source§

fn subscribe(&self, node_id: NodeId, sink: Sink) -> SubscriptionId

Source§

fn try_subscribe( &self, node_id: NodeId, sink: Sink, ) -> Result<SubscriptionId, SubscribeError>

Source§

fn unsubscribe(&self, node_id: NodeId, sub_id: SubscriptionId)

Source§

fn emit(&self, node_id: NodeId, handle: HandleId)

Source§

fn complete(&self, node_id: NodeId)

Source§

fn error(&self, node_id: NodeId, handle: HandleId)

Source§

fn teardown(&self, node_id: NodeId)

See 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)

See Core::invalidate. Sink-side INVALIDATE forwards (higher-order build_inner_sink) route through em.defer.
Source§

fn cache_of(&self, node_id: NodeId) -> HandleId

Source§

fn has_fired_once(&self, node_id: NodeId) -> bool

Source§

fn kind_of(&self, node_id: NodeId) -> Option<NodeKind>

Source§

fn deps_of(&self, node_id: NodeId) -> Vec<NodeId>

Source§

fn is_terminal(&self, node_id: NodeId) -> Option<TerminalKind>

Source§

fn is_dirty(&self, node_id: NodeId) -> bool

Source§

fn serialize_handle(&self, handle: HandleId) -> Option<Value>

Serialize a node’s cached 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>

The wave-drained [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>

The owner-side 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>

Source§

fn emit_or_defer(&self, node_id: NodeId, new_handle: HandleId)

Source§

fn complete_or_defer(&self, node_id: NodeId)

Source§

fn error_or_defer(&self, node_id: NodeId, error_handle: HandleId)

Source§

impl Drop for Core

Source§

fn drop(&mut self)

Executes the destructor for this type. Read more
Source§

fn pin_drop(self: Pin<&mut Self>)

🔬This is a nightly-only experimental API. (pin_ergonomics)
Execute the destructor for this type, but different to Drop::drop, it requires self to be pinned. Read more

Auto Trait Implementations§

§

impl !Freeze for Core

§

impl !RefUnwindSafe for Core

§

impl !Send for Core

§

impl !Sync for Core

§

impl Unpin for Core

§

impl UnsafeUnpin for Core

§

impl !UnwindSafe for Core

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.