Everything that changes over time has both a one-shot read and a subscription, and the two read the same state — a one-shot is semantically subscribeXxx().head. That is what stops two methods disagreeing about the current value. Polling disappears with it: pollForConfirmation(h) becomes subscribeTransactionStatus(h) with no sleep loop and no missed-update window.
Registration is eager and synchronous. Two consequences, both load-bearing:
subscribe(q) followed by submit(tx) on the same thread is race-free — the subscription is registered before subscribe returns, so the submitted transaction's events reach it. This is what makes emulator-driven tests deterministic.
The caller owns releasing the subscription. A subscription that is never consumed still accumulates events. Adapters expose this as a resource (fs2 Resource, pekko KillSwitch, an ox scope); the underlying release is ScalusAsyncSource.cancel().
A request the provider cannot serve throws scalus.cardano.infra.UnsupportedSubscriptionException synchronously, from the call that caused it, before anything is registered — exactly when SubscriptionSupport.of(request, streamCapabilities) says Unsupported, or says Unindexed and the request did not set allowUnindexedScan. Callers that want to decide before asking consult the same function.
The stream type is chosen per call, not per provider, so one provider instance can hand fs2 streams to application code while a test pulls raw ScalusAsyncSources from it. C is inferred from the expected type:
val tips: Stream[IO, ChainTip] = provider.subscribeTip() // needs the fs2 adapter given
val raw: ScalusAsyncSource[ChainTip] = provider.subscribeTip() // no adapter module needed
Where there is no expected type, C is decided by which adapter is in implicit scope — an imported adapter outranks the ScalusAsyncStreamAdapter.identity instance in this companion. Importing an adapter is therefore what makes subscribe return that library's type. Two adapters imported into one file with no expected type is an ambiguity, and the compiler says so; annotate or pass the type argument explicitly.
Latest-value stream of chain-tip updates — newer wins, so a subscriber always sees the most recent tip when it pulls rather than a backlog of stale ones.
Latest-value stream of chain-tip updates — newer wins, so a subscriber always sees the most recent tip when it pulls rather than a backlog of stale ones.
Translates the lambda to a UtxoQuery at compile time and returns a builder that can be further configured before execution. Effect-polymorphic — the resulting .execute() returns the reader's own F[Either[UtxoQueryError, Utxos]], so the same call shape works on Future-typed providers (Blockfrost, JS) and direct-style providers (ox Id).
Example:
// Simple query — execute immediately
reader.queryUtxos { u =>
u.output.address == myAddress
}.execute()
// With pagination and minimum total
reader.queryUtxos { u =>
u.output.address == myAddress && u.output.value.hasAsset(policyId, assetName)
}.minTotal(Coin.ada(100)).limit(10).execute()
Supported expressions:
u.output.address == addr — query by address
u.input.transactionId == txId — query by transaction
u.output.value.hasAsset(policyId, assetName) — query/filter by asset
u.output.value.coin >= amount — filter by minimum lovelace
u.output.hasDatumHash(hash) — filter by datum hash
protected def mapF[A, B](fa: F[A])(f: A => B): F[B]
Map over this reader's effect F — the one primitive needed to give the convenience lookups above (findUtxo and the findUtxos overloads) a single effect-polymorphic default, without imposing an external Functor/Monad constraint on F. Future-based readers map via their captured ExecutionContext; monadic effects map via their monad.
Map over this reader's effect F — the one primitive needed to give the convenience lookups above (findUtxo and the findUtxos overloads) a single effect-polymorphic default, without imposing an external Functor/Monad constraint on F. Future-based readers map via their captured ExecutionContext; monadic effects map via their monad.