diff --git a/api/src/server.rs b/api/src/server.rs index 2af1588a6..d66a84941 100644 --- a/api/src/server.rs +++ b/api/src/server.rs @@ -38,13 +38,13 @@ pub trait Recon: Clone + Send + Sync { type Hash: AssociativeHash + std::fmt::Debug + Serialize + for<'de> Deserialize<'de>; async fn insert(&self, key: Self::Key, value: Option>) -> Result<()>; - async fn range( + async fn range_with_values( &self, start: Self::Key, end: Self::Key, offset: usize, limit: usize, - ) -> Result>; + ) -> Result)>>; async fn value_for_key(&self, key: Self::Key) -> Result>>; } @@ -63,16 +63,18 @@ where Ok(()) } - async fn range( + async fn range_with_values( &self, start: Self::Key, end: Self::Key, offset: usize, limit: usize, - ) -> Result> { - Ok(recon::Client::range(self, start, end, offset, limit) - .await? - .collect()) + ) -> Result)>> { + Ok( + recon::Client::range_with_values(self, start, end, offset, limit) + .await? + .collect(), + ) } async fn value_for_key(&self, key: Self::Key) -> Result>> { recon::Client::value_for_key(self, key).await @@ -132,13 +134,12 @@ where Ok(resp) } - #[instrument(skip(self, _context), ret(level = Level::DEBUG), err(level = Level::ERROR))] + #[instrument(skip(self, _context, event), fields(event.id = event.event_id, event.data.len = event.event_data.len()), ret(level = Level::DEBUG), err(level = Level::ERROR))] async fn events_post( &self, event: Event, _context: &C, ) -> Result { - debug!(event_id = event.event_id, "events_post"); let event_id = decode_event_id(&event.event_id)?; let event_data = decode_event_data(&event.event_data)?; self.model @@ -233,33 +234,24 @@ where .with_not_after(0) .build(); self.interest - .insert(interest, None) + // We must store a value for the interest otherwise Recon will try forever to + // synchronize the value. + // In the case of interests an empty value is sufficient. + .insert(interest, Some(vec![])) .await .map_err(|err| ApiError(format!("failed to update interest: {err}")))?; - let mut events = Vec::new(); - for id in self + let events = self .model - .range(start, stop, offset, limit) + .range_with_values(start, stop, offset, limit) .await .map_err(|err| ApiError(format!("failed to get keys: {err}")))? .into_iter() - { - let event_data = self - .model - .value_for_key(id.clone()) - .await - .map_err(|err| ApiError(format!("failed to get event data: {err}")))?; - events.push(Event { + .map(|(id, data)| Event { event_id: multibase::encode(multibase::Base::Base16Lower, id.as_bytes()), - event_data: multibase::encode( - multibase::Base::Base64, - // Use the empty bytes for keys with no value. - // This way we are explicit there is no value rather that its just missing. - &event_data.unwrap_or_default(), - ), - }); - } + event_data: multibase::encode(multibase::Base::Base64, data), + }) + .collect(); Ok(SubscribeSortKeySortValueGetResponse::Success(events)) } } @@ -291,13 +283,13 @@ mod tests { mock! { pub ReconInterestTest { fn insert(&self, key: Interest, value: Option>) -> Result<()>; - fn range( + fn range_with_values( &self, start: Interest, end: Interest, offset: usize, limit: usize, - ) -> Result>; + ) -> Result)>>; } impl Clone for ReconInterestTest { @@ -312,14 +304,14 @@ mod tests { async fn insert(&self, key: Self::Key, value: Option>) -> Result<()> { self.insert(key, value) } - async fn range( + async fn range_with_values( &self, start: Self::Key, end: Self::Key, offset: usize, limit: usize, - ) -> Result> { - self.range(start, end, offset, limit) + ) -> Result)>> { + self.range_with_values(start, end, offset, limit) } async fn value_for_key(&self, _key: Self::Key) -> Result>> { Ok(None) @@ -329,13 +321,13 @@ mod tests { mock! { pub ReconModelTest { fn insert(&self, key: EventId, value: Option>) -> Result<()>; - fn range( + fn range_with_values( &self, start: EventId, end: EventId, offset: usize, limit: usize, - ) -> Result>; + ) -> Result)>>; } impl Clone for ReconModelTest { fn clone(&self) -> Self; @@ -349,14 +341,14 @@ mod tests { async fn insert(&self, key: Self::Key, value: Option>) -> Result<()> { self.insert(key, value) } - async fn range( + async fn range_with_values( &self, start: Self::Key, end: Self::Key, offset: usize, limit: usize, - ) -> Result> { - self.range(start, end, offset, limit) + ) -> Result)>> { + self.range_with_values(start, end, offset, limit) } async fn value_for_key(&self, _key: Self::Key) -> Result>> { Ok(None) @@ -444,9 +436,10 @@ mod tests { .unwrap(), ) .build(); + let event_data = b"hello world"; let event = models::Event { event_id: multibase::encode(multibase::Base::Base16Lower, event_id.as_slice()), - event_data: multibase::encode(multibase::Base::Base64, b""), + event_data: multibase::encode(multibase::Base::Base64, event_data), }; // Setup mock expectations let mut mock_interest = MockReconInterestTest::new(); @@ -461,13 +454,13 @@ mod tests { .with_not_after(0) .build(), ), - predicate::eq(None), + predicate::eq(Some(vec![])), ) .times(1) .returning(|_, _| Ok(())); let mut mock_model = MockReconModelTest::new(); mock_model - .expect_range() + .expect_range_with_values() .with( predicate::eq(start), predicate::eq(end), @@ -475,7 +468,7 @@ mod tests { predicate::eq(usize::MAX), ) .times(1) - .returning(move |_, _, _, _| Ok(vec![event_id.clone()])); + .returning(move |_, _, _, _| Ok(vec![(event_id.clone(), event_data.into())])); let server = Server::new(peer_id, network, mock_interest, mock_model); let resp = server .subscribe_sort_key_sort_value_get( @@ -527,13 +520,13 @@ mod tests { .with_not_after(0) .build(), ), - predicate::eq(None), + predicate::eq(Some(vec![])), ) .times(1) .returning(|_, _| Ok(())); let mut mock_model = MockReconModelTest::new(); mock_model - .expect_range() + .expect_range_with_values() .with( predicate::eq(start), predicate::eq(end), @@ -593,13 +586,13 @@ mod tests { .with_not_after(0) .build(), ), - predicate::eq(None), + predicate::eq(Some(vec![])), ) .times(1) .returning(|_, _| Ok(())); let mut mock_model = MockReconModelTest::new(); mock_model - .expect_range() + .expect_range_with_values() .with( predicate::eq(start), predicate::eq(end), @@ -659,13 +652,13 @@ mod tests { .with_not_after(0) .build(), ), - predicate::eq(None), + predicate::eq(Some(vec![])), ) .times(1) .returning(|_, _| Ok(())); let mut mock_model = MockReconModelTest::new(); mock_model - .expect_range() + .expect_range_with_values() .with( predicate::eq(start), predicate::eq(end), diff --git a/core/src/event_id.rs b/core/src/event_id.rs index 6ba6d4239..c9b323679 100644 --- a/core/src/event_id.rs +++ b/core/src/event_id.rs @@ -23,6 +23,7 @@ use serde::{Deserialize, Serialize}; use std::{ cmp::{Eq, Ord}, fmt::Display, + ops::Range, }; use unsigned_varint::{decode::u64 as de_varint, encode::u64 as varint}; @@ -84,8 +85,34 @@ impl EventId { self.0.as_slice() } - /// try to parse a CID out of a CIP-124 EventID + /// Report the network id of the EventId + pub fn network_id(&self) -> Option { + self.as_parts().map(|parts| parts.network_id) + } + /// Report the separator bytes of the EventId + pub fn separator(&self) -> Option<&[u8]> { + self.as_parts().map(|parts| parts.separator) + } + /// Report the controller bytes of the EventId + pub fn controller(&self) -> Option<&[u8]> { + self.as_parts().map(|parts| parts.controller) + } + /// Report the stream_id bytes of the EventId + pub fn stream_id(&self) -> Option<&[u8]> { + self.as_parts().map(|parts| parts.stream_id) + } + /// Report the event height of the EventId + pub fn event_height(&self) -> Option { + self.as_parts().map(|parts| parts.height) + } + + /// Report the event CID of the EventId pub fn cid(&self) -> Option { + let parts = self.as_parts()?; + Cid::read_bytes(parts.cid).ok() + } + + fn as_parts(&self) -> Option> { let (streamid, remainder) = de_varint(&self.0).unwrap_or_default(); if streamid != 0xce { return None; // not a streamid @@ -96,49 +123,105 @@ impl EventId { return None; // not a CIP-124 EventID }; - let (_network_id, mut remainder) = de_varint(remainder).unwrap_or_default(); - - // strip separator [u8; 8] controller [u8; 8] StreamID [u8; 4] - remainder = &remainder[(8 + 8 + 4)..]; - - // height cbor unsigned integer - if remainder[0] <= 23 { - // 0 - 23 - remainder = &remainder[1..] - } else if remainder[0] == 24 { - // u8 - remainder = &remainder[2..] - } else if remainder[0] == 25 { - // u16 - remainder = &remainder[3..] - } else if remainder[0] == 26 { - // u32 - remainder = &remainder[5..] - } else if remainder[0] == 27 { - // u64 - remainder = &remainder[9..] - } else { - // not a cbor unsigned int - return None; - }; - match Cid::read_bytes(remainder) { - Ok(v) => Some(v), - Err(_) => None, // not a CID - } + let (network_id, remainder) = de_varint(remainder).unwrap_or_default(); + + // separator [u8; 8] + const SEPARATOR_RANGE: Range = 0..8; + // controller [u8; 8] + const CONTROLLER_RANGE: Range = 8..(8 + 8); + // StreamID [u8; 4] + const STREAM_ID_RANGE: Range = (8 + 8)..(8 + 8 + 4); + + let separator = &remainder[SEPARATOR_RANGE]; + let controller = &remainder[CONTROLLER_RANGE]; + let stream_id = &remainder[STREAM_ID_RANGE]; + + let (height, cid) = cbor_uint_decode(&remainder[STREAM_ID_RANGE.end..]); + + height.map(|height| EventIdParts { + network_id, + separator, + controller, + stream_id, + height, + cid, + }) } } +// Decode a cbor unsigned integer and return the remaining bytes from the buffer +fn cbor_uint_decode(data: &[u8]) -> (Option, &[u8]) { + // From the spec: https://datatracker.ietf.org/doc/html/rfc7049#section-2.1 + // + // Major type 0: an unsigned integer. The 5-bit additional information + // is either the integer itself (for additional information values 0 + // through 23) or the length of additional data. Additional + // information 24 means the value is represented in an additional + // uint8_t, 25 means a uint16_t, 26 means a uint32_t, and 27 means a + // uint64_t. For example, the integer 10 is denoted as the one byte + // 0b000_01010 (major type 0, additional information 10). The + // integer 500 would be 0b000_11001 (major type 0, additional + // information 25) followed by the two bytes 0x01f4, which is 500 in + // decimal. + + match data[0] { + // 0 - 23 + x if x <= 23 => (Some(x as u64), &data[1..]), + // u8 + 24 => ( + data[1..2] + .try_into() + .ok() + .map(|h| u8::from_be_bytes(h) as u64), + &data[2..], + ), + // u16 + 25 => ( + data[1..3] + .try_into() + .ok() + .map(|h| u16::from_be_bytes(h) as u64), + &data[3..], + ), + // u32 + 26 => ( + data[1..5] + .try_into() + .ok() + .map(|h| u32::from_be_bytes(h) as u64), + &data[5..], + ), + // u64 + 27 => ( + data[1..9].try_into().ok().map(u64::from_be_bytes), + &data[9..], + ), + // not a cbor unsigned int + _ => (None, data), + } +} + +struct EventIdParts<'a> { + network_id: u64, + separator: &'a [u8], + controller: &'a [u8], + stream_id: &'a [u8], + height: u64, + cid: &'a [u8], +} impl std::fmt::Debug for EventId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { if f.alternate() { - f.debug_tuple("EventId").field(&self.0).finish() + f.debug_struct("EventId") + .field("network_id", &self.network_id()) + .field("separator", &self.separator().map(hex::encode)) + .field("controller", &self.controller().map(hex::encode)) + .field("stream_id", &self.stream_id().map(hex::encode)) + .field("event_height", &self.event_height()) + .field("cid", &self.cid().map(|cid| cid.to_string())) + .finish() } else { - let bytes = self.as_slice(); - if bytes.len() < 6 { - write!(f, "{}", hex::encode_upper(bytes)) - } else { - write!(f, "{}", hex::encode_upper(&bytes[bytes.len() - 6..])) - } + write!(f, "{}", hex::encode_upper(self.as_slice())) } } } @@ -394,6 +477,7 @@ mod tests { use cid::multibase::{self, Base}; use expect_test::expect; use std::str::FromStr; + use test_log::test; #[test] fn blessing() { @@ -503,72 +587,26 @@ mod tests { let cid = received.cid(); println!("{:?}, {:?}", &received, &cid); expect![[r#" - EventId( - [ - 206, - 1, - 5, + EventId { + network_id: Some( 0, - 126, - 113, - 14, - 33, - 127, - 160, - 226, - 89, - 69, - 204, - 124, - 7, - 47, - 247, - 41, - 234, - 104, - 59, - 117, - 23, - 24, + ), + separator: Some( + "7e710e217fa0e259", + ), + controller: Some( + "45cc7c072ff729ea", + ), + stream_id: Some( + "683b7517", + ), + event_height: Some( 255, - 1, - 113, - 18, - 32, - 244, - 239, - 126, - 194, - 8, - 148, - 77, - 37, - 112, - 37, - 64, - 139, - 182, - 71, - 148, - 158, - 107, - 114, - 147, - 5, - 32, - 188, - 128, - 243, - 77, - 139, - 251, - 175, - 210, - 100, - 61, - 134, - ], - ) + ), + cid: Some( + "bafyreihu557meceujusxajkaro3epfe6nnzjgbjaxsapgtml7ox5ezb5qy", + ), + } "#]] .assert_debug_eq(&received); @@ -625,4 +663,81 @@ mod tests { .build_fencepost(); assert_eq!(None, event_id.cid()); } + #[test] + fn debug() { + let sort_key = "model".to_string(); + let separator = "kh4q0ozorrgaq2mezktnrmdwleo1d".to_string(); // cspell:disable-line + let controller = "did:key:z6MkgSV3tAuw7gUWqKCUY7ae6uWNxqYgdwPhUJbJhF9EFXm9".to_string(); + let init = + Cid::from_str("bagcqceraplay4erv6l32qrki522uhiz7rf46xccwniw7ypmvs3cvu2b3oulq").unwrap(); // cspell:disable-line + let event_height = 255; // so we get 2 bytes b'\x18\xff' + let event_cid = + Cid::from_str("bafyreihu557meceujusxajkaro3epfe6nnzjgbjaxsapgtml7ox5ezb5qy").unwrap(); // cspell:disable-line + + let event_id = EventId::new( + &Network::TestnetClay, + &sort_key, + &separator, + &controller, + &init, + event_height, + &event_cid, + ); + expect![[r#" + EventId { + network_id: Some( + 1, + ), + separator: Some( + "7e710e217fa0e259", + ), + controller: Some( + "45cc7c072ff729ea", + ), + stream_id: Some( + "683b7517", + ), + event_height: Some( + 255, + ), + cid: Some( + "bafyreihu557meceujusxajkaro3epfe6nnzjgbjaxsapgtml7ox5ezb5qy", + ), + } + "#]] + .assert_debug_eq(&event_id); + } + #[test] + fn event_height() { + let sort_key = "model".to_string(); + let separator = "kh4q0ozorrgaq2mezktnrmdwleo1d".to_string(); // cspell:disable-line + let controller = "did:key:z6MkgSV3tAuw7gUWqKCUY7ae6uWNxqYgdwPhUJbJhF9EFXm9".to_string(); + let init = + Cid::from_str("bagcqceraplay4erv6l32qrki522uhiz7rf46xccwniw7ypmvs3cvu2b3oulq").unwrap(); // cspell:disable-line + let event_cid = + Cid::from_str("bafyreihu557meceujusxajkaro3epfe6nnzjgbjaxsapgtml7ox5ezb5qy").unwrap(); // cspell:disable-line + + for event_height in [ + 1, + 18, + 255, + 256, + 65535, + 65536, + 4294967295, + 4294967296, + 18446744073709551615, + ] { + let event_id = EventId::new( + &Network::TestnetClay, + &sort_key, + &separator, + &controller, + &init, + event_height, + &event_cid, + ); + assert_eq!(Some(event_height), event_id.event_height()); + } + } } diff --git a/core/src/interest.rs b/core/src/interest.rs index 424fce04b..882ea7f43 100644 --- a/core/src/interest.rs +++ b/core/src/interest.rs @@ -99,12 +99,7 @@ impl std::fmt::Debug for Interest { .field("not_after", &self.not_after().map_err(|_| std::fmt::Error)?) .finish() } else { - let bytes = self.as_slice(); - if bytes.len() < 6 { - write!(f, "{}", hex::encode_upper(bytes)) - } else { - write!(f, "{}", hex::encode_upper(&bytes[bytes.len() - 6..])) - } + write!(f, "{}", hex::encode_upper(self.as_slice())) } } } diff --git a/p2p/src/node.rs b/p2p/src/node.rs index 06c08ebf3..31ba57048 100644 --- a/p2p/src/node.rs +++ b/p2p/src/node.rs @@ -1268,7 +1268,12 @@ mod tests { async fn value_for_key(&self, _key: Self::Key) -> Result>> { Ok(None) } - + async fn keys_with_missing_values( + &self, + _range: RangeOpen, + ) -> Result> { + unreachable!() + } async fn interests(&self) -> Result>> { unreachable!() } diff --git a/recon/src/client.rs b/recon/src/client.rs index d287203f0..0b3fdc661 100644 --- a/recon/src/client.rs +++ b/recon/src/client.rs @@ -64,6 +64,26 @@ where .await?; rx.await? } + /// Sends a range request to the server and awaits the response. + pub async fn range_with_values( + &self, + left_fencepost: K, + right_fencepost: K, + offset: usize, + limit: usize, + ) -> Result)> + Send + '_>> { + let (ret, rx) = oneshot::channel(); + self.sender + .send(Request::RangeWithValues { + left_fencepost, + right_fencepost, + offset, + limit, + ret, + }) + .await?; + rx.await? + } /// Sends a full_range request to the server and awaits the response. pub async fn full_range(&self) -> Result + Send + '_>> { @@ -79,6 +99,14 @@ where rx.await? } + /// Report all keys in the range that are missing a value + pub async fn keys_with_missing_values(&self, range: RangeOpen) -> Result> { + let (ret, rx) = oneshot::channel(); + self.sender + .send(Request::KeysWithMissingValues { range, ret }) + .await?; + rx.await? + } /// Report the local nodes interests. pub async fn interests(&self) -> Result>> { let (ret, rx) = oneshot::channel(); @@ -135,6 +163,13 @@ enum Request { limit: usize, ret: oneshot::Sender + Send>>>, }, + RangeWithValues { + left_fencepost: K, + right_fencepost: K, + offset: usize, + limit: usize, + ret: oneshot::Sender>, + }, FullRange { ret: oneshot::Sender + Send>>>, }, @@ -142,6 +177,10 @@ enum Request { key: K, ret: oneshot::Sender>>>, }, + KeysWithMissingValues { + range: RangeOpen, + ret: oneshot::Sender>>, + }, Interests { ret: oneshot::Sender>>>, }, @@ -155,11 +194,12 @@ enum Request { }, ProcessRange { range: Range, - ret: oneshot::Sender>, + ret: oneshot::Sender>, }, } -type RangeResult = Result<(SyncState, Vec)>; +type RangeWithValuesResult = Result)> + Send>>; +type ProcessRangeResult = Result<(SyncState, Vec)>; /// Server that processed received Recon messages in a single task. #[derive(Debug)] @@ -235,6 +275,19 @@ where .await; send(ret, keys); } + Request::RangeWithValues { + left_fencepost, + right_fencepost, + offset, + limit, + ret, + } => { + let keys = self + .recon + .range_with_values(&left_fencepost, &right_fencepost, offset, limit) + .await; + send(ret, keys); + } Request::FullRange { ret } => { let keys = self.recon.full_range().await; send(ret, keys); @@ -243,6 +296,10 @@ where let value = self.recon.value_for_key(key).await; send(ret, value); } + Request::KeysWithMissingValues { range, ret } => { + let ok = self.recon.keys_with_missing_values(range).await; + send(ret, ok); + } Request::Interests { ret } => { let value = self.recon.interests().await; send(ret, value); diff --git a/recon/src/metrics.rs b/recon/src/metrics.rs index 5bfbfdc27..d82b16d1f 100644 --- a/recon/src/metrics.rs +++ b/recon/src/metrics.rs @@ -36,7 +36,7 @@ pub struct Metrics { protocol_range_dequeued_count: Counter, protocol_loop_count: Counter, - protocol_run_count: Counter, + protocol_run_duration: Histogram, } #[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)] @@ -192,9 +192,9 @@ impl Metrics { sub_registry ); register!( - protocol_run_count, - "Number times the protocol has run to completion", - Counter::default(), + protocol_run_duration, + "Duration of protocol runs to completion", + Histogram::new(exponential_buckets(0.005, 2.0, 20)), sub_registry ); @@ -211,7 +211,7 @@ impl Metrics { protocol_range_enqueued_count, protocol_range_dequeued_count, protocol_loop_count, - protocol_run_count, + protocol_run_duration, } } } @@ -323,9 +323,9 @@ impl Recorder for Metrics { self.protocol_loop_count.inc(); } } -pub(crate) struct ProtocolRun; +pub(crate) struct ProtocolRun(pub Duration); impl Recorder for Metrics { - fn record(&self, _event: &ProtocolRun) { - self.protocol_run_count.inc(); + fn record(&self, event: &ProtocolRun) { + self.protocol_run_duration.observe(event.0.as_secs_f64()); } } diff --git a/recon/src/protocol.rs b/recon/src/protocol.rs index f3eb58496..59362cfb2 100644 --- a/recon/src/protocol.rs +++ b/recon/src/protocol.rs @@ -21,6 +21,7 @@ use serde::{Deserialize, Serialize}; use tokio::{ select, sync::mpsc::{channel, Receiver, Sender}, + time::Instant, }; use tracing::{trace, Level}; @@ -35,6 +36,8 @@ use crate::{ // Number of want value requests to buffer. const WANT_VALUES_BUFFER: usize = 10000; +// Number of sync ranges to buffer. +const SYNC_RANGES_BUFFER: usize = 1000; // Number of message to buffer on the sink before flushing. const SINK_BUFFER_COUNT: usize = 100; // Limit to the number of pending range requests. @@ -62,10 +65,12 @@ where E: std::error::Error + Send + Sync + 'static, { let (tx_want_values, rx_want_values) = channel(WANT_VALUES_BUFFER); + let (tx_sync_ranges, rx_sync_ranges) = channel(SYNC_RANGES_BUFFER); let metrics = recon.metrics(); let protocol = Protocol::new( - Initiator::new(stream, recon, tx_want_values), + Initiator::new(stream, recon, tx_want_values, tx_sync_ranges), rx_want_values, + rx_sync_ranges, metrics, ); protocol.run().await?; @@ -82,10 +87,12 @@ where E: std::error::Error + Send + Sync + 'static, { let (tx_want_values, rx_want_values) = channel(WANT_VALUES_BUFFER); + let (tx_sync_ranges, rx_sync_ranges) = channel(SYNC_RANGES_BUFFER); let metrics = recon.metrics(); let protocol = Protocol::new( - Responder::new(stream, recon, tx_want_values), + Responder::new(stream, recon, tx_want_values, tx_sync_ranges), rx_want_values, + rx_sync_ranges, metrics, ); protocol.run().await?; @@ -151,10 +158,12 @@ struct Protocol { role: R, rx_want_values: Receiver, + rx_sync_ranges: Receiver>, listen_only_sent: bool, remote_done: bool, want_values_done: bool, + sync_ranges_done: bool, metrics: Metrics, } @@ -166,17 +175,25 @@ where R::Stream: Stream>, E: std::error::Error + Send + Sync + 'static, { - fn new(role: R, rx_want_values: Receiver, metrics: Metrics) -> Self { + fn new( + role: R, + rx_want_values: Receiver, + rx_sync_ranges: Receiver>, + metrics: Metrics, + ) -> Self { Self { role, rx_want_values, + rx_sync_ranges, listen_only_sent: false, remote_done: false, want_values_done: false, + sync_ranges_done: false, metrics, } } async fn run(mut self) -> Result<()> { + let start = Instant::now(); self.role .init() .await @@ -209,10 +226,22 @@ where self.want_values_done = true; } } + // Handle any synchronized ranges + range = self.rx_sync_ranges.recv(), if !self.sync_ranges_done => { + if let Some(range) = range { + self.metrics.record(&RangeDequeued); + self.role.handle_sync_range(range).await.context("handle sync range")?; + } else { + // We have drained all sync ranges, + // we can now close and drain want values. + self.rx_want_values.close(); + self.sync_ranges_done = true; + } + } } self.role.each().await?; - if self.want_values_done && self.role.is_done() { + if self.sync_ranges_done && self.want_values_done && self.role.is_done() { if !self.listen_only_sent { self.listen_only_sent = true; self.role @@ -232,7 +261,7 @@ where .context("finishing protocol loop")?; self.role.close().await.context("closing stream")?; - self.metrics.record(&ProtocolRun); + self.metrics.record(&ProtocolRun(start.elapsed())); Ok(()) } @@ -241,9 +270,9 @@ where RemoteStatus::Active => {} RemoteStatus::ListenOnly => { // Remote will no longer send ranges, therefore local will no longer enqueue any - // new wants. We close the channels to signal they will not grow and can be + // new ranges. We close the channel to signal it will not grow and can be // drained. - self.rx_want_values.close(); + self.rx_sync_ranges.close(); } RemoteStatus::Finished => { self.remote_done = true; @@ -285,6 +314,8 @@ trait Role { async fn send_listen_only(&mut self) -> Result<()>; // Feed a want value request to the remote. async fn feed_want_value(&mut self, key: Self::Key) -> Result<()>; + // Handle a range that has been synchronized + async fn handle_sync_range(&mut self, range: RangeOpen) -> Result<()>; } type InitiatorValueResponseFn = fn(ValueResponse) -> InitiatorMessage; @@ -311,7 +342,12 @@ where + Sink, Error = E>, E: std::error::Error + Send + Sync + 'static, { - fn new(stream: S, recon: R, tx_want_values: Sender) -> Self { + fn new( + stream: S, + recon: R, + tx_want_values: Sender, + tx_sync_ranges: Sender>, + ) -> Self { let metrics = recon.metrics(); let stream = SinkFlusher::new(stream, metrics.clone()); // Use a stack size large enough to handle the split factor of range requests. @@ -323,6 +359,7 @@ where recon, value_resp_fn: InitiatorMessage::ValueResponse, tx_want_values, + tx_sync_ranges, metrics: metrics.clone(), }, ranges_stack, @@ -335,9 +372,8 @@ where let (sync_state, new_keys) = self.common.recon.process_range(remote_range).await?; self.common.process_new_keys(&new_keys); match sync_state { - SyncState::Synchronized => { - // TODO: This is where we can append this range to a queue of synced ranges to - // later process and ensure we have all values for this range. + SyncState::Synchronized { range } => { + self.common.enqueue_sync_range(range.into()); } SyncState::RemoteMissing { range } => { self.common.process_remote_missing_range(&range).await?; @@ -492,6 +528,9 @@ where async fn send_listen_only(&mut self) -> Result<()> { self.common.stream.send(InitiatorMessage::ListenOnly).await } + async fn handle_sync_range(&mut self, range: RangeOpen) -> Result<()> { + self.common.handle_sync_range(range).await + } } type ResponderValueResponseFn = fn(ValueResponse) -> ResponderMessage; @@ -511,7 +550,12 @@ where + Sink, Error = E>, E: std::error::Error + Send + Sync + 'static, { - fn new(stream: S, recon: R, tx_want_values: Sender) -> Self { + fn new( + stream: S, + recon: R, + tx_want_values: Sender, + tx_sync_ranges: Sender>, + ) -> Self { let metrics = recon.metrics(); let stream = SinkFlusher::new(stream, metrics.clone()); @@ -521,18 +565,18 @@ where recon, value_resp_fn: ResponderMessage::ValueResponse, tx_want_values, + tx_sync_ranges, metrics, }, } } async fn process_range(&mut self, range: Range) -> Result<()> { - let (sync_state, new_keys) = self.common.recon.process_range(range.clone()).await?; + let (sync_state, new_keys) = self.common.recon.process_range(range).await?; self.common.process_new_keys(&new_keys); match sync_state { - SyncState::Synchronized => { - // TODO: This is where we can append this range to a queue of synced ranges to - // later process and ensure we have all values for this range. + SyncState::Synchronized { range } => { + self.common.enqueue_sync_range(range.clone().into()); // We are sync echo back the same range so that the remote learns we are in sync. self.common @@ -550,12 +594,10 @@ where .await?; } SyncState::Unsynchronized { ranges: splits } => { - trace!("unsynchronized sending response"); self.common .stream .send(ResponderMessage::RangeResponse(splits)) .await?; - trace!("unsynchronized sent response"); } } Ok(()) @@ -630,6 +672,9 @@ where async fn send_listen_only(&mut self) -> Result<()> { self.common.stream.send(ResponderMessage::ListenOnly).await } + async fn handle_sync_range(&mut self, range: RangeOpen) -> Result<()> { + self.common.handle_sync_range(range).await + } } // Common implments common behaviors to both [`Initiator`] and [`Responder`]. @@ -640,6 +685,7 @@ struct Common { value_resp_fn: V, tx_want_values: Sender, + tx_sync_ranges: Sender>, metrics: Metrics, } @@ -712,6 +758,18 @@ where .context("feeding value request")?; Ok(()) } + async fn handle_sync_range(&mut self, range: RangeOpen) -> Result<()> { + let keys = self.recon.keys_with_missing_values(range).await?; + self.process_new_keys(&keys); + Ok(()) + } + fn enqueue_sync_range(&mut self, range: RangeOpen) { + if self.tx_sync_ranges.try_send(range).is_err() { + self.metrics.record(&RangeEnqueueFailed); + } else { + self.metrics.record(&RangeEnqueued); + } + } } enum RemoteStatus { @@ -826,9 +884,13 @@ pub trait Recon: Clone + Send + Sync + 'static { Ok(self.len().await? == 0) } - /// retrieve a value associated with a recon key + /// Retrieve a value associated with a recon key async fn value_for_key(&self, key: Self::Key) -> Result>>; + /// Report all keys in the range that are missing a value + async fn keys_with_missing_values(&self, range: RangeOpen) + -> Result>; + /// Reports the interests of this recon instance async fn interests(&self) -> Result>>; @@ -889,7 +951,12 @@ where async fn value_for_key(&self, key: Self::Key) -> Result>> { Client::value_for_key(self, key).await } - + async fn keys_with_missing_values( + &self, + range: RangeOpen, + ) -> Result> { + Client::keys_with_missing_values(self, range).await + } async fn interests(&self) -> Result>> { Client::interests(self).await } diff --git a/recon/src/recon.rs b/recon/src/recon.rs index c48e1b568..b49d994c1 100644 --- a/recon/src/recon.rs +++ b/recon/src/recon.rs @@ -128,7 +128,7 @@ where let calculated_hash = self.store.hash_range(&range.first, &range.last).await?; if calculated_hash == range.hash { - Ok((SyncState::Synchronized, new_keys)) + Ok((SyncState::Synchronized { range }, new_keys)) } else if calculated_hash.hash.is_zero() { Ok(( SyncState::Unsynchronized { @@ -258,6 +258,10 @@ where Ok(new) } + /// Report all keys in the range that are missing a value + pub async fn keys_with_missing_values(&mut self, range: RangeOpen) -> Result> { + self.store.keys_with_missing_values(range).await + } /// Insert many keys into the key space. Includes an optional value for each key. /// Returns an array with a boolean for each key indicating if the key was new. @@ -305,6 +309,22 @@ where .await } + /// Return all keys and values in the range between left_fencepost and right_fencepost. + /// Both range bounds are exclusive. + /// + /// Offset and limit values are applied within the range of keys. + pub async fn range_with_values( + &mut self, + left_fencepost: &K, + right_fencepost: &K, + offset: usize, + limit: usize, + ) -> Result)> + Send + 'static>> { + self.store + .range_with_values(left_fencepost, right_fencepost, offset, limit) + .await + } + /// Return all keys. pub async fn full_range(&mut self) -> Result + Send + 'static>> { self.store.full_range().await @@ -448,6 +468,18 @@ pub trait Store: std::fmt::Debug { limit: usize, ) -> Result + Send + 'static>>; + /// Return all keys and values in the range between left_fencepost and right_fencepost. + /// Both range bounds are exclusive. + /// + /// Offset and limit values are applied within the range of keys. + async fn range_with_values( + &mut self, + left_fencepost: &Self::Key, + right_fencepost: &Self::Key, + offset: usize, + limit: usize, + ) -> Result)> + Send + 'static>>; + /// Return all keys. async fn full_range(&mut self) -> Result + Send + 'static>> { self.range( @@ -549,6 +581,12 @@ pub trait Store: std::fmt::Debug { /// Ok(None) if not stored, and /// Err(e) if retrieving failed. async fn value_for_key(&mut self, key: &Self::Key) -> Result>>; + + /// Report all keys in the range that are missing a value. + async fn keys_with_missing_values( + &mut self, + range: RangeOpen, + ) -> Result>; } /// Represents a key that can be reconciled via Recon. @@ -749,11 +787,23 @@ pub struct Range { pub last: K, } +impl From> for RangeOpen { + fn from(value: Range) -> Self { + Self { + start: value.first, + end: value.last, + } + } +} + /// Enumerates the possible synchronization states between local and remote peers. #[derive(Debug)] pub enum SyncState { /// The local is synchronized with the remote. - Synchronized, + Synchronized { + /// The range and hash of the synchronized range + range: Range, + }, /// The remote range is missing all data in the range. RemoteMissing { /// The range and hash of the local data the remote is missing. diff --git a/recon/src/recon/btreestore.rs b/recon/src/recon/btreestore.rs index 14430cf4e..08e1885dd 100644 --- a/recon/src/recon/btreestore.rs +++ b/recon/src/recon/btreestore.rs @@ -1,5 +1,6 @@ use anyhow::Result; use async_trait::async_trait; +use ceramic_core::RangeOpen; use std::{collections::BTreeMap, ops::Bound}; use crate::recon::{AssociativeHash, Key, MaybeHashedKey, ReconItem, Store}; @@ -37,7 +38,7 @@ where H: AssociativeHash, { /// make a new recon from a set of keys and values - pub fn from_set(s: BTreeMap>) -> Self { + pub fn from_set(s: BTreeMap>>) -> Self { let mut r = Self { keys: Default::default(), values: Default::default(), @@ -45,7 +46,9 @@ where for (key, value) in s { let hash = H::digest(&key); r.keys.insert(key.clone(), hash); - r.values.insert(key, value); + if let Some(value) = value { + r.values.insert(key, value); + } } r } @@ -104,6 +107,34 @@ where .collect(); Ok(Box::new(keys.into_iter())) } + /// Return all keys and values in the range between left_fencepost and right_fencepost. + /// Both range bounds are exclusive. + /// + /// Offset and limit values are applied within the range of keys. + pub fn range_with_values( + &self, + left_fencepost: &K, + right_fencepost: &K, + offset: usize, + limit: usize, + ) -> Result)> + Send + 'static>> { + let range = ( + Bound::Excluded(left_fencepost), + Bound::Excluded(right_fencepost), + ); + let keys: Vec<(K, Vec)> = self + .keys + .range(range) + .skip(offset) + .take(limit) + .filter_map(|(key, _hash)| { + self.values + .get(key) + .map(|value| (key.clone(), value.clone())) + }) + .collect(); + Ok(Box::new(keys.into_iter())) + } } #[async_trait] @@ -163,6 +194,15 @@ where // and we delegate to its implementation here. BTreeStore::range(self, left_fencepost, right_fencepost, offset, limit) } + async fn range_with_values( + &mut self, + left_fencepost: &Self::Key, + right_fencepost: &Self::Key, + offset: usize, + limit: usize, + ) -> Result)> + Send + 'static>> { + BTreeStore::range_with_values(self, left_fencepost, right_fencepost, offset, limit) + } async fn last( &mut self, @@ -206,4 +246,14 @@ where async fn value_for_key(&mut self, key: &Self::Key) -> Result>> { Ok(self.values.get(key).cloned()) } + async fn keys_with_missing_values( + &mut self, + range: RangeOpen, + ) -> Result> { + Ok(self + .keys + .range(range) + .filter_map(|(key, _hash)| (!self.values.contains_key(key)).then(|| key.clone())) + .collect()) + } } diff --git a/recon/src/recon/parser.lalrpop b/recon/src/recon/parser.lalrpop index eb9c5c13e..cfb2379a2 100644 --- a/recon/src/recon/parser.lalrpop +++ b/recon/src/recon/parser.lalrpop @@ -29,13 +29,13 @@ Interest: RangeOpen = { "(" "," ")" => (start, end).into() }; -State: BTreeMap = { +State: BTreeMap> = { "[" "]" => <> }; -StateInner: BTreeMap = { +StateInner: BTreeMap> = { )*> => { - let mut state = BTreeMap::::new(); + let mut state = BTreeMap::>::new(); state.extend(rest.into_iter()); if let Some(first) = first { state.insert(first.0, first.1); @@ -44,8 +44,9 @@ StateInner: BTreeMap = { } }; -KeyValue: (AlphaNumBytes, AlphaNumBytes) = { - ":" => (key.clone(), value), +KeyValue: (AlphaNumBytes, Option) = { + ":" => (key.clone(), Some(value)), + ":" "∅" => (key.clone(), None), } Word : AlphaNumBytes = { diff --git a/recon/src/recon/sqlitestore.rs b/recon/src/recon/sqlitestore.rs index d3098366b..d3e798c4a 100644 --- a/recon/src/recon/sqlitestore.rs +++ b/recon/src/recon/sqlitestore.rs @@ -4,7 +4,7 @@ use super::{HashCount, InsertResult, ReconItem}; use crate::{AssociativeHash, Key, Store}; use anyhow::Result; use async_trait::async_trait; -use ceramic_core::{DbTx, SqlitePool}; +use ceramic_core::{DbTx, RangeOpen, SqlitePool}; use sqlx::Row; use std::marker::PhantomData; use std::result::Result::Ok; @@ -49,7 +49,7 @@ where { /// Initialize the recon table. async fn create_table_if_not_exists(&mut self) -> Result<()> { - // Do we want to remove CID and block_retrieved from the table? + // Do we want to remove CID from the table? const CREATE_RECON_TABLE: &str = "CREATE TABLE IF NOT EXISTS recon ( sort_key TEXT, -- the field in the event header to sort by e.g. model key BLOB, -- network_id sort_value controller StreamID height event_cid @@ -62,19 +62,25 @@ where ahash_6 INTEGER, ahash_7 INTEGER, CID TEXT, - block_retrieved BOOL, -- indicates if we still want the block + value_retrieved BOOL, -- indicates if we still want the value PRIMARY KEY(sort_key, key) )"; + const CREATE_VALUE_RETRIEVED_INDEX: &str = + "CREATE INDEX IF NOT EXISTS idx_recon_value_retrieved + ON recon (sort_key, key, value_retrieved)"; const CREATE_RECON_VALUE_TABLE: &str = "CREATE TABLE IF NOT EXISTS recon_value ( - sort_key TEXT, - key BLOB, - value BLOB, + sort_key TEXT, + key BLOB, + value BLOB, PRIMARY KEY(sort_key, key) )"; let mut tx = self.pool.tx().await?; sqlx::query(CREATE_RECON_TABLE).execute(&mut *tx).await?; + sqlx::query(CREATE_VALUE_RETRIEVED_INDEX) + .execute(&mut *tx) + .await?; sqlx::query(CREATE_RECON_VALUE_TABLE) .execute(&mut *tx) .await?; @@ -91,45 +97,64 @@ where // we insert the value first as it's possible we already have the key and can skip that step // as it happens in a transaction, we'll roll back the value insert if the key insert fails and try again if let Some(val) = item.value { - if self.insert_value_int(item.key, val, conn).await? { + // Update the value_retrieved flag, and report if the key already exists. + let key_exists = self.update_value_retrieved_int(item.key, conn).await?; + self.insert_value_int(item.key, val, conn).await?; + if key_exists { return Ok((false, true)); } } - let new_key = self.insert_key_int(item.key, conn).await?; + let new_key = self + .insert_key_int(item.key, item.value.is_some(), conn) + .await?; Ok((new_key, item.value.is_some())) } + // set value_retrieved to true and return if the key already exists + async fn update_value_retrieved_int(&mut self, key: &K, conn: &mut DbTx<'_>) -> Result { + let update = + sqlx::query("UPDATE recon SET value_retrieved = true WHERE sort_key = ? AND key = ?"); + let resp = update + .bind(&self.sort_key) + .bind(key.as_bytes()) + .execute(&mut **conn) + .await?; + let rows_affected = resp.rows_affected(); + debug_assert!(rows_affected <= 1); + Ok(rows_affected == 1) + } + /// returns true if the key already exists in the recon table - async fn insert_value_int(&mut self, key: &K, val: &[u8], conn: &mut DbTx<'_>) -> Result { + async fn insert_value_int(&mut self, key: &K, val: &[u8], conn: &mut DbTx<'_>) -> Result<()> { let value_insert = sqlx::query( - r#"INSERT INTO recon_value (value, sort_key, key) - VALUES (?, ?, ?) - ON CONFLICT (sort_key, key) DO UPDATE - SET value=excluded.value - RETURNING - EXISTS(select 1 from recon where sort_key=? and key=?)"#, + r#"INSERT INTO recon_value (value, sort_key, key) + VALUES (?, ?, ?) + ON CONFLICT (sort_key, key) DO UPDATE + SET value=excluded.value"#, ); - let resp = value_insert + value_insert .bind(val) .bind(&self.sort_key) .bind(key.as_bytes()) - .bind(&self.sort_key) - .bind(key.as_bytes()) - .fetch_one(&mut **conn) + .execute(&mut **conn) .await?; - let v = resp.get::<'_, bool, _>(0); - Ok(v) + Ok(()) } - async fn insert_key_int(&mut self, key: &K, conn: &mut DbTx<'_>) -> Result { + async fn insert_key_int( + &mut self, + key: &K, + has_value: bool, + conn: &mut DbTx<'_>, + ) -> Result { let key_insert = sqlx::query( "INSERT INTO recon ( sort_key, key, ahash_0, ahash_1, ahash_2, ahash_3, ahash_4, ahash_5, ahash_6, ahash_7, - block_retrieved + value_retrieved ) VALUES ( ?, ?, ?, ?, ?, ?, @@ -150,7 +175,7 @@ where .bind(hash.as_u32s()[5]) .bind(hash.as_u32s()[6]) .bind(hash.as_u32s()[7]) - .bind(false) + .bind(has_value) .execute(&mut **conn) .await; match resp { @@ -298,6 +323,46 @@ where K::from(bytes) }))) } + #[instrument(skip(self))] + async fn range_with_values( + &mut self, + left_fencepost: &Self::Key, + right_fencepost: &Self::Key, + offset: usize, + limit: usize, + ) -> Result)> + Send + 'static>> { + let query = sqlx::query( + " + SELECT + key, value + FROM + recon_value + WHERE + sort_key = ? AND + key > ? AND key < ? + AND value IS NOT NULL + ORDER BY + key ASC + LIMIT + ? + OFFSET + ?; + ", + ); + let rows = query + .bind(&self.sort_key) + .bind(left_fencepost.as_bytes()) + .bind(right_fencepost.as_bytes()) + .bind(limit as i64) + .bind(offset as i64) + .fetch_all(self.pool.reader()) + .await?; + Ok(Box::new(rows.into_iter().map(|row| { + let key: Vec = row.get(0); + let value: Vec = row.get(1); + (K::from(key), value) + }))) + } /// Return the number of keys within the range. #[instrument(skip(self))] async fn count( @@ -451,6 +516,34 @@ where .await?; Ok(row.map(|row| row.get(0))) } + + #[instrument(skip(self))] + async fn keys_with_missing_values( + &mut self, + range: RangeOpen, + ) -> Result> { + if range.start >= range.end { + return Ok(vec![]); + }; + let query = sqlx::query( + " + SELECT key + FROM recon + WHERE + sort_key=? + AND key > ? + AND key < ? + AND value_retrieved = false + ;", + ); + let row = query + .bind(&self.sort_key) + .bind(range.start.as_bytes()) + .bind(range.end.as_bytes()) + .fetch_all(self.pool.reader()) + .await?; + Ok(row.into_iter().map(|row| K::from(row.get(0))).collect()) + } } #[cfg(test)] @@ -630,4 +723,36 @@ mod tests { let value = store.value_for_key(&key).await.unwrap().unwrap(); expect![[r#"776f726c64"#]].assert_eq(hex::encode(&value).as_str()); } + #[test(tokio::test)] + async fn test_keys_with_missing_value() { + let mut store = new_store().await; + let key = AlphaNumBytes::from("hello"); + store.insert(ReconItem::new(&key, None)).await.unwrap(); + let missing_keys = store + .keys_with_missing_values( + (AlphaNumBytes::min_value(), AlphaNumBytes::max_value()).into(), + ) + .await + .unwrap(); + expect![[r#" + [ + Bytes( + "hello", + ), + ] + "#]] + .assert_debug_eq(&missing_keys); + + store.insert(ReconItem::new(&key, Some(&[]))).await.unwrap(); + let missing_keys = store + .keys_with_missing_values( + (AlphaNumBytes::min_value(), AlphaNumBytes::max_value()).into(), + ) + .await + .unwrap(); + expect![[r#" + [] + "#]] + .assert_debug_eq(&missing_keys); + } } diff --git a/recon/src/recon/store_metrics.rs b/recon/src/recon/store_metrics.rs index be96b2467..ac6d738e6 100644 --- a/recon/src/recon/store_metrics.rs +++ b/recon/src/recon/store_metrics.rs @@ -1,5 +1,6 @@ use anyhow::Result; use async_trait::async_trait; +use ceramic_core::RangeOpen; use ceramic_metrics::Recorder; use futures::Future; use tokio::time::Instant; @@ -87,6 +88,21 @@ where ) .await } + async fn range_with_values( + &mut self, + left_fencepost: &Self::Key, + right_fencepost: &Self::Key, + offset: usize, + limit: usize, + ) -> Result)> + Send + 'static>> { + StoreMetricsMiddleware::::record( + self.metrics.clone(), + "range_with_values", + self.store + .range_with_values(left_fencepost, right_fencepost, offset, limit), + ) + .await + } async fn full_range(&mut self) -> Result + Send + 'static>> { StoreMetricsMiddleware::::record( @@ -176,4 +192,15 @@ where ) .await } + async fn keys_with_missing_values( + &mut self, + range: RangeOpen, + ) -> Result> { + StoreMetricsMiddleware::::record( + self.metrics.clone(), + "keys_with_missing_values", + self.store.keys_with_missing_values(range), + ) + .await + } } diff --git a/recon/src/recon/tests.rs b/recon/src/recon/tests.rs index db5ad52e0..486def856 100644 --- a/recon/src/recon/tests.rs +++ b/recon/src/recon/tests.rs @@ -216,7 +216,7 @@ where #[derive(Clone, Debug)] pub struct SetupState { interests: FixedInterests, - state: BTreeMap, + state: BTreeMap>, } impl From> for ReconMemoryBytes { @@ -227,7 +227,7 @@ impl From> for ReconMemoryBytes { value .state .into_iter() - .map(|(k, v)| (k, v.into_inner())) + .map(|(k, v)| (k, v.map(|v| v.into_inner()))) .collect(), ), metrics: Metrics::register(&mut Registry::default()), @@ -286,7 +286,7 @@ where Message::CatToDog(_) => allocator.text("cat: "), Message::DogToCat(_) => allocator.text("dog: "), } - .append(PrettySetOpt(&self.state).pretty(allocator)) + .append(PrettySet(&self.state).pretty(allocator)) .indent(4), ) } @@ -391,10 +391,10 @@ where fn pretty(self, allocator: &'a D) -> DocBuilder<'a, D, A> { allocator .text("cat: ") - .append(PrettySetOpt(&self.cat).pretty(allocator)) + .append(PrettySet(&self.cat).pretty(allocator)) .append(allocator.hardline()) .append(allocator.text("dog: ")) - .append(PrettySetOpt(&self.dog).pretty(allocator)) + .append(PrettySet(&self.dog).pretty(allocator)) } } @@ -492,34 +492,9 @@ where } } -struct PrettySet<'a, K, V>(pub &'a BTreeMap); +struct PrettySet<'a, K, V>(pub &'a BTreeMap>); impl<'a, D, A, K, V> Pretty<'a, D, A> for PrettySet<'a, K, V> -where - A: 'a + Clone, - D: DocAllocator<'a, A>, - D::Doc: Clone, - K: std::fmt::Display, - V: std::fmt::Display, -{ - fn pretty(self, allocator: &'a D) -> DocBuilder<'a, D, A> { - allocator - .intersperse( - self.0.iter().map(|(k, v)| { - allocator - .text(k.to_string()) - .append(allocator.text(": ")) - .append(allocator.text(v.to_string())) - }), - allocator.text(", "), - ) - .brackets() - } -} - -struct PrettySetOpt<'a, K, V>(pub &'a BTreeMap>); - -impl<'a, D, A, K, V> Pretty<'a, D, A> for PrettySetOpt<'a, K, V> where A: 'a + Clone, D: DocAllocator<'a, A>, @@ -537,7 +512,7 @@ where .append(if let Some(v) = v { allocator.text(v.to_string()) } else { - allocator.nil() + allocator.text("∅") }) }), allocator.text(", "), @@ -741,18 +716,24 @@ fn parse_sequence_small() { state: { Bytes( "a", - ): Bytes( - "A", + ): Some( + Bytes( + "A", + ), ), Bytes( "b", - ): Bytes( - "B", + ): Some( + Bytes( + "B", + ), ), Bytes( "c", - ): Bytes( - "C", + ): Some( + Bytes( + "C", + ), ), }, }, @@ -772,18 +753,24 @@ fn parse_sequence_small() { state: { Bytes( "e", - ): Bytes( - "E", + ): Some( + Bytes( + "E", + ), ), Bytes( "f", - ): Bytes( - "F", + ): Some( + Bytes( + "F", + ), ), Bytes( "g", - ): Bytes( - "G", + ): Some( + Bytes( + "G", + ), ), }, }, @@ -817,8 +804,10 @@ dog: [] state: { Bytes( "a", - ): Bytes( - "X", + ): Some( + Bytes( + "X", + ), ), }, }, @@ -866,18 +855,24 @@ fn parse_sequence_interests_alpha_omega() { state: { Bytes( "a", - ): Bytes( - "A", + ): Some( + Bytes( + "A", + ), ), Bytes( "b", - ): Bytes( - "B", + ): Some( + Bytes( + "B", + ), ), Bytes( "c", - ): Bytes( - "C", + ): Some( + Bytes( + "C", + ), ), }, }, @@ -905,18 +900,24 @@ fn parse_sequence_interests_alpha_omega() { state: { Bytes( "e", - ): Bytes( - "E", + ): Some( + Bytes( + "E", + ), ), Bytes( "f", - ): Bytes( - "F", + ): Some( + Bytes( + "F", + ), ), Bytes( "g", - ): Bytes( - "G", + ): Some( + Bytes( + "G", + ), ), }, }, @@ -924,6 +925,57 @@ fn parse_sequence_interests_alpha_omega() { "#]], ) } +#[test] +fn parse_sequence_missing_value() { + test_parse_sequence( + r#" +cat: [a: ∅] +dog: [b: ∅] + "#, + expect![[r#" + SequenceSetup { + cat: SetupState { + interests: FixedInterests( + [ + RangeOpen { + start: Bytes( + "", + ), + end: Bytes( + "0xFF", + ), + }, + ], + ), + state: { + Bytes( + "a", + ): None, + }, + }, + dog: SetupState { + interests: FixedInterests( + [ + RangeOpen { + start: Bytes( + "", + ), + end: Bytes( + "0xFF", + ), + }, + ], + ), + state: { + Bytes( + "b", + ): None, + }, + }, + } + "#]], + ) +} #[pin_project] struct DuplexChannel { @@ -1231,15 +1283,15 @@ async fn abcde() { <- range_resp({𝚨 0 a}, {a 0 e}, {e 0 𝛀 }) dog: [a: A, e: E] -> value_resp(b: B) - cat: [a: , b: B, c: C, d: D, e: E] + cat: [a: ∅, b: B, c: C, d: D, e: E] -> value_resp(c: C) - cat: [a: , b: B, c: C, d: D, e: E] + cat: [a: ∅, b: B, c: C, d: D, e: E] -> value_resp(d: D) - cat: [a: , b: B, c: C, d: D, e: E] + cat: [a: ∅, b: B, c: C, d: D, e: E] -> value_req(a) - cat: [a: , b: B, c: C, d: D, e: E] + cat: [a: ∅, b: B, c: C, d: D, e: E] -> listen_only - cat: [a: , b: B, c: C, d: D, e: E] + cat: [a: ∅, b: B, c: C, d: D, e: E] <- value_resp(a: A) dog: [a: A, b: B, c: C, d: D, e: E] <- listen_only @@ -1295,23 +1347,23 @@ async fn disjoint() { <- range_resp({𝚨 0 e}, {e 0 f}, {f 0 g}, {g 0 𝛀 }) dog: [e: E, f: F, g: G] -> value_resp(a: A) - cat: [a: A, b: B, c: C, e: ] + cat: [a: A, b: B, c: C, e: ∅] -> value_resp(b: B) - cat: [a: A, b: B, c: C, e: , f: , g: ] + cat: [a: A, b: B, c: C, e: ∅, f: ∅, g: ∅] -> value_resp(c: C) - cat: [a: A, b: B, c: C, e: , f: , g: ] + cat: [a: A, b: B, c: C, e: ∅, f: ∅, g: ∅] -> value_req(e) - cat: [a: A, b: B, c: C, e: , f: , g: ] + cat: [a: A, b: B, c: C, e: ∅, f: ∅, g: ∅] -> value_req(f) - cat: [a: A, b: B, c: C, e: , f: , g: ] + cat: [a: A, b: B, c: C, e: ∅, f: ∅, g: ∅] <- value_resp(e: E) dog: [a: A, b: B, c: C, e: E, f: F, g: G] -> value_req(g) - cat: [a: A, b: B, c: C, e: E, f: , g: ] + cat: [a: A, b: B, c: C, e: E, f: ∅, g: ∅] <- value_resp(f: F) dog: [a: A, b: B, c: C, e: E, f: F, g: G] -> listen_only - cat: [a: A, b: B, c: C, e: E, f: F, g: ] + cat: [a: A, b: B, c: C, e: E, f: F, g: ∅] <- value_resp(g: G) dog: [a: A, b: B, c: C, e: E, f: F, g: G] <- listen_only @@ -1429,6 +1481,80 @@ async fn two_in_sync() { .await } +#[test(tokio::test)] +async fn dog_missing_sync() { + recon_test(expect![[r#" + cat: [a: A, b: B, c: C] + dog: [a: ∅, b: ∅, c: ∅] + -> interest_req((𝚨, 𝛀 )) + cat: [a: A, b: B, c: C] + <- interest_resp((𝚨, 𝛀 )) + dog: [a: ∅, b: ∅, c: ∅] + -> range_req({𝚨 h(a, b, c)#3 𝛀 }) + cat: [a: A, b: B, c: C] + <- range_resp({𝚨 h(a, b, c)#3 𝛀 }) + dog: [a: ∅, b: ∅, c: ∅] + <- value_req(a) + dog: [a: ∅, b: ∅, c: ∅] + -> listen_only + cat: [a: A, b: B, c: C] + <- value_req(b) + dog: [a: ∅, b: ∅, c: ∅] + -> value_resp(a: A) + cat: [a: A, b: B, c: C] + <- value_req(c) + dog: [a: A, b: ∅, c: ∅] + -> value_resp(b: B) + cat: [a: A, b: B, c: C] + <- listen_only + dog: [a: A, b: B, c: ∅] + -> value_resp(c: C) + cat: [a: A, b: B, c: C] + -> finished + cat: [a: A, b: B, c: C] + cat: [a: A, b: B, c: C] + dog: [a: A, b: B, c: C] + "#]]) + .await; +} + +#[test(tokio::test)] +async fn cat_missing_sync() { + recon_test(expect![[r#" + cat: [a: ∅, b: ∅, c: ∅] + dog: [a: A, b: B, c: C] + -> interest_req((𝚨, 𝛀 )) + cat: [a: ∅, b: ∅, c: ∅] + <- interest_resp((𝚨, 𝛀 )) + dog: [a: A, b: B, c: C] + -> range_req({𝚨 h(a, b, c)#3 𝛀 }) + cat: [a: ∅, b: ∅, c: ∅] + <- range_resp({𝚨 h(a, b, c)#3 𝛀 }) + dog: [a: A, b: B, c: C] + -> value_req(a) + cat: [a: ∅, b: ∅, c: ∅] + -> value_req(b) + cat: [a: ∅, b: ∅, c: ∅] + <- value_resp(a: A) + dog: [a: A, b: B, c: C] + -> value_req(c) + cat: [a: A, b: ∅, c: ∅] + <- value_resp(b: B) + dog: [a: A, b: B, c: C] + -> listen_only + cat: [a: A, b: B, c: ∅] + <- value_resp(c: C) + dog: [a: A, b: B, c: C] + <- listen_only + dog: [a: A, b: B, c: C] + -> finished + cat: [a: A, b: B, c: C] + cat: [a: A, b: B, c: C] + dog: [a: A, b: B, c: C] + "#]]) + .await; +} + #[test(tokio::test)] async fn paper() { recon_test(expect![[r#" @@ -1443,49 +1569,49 @@ async fn paper() { <- range_resp({𝚨 h(bee, cot)#2 doe}, {doe h(eel, fox, hog)#3 𝛀 }) dog: [bee: BEE, cot: COT, doe: DOE, eel: EEL, fox: FOX, hog: HOG] -> range_req({𝚨 0 ape}) - cat: [ape: APE, doe: , eel: EEL, fox: FOX, gnu: GNU] + cat: [ape: APE, doe: ∅, eel: EEL, fox: FOX, gnu: GNU] -> range_req({ape 0 doe}) - cat: [ape: APE, doe: , eel: EEL, fox: FOX, gnu: GNU] + cat: [ape: APE, doe: ∅, eel: EEL, fox: FOX, gnu: GNU] <- range_resp({𝚨 0 ape}) - dog: [ape: , bee: BEE, cot: COT, doe: DOE, eel: EEL, fox: FOX, hog: HOG] + dog: [ape: ∅, bee: BEE, cot: COT, doe: DOE, eel: EEL, fox: FOX, hog: HOG] -> range_req({doe 0 eel}) - cat: [ape: APE, doe: , eel: EEL, fox: FOX, gnu: GNU] + cat: [ape: APE, doe: ∅, eel: EEL, fox: FOX, gnu: GNU] <- value_req(ape) - dog: [ape: , bee: BEE, cot: COT, doe: DOE, eel: EEL, fox: FOX, hog: HOG] + dog: [ape: ∅, bee: BEE, cot: COT, doe: DOE, eel: EEL, fox: FOX, hog: HOG] -> range_req({eel 0 fox}) - cat: [ape: APE, doe: , eel: EEL, fox: FOX, gnu: GNU] + cat: [ape: APE, doe: ∅, eel: EEL, fox: FOX, gnu: GNU] <- value_resp(bee: BEE) - dog: [ape: , bee: BEE, cot: COT, doe: DOE, eel: EEL, fox: FOX, hog: HOG] + dog: [ape: ∅, bee: BEE, cot: COT, doe: DOE, eel: EEL, fox: FOX, hog: HOG] -> range_req({fox 0 gnu}) - cat: [ape: APE, doe: , eel: EEL, fox: FOX, gnu: GNU] + cat: [ape: APE, doe: ∅, eel: EEL, fox: FOX, gnu: GNU] <- value_resp(cot: COT) - dog: [ape: , bee: BEE, cot: COT, doe: DOE, eel: EEL, fox: FOX, hog: HOG] + dog: [ape: ∅, bee: BEE, cot: COT, doe: DOE, eel: EEL, fox: FOX, hog: HOG] -> range_req({gnu 0 𝛀 }) - cat: [ape: APE, bee: BEE, doe: , eel: EEL, fox: FOX, gnu: GNU] + cat: [ape: APE, bee: BEE, doe: ∅, eel: EEL, fox: FOX, gnu: GNU] <- range_resp({ape h(bee, cot)#2 doe}) - dog: [ape: , bee: BEE, cot: COT, doe: DOE, eel: EEL, fox: FOX, gnu: , hog: HOG] + dog: [ape: ∅, bee: BEE, cot: COT, doe: DOE, eel: EEL, fox: FOX, gnu: ∅, hog: HOG] -> value_req(doe) - cat: [ape: APE, bee: BEE, cot: COT, doe: , eel: EEL, fox: FOX, gnu: GNU] + cat: [ape: APE, bee: BEE, cot: COT, doe: ∅, eel: EEL, fox: FOX, gnu: GNU] <- range_resp({doe 0 eel}) - dog: [ape: , bee: BEE, cot: COT, doe: DOE, eel: EEL, fox: FOX, gnu: , hog: HOG] + dog: [ape: ∅, bee: BEE, cot: COT, doe: DOE, eel: EEL, fox: FOX, gnu: ∅, hog: HOG] -> value_resp(ape: APE) - cat: [ape: APE, bee: BEE, cot: COT, doe: , eel: EEL, fox: FOX, gnu: GNU] + cat: [ape: APE, bee: BEE, cot: COT, doe: ∅, eel: EEL, fox: FOX, gnu: GNU] <- range_resp({eel 0 fox}) - dog: [ape: , bee: BEE, cot: COT, doe: DOE, eel: EEL, fox: FOX, gnu: , hog: HOG] + dog: [ape: ∅, bee: BEE, cot: COT, doe: DOE, eel: EEL, fox: FOX, gnu: ∅, hog: HOG] <- range_resp({fox 0 gnu}) - dog: [ape: APE, bee: BEE, cot: COT, doe: DOE, eel: EEL, fox: FOX, gnu: , hog: HOG] + dog: [ape: APE, bee: BEE, cot: COT, doe: DOE, eel: EEL, fox: FOX, gnu: ∅, hog: HOG] <- value_req(gnu) - dog: [ape: APE, bee: BEE, cot: COT, doe: DOE, eel: EEL, fox: FOX, gnu: , hog: HOG] + dog: [ape: APE, bee: BEE, cot: COT, doe: DOE, eel: EEL, fox: FOX, gnu: ∅, hog: HOG] <- value_resp(hog: HOG) - dog: [ape: APE, bee: BEE, cot: COT, doe: DOE, eel: EEL, fox: FOX, gnu: , hog: HOG] + dog: [ape: APE, bee: BEE, cot: COT, doe: DOE, eel: EEL, fox: FOX, gnu: ∅, hog: HOG] -> value_resp(gnu: GNU) - cat: [ape: APE, bee: BEE, cot: COT, doe: , eel: EEL, fox: FOX, gnu: GNU] + cat: [ape: APE, bee: BEE, cot: COT, doe: ∅, eel: EEL, fox: FOX, gnu: GNU] <- range_resp({gnu h(hog)#1 𝛀 }) dog: [ape: APE, bee: BEE, cot: COT, doe: DOE, eel: EEL, fox: FOX, gnu: GNU, hog: HOG] <- value_resp(doe: DOE) dog: [ape: APE, bee: BEE, cot: COT, doe: DOE, eel: EEL, fox: FOX, gnu: GNU, hog: HOG] -> listen_only - cat: [ape: APE, bee: BEE, cot: COT, doe: , eel: EEL, fox: FOX, gnu: GNU, hog: HOG] + cat: [ape: APE, bee: BEE, cot: COT, doe: ∅, eel: EEL, fox: FOX, gnu: GNU, hog: HOG] <- listen_only dog: [ape: APE, bee: BEE, cot: COT, doe: DOE, eel: EEL, fox: FOX, gnu: GNU, hog: HOG] -> finished @@ -1526,35 +1652,35 @@ async fn small_diff() { -> range_req({n 0 o}) cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: V, w: W, x: X, y: Y, z: Z] <- range_resp({m 0 n}) - dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: , o: O, p: P, q: Q, r: R, s: S, t: T, u: U, w: W, x: X, y: Y, z: Z] + dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: ∅, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, w: W, x: X, y: Y, z: Z] <- value_req(n) - dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: , o: O, p: P, q: Q, r: R, s: S, t: T, u: U, w: W, x: X, y: Y, z: Z] + dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: ∅, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, w: W, x: X, y: Y, z: Z] -> range_req({o 0 p}) cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: V, w: W, x: X, y: Y, z: Z] <- range_resp({n 0 o}) - dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: , o: O, p: P, q: Q, r: R, s: S, t: T, u: U, w: W, x: X, y: Y, z: Z] + dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: ∅, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, w: W, x: X, y: Y, z: Z] -> range_req({s 0 t}) cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: V, w: W, x: X, y: Y, z: Z] <- range_resp({o 0 p}) - dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: , o: O, p: P, q: Q, r: R, s: S, t: T, u: U, w: W, x: X, y: Y, z: Z] + dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: ∅, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, w: W, x: X, y: Y, z: Z] -> range_req({t 0 u}) cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: V, w: W, x: X, y: Y, z: Z] <- range_resp({s 0 t}) - dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: , o: O, p: P, q: Q, r: R, s: S, t: T, u: U, w: W, x: X, y: Y, z: Z] + dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: ∅, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, w: W, x: X, y: Y, z: Z] -> range_req({u 0 v}) cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: V, w: W, x: X, y: Y, z: Z] <- range_resp({t 0 u}) - dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: , o: O, p: P, q: Q, r: R, s: S, t: T, u: U, w: W, x: X, y: Y, z: Z] + dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: ∅, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, w: W, x: X, y: Y, z: Z] -> range_req({v 0 w}) cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: V, w: W, x: X, y: Y, z: Z] <- range_resp({u 0 v}) - dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: , o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: , w: W, x: X, y: Y, z: Z] + dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: ∅, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: ∅, w: W, x: X, y: Y, z: Z] -> value_resp(n: N) cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: V, w: W, x: X, y: Y, z: Z] <- value_req(v) - dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: , w: W, x: X, y: Y, z: Z] + dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: ∅, w: W, x: X, y: Y, z: Z] <- range_resp({v 0 w}) - dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: , w: W, x: X, y: Y, z: Z] + dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: ∅, w: W, x: X, y: Y, z: Z] -> value_resp(v: V) cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: V, w: W, x: X, y: Y, z: Z] -> listen_only @@ -1602,33 +1728,33 @@ async fn small_diff_off_by_one() { -> range_req({o 0 p}) cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: V, w: W, x: X, y: Y, z: Z] <- range_resp({n 0 o}) - dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: , p: P, q: Q, r: R, s: S, t: T, u: U, w: W, x: X, y: Y, z: Z] + dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: ∅, p: P, q: Q, r: R, s: S, t: T, u: U, w: W, x: X, y: Y, z: Z] <- value_req(o) - dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: , p: P, q: Q, r: R, s: S, t: T, u: U, w: W, x: X, y: Y, z: Z] + dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: ∅, p: P, q: Q, r: R, s: S, t: T, u: U, w: W, x: X, y: Y, z: Z] -> range_req({s 0 t}) cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: V, w: W, x: X, y: Y, z: Z] <- range_resp({o 0 p}) - dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: , p: P, q: Q, r: R, s: S, t: T, u: U, w: W, x: X, y: Y, z: Z] + dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: ∅, p: P, q: Q, r: R, s: S, t: T, u: U, w: W, x: X, y: Y, z: Z] -> range_req({t 0 u}) cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: V, w: W, x: X, y: Y, z: Z] <- range_resp({s 0 t}) - dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: , p: P, q: Q, r: R, s: S, t: T, u: U, w: W, x: X, y: Y, z: Z] + dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: ∅, p: P, q: Q, r: R, s: S, t: T, u: U, w: W, x: X, y: Y, z: Z] -> range_req({u 0 v}) cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: V, w: W, x: X, y: Y, z: Z] <- range_resp({t 0 u}) - dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: , p: P, q: Q, r: R, s: S, t: T, u: U, w: W, x: X, y: Y, z: Z] + dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: ∅, p: P, q: Q, r: R, s: S, t: T, u: U, w: W, x: X, y: Y, z: Z] -> range_req({v 0 w}) cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: V, w: W, x: X, y: Y, z: Z] <- range_resp({u 0 v}) - dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: , p: P, q: Q, r: R, s: S, t: T, u: U, v: , w: W, x: X, y: Y, z: Z] + dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: ∅, p: P, q: Q, r: R, s: S, t: T, u: U, v: ∅, w: W, x: X, y: Y, z: Z] -> value_resp(o: O) cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: V, w: W, x: X, y: Y, z: Z] <- value_req(v) - dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: , w: W, x: X, y: Y, z: Z] + dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: ∅, w: W, x: X, y: Y, z: Z] -> value_resp(v: V) cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: V, w: W, x: X, y: Y, z: Z] <- range_resp({v 0 w}) - dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: , w: W, x: X, y: Y, z: Z] + dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: ∅, w: W, x: X, y: Y, z: Z] -> listen_only cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: V, w: W, x: X, y: Y, z: Z] <- listen_only @@ -1654,93 +1780,93 @@ async fn alternating() { <- range_resp({𝚨 h(a, c, d, f, h, j, l)#7 n}, {n h(p, q, s, u, w, y, z)#7 𝛀 }) dog: [a: A, c: C, d: D, f: F, h: H, j: J, l: L, n: N, p: P, q: Q, s: S, u: U, w: W, y: Y, z: Z] -> range_req({𝚨 h(a, b, c)#3 e}) - cat: [a: A, b: B, c: C, e: E, g: G, i: I, k: K, m: M, n: , o: O, p: P, r: R, t: T, v: V, x: X, z: Z] + cat: [a: A, b: B, c: C, e: E, g: G, i: I, k: K, m: M, n: ∅, o: O, p: P, r: R, t: T, v: V, x: X, z: Z] -> range_req({e h(g, i, k, m)#4 n}) - cat: [a: A, b: B, c: C, e: E, g: G, i: I, k: K, m: M, n: , o: O, p: P, r: R, t: T, v: V, x: X, z: Z] + cat: [a: A, b: B, c: C, e: E, g: G, i: I, k: K, m: M, n: ∅, o: O, p: P, r: R, t: T, v: V, x: X, z: Z] <- range_resp({𝚨 0 a}, {a 0 c}, {c 0 d}, {d 0 e}) - dog: [a: A, c: C, d: D, e: , f: F, h: H, j: J, l: L, n: N, p: P, q: Q, s: S, u: U, w: W, y: Y, z: Z] + dog: [a: A, c: C, d: D, e: ∅, f: F, h: H, j: J, l: L, n: N, p: P, q: Q, s: S, u: U, w: W, y: Y, z: Z] -> range_req({n h(o, p, r)#3 t}) - cat: [a: A, b: B, c: C, e: E, g: G, i: I, k: K, m: M, n: , o: O, p: P, r: R, t: T, v: V, x: X, z: Z] + cat: [a: A, b: B, c: C, e: E, g: G, i: I, k: K, m: M, n: ∅, o: O, p: P, r: R, t: T, v: V, x: X, z: Z] <- value_req(e) - dog: [a: A, c: C, d: D, e: , f: F, h: H, j: J, l: L, n: N, p: P, q: Q, s: S, u: U, w: W, y: Y, z: Z] + dog: [a: A, c: C, d: D, e: ∅, f: F, h: H, j: J, l: L, n: N, p: P, q: Q, s: S, u: U, w: W, y: Y, z: Z] -> range_req({t h(v, x, z)#3 𝛀 }) - cat: [a: A, b: B, c: C, d: , e: E, g: G, i: I, k: K, m: M, n: , o: O, p: P, r: R, t: T, v: V, x: X, z: Z] + cat: [a: A, b: B, c: C, d: ∅, e: E, g: G, i: I, k: K, m: M, n: ∅, o: O, p: P, r: R, t: T, v: V, x: X, z: Z] <- range_resp({e 0 f}, {f 0 h}, {h 0 j}, {j 0 l}, {l 0 n}) - dog: [a: A, c: C, d: D, e: , f: F, h: H, j: J, l: L, n: N, p: P, q: Q, s: S, t: , u: U, w: W, y: Y, z: Z] + dog: [a: A, c: C, d: D, e: ∅, f: F, h: H, j: J, l: L, n: N, p: P, q: Q, s: S, t: ∅, u: U, w: W, y: Y, z: Z] -> value_req(n) - cat: [a: A, b: B, c: C, d: , e: E, g: G, i: I, k: K, m: M, n: , o: O, p: P, r: R, t: T, v: V, x: X, z: Z] + cat: [a: A, b: B, c: C, d: ∅, e: E, g: G, i: I, k: K, m: M, n: ∅, o: O, p: P, r: R, t: T, v: V, x: X, z: Z] <- range_resp({n 0 p}, {p 0 q}, {q 0 s}, {s 0 t}) - dog: [a: A, c: C, d: D, e: , f: F, h: H, j: J, l: L, n: N, p: P, q: Q, s: S, t: , u: U, w: W, y: Y, z: Z] + dog: [a: A, c: C, d: D, e: ∅, f: F, h: H, j: J, l: L, n: N, p: P, q: Q, s: S, t: ∅, u: U, w: W, y: Y, z: Z] -> value_resp(b: B) - cat: [a: A, b: B, c: C, d: , e: E, f: , g: G, h: , i: I, j: , k: K, l: , m: M, n: , o: O, p: P, r: R, t: T, v: V, x: X, z: Z] + cat: [a: A, b: B, c: C, d: ∅, e: E, f: ∅, g: G, h: ∅, i: I, j: ∅, k: K, l: ∅, m: M, n: ∅, o: O, p: P, r: R, t: T, v: V, x: X, z: Z] <- value_req(t) - dog: [a: A, b: B, c: C, d: D, e: , f: F, h: H, j: J, l: L, n: N, p: P, q: Q, s: S, t: , u: U, w: W, y: Y, z: Z] + dog: [a: A, b: B, c: C, d: D, e: ∅, f: F, h: H, j: J, l: L, n: N, p: P, q: Q, s: S, t: ∅, u: U, w: W, y: Y, z: Z] -> value_req(d) - cat: [a: A, b: B, c: C, d: , e: E, f: , g: G, h: , i: I, j: , k: K, l: , m: M, n: , o: O, p: P, q: , r: R, s: , t: T, v: V, x: X, z: Z] + cat: [a: A, b: B, c: C, d: ∅, e: E, f: ∅, g: G, h: ∅, i: I, j: ∅, k: K, l: ∅, m: M, n: ∅, o: O, p: P, q: ∅, r: R, s: ∅, t: T, v: V, x: X, z: Z] <- range_resp({t 0 u}, {u 0 w}, {w 0 y}, {y 0 z}, {z 0 𝛀 }) - dog: [a: A, b: B, c: C, d: D, e: , f: F, h: H, j: J, l: L, n: N, p: P, q: Q, s: S, t: , u: U, w: W, y: Y, z: Z] + dog: [a: A, b: B, c: C, d: D, e: ∅, f: F, h: H, j: J, l: L, n: N, p: P, q: Q, s: S, t: ∅, u: U, w: W, y: Y, z: Z] -> value_resp(e: E) - cat: [a: A, b: B, c: C, d: , e: E, f: , g: G, h: , i: I, j: , k: K, l: , m: M, n: , o: O, p: P, q: , r: R, s: , t: T, v: V, x: X, z: Z] + cat: [a: A, b: B, c: C, d: ∅, e: E, f: ∅, g: G, h: ∅, i: I, j: ∅, k: K, l: ∅, m: M, n: ∅, o: O, p: P, q: ∅, r: R, s: ∅, t: T, v: V, x: X, z: Z] <- value_resp(n: N) - dog: [a: A, b: B, c: C, d: D, e: E, f: F, h: H, j: J, l: L, n: N, p: P, q: Q, s: S, t: , u: U, w: W, y: Y, z: Z] + dog: [a: A, b: B, c: C, d: D, e: E, f: F, h: H, j: J, l: L, n: N, p: P, q: Q, s: S, t: ∅, u: U, w: W, y: Y, z: Z] -> value_resp(g: G) - cat: [a: A, b: B, c: C, d: , e: E, f: , g: G, h: , i: I, j: , k: K, l: , m: M, n: , o: O, p: P, q: , r: R, s: , t: T, u: , v: V, w: , x: X, y: , z: Z] + cat: [a: A, b: B, c: C, d: ∅, e: E, f: ∅, g: G, h: ∅, i: I, j: ∅, k: K, l: ∅, m: M, n: ∅, o: O, p: P, q: ∅, r: R, s: ∅, t: T, u: ∅, v: V, w: ∅, x: X, y: ∅, z: Z] <- value_resp(d: D) - dog: [a: A, b: B, c: C, d: D, e: E, f: F, h: H, j: J, l: L, n: N, p: P, q: Q, s: S, t: , u: U, w: W, y: Y, z: Z] + dog: [a: A, b: B, c: C, d: D, e: E, f: F, h: H, j: J, l: L, n: N, p: P, q: Q, s: S, t: ∅, u: U, w: W, y: Y, z: Z] -> value_resp(i: I) - cat: [a: A, b: B, c: C, d: , e: E, f: , g: G, h: , i: I, j: , k: K, l: , m: M, n: N, o: O, p: P, q: , r: R, s: , t: T, u: , v: V, w: , x: X, y: , z: Z] + cat: [a: A, b: B, c: C, d: ∅, e: E, f: ∅, g: G, h: ∅, i: I, j: ∅, k: K, l: ∅, m: M, n: N, o: O, p: P, q: ∅, r: R, s: ∅, t: T, u: ∅, v: V, w: ∅, x: X, y: ∅, z: Z] -> value_resp(k: K) - cat: [a: A, b: B, c: C, d: D, e: E, f: , g: G, h: , i: I, j: , k: K, l: , m: M, n: N, o: O, p: P, q: , r: R, s: , t: T, u: , v: V, w: , x: X, y: , z: Z] + cat: [a: A, b: B, c: C, d: D, e: E, f: ∅, g: G, h: ∅, i: I, j: ∅, k: K, l: ∅, m: M, n: N, o: O, p: P, q: ∅, r: R, s: ∅, t: T, u: ∅, v: V, w: ∅, x: X, y: ∅, z: Z] -> value_resp(m: M) - cat: [a: A, b: B, c: C, d: D, e: E, f: , g: G, h: , i: I, j: , k: K, l: , m: M, n: N, o: O, p: P, q: , r: R, s: , t: T, u: , v: V, w: , x: X, y: , z: Z] + cat: [a: A, b: B, c: C, d: D, e: E, f: ∅, g: G, h: ∅, i: I, j: ∅, k: K, l: ∅, m: M, n: N, o: O, p: P, q: ∅, r: R, s: ∅, t: T, u: ∅, v: V, w: ∅, x: X, y: ∅, z: Z] -> value_req(f) - cat: [a: A, b: B, c: C, d: D, e: E, f: , g: G, h: , i: I, j: , k: K, l: , m: M, n: N, o: O, p: P, q: , r: R, s: , t: T, u: , v: V, w: , x: X, y: , z: Z] + cat: [a: A, b: B, c: C, d: D, e: E, f: ∅, g: G, h: ∅, i: I, j: ∅, k: K, l: ∅, m: M, n: N, o: O, p: P, q: ∅, r: R, s: ∅, t: T, u: ∅, v: V, w: ∅, x: X, y: ∅, z: Z] <- value_resp(f: F) - dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, p: P, q: Q, s: S, t: , u: U, w: W, y: Y, z: Z] + dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, p: P, q: Q, s: S, t: ∅, u: U, w: W, y: Y, z: Z] -> value_req(h) - cat: [a: A, b: B, c: C, d: D, e: E, f: , g: G, h: , i: I, j: , k: K, l: , m: M, n: N, o: O, p: P, q: , r: R, s: , t: T, u: , v: V, w: , x: X, y: , z: Z] + cat: [a: A, b: B, c: C, d: D, e: E, f: ∅, g: G, h: ∅, i: I, j: ∅, k: K, l: ∅, m: M, n: N, o: O, p: P, q: ∅, r: R, s: ∅, t: T, u: ∅, v: V, w: ∅, x: X, y: ∅, z: Z] <- value_resp(h: H) - dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, p: P, q: Q, s: S, t: , u: U, w: W, y: Y, z: Z] + dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, p: P, q: Q, s: S, t: ∅, u: U, w: W, y: Y, z: Z] -> value_req(j) - cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: , i: I, j: , k: K, l: , m: M, n: N, o: O, p: P, q: , r: R, s: , t: T, u: , v: V, w: , x: X, y: , z: Z] + cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: ∅, i: I, j: ∅, k: K, l: ∅, m: M, n: N, o: O, p: P, q: ∅, r: R, s: ∅, t: T, u: ∅, v: V, w: ∅, x: X, y: ∅, z: Z] <- value_resp(j: J) - dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, p: P, q: Q, s: S, t: , u: U, w: W, y: Y, z: Z] + dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, p: P, q: Q, s: S, t: ∅, u: U, w: W, y: Y, z: Z] -> value_req(l) - cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: , k: K, l: , m: M, n: N, o: O, p: P, q: , r: R, s: , t: T, u: , v: V, w: , x: X, y: , z: Z] + cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: ∅, k: K, l: ∅, m: M, n: N, o: O, p: P, q: ∅, r: R, s: ∅, t: T, u: ∅, v: V, w: ∅, x: X, y: ∅, z: Z] <- value_resp(l: L) - dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, p: P, q: Q, s: S, t: , u: U, w: W, y: Y, z: Z] + dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, p: P, q: Q, s: S, t: ∅, u: U, w: W, y: Y, z: Z] -> value_resp(o: O) - cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: , m: M, n: N, o: O, p: P, q: , r: R, s: , t: T, u: , v: V, w: , x: X, y: , z: Z] + cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: ∅, m: M, n: N, o: O, p: P, q: ∅, r: R, s: ∅, t: T, u: ∅, v: V, w: ∅, x: X, y: ∅, z: Z] -> value_resp(r: R) - cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: , r: R, s: , t: T, u: , v: V, w: , x: X, y: , z: Z] + cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: ∅, r: R, s: ∅, t: T, u: ∅, v: V, w: ∅, x: X, y: ∅, z: Z] -> value_req(q) - cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: , r: R, s: , t: T, u: , v: V, w: , x: X, y: , z: Z] + cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: ∅, r: R, s: ∅, t: T, u: ∅, v: V, w: ∅, x: X, y: ∅, z: Z] <- value_resp(q: Q) - dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: , u: U, w: W, y: Y, z: Z] + dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: ∅, u: U, w: W, y: Y, z: Z] -> value_req(s) - cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: , r: R, s: , t: T, u: , v: V, w: , x: X, y: , z: Z] + cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: ∅, r: R, s: ∅, t: T, u: ∅, v: V, w: ∅, x: X, y: ∅, z: Z] <- value_resp(s: S) - dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: , u: U, w: W, y: Y, z: Z] + dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: ∅, u: U, w: W, y: Y, z: Z] -> value_resp(t: T) - cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: , t: T, u: , v: V, w: , x: X, y: , z: Z] + cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: ∅, t: T, u: ∅, v: V, w: ∅, x: X, y: ∅, z: Z] -> value_resp(v: V) - cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: , v: V, w: , x: X, y: , z: Z] + cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: ∅, v: V, w: ∅, x: X, y: ∅, z: Z] -> value_resp(x: X) - cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: , v: V, w: , x: X, y: , z: Z] + cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: ∅, v: V, w: ∅, x: X, y: ∅, z: Z] -> value_req(u) - cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: , v: V, w: , x: X, y: , z: Z] + cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: ∅, v: V, w: ∅, x: X, y: ∅, z: Z] <- value_resp(u: U) dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: V, w: W, x: X, y: Y, z: Z] -> value_req(w) - cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: , v: V, w: , x: X, y: , z: Z] + cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: ∅, v: V, w: ∅, x: X, y: ∅, z: Z] <- value_resp(w: W) dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: V, w: W, x: X, y: Y, z: Z] -> value_req(y) - cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: V, w: , x: X, y: , z: Z] + cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: V, w: ∅, x: X, y: ∅, z: Z] <- value_resp(y: Y) dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: V, w: W, x: X, y: Y, z: Z] -> listen_only - cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: V, w: W, x: X, y: , z: Z] + cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: V, w: W, x: X, y: ∅, z: Z] <- listen_only dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: V, w: W, x: X, y: Y, z: Z] -> finished @@ -1782,53 +1908,53 @@ async fn small_diff_zz() { -> range_req({n 0 o}) cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: V, w: W, x: X, y: Y, z: Z, zz: ZZ] <- range_resp({n 0 o}) - dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: , p: P, q: Q, r: R, s: S, t: T, u: U, w: W, x: X, y: Y, z: Z] + dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: ∅, p: P, q: Q, r: R, s: S, t: T, u: U, w: W, x: X, y: Y, z: Z] -> range_req({o 0 p}) cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: V, w: W, x: X, y: Y, z: Z, zz: ZZ] <- value_req(o) - dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: , p: P, q: Q, r: R, s: S, t: T, u: U, w: W, x: X, y: Y, z: Z] + dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: ∅, p: P, q: Q, r: R, s: S, t: T, u: U, w: W, x: X, y: Y, z: Z] -> range_req({t 0 u}) cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: V, w: W, x: X, y: Y, z: Z, zz: ZZ] <- range_resp({o 0 p}) - dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: , p: P, q: Q, r: R, s: S, t: T, u: U, w: W, x: X, y: Y, z: Z] + dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: ∅, p: P, q: Q, r: R, s: S, t: T, u: U, w: W, x: X, y: Y, z: Z] -> range_req({u 0 v}) cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: V, w: W, x: X, y: Y, z: Z, zz: ZZ] <- range_resp({t 0 u}) - dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: , p: P, q: Q, r: R, s: S, t: T, u: U, w: W, x: X, y: Y, z: Z] + dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: ∅, p: P, q: Q, r: R, s: S, t: T, u: U, w: W, x: X, y: Y, z: Z] -> range_req({v 0 w}) cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: V, w: W, x: X, y: Y, z: Z, zz: ZZ] <- range_resp({u 0 v}) - dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: , p: P, q: Q, r: R, s: S, t: T, u: U, v: , w: W, x: X, y: Y, z: Z] + dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: ∅, p: P, q: Q, r: R, s: S, t: T, u: U, v: ∅, w: W, x: X, y: Y, z: Z] -> range_req({w 0 x}) cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: V, w: W, x: X, y: Y, z: Z, zz: ZZ] <- value_req(v) - dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: , p: P, q: Q, r: R, s: S, t: T, u: U, v: , w: W, x: X, y: Y, z: Z] + dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: ∅, p: P, q: Q, r: R, s: S, t: T, u: U, v: ∅, w: W, x: X, y: Y, z: Z] -> range_req({x 0 y}) cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: V, w: W, x: X, y: Y, z: Z, zz: ZZ] <- range_resp({v 0 w}) - dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: , p: P, q: Q, r: R, s: S, t: T, u: U, v: , w: W, x: X, y: Y, z: Z] + dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: ∅, p: P, q: Q, r: R, s: S, t: T, u: U, v: ∅, w: W, x: X, y: Y, z: Z] -> range_req({y 0 z}) cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: V, w: W, x: X, y: Y, z: Z, zz: ZZ] <- range_resp({w 0 x}) - dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: , p: P, q: Q, r: R, s: S, t: T, u: U, v: , w: W, x: X, y: Y, z: Z] + dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: ∅, p: P, q: Q, r: R, s: S, t: T, u: U, v: ∅, w: W, x: X, y: Y, z: Z] -> range_req({z 0 zz}) cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: V, w: W, x: X, y: Y, z: Z, zz: ZZ] <- range_resp({x 0 y}) - dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: , p: P, q: Q, r: R, s: S, t: T, u: U, v: , w: W, x: X, y: Y, z: Z] + dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: ∅, p: P, q: Q, r: R, s: S, t: T, u: U, v: ∅, w: W, x: X, y: Y, z: Z] -> range_req({zz 0 𝛀 }) cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: V, w: W, x: X, y: Y, z: Z, zz: ZZ] <- range_resp({y 0 z}) - dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: , p: P, q: Q, r: R, s: S, t: T, u: U, v: , w: W, x: X, y: Y, z: Z, zz: ] + dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: ∅, p: P, q: Q, r: R, s: S, t: T, u: U, v: ∅, w: W, x: X, y: Y, z: Z, zz: ∅] -> value_resp(o: O) cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: V, w: W, x: X, y: Y, z: Z, zz: ZZ] <- range_resp({z 0 zz}) - dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: , w: W, x: X, y: Y, z: Z, zz: ] + dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: ∅, w: W, x: X, y: Y, z: Z, zz: ∅] -> value_resp(v: V) cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: V, w: W, x: X, y: Y, z: Z, zz: ZZ] <- value_req(zz) - dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: V, w: W, x: X, y: Y, z: Z, zz: ] + dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: V, w: W, x: X, y: Y, z: Z, zz: ∅] <- range_resp({zz 0 𝛀 }) - dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: V, w: W, x: X, y: Y, z: Z, zz: ] + dog: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: V, w: W, x: X, y: Y, z: Z, zz: ∅] -> value_resp(zz: ZZ) cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: V, w: W, x: X, y: Y, z: Z, zz: ZZ] -> listen_only @@ -1934,37 +2060,37 @@ async fn subset_interest() { <- range_resp({b h(c, d)#2 e}, {e h(f, g, h)#3 i}) dog: [b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N] -> range_req({b 0 c}) - cat: [b: , c: C, e: , f: F, g: G, i: , r: R] + cat: [b: ∅, c: C, e: ∅, f: F, g: G, i: ∅, r: R] <- value_resp(n: N) - dog: [b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, r: ] + dog: [b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, r: ∅] -> range_req({c 0 e}) - cat: [b: , c: C, e: , f: F, g: G, i: , r: R] + cat: [b: ∅, c: C, e: ∅, f: F, g: G, i: ∅, r: R] -> range_req({e 0 f}) - cat: [b: , c: C, e: , f: F, g: G, i: , n: N, r: R] + cat: [b: ∅, c: C, e: ∅, f: F, g: G, i: ∅, n: N, r: R] <- range_resp({m h(n)#1 r}) - dog: [b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, r: ] + dog: [b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, r: ∅] -> range_req({f 0 g}) - cat: [b: , c: C, e: , f: F, g: G, i: , n: N, r: R] + cat: [b: ∅, c: C, e: ∅, f: F, g: G, i: ∅, n: N, r: R] <- value_req(r) - dog: [b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, r: ] + dog: [b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, r: ∅] -> range_req({g 0 i}) - cat: [b: , c: C, e: , f: F, g: G, i: , m: , n: N, r: R] + cat: [b: ∅, c: C, e: ∅, f: F, g: G, i: ∅, m: ∅, n: N, r: R] -> value_req(b) - cat: [b: , c: C, e: , f: F, g: G, i: , m: , n: N, r: R] + cat: [b: ∅, c: C, e: ∅, f: F, g: G, i: ∅, m: ∅, n: N, r: R] <- range_resp({b 0 c}) - dog: [b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, r: ] + dog: [b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, r: ∅] -> value_req(e) - cat: [b: , c: C, e: , f: F, g: G, i: , m: , n: N, r: R] + cat: [b: ∅, c: C, e: ∅, f: F, g: G, i: ∅, m: ∅, n: N, r: R] <- value_resp(d: D) - dog: [b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, r: ] + dog: [b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, r: ∅] -> value_req(i) - cat: [b: , c: C, e: , f: F, g: G, i: , m: , n: N, r: R] + cat: [b: ∅, c: C, e: ∅, f: F, g: G, i: ∅, m: ∅, n: N, r: R] <- range_resp({c h(d)#1 e}) - dog: [b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, r: ] + dog: [b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, r: ∅] -> value_req(m) - cat: [b: , c: C, d: D, e: , f: F, g: G, i: , m: , n: N, r: R] + cat: [b: ∅, c: C, d: D, e: ∅, f: F, g: G, i: ∅, m: ∅, n: N, r: R] -> value_resp(r: R) - cat: [b: , c: C, d: D, e: , f: F, g: G, i: , m: , n: N, r: R] + cat: [b: ∅, c: C, d: D, e: ∅, f: F, g: G, i: ∅, m: ∅, n: N, r: R] <- range_resp({e 0 f}) dog: [b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, r: R] <- range_resp({f 0 g}) @@ -1974,7 +2100,7 @@ async fn subset_interest() { <- range_resp({g h(h)#1 i}) dog: [b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, r: R] -> listen_only - cat: [b: , c: C, d: D, e: , f: F, g: G, h: H, i: , m: , n: N, r: R] + cat: [b: ∅, c: C, d: D, e: ∅, f: F, g: G, h: H, i: ∅, m: ∅, n: N, r: R] <- value_resp(b: B) dog: [b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, r: R] <- value_resp(e: E) @@ -2007,15 +2133,15 @@ async fn partial_interest() { <- range_resp({k 0 n}, {n 0 o}, {o 0 p}, {p 0 q}) dog: [j: J, k: K, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, w: W, x: X, y: Y, z: Z] -> value_resp(l: L) - cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: , o: O, p: P, q: Q] + cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: ∅, o: O, p: P, q: Q] -> value_resp(m: M) - cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: , o: O, p: P, q: Q] + cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: ∅, o: O, p: P, q: Q] -> value_req(n) - cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: , o: O, p: P, q: Q] + cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: ∅, o: O, p: P, q: Q] <- value_resp(n: N) dog: [j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, w: W, x: X, y: Y, z: Z] -> listen_only - cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: , o: O, p: P, q: Q] + cat: [a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: ∅, o: O, p: P, q: Q] <- listen_only dog: [j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, w: W, x: X, y: Y, z: Z] -> finished