scalus.cardano.node.stream.internal

Members list

Type members

Classlikes

case class AppliedBlock(point: ChainPoint, blockNo: BlockNo, txs: Seq[AppliedTransaction], block: Option[Block] = ..., coverage: BlockCoverage = ...)

A block as the subscription hub consumes it.

A block as the subscription hub consumes it.

block is the raw ledger block when the provider has one. Providers that synthesise blocks — an emulator applying one transaction at a time — have nothing truthful to put here, which is why it is optional and why such providers declare that they do not serve Block subscriptions rather than fabricating a header.

The contract for partial coverage

A provider producing anything other than BlockCoverage.Complete owes the hub two things.

  • One block per height, carrying the union of every source probed at that height — not one block per watcher. A query spanning two sources is covered only by a block that probed both (a union is as covered as its worst arm), so splitting a height across per-watcher blocks would leave such a subscription permanently uncovered and silent.
  • Every height examined, whether or not it matched anything. An empty txs with the right coverage is how a provider says "I looked here for these sources and there was nothing", which is both what makes Idle truthful and what keeps watermarks tracking the tip. A provider that reported only the heights it found matches in would let its subscriptions fall arbitrarily far behind the retention window and lose events to pruning.

Heights are applied in ascending order and each is applied once — SubscriptionHub.applyBlock takes the block's height as the new tip unconditionally, so it has always required this.

Not yet supported: backfill. A provider that runs out of request budget mid-height and wants to come back for the sources it skipped would have to re-report a height it has already applied, and there is no representation for that: recent is ordered by height and a subscription's progress is a single watermark. Such a provider must instead defer the whole height until it can cover everything it has subscriptions for. Lifting this needs per-height observation sets and is a deliberate design step, not an implementation detail — see the M2 plan.

Attributes

Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
case class AppliedTransaction(tx: Transaction, created: Utxos, spent: Utxos)

One applied transaction, with its UTxO effects already resolved.

One applied transaction, with its UTxO effects already resolved.

spent carries the resolved consumed outputs, not just the inputs: a subscriber watching an address needs to know which of its UTxOs disappeared, and the input alone does not say. Every provider can resolve this — the emulator from its ledger, a chain-following provider from the block it just fetched — so resolving once at the source beats every subscriber doing it.

Attributes

Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all

What the provider actually examined when it produced an AppliedBlock.

What the provider actually examined when it produced an AppliedBlock.

A provider that holds the whole block can answer any subscription from it. A metered provider cannot afford to: Blockfrost's cheap path is per-address and per-asset endpoints, so what it learns about a block is only ever "what these sources did in it", and it must say so.

The distinction is load-bearing rather than informational. The hub delivers a block to a subscription and advances that subscription's watermark past it in the same step, so handing it a block that did not cover a subscription would tell that subscriber "nothing here for you" — as an scalus.cardano.node.stream.UtxoEvent.Idle, no less — and then make the real events for that height undeliverable, because the watermark has moved on. Silently.

Attributes

Companion
object
Supertypes
trait Enum
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
object BlockCoverage

Attributes

Companion
enum
Supertypes
trait Sum
trait Mirror
class Object
trait Matchable
class Any
Self type
sealed trait Mailbox[A] extends ScalusAsyncSource[A]

Producer-synchronous, consumer-asynchronous bridge between a provider and a subscriber's stream.

Producer-synchronous, consumer-asynchronous bridge between a provider and a subscriber's stream.

Two shapes, because two kinds of stream want opposite things on overflow:

  • DeltaMailbox — FIFO for state-mutating events. Silent drops are never acceptable: a missed Spent corrupts the subscriber's view permanently and invisibly. A bounded buffer that fills therefore fails the subscription rather than dropping.
  • LatestValueMailbox — size-1, newer wins, for single-source-of-truth cells (tip, params, transaction status). Here dropping intermediate values is precisely correct.

Not public API: this is one implementation of ScalusAsyncSource, and adapters are written against that interface so the buffering strategy stays free to change.

Buffering and delivery are separable, on purpose

offer is buffer-then-deliver, and a producer that needs several events to land atomically relative to its own state can split the two: offerBuffered under its lock, flush after releasing it. That is what lets the hub register a subscription and enqueue its seed as one indivisible step without ever running a consumer's continuation while holding the hub monitor.

Nothing observable escapes during the buffered phase — no promise is completed, no onCancel hook runs. Both are deferred to flush.

Termination always unregisters

Every way a mailbox can die — explicit cancel, clean close, producer fail, or a bounded buffer overflowing — fires onCancel exactly once. A subscription that ends by any route must stop costing the provider work; the alternative is a dead subscription matched against every block for the lifetime of the process.

Attributes

Companion
object
Supertypes
class Object
trait Matchable
class Any
object Mailbox

Attributes

Companion
trait
Supertypes
class Object
trait Matchable
class Any
Self type
Mailbox.type
final class SubscriptionHub(val cardanoInfo: CardanoInfo, val capabilities: StreamCapabilities)

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

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

Supertypes
class Object
trait Matchable
class Any