reactor-effect-native 0.2.0 → 0.3.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/Dockerfile +4 -2
  2. package/README.md +90 -15
  3. package/dist/_internal/bridge.d.ts +70 -14
  4. package/dist/_internal/bridge.d.ts.map +1 -1
  5. package/dist/_internal/bridge.js +290 -145
  6. package/dist/_internal/bridge.js.map +1 -1
  7. package/dist/_internal/isolated/child.d.ts +2 -0
  8. package/dist/_internal/isolated/child.d.ts.map +1 -0
  9. package/dist/_internal/isolated/child.js +176 -0
  10. package/dist/_internal/isolated/child.js.map +1 -0
  11. package/dist/_internal/isolated/host.d.ts +147 -0
  12. package/dist/_internal/isolated/host.d.ts.map +1 -0
  13. package/dist/_internal/isolated/host.js +645 -0
  14. package/dist/_internal/isolated/host.js.map +1 -0
  15. package/dist/_internal/isolated/protocol.d.ts +399 -0
  16. package/dist/_internal/isolated/protocol.d.ts.map +1 -0
  17. package/dist/_internal/isolated/protocol.js +250 -0
  18. package/dist/_internal/isolated/protocol.js.map +1 -0
  19. package/dist/_internal/peer.d.ts +68 -13
  20. package/dist/_internal/peer.d.ts.map +1 -1
  21. package/dist/_internal/peer.js +233 -157
  22. package/dist/_internal/peer.js.map +1 -1
  23. package/dist/index.d.ts +19 -10
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +37 -33
  26. package/dist/index.js.map +1 -1
  27. package/dist/isolated.d.ts +25 -0
  28. package/dist/isolated.d.ts.map +1 -0
  29. package/dist/isolated.js +39 -0
  30. package/dist/isolated.js.map +1 -0
  31. package/lib/darwin-arm64/libreactor_effect_native.dylib +0 -0
  32. package/lib/darwin-arm64/native-identity.json +4 -3
  33. package/lib/linux-x64/libreactor_effect_native.so +0 -0
  34. package/lib/linux-x64/native-identity.json +4 -3
  35. package/package.json +7 -3
  36. package/rust/Cargo.toml +108 -2
  37. package/rust/build.rs +253 -114
  38. package/rust/clippy.toml +7 -0
  39. package/rust/include/reactor_effect_native.h +101 -42
  40. package/rust/src/abi.rs +258 -0
  41. package/rust/src/error.rs +149 -0
  42. package/rust/src/ffi/memory.rs +256 -0
  43. package/rust/src/ffi/tests.rs +719 -0
  44. package/rust/src/ffi.rs +474 -0
  45. package/rust/src/lib.rs +36 -2341
  46. package/rust/src/peer/callbacks.rs +145 -0
  47. package/rust/src/peer/media.rs +221 -0
  48. package/rust/src/peer/owner/tests.rs +188 -0
  49. package/rust/src/peer/owner.rs +353 -0
  50. package/rust/src/peer/shared.rs +323 -0
  51. package/rust/src/peer/tests.rs +513 -0
  52. package/rust/src/peer.rs +189 -0
  53. package/rust/src/protocol/event.rs +238 -0
  54. package/rust/src/protocol/request.rs +347 -0
  55. package/rust/src/protocol/stats.rs +356 -0
  56. package/rust/src/protocol.rs +71 -0
  57. package/rust/src/sync/gate.rs +159 -0
  58. package/rust/src/sync/notifier.rs +136 -0
  59. package/rust/src/sync/queue.rs +384 -0
  60. package/rust/src/sync.rs +20 -0
  61. package/rust/src/test_support.rs +99 -0
  62. package/rust-toolchain.toml +7 -0
  63. package/scripts/stage.mjs +41 -1
@@ -0,0 +1,145 @@
1
+ //! libwebrtc callbacks. Each runs on a libwebrtc thread, is admitted through
2
+ //! the peer's callback gate, and only copies into the shared queues: none
3
+ //! calls or waits on the host.
4
+
5
+ use super::Shared;
6
+ use crate::abi::{Channel, MAX_MESSAGE_BYTES};
7
+ use crate::error::{BridgeError, Classify, FailureClass};
8
+ use crate::protocol::{Event, LocalCandidate, connection_state};
9
+ use reactor_webrtc::{
10
+ DataChannel, DataChannelState, IceGatheringState, PeerConnection, PeerConnectionObserver,
11
+ };
12
+ use std::sync::Arc;
13
+
14
+ /// The connection's observer: state changes, local ICE candidates and remote
15
+ /// tracks become events.
16
+ pub(crate) fn observer(shared: &Arc<Shared>) -> PeerConnectionObserver {
17
+ PeerConnectionObserver::new()
18
+ .on_connection_state_change({
19
+ let shared = Arc::clone(shared);
20
+ move |state| {
21
+ shared.admit(|| {
22
+ shared.emit(&Event::State {
23
+ state: connection_state(state),
24
+ });
25
+ });
26
+ }
27
+ })
28
+ .on_ice_gathering_change({
29
+ let shared = Arc::clone(shared);
30
+ move |state| {
31
+ shared.admit(|| {
32
+ if state == IceGatheringState::Complete {
33
+ shared.emit(&Event::Ice { candidate: None });
34
+ }
35
+ });
36
+ }
37
+ })
38
+ .on_ice_candidate({
39
+ let shared = Arc::clone(shared);
40
+ move |candidate| {
41
+ shared.admit(|| {
42
+ shared.emit(&Event::Ice {
43
+ candidate: Some(LocalCandidate::from(&candidate)),
44
+ });
45
+ });
46
+ }
47
+ })
48
+ .on_track({
49
+ let shared = Arc::clone(shared);
50
+ move |track| shared.admit(|| shared.accept_remote(track))
51
+ })
52
+ }
53
+
54
+ /// Create one of the bridge's data channels and turn its messages and state
55
+ /// changes into events.
56
+ pub(crate) fn open_channel(
57
+ peer: &PeerConnection,
58
+ channel: Channel,
59
+ shared: &Arc<Shared>,
60
+ ) -> Result<DataChannel, BridgeError> {
61
+ let mut data_channel = peer
62
+ .create_data_channel(channel.label())
63
+ .classify(FailureClass::Native, "create_data_channel")?;
64
+ data_channel.on_message({
65
+ let shared = Arc::clone(shared);
66
+ move |bytes, binary| shared.admit(|| receive(&shared, channel, bytes, binary))
67
+ });
68
+ data_channel.on_state_change({
69
+ let shared = Arc::clone(shared);
70
+ move |state| {
71
+ shared.admit(|| {
72
+ let open = match state {
73
+ DataChannelState::Open => true,
74
+ DataChannelState::Closed => false,
75
+ DataChannelState::Connecting | DataChannelState::Closing => return,
76
+ };
77
+ shared.emit(&Event::Channel { channel, open });
78
+ });
79
+ }
80
+ });
81
+ Ok(data_channel)
82
+ }
83
+
84
+ /// Pass a message to the host. The bridge's channels carry binary messages
85
+ /// within the local bound; anything else fails the connection.
86
+ fn receive(shared: &Shared, channel: Channel, bytes: &[u8], binary: bool) {
87
+ let label = channel.label();
88
+ if !binary {
89
+ shared.emit_error(
90
+ FailureClass::Protocol,
91
+ &format!("{label} data channel delivered a nonbinary message"),
92
+ );
93
+ } else if bytes.len() > MAX_MESSAGE_BYTES {
94
+ shared.emit_error(
95
+ FailureClass::Overflow,
96
+ &format!("{label} data channel message exceeds local bound"),
97
+ );
98
+ } else {
99
+ shared.emit(&Event::Message { channel, bytes });
100
+ }
101
+ }
102
+
103
+ #[cfg(test)]
104
+ mod tests {
105
+ use super::*;
106
+ use crate::abi::Status;
107
+ use crate::sync::Taken;
108
+ use crate::test_support::parse_packet;
109
+ use serde_json::{Value, json};
110
+
111
+ fn next_event(shared: &Shared) -> (Value, Vec<u8>) {
112
+ let Taken::Item(packet) = shared.events.take(|_| true) else {
113
+ panic!("an event must be queued");
114
+ };
115
+ let (header, payload) = parse_packet(&packet);
116
+ (header, payload.to_vec())
117
+ }
118
+
119
+ #[test]
120
+ fn a_binary_message_within_the_bound_reaches_the_host_unchanged() {
121
+ let shared = Shared::new();
122
+ let message = vec![7; MAX_MESSAGE_BYTES];
123
+ receive(&shared, Channel::Control, &message, true);
124
+ let (header, payload) = next_event(&shared);
125
+ assert_eq!(header, json!({ "type": "message", "channel": "control" }));
126
+ assert_eq!(payload, message);
127
+ }
128
+
129
+ #[test]
130
+ fn a_text_or_oversized_message_fails_the_connection() {
131
+ let shared = Shared::new();
132
+ receive(&shared, Channel::Data, b"text", false);
133
+ receive(
134
+ &shared,
135
+ Channel::Data,
136
+ &vec![0; MAX_MESSAGE_BYTES + 1],
137
+ true,
138
+ );
139
+ let (text, _) = next_event(&shared);
140
+ assert_eq!(text["type"], "error");
141
+ assert_eq!(text["status"], Status::Protocol.code());
142
+ let (oversized, _) = next_event(&shared);
143
+ assert_eq!(oversized["status"], Status::Overflow.code());
144
+ }
145
+ }
@@ -0,0 +1,221 @@
1
+ //! Decoded media: the items the media queues hold, and the sinks that copy
2
+ //! each frame libwebrtc decodes into them.
3
+
4
+ use super::Shared;
5
+ use crate::ffi::{ReactorEffectAudioHeader, ReactorEffectVideoHeader};
6
+ use crate::protocol::TrackKind;
7
+ use crate::sync::QueueItem;
8
+ use reactor_webrtc::{AudioFrame, RemoteTrack, VideoFrame};
9
+ use std::sync::Arc;
10
+ use std::sync::atomic::{AtomicU64, Ordering};
11
+
12
+ /// One decoded BGRA frame and its sender metadata.
13
+ #[derive(Debug, Clone, PartialEq, Eq)]
14
+ pub(crate) struct VideoItem {
15
+ /// The track's index in the prepare request.
16
+ pub(crate) track: u32,
17
+ pub(crate) width: u32,
18
+ pub(crate) height: u32,
19
+ /// 0 when the sender supplied none.
20
+ pub(crate) frame_id: u64,
21
+ /// The sender's capture time; 0 when absent.
22
+ pub(crate) timestamp_us: u64,
23
+ /// The frame's admission sequence on its track.
24
+ pub(crate) sequence: u64,
25
+ pub(crate) bgra: Vec<u8>,
26
+ pub(crate) metadata: Vec<u8>,
27
+ }
28
+
29
+ impl VideoItem {
30
+ /// Copy a frame that libwebrtc lends only for the duration of its
31
+ /// callback. This is the one native copy of each frame.
32
+ fn copy(track: u32, sequence: u64, frame: VideoFrame<'_>) -> Self {
33
+ let (frame_id, timestamp_us, metadata) = frame
34
+ .metadata
35
+ .map(|metadata| {
36
+ (
37
+ metadata.frame_id,
38
+ metadata.capture_time_us,
39
+ metadata.user_data,
40
+ )
41
+ })
42
+ .unwrap_or_default();
43
+ Self {
44
+ track,
45
+ width: frame.width,
46
+ height: frame.height,
47
+ frame_id,
48
+ timestamp_us,
49
+ sequence,
50
+ bgra: frame.bgra.to_vec(),
51
+ metadata,
52
+ }
53
+ }
54
+
55
+ pub(crate) fn header(&self) -> ReactorEffectVideoHeader {
56
+ ReactorEffectVideoHeader {
57
+ width: self.width,
58
+ height: self.height,
59
+ data_len: queued_len(self.bgra.len()),
60
+ metadata_len: queued_len(self.metadata.len()),
61
+ frame_id: self.frame_id,
62
+ timestamp_us: self.timestamp_us,
63
+ track: self.track,
64
+ reserved: 0,
65
+ sequence: self.sequence,
66
+ }
67
+ }
68
+ }
69
+
70
+ impl QueueItem for VideoItem {
71
+ fn byte_len(&self) -> usize {
72
+ self.bgra.len() + self.metadata.len()
73
+ }
74
+ }
75
+
76
+ /// One block of decoded, interleaved PCM.
77
+ #[derive(Debug, Clone, PartialEq, Eq)]
78
+ pub(crate) struct AudioItem {
79
+ /// The track's index in the prepare request.
80
+ pub(crate) track: u32,
81
+ pub(crate) sample_rate: u32,
82
+ pub(crate) channels: u32,
83
+ /// The block's admission sequence on its track.
84
+ pub(crate) sequence: u64,
85
+ pub(crate) pcm: Vec<i16>,
86
+ }
87
+
88
+ impl AudioItem {
89
+ /// Copy a block that libwebrtc lends only for the duration of its callback.
90
+ fn copy(track: u32, sequence: u64, frame: &AudioFrame<'_>) -> Self {
91
+ Self {
92
+ track,
93
+ sample_rate: frame.sample_rate,
94
+ channels: frame.channels,
95
+ sequence,
96
+ pcm: frame.pcm.to_vec(),
97
+ }
98
+ }
99
+
100
+ pub(crate) fn header(&self) -> ReactorEffectAudioHeader {
101
+ ReactorEffectAudioHeader {
102
+ sample_rate: self.sample_rate,
103
+ channels: self.channels,
104
+ samples: queued_len(self.pcm.len()),
105
+ track: self.track,
106
+ sequence: self.sequence,
107
+ }
108
+ }
109
+ }
110
+
111
+ impl QueueItem for AudioItem {
112
+ fn byte_len(&self) -> usize {
113
+ size_of_val(self.pcm.as_slice())
114
+ }
115
+ }
116
+
117
+ /// A length for a C header. Only queued items get headers, and a queue refuses
118
+ /// any item over its byte bound, which `Shared` keeps within `u32`: the
119
+ /// saturation never happens.
120
+ fn queued_len(len: usize) -> u32 {
121
+ u32::try_from(len).unwrap_or(u32::MAX)
122
+ }
123
+
124
+ pub(crate) fn kind_of(track: &RemoteTrack) -> TrackKind {
125
+ match track {
126
+ RemoteTrack::Video(_) => TrackKind::Video,
127
+ RemoteTrack::Audio(_) => TrackKind::Audio,
128
+ }
129
+ }
130
+
131
+ /// Copy a remote track's decoded frames into the media queues, labelled with
132
+ /// the declared track's index and numbered in admission order. The number is
133
+ /// taken before the queue can drop the frame, so a reader sees each drop as a
134
+ /// gap. libwebrtc delivers one track's frames on one thread, in order.
135
+ pub(crate) fn route(track: &RemoteTrack, index: u32, shared: &Arc<Shared>) {
136
+ let shared = Arc::clone(shared);
137
+ let admitted = AtomicU64::new(0);
138
+ match track {
139
+ RemoteTrack::Video(video) => video.on_frame(move |frame| {
140
+ let sequence = admitted.fetch_add(1, Ordering::Relaxed);
141
+ shared.admit(|| shared.push_video(VideoItem::copy(index, sequence, frame)));
142
+ }),
143
+ RemoteTrack::Audio(audio) => audio.on_frame(move |frame| {
144
+ let sequence = admitted.fetch_add(1, Ordering::Relaxed);
145
+ shared.admit(|| shared.push_audio(AudioItem::copy(index, sequence, &frame)));
146
+ }),
147
+ }
148
+ }
149
+
150
+ #[cfg(test)]
151
+ mod tests {
152
+ use super::*;
153
+
154
+ #[test]
155
+ fn media_items_count_their_payload_bytes() {
156
+ let video = VideoItem {
157
+ track: 0,
158
+ width: 2,
159
+ height: 1,
160
+ frame_id: 0,
161
+ timestamp_us: 0,
162
+ sequence: 0,
163
+ bgra: vec![0; 8],
164
+ metadata: vec![0; 3],
165
+ };
166
+ let audio = AudioItem {
167
+ track: 1,
168
+ sample_rate: 48_000,
169
+ channels: 2,
170
+ sequence: 0,
171
+ pcm: vec![0; 480],
172
+ };
173
+ assert_eq!(video.byte_len(), 11);
174
+ assert_eq!(audio.byte_len(), 960, "samples are two bytes each");
175
+ }
176
+
177
+ #[test]
178
+ fn headers_describe_the_queued_payload() {
179
+ let video = VideoItem {
180
+ track: 3,
181
+ width: 2,
182
+ height: 1,
183
+ frame_id: u64::MAX,
184
+ timestamp_us: 9_007_199_254_740_993,
185
+ sequence: 41,
186
+ bgra: vec![0x21; 8],
187
+ metadata: b"meta".to_vec(),
188
+ };
189
+ assert_eq!(
190
+ video.header(),
191
+ ReactorEffectVideoHeader {
192
+ width: 2,
193
+ height: 1,
194
+ data_len: 8,
195
+ metadata_len: 4,
196
+ frame_id: u64::MAX,
197
+ timestamp_us: 9_007_199_254_740_993,
198
+ track: 3,
199
+ reserved: 0,
200
+ sequence: 41,
201
+ }
202
+ );
203
+ let audio = AudioItem {
204
+ track: 1,
205
+ sample_rate: 48_000,
206
+ channels: 2,
207
+ sequence: 7,
208
+ pcm: vec![0; 960],
209
+ };
210
+ assert_eq!(
211
+ audio.header(),
212
+ ReactorEffectAudioHeader {
213
+ sample_rate: 48_000,
214
+ channels: 2,
215
+ samples: 960,
216
+ track: 1,
217
+ sequence: 7,
218
+ }
219
+ );
220
+ }
221
+ }
@@ -0,0 +1,188 @@
1
+ use super::*;
2
+ use crate::protocol::TrackKind;
3
+ use crate::sync::Taken;
4
+ use serde_json::{Value, json};
5
+
6
+ fn owner() -> Owner {
7
+ Owner::new(Arc::new(Shared::new()))
8
+ }
9
+
10
+ fn prepare_request(tracks: &Value) -> Vec<u8> {
11
+ serde_json::to_vec(&json!({ "servers": [], "tracks": tracks })).unwrap()
12
+ }
13
+
14
+ /// An owner prepared with one receive and one send track.
15
+ fn prepared() -> Owner {
16
+ let mut owner = owner();
17
+ owner
18
+ .prepare(&prepare_request(&json!([
19
+ { "name": "in", "kind": "video", "direction": "recvonly" },
20
+ { "name": "out", "kind": "video", "direction": "sendonly" },
21
+ ])))
22
+ .expect("prepare");
23
+ owner
24
+ }
25
+
26
+ #[test]
27
+ fn prepare_offers_every_track_and_maps_each_to_its_mid_in_request_order() {
28
+ let mut owner = owner();
29
+ let response = owner
30
+ .prepare(&prepare_request(&json!([
31
+ { "name": "video", "kind": "video", "direction": "recvonly" },
32
+ { "name": "audio", "kind": "audio", "direction": "recvonly" },
33
+ { "name": "camera", "kind": "video", "direction": "sendonly" },
34
+ ])))
35
+ .expect("prepare");
36
+ let response: Value = serde_json::from_slice(&response).unwrap();
37
+ let sdp = response["sdp"].as_str().unwrap();
38
+ assert!(sdp.contains("m=video") && sdp.contains("m=audio"), "{sdp}");
39
+ let mapping: Vec<Mapping> = serde_json::from_value(response["mapping"].clone()).unwrap();
40
+ let declared: Vec<_> = mapping
41
+ .iter()
42
+ .map(|entry| (entry.name.as_str(), entry.kind, entry.direction))
43
+ .collect();
44
+ assert_eq!(
45
+ declared,
46
+ [
47
+ ("video", TrackKind::Video, Direction::RecvOnly),
48
+ ("audio", TrackKind::Audio, Direction::RecvOnly),
49
+ ("camera", TrackKind::Video, Direction::SendOnly),
50
+ ]
51
+ );
52
+ for entry in &mapping {
53
+ assert!(sdp.contains(&format!("a=mid:{}", entry.mid)), "{entry:?}");
54
+ }
55
+ owner.shutdown();
56
+ }
57
+
58
+ #[test]
59
+ fn a_peer_prepares_once() {
60
+ let mut owner = prepared();
61
+ let error = owner.prepare(&prepare_request(&json!([]))).unwrap_err();
62
+ assert_eq!(
63
+ error,
64
+ BridgeError::invalid("native peer is already prepared")
65
+ );
66
+ owner.shutdown();
67
+ }
68
+
69
+ #[test]
70
+ fn a_rejected_answer_is_classified_as_sdp_rejected() {
71
+ let owner = prepared();
72
+ let error = owner
73
+ .answer(b"v=0\r\nthis is not an answer\r\n")
74
+ .expect_err("libwebrtc must reject a malformed answer");
75
+ assert_eq!(error.class, FailureClass::SdpRejected);
76
+ assert!(
77
+ error.message.starts_with("set_remote_description: "),
78
+ "{error}"
79
+ );
80
+ owner.shutdown();
81
+ }
82
+
83
+ #[test]
84
+ fn an_empty_or_non_utf8_answer_is_invalid_input() {
85
+ let owner = prepared();
86
+ assert_eq!(
87
+ owner.answer(b"").unwrap_err(),
88
+ BridgeError::invalid("answer SDP is empty")
89
+ );
90
+ let error = owner.answer(&[0xff, 0xfe]).unwrap_err();
91
+ assert_eq!(error.class, FailureClass::InvalidInput);
92
+ assert!(
93
+ error.message.starts_with("answer SDP is not UTF-8: "),
94
+ "{error}"
95
+ );
96
+ owner.shutdown();
97
+ }
98
+
99
+ #[test]
100
+ fn before_prepare_each_call_fails_with_its_class() {
101
+ let owner = owner();
102
+ assert_eq!(owner.answer(b"v=0").unwrap_err(), BridgeError::closed());
103
+ assert_eq!(owner.stats().unwrap_err(), BridgeError::closed());
104
+ let error = owner
105
+ .set_direction(br#"{"name":"out","active":false}"#)
106
+ .unwrap_err();
107
+ assert_eq!(error, BridgeError::invalid("unknown track: out"));
108
+ let error = owner.send(Channel::Data, b"early").unwrap_err();
109
+ assert_eq!(
110
+ error,
111
+ BridgeError::new(FailureClass::ChannelClosed, "data channel is not open")
112
+ );
113
+ owner.shutdown();
114
+ }
115
+
116
+ #[test]
117
+ fn a_declared_track_pauses_resumes_and_caps_its_bitrate() {
118
+ let owner = prepared();
119
+ for request in [
120
+ br#"{"name":"out","active":false}"#.as_slice(),
121
+ br#"{"name":"out","active":true}"#,
122
+ br#"{"name":"in","active":false}"#,
123
+ br#"{"name":"in","active":true}"#,
124
+ ] {
125
+ let response = owner.set_direction(request).expect("direction");
126
+ assert_eq!(response, b"{}");
127
+ }
128
+ let response = owner
129
+ .set_max_bitrate(br#"{"name":"out","bitsPerSecond":2000000}"#)
130
+ .expect("bitrate");
131
+ assert_eq!(response, b"{}");
132
+ owner.shutdown();
133
+ }
134
+
135
+ #[test]
136
+ fn only_a_declared_sending_track_takes_a_bitrate() {
137
+ let owner = prepared();
138
+ assert_eq!(
139
+ owner
140
+ .set_max_bitrate(br#"{"name":"in","bitsPerSecond":1000}"#)
141
+ .unwrap_err(),
142
+ BridgeError::invalid("in is not an outgoing track")
143
+ );
144
+ assert_eq!(
145
+ owner
146
+ .set_max_bitrate(br#"{"name":"missing","bitsPerSecond":1000}"#)
147
+ .unwrap_err(),
148
+ BridgeError::invalid("unknown track: missing")
149
+ );
150
+ assert_eq!(
151
+ owner
152
+ .set_direction(br#"{"name":"missing","active":true}"#)
153
+ .unwrap_err(),
154
+ BridgeError::invalid("unknown track: missing")
155
+ );
156
+ owner.shutdown();
157
+ }
158
+
159
+ #[test]
160
+ fn a_prepared_peer_reports_stats_before_it_connects() {
161
+ let owner = prepared();
162
+ let stats: Value = serde_json::from_slice(&owner.stats().expect("stats")).unwrap();
163
+ assert!(stats.is_array(), "{stats}");
164
+ owner.shutdown();
165
+ }
166
+
167
+ #[test]
168
+ fn commands_queued_behind_close_are_refused() {
169
+ let mut owner = prepared();
170
+ owner.shared.close();
171
+ let refused = owner
172
+ .unless_closed(|_| -> Result<(), BridgeError> { panic!("a closed peer runs no command") });
173
+ assert_eq!(refused, Err(BridgeError::closed()));
174
+ owner.shutdown();
175
+ }
176
+
177
+ #[test]
178
+ fn shutdown_closes_every_queue() {
179
+ let shared = Arc::new(Shared::new());
180
+ let mut owner = Owner::new(Arc::clone(&shared));
181
+ owner
182
+ .prepare(&prepare_request(&json!([])))
183
+ .expect("prepare");
184
+ owner.shutdown();
185
+ assert_eq!(shared.events.take(|_| true), Taken::Closed);
186
+ assert_eq!(shared.video.take(|_| true), Taken::Closed);
187
+ assert_eq!(shared.audio.take(|_| true), Taken::Closed);
188
+ }