mirror of
https://github.com/lexmount/moli.git
synced 2026-10-06 08:00:59 +00:00
838 lines
24 KiB
Rust
838 lines
24 KiB
Rust
//! Runtime-independent transferable stream channel coordination.
|
|
//!
|
|
//! MessagePort endpoints, structured-clone payloads, JavaScript chunks and
|
|
//! errors, stream wrappers, and promises remain adapter-owned. This module
|
|
//! owns the four-message protocol and the sender/receiver decisions derived
|
|
//! from primitive channel state.
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum TransferMessageKind {
|
|
Pull,
|
|
Chunk,
|
|
Close,
|
|
Error,
|
|
}
|
|
|
|
impl TransferMessageKind {
|
|
#[must_use]
|
|
pub const fn wire_code(self) -> u32 {
|
|
match self {
|
|
Self::Pull => 0,
|
|
Self::Chunk => 1,
|
|
Self::Close => 2,
|
|
Self::Error => 3,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl TryFrom<u32> for TransferMessageKind {
|
|
type Error = InvalidTransferMessageKind;
|
|
|
|
fn try_from(value: u32) -> Result<Self, InvalidTransferMessageKind> {
|
|
match value {
|
|
0 => Ok(Self::Pull),
|
|
1 => Ok(Self::Chunk),
|
|
2 => Ok(Self::Close),
|
|
3 => Ok(Self::Error),
|
|
_ => Err(InvalidTransferMessageKind),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub struct InvalidTransferMessageKind;
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub struct TransferEntrySnapshot {
|
|
source_locked: bool,
|
|
}
|
|
|
|
impl TransferEntrySnapshot {
|
|
#[must_use]
|
|
pub const fn new(source_locked: bool) -> Self {
|
|
Self { source_locked }
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn plan(self) -> TransferEntryPlan {
|
|
if self.source_locked {
|
|
TransferEntryPlan::RejectLocked
|
|
} else {
|
|
TransferEntryPlan::Prepare
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum TransferEntryPlan {
|
|
Prepare,
|
|
RejectLocked,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub struct TransferSnapshot {
|
|
active: bool,
|
|
read_in_flight: bool,
|
|
pull_demand: u32,
|
|
has_staged_chunk: bool,
|
|
}
|
|
|
|
impl TransferSnapshot {
|
|
#[must_use]
|
|
pub const fn new(
|
|
active: bool,
|
|
read_in_flight: bool,
|
|
pull_demand: u32,
|
|
has_staged_chunk: bool,
|
|
) -> Self {
|
|
Self {
|
|
active,
|
|
read_in_flight,
|
|
pull_demand,
|
|
has_staged_chunk,
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn initial() -> Self {
|
|
Self::new(true, false, 0, false)
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn active(self) -> bool {
|
|
self.active
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn read_in_flight(self) -> bool {
|
|
self.read_in_flight
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn pull_demand(self) -> u32 {
|
|
self.pull_demand
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn plan_finish(self) -> TransferFinishPlan {
|
|
if self.active {
|
|
TransferFinishPlan::DeactivateAndClosePort
|
|
} else {
|
|
TransferFinishPlan::AlreadyFinished
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn plan_sender_start_read(self) -> SenderStartReadPlan {
|
|
if self.active && !self.has_staged_chunk && !self.read_in_flight {
|
|
SenderStartReadPlan::StartRead
|
|
} else {
|
|
SenderStartReadPlan::Ignore
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn plan_sender_read_reaction(self) -> SenderReadReactionPlan {
|
|
if self.active {
|
|
SenderReadReactionPlan::InspectResult
|
|
} else {
|
|
SenderReadReactionPlan::Ignore
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn plan_sender_read_fulfilled(
|
|
self,
|
|
result: SenderReadResultSnapshot,
|
|
) -> SenderReadFulfillmentPlan {
|
|
if !self.active {
|
|
return SenderReadFulfillmentPlan::Ignore;
|
|
}
|
|
if !result.valid_result {
|
|
return SenderReadFulfillmentPlan::FailInvalidResult;
|
|
}
|
|
if result.done {
|
|
return SenderReadFulfillmentPlan::PostCloseAndFinish;
|
|
}
|
|
if self.pull_demand == 0 {
|
|
SenderReadFulfillmentPlan::StageChunk
|
|
} else {
|
|
SenderReadFulfillmentPlan::PostChunk {
|
|
pull_demand_after: self.pull_demand - 1,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn plan_sender_read_rejected(self) -> SenderReadRejectionPlan {
|
|
if self.active {
|
|
SenderReadRejectionPlan::PostErrorAndFinish
|
|
} else {
|
|
SenderReadRejectionPlan::Ignore
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn plan_sender_message(
|
|
self,
|
|
message: Option<TransferMessageKind>,
|
|
) -> SenderMessagePlan {
|
|
if !self.active {
|
|
return SenderMessagePlan::IgnoreLateMessage;
|
|
}
|
|
match message {
|
|
Some(TransferMessageKind::Pull) => {
|
|
let pull_demand = self.pull_demand.saturating_add(1);
|
|
let next = Self::new(
|
|
self.active,
|
|
self.read_in_flight,
|
|
pull_demand,
|
|
self.has_staged_chunk,
|
|
);
|
|
SenderMessagePlan::RecordPull {
|
|
pull_demand,
|
|
drain: next.plan_sender_drain(),
|
|
}
|
|
}
|
|
Some(TransferMessageKind::Error) => SenderMessagePlan::CancelSourceAndFinish,
|
|
Some(TransferMessageKind::Chunk | TransferMessageKind::Close) | None => {
|
|
SenderMessagePlan::FailProtocol
|
|
}
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn plan_sender_drain(self) -> SenderDrainPlan {
|
|
if !self.active || self.pull_demand == 0 {
|
|
return SenderDrainPlan::Ignore;
|
|
}
|
|
if self.has_staged_chunk {
|
|
SenderDrainPlan::PostStagedChunk {
|
|
pull_demand_after: self.pull_demand - 1,
|
|
}
|
|
} else {
|
|
match self.plan_sender_start_read() {
|
|
SenderStartReadPlan::StartRead => SenderDrainPlan::StartRead,
|
|
SenderStartReadPlan::Ignore => SenderDrainPlan::Ignore,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn plan_sender_failure(self) -> SenderFailurePlan {
|
|
if self.active {
|
|
SenderFailurePlan::ErrorSourcePostErrorAndFinish
|
|
} else {
|
|
SenderFailurePlan::Ignore
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn plan_receiver_message(
|
|
self,
|
|
message: Option<TransferMessageKind>,
|
|
) -> ReceiverMessagePlan {
|
|
if !self.active {
|
|
return ReceiverMessagePlan::IgnoreLateMessage;
|
|
}
|
|
match message {
|
|
Some(TransferMessageKind::Chunk) => ReceiverMessagePlan::EnqueueChunk,
|
|
Some(TransferMessageKind::Close) => ReceiverMessagePlan::CloseStreamAndFinish,
|
|
Some(TransferMessageKind::Error) => ReceiverMessagePlan::ErrorStreamAndFinish,
|
|
Some(TransferMessageKind::Pull) | None => ReceiverMessagePlan::FailProtocol,
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn plan_receiver_message_error(self) -> ReceiverMessageErrorPlan {
|
|
if self.active {
|
|
ReceiverMessageErrorPlan::ErrorStreamAndFinish
|
|
} else {
|
|
ReceiverMessageErrorPlan::Ignore
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn plan_receiver_pull(self) -> ReceiverPullPlan {
|
|
if self.active {
|
|
ReceiverPullPlan::PostPull
|
|
} else {
|
|
ReceiverPullPlan::Ignore
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn plan_receiver_cancel(self) -> ReceiverCancelPlan {
|
|
if self.active {
|
|
ReceiverCancelPlan::PostErrorAndFinish
|
|
} else {
|
|
ReceiverCancelPlan::Ignore
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum TransferFinishPlan {
|
|
AlreadyFinished,
|
|
DeactivateAndClosePort,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum SenderStartReadPlan {
|
|
Ignore,
|
|
StartRead,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum SenderReadReactionPlan {
|
|
Ignore,
|
|
InspectResult,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub struct SenderReadResultSnapshot {
|
|
valid_result: bool,
|
|
done: bool,
|
|
}
|
|
|
|
impl SenderReadResultSnapshot {
|
|
#[must_use]
|
|
pub const fn new(valid_result: bool, done: bool) -> Self {
|
|
Self { valid_result, done }
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum SenderReadFulfillmentPlan {
|
|
Ignore,
|
|
FailInvalidResult,
|
|
PostCloseAndFinish,
|
|
StageChunk,
|
|
PostChunk { pull_demand_after: u32 },
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum SenderReadRejectionPlan {
|
|
Ignore,
|
|
PostErrorAndFinish,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum SenderMessagePlan {
|
|
IgnoreLateMessage,
|
|
RecordPull {
|
|
pull_demand: u32,
|
|
drain: SenderDrainPlan,
|
|
},
|
|
CancelSourceAndFinish,
|
|
FailProtocol,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum SenderDrainPlan {
|
|
Ignore,
|
|
StartRead,
|
|
PostStagedChunk { pull_demand_after: u32 },
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum SenderFailurePlan {
|
|
Ignore,
|
|
ErrorSourcePostErrorAndFinish,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum ReceiverMessagePlan {
|
|
IgnoreLateMessage,
|
|
EnqueueChunk,
|
|
CloseStreamAndFinish,
|
|
ErrorStreamAndFinish,
|
|
FailProtocol,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum ReceiverEnqueueOutcome {
|
|
Enqueued,
|
|
StreamTerminal,
|
|
StrategyError,
|
|
}
|
|
|
|
impl ReceiverEnqueueOutcome {
|
|
#[must_use]
|
|
pub const fn plan(self) -> ReceiverEnqueuePlan {
|
|
match self {
|
|
Self::Enqueued | Self::StreamTerminal => ReceiverEnqueuePlan::Continue,
|
|
Self::StrategyError => ReceiverEnqueuePlan::Finish,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum ReceiverEnqueuePlan {
|
|
Continue,
|
|
Finish,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum ReceiverMessageErrorPlan {
|
|
Ignore,
|
|
ErrorStreamAndFinish,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum ReceiverPullPlan {
|
|
Ignore,
|
|
PostPull,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum ReceiverCancelPlan {
|
|
Ignore,
|
|
PostErrorAndFinish,
|
|
}
|
|
|
|
/// Primitive state for the WritableStream endpoint reconstructed in the
|
|
/// receiving realm. JavaScript chunks and promise capabilities stay in the
|
|
/// renderer; this snapshot owns admission and protocol ordering only.
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub struct WritableTransferSnapshot {
|
|
active: bool,
|
|
pull_demand: u32,
|
|
pending_write: bool,
|
|
}
|
|
|
|
impl WritableTransferSnapshot {
|
|
#[must_use]
|
|
pub const fn new(active: bool, pull_demand: u32, pending_write: bool) -> Self {
|
|
Self {
|
|
active,
|
|
pull_demand,
|
|
pending_write,
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn initial() -> Self {
|
|
Self::new(true, 0, false)
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn active(self) -> bool {
|
|
self.active
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn pull_demand(self) -> u32 {
|
|
self.pull_demand
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn plan_write(self) -> WritableTransferWritePlan {
|
|
if !self.active {
|
|
return WritableTransferWritePlan::RejectInactive;
|
|
}
|
|
if self.pending_write {
|
|
return WritableTransferWritePlan::RejectConcurrentWrite;
|
|
}
|
|
if self.pull_demand == 0 {
|
|
WritableTransferWritePlan::WaitForPull
|
|
} else {
|
|
WritableTransferWritePlan::PostChunk {
|
|
pull_demand_after: self.pull_demand - 1,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn plan_message(
|
|
self,
|
|
message: Option<TransferMessageKind>,
|
|
) -> WritableTransferMessagePlan {
|
|
if !self.active {
|
|
return WritableTransferMessagePlan::IgnoreLateMessage;
|
|
}
|
|
match message {
|
|
Some(TransferMessageKind::Pull) if self.pending_write => {
|
|
WritableTransferMessagePlan::PostPendingChunk
|
|
}
|
|
Some(TransferMessageKind::Pull) => WritableTransferMessagePlan::RecordPull {
|
|
pull_demand: self.pull_demand.saturating_add(1),
|
|
},
|
|
Some(TransferMessageKind::Error) => WritableTransferMessagePlan::ErrorStreamAndFinish,
|
|
Some(TransferMessageKind::Chunk | TransferMessageKind::Close) | None => {
|
|
WritableTransferMessagePlan::FailProtocol
|
|
}
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn plan_terminal(self) -> WritableTransferTerminalPlan {
|
|
if self.active {
|
|
WritableTransferTerminalPlan::PostAndFinish
|
|
} else {
|
|
WritableTransferTerminalPlan::Ignore
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn plan_message_error(self) -> WritableTransferMessageErrorPlan {
|
|
if self.active {
|
|
WritableTransferMessageErrorPlan::ErrorStreamAndFinish
|
|
} else {
|
|
WritableTransferMessageErrorPlan::Ignore
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum WritableTransferWritePlan {
|
|
PostChunk { pull_demand_after: u32 },
|
|
WaitForPull,
|
|
RejectInactive,
|
|
RejectConcurrentWrite,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum WritableTransferMessagePlan {
|
|
IgnoreLateMessage,
|
|
RecordPull { pull_demand: u32 },
|
|
PostPendingChunk,
|
|
ErrorStreamAndFinish,
|
|
FailProtocol,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum WritableTransferTerminalPlan {
|
|
Ignore,
|
|
PostAndFinish,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum WritableTransferMessageErrorPlan {
|
|
Ignore,
|
|
ErrorStreamAndFinish,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn snapshot(
|
|
active: bool,
|
|
read_in_flight: bool,
|
|
pull_demand: u32,
|
|
has_staged_chunk: bool,
|
|
) -> TransferSnapshot {
|
|
TransferSnapshot::new(active, read_in_flight, pull_demand, has_staged_chunk)
|
|
}
|
|
|
|
#[test]
|
|
fn protocol_wire_codes_are_exact_and_reject_unknown_values() {
|
|
for (kind, code) in [
|
|
(TransferMessageKind::Pull, 0),
|
|
(TransferMessageKind::Chunk, 1),
|
|
(TransferMessageKind::Close, 2),
|
|
(TransferMessageKind::Error, 3),
|
|
] {
|
|
assert_eq!(kind.wire_code(), code);
|
|
assert_eq!(TransferMessageKind::try_from(code), Ok(kind));
|
|
}
|
|
assert_eq!(
|
|
TransferMessageKind::try_from(4),
|
|
Err(InvalidTransferMessageKind)
|
|
);
|
|
assert_eq!(
|
|
TransferMessageKind::try_from(u32::MAX),
|
|
Err(InvalidTransferMessageKind)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn entry_rejects_only_a_locked_source() {
|
|
assert_eq!(
|
|
TransferEntrySnapshot::new(false).plan(),
|
|
TransferEntryPlan::Prepare
|
|
);
|
|
assert_eq!(
|
|
TransferEntrySnapshot::new(true).plan(),
|
|
TransferEntryPlan::RejectLocked
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn initial_channel_is_active_idle_and_without_demand() {
|
|
let initial = TransferSnapshot::initial();
|
|
assert!(initial.active());
|
|
assert!(!initial.read_in_flight());
|
|
assert_eq!(initial.pull_demand(), 0);
|
|
assert_eq!(
|
|
initial.plan_sender_start_read(),
|
|
SenderStartReadPlan::StartRead
|
|
);
|
|
assert_eq!(
|
|
initial.plan_sender_read_reaction(),
|
|
SenderReadReactionPlan::InspectResult
|
|
);
|
|
assert_eq!(
|
|
initial.plan_finish(),
|
|
TransferFinishPlan::DeactivateAndClosePort
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn writable_endpoint_serializes_writes_against_remote_pull_demand() {
|
|
let initial = WritableTransferSnapshot::initial();
|
|
assert_eq!(initial.plan_write(), WritableTransferWritePlan::WaitForPull);
|
|
assert_eq!(
|
|
initial.plan_message(Some(TransferMessageKind::Pull)),
|
|
WritableTransferMessagePlan::RecordPull { pull_demand: 1 }
|
|
);
|
|
assert_eq!(
|
|
WritableTransferSnapshot::new(true, 1, false).plan_write(),
|
|
WritableTransferWritePlan::PostChunk {
|
|
pull_demand_after: 0
|
|
}
|
|
);
|
|
assert_eq!(
|
|
WritableTransferSnapshot::new(true, 0, true)
|
|
.plan_message(Some(TransferMessageKind::Pull)),
|
|
WritableTransferMessagePlan::PostPendingChunk
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn writable_endpoint_routes_terminal_and_invalid_messages_once() {
|
|
let active = WritableTransferSnapshot::initial();
|
|
assert_eq!(
|
|
active.plan_message(Some(TransferMessageKind::Error)),
|
|
WritableTransferMessagePlan::ErrorStreamAndFinish
|
|
);
|
|
assert_eq!(
|
|
active.plan_message(Some(TransferMessageKind::Chunk)),
|
|
WritableTransferMessagePlan::FailProtocol
|
|
);
|
|
assert_eq!(
|
|
active.plan_terminal(),
|
|
WritableTransferTerminalPlan::PostAndFinish
|
|
);
|
|
|
|
let inactive = WritableTransferSnapshot::new(false, 1, true);
|
|
assert_eq!(
|
|
inactive.plan_message(Some(TransferMessageKind::Pull)),
|
|
WritableTransferMessagePlan::IgnoreLateMessage
|
|
);
|
|
assert_eq!(
|
|
inactive.plan_terminal(),
|
|
WritableTransferTerminalPlan::Ignore
|
|
);
|
|
assert_eq!(
|
|
inactive.plan_message_error(),
|
|
WritableTransferMessageErrorPlan::Ignore
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn sender_start_read_exhaustively_requires_active_idle_unstaged_state() {
|
|
for active in [false, true] {
|
|
for read_in_flight in [false, true] {
|
|
for has_staged_chunk in [false, true] {
|
|
let current = snapshot(active, read_in_flight, 0, has_staged_chunk);
|
|
assert_eq!(
|
|
current.plan_sender_start_read(),
|
|
if active && !read_in_flight && !has_staged_chunk {
|
|
SenderStartReadPlan::StartRead
|
|
} else {
|
|
SenderStartReadPlan::Ignore
|
|
}
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn sender_read_fulfillment_prioritizes_terminal_and_validation() {
|
|
let inactive = snapshot(false, false, 1, false);
|
|
assert_eq!(
|
|
inactive.plan_sender_read_fulfilled(SenderReadResultSnapshot::new(false, false)),
|
|
SenderReadFulfillmentPlan::Ignore
|
|
);
|
|
let active = snapshot(true, false, 0, false);
|
|
assert_eq!(
|
|
active.plan_sender_read_fulfilled(SenderReadResultSnapshot::new(false, false)),
|
|
SenderReadFulfillmentPlan::FailInvalidResult
|
|
);
|
|
assert_eq!(
|
|
active.plan_sender_read_fulfilled(SenderReadResultSnapshot::new(true, true)),
|
|
SenderReadFulfillmentPlan::PostCloseAndFinish
|
|
);
|
|
assert_eq!(
|
|
active.plan_sender_read_fulfilled(SenderReadResultSnapshot::new(true, false)),
|
|
SenderReadFulfillmentPlan::StageChunk
|
|
);
|
|
assert_eq!(
|
|
snapshot(true, false, 3, false)
|
|
.plan_sender_read_fulfilled(SenderReadResultSnapshot::new(true, false)),
|
|
SenderReadFulfillmentPlan::PostChunk {
|
|
pull_demand_after: 2,
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn sender_drain_exhaustively_routes_stage_read_and_wait() {
|
|
for active in [false, true] {
|
|
for read_in_flight in [false, true] {
|
|
for demand in [0, 1, u32::MAX] {
|
|
for staged in [false, true] {
|
|
let current = snapshot(active, read_in_flight, demand, staged);
|
|
let expected = if !active || demand == 0 {
|
|
SenderDrainPlan::Ignore
|
|
} else if staged {
|
|
SenderDrainPlan::PostStagedChunk {
|
|
pull_demand_after: demand - 1,
|
|
}
|
|
} else if read_in_flight {
|
|
SenderDrainPlan::Ignore
|
|
} else {
|
|
SenderDrainPlan::StartRead
|
|
};
|
|
assert_eq!(current.plan_sender_drain(), expected);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn pull_message_saturates_demand_and_freezes_the_matching_drain_plan() {
|
|
assert_eq!(
|
|
snapshot(true, false, 0, true).plan_sender_message(Some(TransferMessageKind::Pull)),
|
|
SenderMessagePlan::RecordPull {
|
|
pull_demand: 1,
|
|
drain: SenderDrainPlan::PostStagedChunk {
|
|
pull_demand_after: 0,
|
|
},
|
|
}
|
|
);
|
|
assert_eq!(
|
|
snapshot(true, true, u32::MAX, false)
|
|
.plan_sender_message(Some(TransferMessageKind::Pull)),
|
|
SenderMessagePlan::RecordPull {
|
|
pull_demand: u32::MAX,
|
|
drain: SenderDrainPlan::Ignore,
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn sender_protocol_accepts_only_pull_and_remote_error() {
|
|
let active = TransferSnapshot::initial();
|
|
assert_eq!(
|
|
active.plan_sender_message(Some(TransferMessageKind::Error)),
|
|
SenderMessagePlan::CancelSourceAndFinish
|
|
);
|
|
for message in [
|
|
Some(TransferMessageKind::Chunk),
|
|
Some(TransferMessageKind::Close),
|
|
None,
|
|
] {
|
|
assert_eq!(
|
|
active.plan_sender_message(message),
|
|
SenderMessagePlan::FailProtocol
|
|
);
|
|
}
|
|
assert_eq!(
|
|
snapshot(false, false, 0, false).plan_sender_message(Some(TransferMessageKind::Pull)),
|
|
SenderMessagePlan::IgnoreLateMessage
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn sender_rejection_and_failure_are_one_shot() {
|
|
let active = TransferSnapshot::initial();
|
|
assert_eq!(
|
|
active.plan_sender_read_rejected(),
|
|
SenderReadRejectionPlan::PostErrorAndFinish
|
|
);
|
|
assert_eq!(
|
|
active.plan_sender_failure(),
|
|
SenderFailurePlan::ErrorSourcePostErrorAndFinish
|
|
);
|
|
let inactive = snapshot(false, false, 0, false);
|
|
assert_eq!(
|
|
inactive.plan_sender_read_rejected(),
|
|
SenderReadRejectionPlan::Ignore
|
|
);
|
|
assert_eq!(
|
|
inactive.plan_sender_read_reaction(),
|
|
SenderReadReactionPlan::Ignore
|
|
);
|
|
assert_eq!(inactive.plan_sender_failure(), SenderFailurePlan::Ignore);
|
|
assert_eq!(inactive.plan_finish(), TransferFinishPlan::AlreadyFinished);
|
|
}
|
|
|
|
#[test]
|
|
fn receiver_protocol_routes_data_terminal_and_invalid_messages() {
|
|
let active = TransferSnapshot::initial();
|
|
assert_eq!(
|
|
active.plan_receiver_message(Some(TransferMessageKind::Chunk)),
|
|
ReceiverMessagePlan::EnqueueChunk
|
|
);
|
|
assert_eq!(
|
|
active.plan_receiver_message(Some(TransferMessageKind::Close)),
|
|
ReceiverMessagePlan::CloseStreamAndFinish
|
|
);
|
|
assert_eq!(
|
|
active.plan_receiver_message(Some(TransferMessageKind::Error)),
|
|
ReceiverMessagePlan::ErrorStreamAndFinish
|
|
);
|
|
assert_eq!(
|
|
active.plan_receiver_message(Some(TransferMessageKind::Pull)),
|
|
ReceiverMessagePlan::FailProtocol
|
|
);
|
|
assert_eq!(
|
|
active.plan_receiver_message(None),
|
|
ReceiverMessagePlan::FailProtocol
|
|
);
|
|
assert_eq!(
|
|
snapshot(false, false, 0, false)
|
|
.plan_receiver_message(Some(TransferMessageKind::Chunk)),
|
|
ReceiverMessagePlan::IgnoreLateMessage
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn receiver_enqueue_messageerror_pull_and_cancel_are_typed() {
|
|
assert_eq!(
|
|
ReceiverEnqueueOutcome::Enqueued.plan(),
|
|
ReceiverEnqueuePlan::Continue
|
|
);
|
|
assert_eq!(
|
|
ReceiverEnqueueOutcome::StreamTerminal.plan(),
|
|
ReceiverEnqueuePlan::Continue
|
|
);
|
|
assert_eq!(
|
|
ReceiverEnqueueOutcome::StrategyError.plan(),
|
|
ReceiverEnqueuePlan::Finish
|
|
);
|
|
let active = TransferSnapshot::initial();
|
|
assert_eq!(
|
|
active.plan_receiver_message_error(),
|
|
ReceiverMessageErrorPlan::ErrorStreamAndFinish
|
|
);
|
|
assert_eq!(active.plan_receiver_pull(), ReceiverPullPlan::PostPull);
|
|
assert_eq!(
|
|
active.plan_receiver_cancel(),
|
|
ReceiverCancelPlan::PostErrorAndFinish
|
|
);
|
|
let inactive = snapshot(false, false, 0, false);
|
|
assert_eq!(
|
|
inactive.plan_receiver_message_error(),
|
|
ReceiverMessageErrorPlan::Ignore
|
|
);
|
|
assert_eq!(inactive.plan_receiver_pull(), ReceiverPullPlan::Ignore);
|
|
assert_eq!(inactive.plan_receiver_cancel(), ReceiverCancelPlan::Ignore);
|
|
}
|
|
}
|