Files
2026-08-11 00:10:12 +08:00

208 lines
6.3 KiB
Rust

//! Runtime-independent lifecycle for a `ReadableStream` async iterator.
//!
//! JavaScript values, Promise resolvers, and queued operation identities stay
//! in the renderer. This module owns the serial-operation invariant and the
//! lifecycle transition selected for each queue-head event.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum IteratorLifecycle {
Active,
Returning,
Closed,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum IteratorOperationKind {
Next,
Return,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct IteratorState {
lifecycle: IteratorLifecycle,
operation_active: bool,
}
impl IteratorState {
#[must_use]
pub const fn new(lifecycle: IteratorLifecycle, operation_active: bool) -> Self {
Self {
lifecycle,
operation_active,
}
}
#[must_use]
pub const fn lifecycle(self) -> IteratorLifecycle {
self.lifecycle
}
#[must_use]
pub const fn operation_active(self) -> bool {
self.operation_active
}
#[must_use]
pub const fn plan_pump(self, head: Option<IteratorOperationKind>) -> IteratorPumpPlan {
if self.operation_active || matches!(self.lifecycle, IteratorLifecycle::Returning) {
return IteratorPumpPlan::WaitForInFlight;
}
let Some(head) = head else {
return IteratorPumpPlan::Idle;
};
match (self.lifecycle, head) {
(IteratorLifecycle::Active, IteratorOperationKind::Next) => {
IteratorPumpPlan::StartNext(self.transition(IteratorLifecycle::Active, true))
}
(IteratorLifecycle::Active, IteratorOperationKind::Return) => {
IteratorPumpPlan::StartReturn(self.transition(IteratorLifecycle::Returning, true))
}
(IteratorLifecycle::Closed, IteratorOperationKind::Next) => {
IteratorPumpPlan::ResolveClosedNext
}
(IteratorLifecycle::Closed, IteratorOperationKind::Return) => {
IteratorPumpPlan::ResolveClosedReturn
}
(IteratorLifecycle::Returning, _) => IteratorPumpPlan::WaitForInFlight,
}
}
#[must_use]
pub const fn plan_next_settlement(self, outcome: IteratorNextOutcome) -> IteratorTransition {
assert!(
self.operation_active && matches!(self.lifecycle, IteratorLifecycle::Active),
"next settlement requires an active next operation"
);
let lifecycle = match outcome {
IteratorNextOutcome::Chunk => IteratorLifecycle::Active,
IteratorNextOutcome::Done | IteratorNextOutcome::Rejected => IteratorLifecycle::Closed,
};
self.transition(lifecycle, false)
}
#[must_use]
pub const fn plan_return_settlement(self) -> IteratorTransition {
assert!(
self.operation_active && matches!(self.lifecycle, IteratorLifecycle::Returning),
"return settlement requires an active return operation"
);
self.transition(IteratorLifecycle::Closed, false)
}
const fn transition(
self,
lifecycle: IteratorLifecycle,
operation_active: bool,
) -> IteratorTransition {
IteratorTransition {
source: self,
next: Self::new(lifecycle, operation_active),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum IteratorPumpPlan {
Idle,
WaitForInFlight,
StartNext(IteratorTransition),
StartReturn(IteratorTransition),
ResolveClosedNext,
ResolveClosedReturn,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum IteratorNextOutcome {
Chunk,
Done,
Rejected,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct IteratorTransition {
source: IteratorState,
next: IteratorState,
}
impl IteratorTransition {
#[must_use]
pub const fn source(self) -> IteratorState {
self.source
}
#[must_use]
pub const fn next(self) -> IteratorState {
self.next
}
}
#[cfg(test)]
mod tests {
use super::*;
const ACTIVE: IteratorState = IteratorState::new(IteratorLifecycle::Active, false);
const CLOSED: IteratorState = IteratorState::new(IteratorLifecycle::Closed, false);
#[test]
fn pump_serializes_operations_and_preserves_closed_return_values() {
let IteratorPumpPlan::StartNext(next) = ACTIVE.plan_pump(Some(IteratorOperationKind::Next))
else {
panic!("active next must start");
};
assert_eq!(
next.next().plan_pump(Some(IteratorOperationKind::Return)),
IteratorPumpPlan::WaitForInFlight
);
let IteratorPumpPlan::StartReturn(returning) =
ACTIVE.plan_pump(Some(IteratorOperationKind::Return))
else {
panic!("active return must start");
};
assert_eq!(returning.next().lifecycle(), IteratorLifecycle::Returning);
assert_eq!(
returning
.next()
.plan_pump(Some(IteratorOperationKind::Next)),
IteratorPumpPlan::WaitForInFlight
);
assert_eq!(
CLOSED.plan_pump(Some(IteratorOperationKind::Next)),
IteratorPumpPlan::ResolveClosedNext
);
assert_eq!(
CLOSED.plan_pump(Some(IteratorOperationKind::Return)),
IteratorPumpPlan::ResolveClosedReturn
);
}
#[test]
fn next_and_return_settlements_release_the_single_in_flight_owner() {
let IteratorPumpPlan::StartNext(next) = ACTIVE.plan_pump(Some(IteratorOperationKind::Next))
else {
panic!("active next must start");
};
let next = next.next();
assert_eq!(
next.plan_next_settlement(IteratorNextOutcome::Chunk).next(),
ACTIVE
);
assert_eq!(
next.plan_next_settlement(IteratorNextOutcome::Done).next(),
CLOSED
);
assert_eq!(
next.plan_next_settlement(IteratorNextOutcome::Rejected)
.next(),
CLOSED
);
let IteratorPumpPlan::StartReturn(returning) =
ACTIVE.plan_pump(Some(IteratorOperationKind::Return))
else {
panic!("active return must start");
};
assert_eq!(returning.next().plan_return_settlement().next(), CLOSED);
}
}