pub struct PoolProjection { /* private fields */ }Expand description
An event-fed PoolView: the serve gates read {owner, remaining} from an
in-memory projection the settlement watcher folds from the PaymentPool event
log, so no serve request costs a getPool eth_call.
The map is published through an ArcSwap so a read (status at admission,
cached_status at every voucher boundary) is a single atomic load — no lock,
no read-modify-write — and never contends with a concurrent reader on the serve
hot path. The settlement watcher’s sink is the projection’s single writer and
each fold clones-then-publishes the whole map; writes are rare (one per
PaymentPool event) next to the per-voucher-boundary reads.
Cheaply cloneable (an Arc around the cell). The settlement watcher’s sink
holds one clone to WRITE the projection; the serve handler holds another as an
Arc<dyn PoolView> to READ it, both sharing the one ArcSwap.
Implementations§
Source§impl PoolProjection
impl PoolProjection
Sourcepub fn record_opened(&self, pool_id: B256, owner: Address, deposit: U256)
pub fn record_opened(&self, pool_id: B256, owner: Address, deposit: U256)
Apply a PoolOpened(poolId, owner, deposit): record the owner and the
initial deposit. Absolute, so a re-delivered log is idempotent. The deposit
is a uint256 on the wire but a uint64 on-chain; an out-of-range value
(impossible by construction) saturates to u64::MAX, which over-states
remaining and so fails toward serving — the fail-open direction.
Sourcepub fn record_topup(&self, pool_id: B256, new_deposit: U256)
pub fn record_topup(&self, pool_id: B256, new_deposit: U256)
Apply a PoolToppedUp(poolId, _, newDeposit): deposit = newDeposit.
Absolute, so idempotent. Skipped for a pool never opened in the projection’s
scan window — there is no owner to serve, and the serve gate fails open.
Sourcepub fn record_redeemed(
&self,
pool_id: B256,
provider: Address,
lanes: &[LaneSettled],
)
pub fn record_redeemed( &self, pool_id: B256, provider: Address, lanes: &[LaneSettled], )
Apply a PoolRedeemed(poolId, provider, lanes) for ANY provider: fold each
lane’s positive advance into total_redeemed, matching on-chain
totalRedeemed += Σ delta. Monotone and idempotent — a replayed event
advances no lane and adds nothing. Skipped for a pool the projection has not
opened (a redemption whose PoolOpened predates the scan window): without a
deposit there is no remaining to reserve, and the serve gate fails open.
Sourcepub fn record_closing(&self, pool_id: B256, deadline: u64)
pub fn record_closing(&self, pool_id: B256, deadline: u64)
Apply a PoolCloseInitiated(poolId, _, disputeDeadline): mark the pool
Closing. Skipped for a pool the projection has not opened — there is no
entry to serve, and reads fail open. A Closing pool keeps its
remaining and stays redeemable until deadline; PoolReclaimed later
removes it via forget.
Sourcepub fn forget(&self, pool_id: B256)
pub fn forget(&self, pool_id: B256)
Apply a PoolReclaimed(poolId, ..): the pool is Closed and its remainder
refunded. Drop it — a later read returns None and the serve gate fails
open, exactly as for a pool the projection has not yet seen. A
PoolCloseInitiated is folded by Self::record_closing, which marks the
pool Closing but keeps it redeemable until the dispute deadline; forget
removes the whole entry, lifecycle included, once the reclaim actually
lands.
Sourcepub fn record_resolved(
&self,
pool_id: B256,
owner: Address,
deposit: U256,
lifecycle: Lifecycle,
)
pub fn record_resolved( &self, pool_id: B256, owner: Address, deposit: U256, lifecycle: Lifecycle, )
Seed a pool the projection has NOT observed through the event log, from a
direct getPool snapshot (the lazy cold-start backfill). The settlement
watcher anchors at head (ColdStart::Head), so a pool opened before this
node’s first-ever boot never appears in the
forward-only PoolOpened scan. A node onboarding as a NEW provider to such
a pool would then have no owner to verify a presented capability against and
could never register the serve lane; the background pool-owner resolver
resolves the owner off the serve hot path and folds it here.
Inserts ONLY IF the pool is still absent, so a concurrent event fold — the
projection’s authoritative writer — always wins and this never clobbers a
live entry. deposit is the pool’s FULL cumulative getPool.deposit (not
its remaining), seeded with total_redeemed == 0, exactly as
Self::record_opened seeds an event-observed open: the projection then
rebuilds total_redeemed from post-seed PoolRedeemed deltas off a zero
per-lane baseline, so no redemption is ever double-counted. Redemptions that
predate the snapshot are simply not folded, which only OVER-states
remaining — the fail-toward-serving direction this module commits to. An
out-of-range deposit (impossible by construction) saturates to u64::MAX,
the same fail-open direction as Self::record_opened.
Sourcepub fn snapshot(&self, pool_id: B256) -> Option<PoolStatus>
pub fn snapshot(&self, pool_id: B256) -> Option<PoolStatus>
A non-blocking snapshot read for callers outside an async trait object.
Sourcepub fn signer_spent(&self, pool_id: B256, signer: Address) -> u64
pub fn signer_spent(&self, pool_id: B256, signer: Address) -> u64
A signer’s total on-chain spent in this pool: Σ of its lane advances
across EVERY provider. A signer’s cap is shared across all providers (ADR
003 §Deposit Economics), and on-chain spent for a signer is exactly what it
has redeemed at each provider summed. The projection folds this per-signer
total in record_redeemed alongside total_redeemed, so this read is an O(1)
map lookup — the serve path’s mid-stream signer cap-headroom re-check calls it
every voucher-boundary interval per live stream (ADR 003 §Pool solvency), and
a scan of the lane map here would cost streams × lanes on a busy pool.
0 for a pool the projection has not folded, or one in which the signer has
redeemed nothing — both make the re-check’s held_cap − spent headroom its
widest, so the re-check fails toward serving on a projection gap (the
admit-time getAuthorization already caught an already-exhausted signer
authoritatively; this only bounds drain SINCE admit). The folded total cannot
exceed total_redeemed, which is itself bounded by the deposit.
Trait Implementations§
Source§impl Clone for PoolProjection
impl Clone for PoolProjection
Source§impl Debug for PoolProjection
impl Debug for PoolProjection
Source§impl Default for PoolProjection
impl Default for PoolProjection
Source§impl PoolView for PoolProjection
impl PoolView for PoolProjection
Source§fn status<'life0, 'async_trait>(
&'life0 self,
pool_id: B256,
) -> Pin<Box<dyn Future<Output = Option<PoolStatus>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
fn status<'life0, 'async_trait>(
&'life0 self,
pool_id: B256,
) -> Pin<Box<dyn Future<Output = Option<PoolStatus>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
None if the pool cannot be confirmed. The
stream-admission read: a caller here may tolerate a blocking source, so an
implementation MAY do slow work. The bare PoolProjection reads from
memory and returns None for a pool it has not folded; the production
wrapper (crate::payment_settlement::ResolvingPoolView) confirms such a
pool with one getPool before returning, so the admit gate refuses a
pool it still cannot confirm rather than failing open.Source§fn signer_spent_cached<'life0, 'async_trait>(
&'life0 self,
pool_id: B256,
signer: Address,
) -> Pin<Box<dyn Future<Output = Option<u64>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
fn signer_spent_cached<'life0, 'async_trait>(
&'life0 self,
pool_id: B256,
signer: Address,
) -> Pin<Box<dyn Future<Output = Option<u64>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
signer’s total on-chain spent across
every provider in pool_id, from the event-fed projection ONLY — never a
chain call. The serve loop pairs it with the cap the node already holds on
the lane to test held_cap − spent headroom at the POOL_RECHECK_INTERVAL
cadence, so a signer draining its shared cap at another node mid-stream stops
this stream before it over-delivers unredeemable bytes (ADR 003 §Pool
solvency). Read moreSource§fn cached_status<'life0, 'async_trait>(
&'life0 self,
pool_id: B256,
) -> Pin<Box<dyn Future<Output = Option<PoolStatus>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
fn cached_status<'life0, 'async_trait>(
&'life0 self,
pool_id: B256,
) -> Pin<Box<dyn Future<Output = Option<PoolStatus>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
None rather than doing any slow or
remote work. The mid-stream serve re-check calls it at each voucher
boundary, where a blocking read would stall delivery. The default delegates
to Self::status for in-memory test doubles; PoolProjection serves
both from the same in-memory map, so neither ever touches the network.signer on-chain
authorization in pool_id (ADR 003 §Capability delegation, §Pool
solvency). A signer’s cap is shared across every provider, so a signer
that has drawn its full cap at other nodes is uncashable here. Its
registered cap and expiry are write-once, so they bound every voucher
whatever capability the client presents. Read moreAuto Trait Implementations§
impl Freeze for PoolProjection
impl RefUnwindSafe for PoolProjection
impl Send for PoolProjection
impl Sync for PoolProjection
impl Unpin for PoolProjection
impl UnsafeUnpin for PoolProjection
impl UnwindSafe for PoolProjection
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<T> FutureExt for T
impl<T> FutureExt for T
Source§fn with_context(self, otel_cx: Context) -> WithContext<Self> ⓘ
fn with_context(self, otel_cx: Context) -> WithContext<Self> ⓘ
Source§fn with_current_context(self) -> WithContext<Self> ⓘ
fn with_current_context(self) -> WithContext<Self> ⓘ
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§impl<T> IntoRequest<T> for T
impl<T> IntoRequest<T> for T
Source§fn into_request(self) -> Request<T>
fn into_request(self) -> Request<T>
T in a tonic::Request