SubscriptionHub

scalus.cardano.node.stream.internal.SubscriptionHub
final class SubscriptionHub(val cardanoInfo: CardanoInfo, val capabilities: StreamCapabilities)

Subscription registry and event fan-out, shared by every provider.

This is the part of a streaming implementation that is the same whatever the chain data comes from: keeping track of who is subscribed to what, deciding which events reach whom, gating on confirmation depth, and turning a block into per-subscription deltas. What differs per backend — polling REST, following a gRPC stream, driving an emulator, replaying a chain store — sits above this, and hands it AppliedBlocks.

No worker thread

State is guarded by this and every operation runs on the caller's thread. That is not a simplification of the threaded design, it is a different trade: registration and fan-out are ordered by the caller's own program order, which makes subscribe(q); submit(tx) race-free without any happens-before argument about a queue. It also compiles to Scala.js, where there is no thread to hand work to.

Events are handed to mailboxes outside the monitor. A consumer's continuation can run inline on offer, and that continuation is allowed to cancel its subscription, which re-enters this hub — holding the lock across offer would deadlock on exactly that path.

Delivery happens outside the monitor

Events are buffered into mailboxes under the lock and delivered after releasing it. Both halves matter. Buffering under the lock is what makes a registration plus its seed one indivisible step, so a subscriber can never see a live Spent before the Created it belongs after. Delivering outside it is what keeps a consumer's continuation — which may cancel, and so re-enter this hub — from running while the monitor is held.

Attributes

Graph
Supertypes
class Object
trait Matchable
class Any

Members list

Value members

Concrete methods

def applyBlock(block: AppliedBlock): Unit

Apply a block: advance the tip, then release whichever block each subscription's confirmation depth now makes visible.

Apply a block: advance the tip, then release whichever block each subscription's confirmation depth now makes visible.

Attributes

def closeAll(): Unit
def failAll(cause: Throwable): Unit

Fail every subscription with cause, and stop accepting new ones.

Fail every subscription with cause, and stop accepting new ones.

Distinct from closeAll, and the distinction is the subscriber's whole world: a closed stream ended, so whatever it delivered was the truth; a failed stream means the view is untrustworthy and must be rebuilt. A follower that loses track of the chain owes subscribers the second, never the first.

Attributes

def nextSubscriptionId(): Long
def notifySubmit(txHash: TransactionHash): Unit

A transaction entered the mempool.

A transaction entered the mempool.

Attributes

def registerBlock(id: Long, query: BlockQuery, opts: SubscriptionOptions, mailbox: Mailbox[BlockEvent]): Unit
def registerParams(id: Long, mailbox: Mailbox[ProtocolParams]): Unit
def registerTip(id: Long, mailbox: Mailbox[ChainTip]): Unit

Register a tip subscription and immediately deliver the current tip — a latest-value stream that made you wait for the next change before telling you anything would be useless as the one-shot's dual.

Register a tip subscription and immediately deliver the current tip — a latest-value stream that made you wait for the next change before telling you anything would be useless as the one-shot's dual.

Attributes

def registerTransaction(id: Long, query: TransactionQuery, opts: SubscriptionOptions, mailbox: Mailbox[TransactionEvent]): Unit
def registerTxStatus(id: Long, txHash: TransactionHash, mailbox: Mailbox[TransactionStatus]): Unit
def registerUtxo(id: Long, query: UtxoEventQuery, opts: SubscriptionOptions, mailbox: Mailbox[UtxoEvent], seed: Utxos): Unit

Register a UTxO subscription, optionally seeding it from a snapshot.

Register a UTxO subscription, optionally seeding it from a snapshot.

The seed is buffered under the same lock live events take, so a subscriber cannot observe a live event before the seed it belongs after; it is delivered once the lock is released.

Seeded events carry ChainPoint.origin rather than the current tip. They describe UTxOs produced in blocks the subscription never saw, so stamping them with the tip would make a later rollback past that tip instruct the subscriber to discard state that is still on chain. origin says what is true: this came from a snapshot, not from a block you observed.

Attributes

def require(request: SubscriptionRequest): Unit

Throw unless this provider can serve the request — synchronously, before anything is registered, so the exception arrives at the call that caused it.

Throw unless this provider can serve the request — synchronously, before anything is registered, so the exception arrives at the call that caused it.

Attributes

def rollbackTo(target: ChainTip): Unit

Roll back to target, which must still be within securityParam of the tip.

Roll back to target, which must still be within securityParam of the tip.

Only subscriptions that actually emitted something past target see a RolledBack — a subscription gated on confirmations may never have been told about the orphaned blocks at all, and telling it to undo events it never received would be worse than silence.

Attributes

The status this hub is currently reporting for a transaction, if it is tracking one.

The status this hub is currently reporting for a transaction, if it is tracking one.

None means the hub has no opinion — the transaction was never submitted through this provider and never appeared in a block it applied — and the caller should fall back to whatever it considers authoritative.

Attributes

def unregisterBlock(id: Long): Unit
def unregisterParams(id: Long): Unit
def unregisterTip(id: Long): Unit
def unregisterTransaction(id: Long): Unit
def unregisterTxStatus(txHash: TransactionHash, id: Long): Unit
def unregisterUtxo(id: Long): Unit
def updateParams(next: ProtocolParams): Unit

Report a protocol-parameter change to subscribers.

Report a protocol-parameter change to subscribers.

The provider's job, not the hub's: only the provider knows what a parameter change looks like on its own source — an epoch boundary observed over chain-sync, a fresh /epochs/latest/parameters, a re-read of local state. The hub cannot detect one, so a provider that never calls this has a subscribeProtocolParams stream that emits the value it was constructed with and nothing further. For an emulator that is exactly right: its parameters are fixed at construction and only its slot advances.

Attributes

Concrete fields

val securityParam: Int

Depth at which a block counts as settled. Taken from the provider's own declaration rather than a separate constructor parameter, so the classifier deciding whether a subscription is serviceable and the hub deciding when to release it cannot disagree.

Depth at which a block counts as settled. Taken from the provider's own declaration rather than a separate constructor parameter, so the classifier deciding whether a subscription is serviceable and the hub deciding when to release it cannot disagree.

Attributes