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.
- package/Dockerfile +4 -2
- package/README.md +90 -15
- package/dist/_internal/bridge.d.ts +70 -14
- package/dist/_internal/bridge.d.ts.map +1 -1
- package/dist/_internal/bridge.js +290 -145
- package/dist/_internal/bridge.js.map +1 -1
- package/dist/_internal/isolated/child.d.ts +2 -0
- package/dist/_internal/isolated/child.d.ts.map +1 -0
- package/dist/_internal/isolated/child.js +176 -0
- package/dist/_internal/isolated/child.js.map +1 -0
- package/dist/_internal/isolated/host.d.ts +147 -0
- package/dist/_internal/isolated/host.d.ts.map +1 -0
- package/dist/_internal/isolated/host.js +645 -0
- package/dist/_internal/isolated/host.js.map +1 -0
- package/dist/_internal/isolated/protocol.d.ts +399 -0
- package/dist/_internal/isolated/protocol.d.ts.map +1 -0
- package/dist/_internal/isolated/protocol.js +250 -0
- package/dist/_internal/isolated/protocol.js.map +1 -0
- package/dist/_internal/peer.d.ts +68 -13
- package/dist/_internal/peer.d.ts.map +1 -1
- package/dist/_internal/peer.js +233 -157
- package/dist/_internal/peer.js.map +1 -1
- package/dist/index.d.ts +19 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +37 -33
- package/dist/index.js.map +1 -1
- package/dist/isolated.d.ts +25 -0
- package/dist/isolated.d.ts.map +1 -0
- package/dist/isolated.js +39 -0
- package/dist/isolated.js.map +1 -0
- package/lib/darwin-arm64/libreactor_effect_native.dylib +0 -0
- package/lib/darwin-arm64/native-identity.json +4 -3
- package/lib/linux-x64/libreactor_effect_native.so +0 -0
- package/lib/linux-x64/native-identity.json +4 -3
- package/package.json +7 -3
- package/rust/Cargo.toml +108 -2
- package/rust/build.rs +253 -114
- package/rust/clippy.toml +7 -0
- package/rust/include/reactor_effect_native.h +101 -42
- package/rust/src/abi.rs +258 -0
- package/rust/src/error.rs +149 -0
- package/rust/src/ffi/memory.rs +256 -0
- package/rust/src/ffi/tests.rs +719 -0
- package/rust/src/ffi.rs +474 -0
- package/rust/src/lib.rs +36 -2341
- package/rust/src/peer/callbacks.rs +145 -0
- package/rust/src/peer/media.rs +221 -0
- package/rust/src/peer/owner/tests.rs +188 -0
- package/rust/src/peer/owner.rs +353 -0
- package/rust/src/peer/shared.rs +323 -0
- package/rust/src/peer/tests.rs +513 -0
- package/rust/src/peer.rs +189 -0
- package/rust/src/protocol/event.rs +238 -0
- package/rust/src/protocol/request.rs +347 -0
- package/rust/src/protocol/stats.rs +356 -0
- package/rust/src/protocol.rs +71 -0
- package/rust/src/sync/gate.rs +159 -0
- package/rust/src/sync/notifier.rs +136 -0
- package/rust/src/sync/queue.rs +384 -0
- package/rust/src/sync.rs +20 -0
- package/rust/src/test_support.rs +99 -0
- package/rust-toolchain.toml +7 -0
- package/scripts/stage.mjs +41 -1
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
//! The `Stats` and `MediaSnapshot` responses.
|
|
2
|
+
|
|
3
|
+
use super::DecimalU64;
|
|
4
|
+
use reactor_webrtc::{
|
|
5
|
+
IceCandidatePairState, IceCandidateType, RelayProtocol, StatsReport, StreamKind,
|
|
6
|
+
};
|
|
7
|
+
use serde::Serialize;
|
|
8
|
+
use serde_json::{Value, json};
|
|
9
|
+
|
|
10
|
+
/// The `Stats` response: an `RTCStatsReport`-shaped array with an entry per
|
|
11
|
+
/// inbound and outbound RTP stream and, per ICE candidate pair, the pair and
|
|
12
|
+
/// its local candidate. The host reads the pairs to tell an ICE failure from
|
|
13
|
+
/// a transport failure above it.
|
|
14
|
+
pub(crate) fn stats_json(report: &StatsReport) -> Value {
|
|
15
|
+
let inbound = report.inbound_rtp.iter().map(|entry| {
|
|
16
|
+
json!({
|
|
17
|
+
"id": format!("inbound-rtp-{}", entry.ssrc),
|
|
18
|
+
"type": "inbound-rtp",
|
|
19
|
+
"ssrc": entry.ssrc,
|
|
20
|
+
"kind": stream_kind(entry.kind),
|
|
21
|
+
"packetsReceived": entry.packets_received,
|
|
22
|
+
"bytesReceived": DecimalU64(entry.bytes_received),
|
|
23
|
+
"jitter": entry.jitter_s,
|
|
24
|
+
"packetsLost": entry.packets_lost,
|
|
25
|
+
"nackCount": entry.nack_count,
|
|
26
|
+
"pliCount": entry.pli_count,
|
|
27
|
+
"firCount": entry.fir_count,
|
|
28
|
+
"totalDecodeTime": entry.total_decode_time_s,
|
|
29
|
+
"framesPerSecond": entry.frames_per_second,
|
|
30
|
+
"framesDecoded": entry.frames_decoded,
|
|
31
|
+
"framesDropped": entry.frames_dropped,
|
|
32
|
+
"frameWidth": entry.frame_width,
|
|
33
|
+
"frameHeight": entry.frame_height,
|
|
34
|
+
})
|
|
35
|
+
});
|
|
36
|
+
let outbound = report.outbound_rtp.iter().map(|entry| {
|
|
37
|
+
json!({
|
|
38
|
+
"id": format!("outbound-rtp-{}", entry.ssrc),
|
|
39
|
+
"type": "outbound-rtp",
|
|
40
|
+
"ssrc": entry.ssrc,
|
|
41
|
+
"kind": stream_kind(entry.kind),
|
|
42
|
+
"packetsSent": DecimalU64(entry.packets_sent),
|
|
43
|
+
"bytesSent": DecimalU64(entry.bytes_sent),
|
|
44
|
+
"targetBitrate": entry.target_bitrate_bps,
|
|
45
|
+
"roundTripTime": entry.round_trip_time_s,
|
|
46
|
+
"totalRoundTripTime": entry.total_round_trip_time_s,
|
|
47
|
+
"fractionLost": entry.fraction_lost,
|
|
48
|
+
"packetsLost": entry.packets_lost,
|
|
49
|
+
"retransmittedPacketsSent": DecimalU64(entry.retransmitted_packets_sent),
|
|
50
|
+
"nackCount": entry.nack_count,
|
|
51
|
+
"pliCount": entry.pli_count,
|
|
52
|
+
"firCount": entry.fir_count,
|
|
53
|
+
"framesPerSecond": entry.frames_per_second,
|
|
54
|
+
"framesSent": entry.frames_sent,
|
|
55
|
+
"frameWidth": entry.frame_width,
|
|
56
|
+
"frameHeight": entry.frame_height,
|
|
57
|
+
})
|
|
58
|
+
});
|
|
59
|
+
let pairs = report
|
|
60
|
+
.candidate_pairs
|
|
61
|
+
.iter()
|
|
62
|
+
.enumerate()
|
|
63
|
+
.flat_map(|(index, pair)| {
|
|
64
|
+
let local_id = format!("local-candidate-{index}");
|
|
65
|
+
[
|
|
66
|
+
json!({
|
|
67
|
+
"id": local_id,
|
|
68
|
+
"type": "local-candidate",
|
|
69
|
+
"candidateType": candidate_type(pair.local_candidate_type),
|
|
70
|
+
"relayProtocol": relay_protocol(pair.local_relay_protocol),
|
|
71
|
+
}),
|
|
72
|
+
json!({
|
|
73
|
+
"id": format!("candidate-pair-{index}"),
|
|
74
|
+
"type": "candidate-pair",
|
|
75
|
+
"state": pair_state(pair.state),
|
|
76
|
+
"nominated": pair.nominated,
|
|
77
|
+
"writable": pair.writable,
|
|
78
|
+
"priority": DecimalU64(pair.priority),
|
|
79
|
+
"bytesSent": DecimalU64(pair.bytes_sent),
|
|
80
|
+
"bytesReceived": DecimalU64(pair.bytes_received),
|
|
81
|
+
"packetsSent": DecimalU64(pair.packets_sent),
|
|
82
|
+
"packetsReceived": DecimalU64(pair.packets_received),
|
|
83
|
+
"currentRoundTripTime": pair.current_round_trip_time_s,
|
|
84
|
+
"totalRoundTripTime": pair.total_round_trip_time_s,
|
|
85
|
+
"availableOutgoingBitrate": pair.available_outgoing_bitrate_bps,
|
|
86
|
+
"availableIncomingBitrate": pair.available_incoming_bitrate_bps,
|
|
87
|
+
"localCandidateId": local_id,
|
|
88
|
+
}),
|
|
89
|
+
]
|
|
90
|
+
});
|
|
91
|
+
inbound.chain(outbound).chain(pairs).collect()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
fn stream_kind(kind: StreamKind) -> &'static str {
|
|
95
|
+
match kind {
|
|
96
|
+
StreamKind::Audio => "audio",
|
|
97
|
+
StreamKind::Video => "video",
|
|
98
|
+
StreamKind::Unknown => "unknown",
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fn pair_state(state: IceCandidatePairState) -> &'static str {
|
|
103
|
+
match state {
|
|
104
|
+
IceCandidatePairState::Waiting => "waiting",
|
|
105
|
+
IceCandidatePairState::InProgress => "in-progress",
|
|
106
|
+
IceCandidatePairState::Failed => "failed",
|
|
107
|
+
IceCandidatePairState::Succeeded => "succeeded",
|
|
108
|
+
IceCandidatePairState::Cancelled => "cancelled",
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
fn candidate_type(kind: IceCandidateType) -> &'static str {
|
|
113
|
+
match kind {
|
|
114
|
+
IceCandidateType::Host => "host",
|
|
115
|
+
IceCandidateType::Srflx => "srflx",
|
|
116
|
+
IceCandidateType::Prflx => "prflx",
|
|
117
|
+
IceCandidateType::Relay => "relay",
|
|
118
|
+
IceCandidateType::Unknown => "unknown",
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
fn relay_protocol(protocol: RelayProtocol) -> &'static str {
|
|
123
|
+
match protocol {
|
|
124
|
+
RelayProtocol::Udp => "udp",
|
|
125
|
+
RelayProtocol::Tcp => "tcp",
|
|
126
|
+
RelayProtocol::Tls => "tls",
|
|
127
|
+
RelayProtocol::NotRelayed => "",
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/// The `MediaSnapshot` response: what each queue dropped, delivered and still
|
|
132
|
+
/// holds.
|
|
133
|
+
#[derive(Debug, Serialize)]
|
|
134
|
+
#[serde(rename_all = "camelCase")]
|
|
135
|
+
pub(crate) struct MediaSnapshot {
|
|
136
|
+
pub(crate) closed: bool,
|
|
137
|
+
pub(crate) queued_control: usize,
|
|
138
|
+
pub(crate) queued_video: usize,
|
|
139
|
+
pub(crate) queued_audio: usize,
|
|
140
|
+
pub(crate) queued_bytes: usize,
|
|
141
|
+
pub(crate) dropped_video: DecimalU64,
|
|
142
|
+
pub(crate) dropped_audio: DecimalU64,
|
|
143
|
+
pub(crate) delivered_video: DecimalU64,
|
|
144
|
+
pub(crate) delivered_audio: DecimalU64,
|
|
145
|
+
/// Always 0: calls wait on the owner thread, never in a native queue.
|
|
146
|
+
pub(crate) pending_requests: usize,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
#[cfg(test)]
|
|
150
|
+
mod tests {
|
|
151
|
+
use super::*;
|
|
152
|
+
use reactor_webrtc::{IceCandidatePairStats, InboundRtpStats, OutboundRtpStats};
|
|
153
|
+
|
|
154
|
+
const BEYOND_DOUBLES: u64 = (1 << 53) + 1;
|
|
155
|
+
|
|
156
|
+
fn report() -> StatsReport {
|
|
157
|
+
StatsReport {
|
|
158
|
+
inbound_rtp: vec![InboundRtpStats {
|
|
159
|
+
ssrc: 11,
|
|
160
|
+
kind: StreamKind::Video,
|
|
161
|
+
packets_received: 5,
|
|
162
|
+
bytes_received: BEYOND_DOUBLES,
|
|
163
|
+
jitter_s: 0.25,
|
|
164
|
+
packets_lost: -1,
|
|
165
|
+
nack_count: 2,
|
|
166
|
+
pli_count: 3,
|
|
167
|
+
fir_count: 4,
|
|
168
|
+
total_decode_time_s: 1.5,
|
|
169
|
+
frames_per_second: 24.0,
|
|
170
|
+
frames_decoded: 90,
|
|
171
|
+
frames_dropped: 1,
|
|
172
|
+
frame_width: 64,
|
|
173
|
+
frame_height: 48,
|
|
174
|
+
}],
|
|
175
|
+
outbound_rtp: vec![OutboundRtpStats {
|
|
176
|
+
ssrc: 22,
|
|
177
|
+
kind: StreamKind::Audio,
|
|
178
|
+
packets_sent: 6,
|
|
179
|
+
bytes_sent: 7,
|
|
180
|
+
target_bitrate_bps: 32_000.0,
|
|
181
|
+
round_trip_time_s: 0.5,
|
|
182
|
+
total_round_trip_time_s: 2.0,
|
|
183
|
+
fraction_lost: 0.125,
|
|
184
|
+
packets_lost: 8,
|
|
185
|
+
retransmitted_packets_sent: 9,
|
|
186
|
+
nack_count: 10,
|
|
187
|
+
pli_count: 0,
|
|
188
|
+
fir_count: 0,
|
|
189
|
+
frames_per_second: 0.0,
|
|
190
|
+
frames_sent: 0,
|
|
191
|
+
frame_width: 0,
|
|
192
|
+
frame_height: 0,
|
|
193
|
+
}],
|
|
194
|
+
candidate_pairs: vec![IceCandidatePairStats {
|
|
195
|
+
current_round_trip_time_s: 0.25,
|
|
196
|
+
total_round_trip_time_s: 0.75,
|
|
197
|
+
priority: u64::MAX,
|
|
198
|
+
state: IceCandidatePairState::Succeeded,
|
|
199
|
+
nominated: true,
|
|
200
|
+
writable: true,
|
|
201
|
+
available_outgoing_bitrate_bps: 1_000_000.0,
|
|
202
|
+
available_incoming_bitrate_bps: 0.0,
|
|
203
|
+
bytes_sent: 12,
|
|
204
|
+
bytes_received: 13,
|
|
205
|
+
packets_sent: 14,
|
|
206
|
+
packets_received: 15,
|
|
207
|
+
local_candidate_type: IceCandidateType::Relay,
|
|
208
|
+
local_relay_protocol: RelayProtocol::Tls,
|
|
209
|
+
}],
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// `connectionFailure` and `statsValue` in packages/native/src/_internal/peer.ts
|
|
214
|
+
// read these entries; every counter in its `statsBigInts` travels as a
|
|
215
|
+
// decimal string.
|
|
216
|
+
#[test]
|
|
217
|
+
fn stats_have_the_entries_and_keys_the_host_reads() {
|
|
218
|
+
assert_eq!(
|
|
219
|
+
stats_json(&report()),
|
|
220
|
+
json!([
|
|
221
|
+
{
|
|
222
|
+
"id": "inbound-rtp-11",
|
|
223
|
+
"type": "inbound-rtp",
|
|
224
|
+
"ssrc": 11,
|
|
225
|
+
"kind": "video",
|
|
226
|
+
"packetsReceived": 5,
|
|
227
|
+
"bytesReceived": "9007199254740993",
|
|
228
|
+
"jitter": 0.25,
|
|
229
|
+
"packetsLost": -1,
|
|
230
|
+
"nackCount": 2,
|
|
231
|
+
"pliCount": 3,
|
|
232
|
+
"firCount": 4,
|
|
233
|
+
"totalDecodeTime": 1.5,
|
|
234
|
+
"framesPerSecond": 24.0,
|
|
235
|
+
"framesDecoded": 90,
|
|
236
|
+
"framesDropped": 1,
|
|
237
|
+
"frameWidth": 64,
|
|
238
|
+
"frameHeight": 48,
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
"id": "outbound-rtp-22",
|
|
242
|
+
"type": "outbound-rtp",
|
|
243
|
+
"ssrc": 22,
|
|
244
|
+
"kind": "audio",
|
|
245
|
+
"packetsSent": "6",
|
|
246
|
+
"bytesSent": "7",
|
|
247
|
+
"targetBitrate": 32_000.0,
|
|
248
|
+
"roundTripTime": 0.5,
|
|
249
|
+
"totalRoundTripTime": 2.0,
|
|
250
|
+
"fractionLost": 0.125,
|
|
251
|
+
"packetsLost": 8,
|
|
252
|
+
"retransmittedPacketsSent": "9",
|
|
253
|
+
"nackCount": 10,
|
|
254
|
+
"pliCount": 0,
|
|
255
|
+
"firCount": 0,
|
|
256
|
+
"framesPerSecond": 0.0,
|
|
257
|
+
"framesSent": 0,
|
|
258
|
+
"frameWidth": 0,
|
|
259
|
+
"frameHeight": 0,
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
"id": "local-candidate-0",
|
|
263
|
+
"type": "local-candidate",
|
|
264
|
+
"candidateType": "relay",
|
|
265
|
+
"relayProtocol": "tls",
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
"id": "candidate-pair-0",
|
|
269
|
+
"type": "candidate-pair",
|
|
270
|
+
"state": "succeeded",
|
|
271
|
+
"nominated": true,
|
|
272
|
+
"writable": true,
|
|
273
|
+
"priority": "18446744073709551615",
|
|
274
|
+
"bytesSent": "12",
|
|
275
|
+
"bytesReceived": "13",
|
|
276
|
+
"packetsSent": "14",
|
|
277
|
+
"packetsReceived": "15",
|
|
278
|
+
"currentRoundTripTime": 0.25,
|
|
279
|
+
"totalRoundTripTime": 0.75,
|
|
280
|
+
"availableOutgoingBitrate": 1_000_000.0,
|
|
281
|
+
"availableIncomingBitrate": 0.0,
|
|
282
|
+
"localCandidateId": "local-candidate-0",
|
|
283
|
+
},
|
|
284
|
+
])
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
#[test]
|
|
289
|
+
fn an_empty_report_is_an_empty_array() {
|
|
290
|
+
assert_eq!(stats_json(&StatsReport::default()), json!([]));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
#[test]
|
|
294
|
+
fn every_pair_state_candidate_type_and_relay_protocol_has_a_stats_name() {
|
|
295
|
+
let states = [
|
|
296
|
+
IceCandidatePairState::Waiting,
|
|
297
|
+
IceCandidatePairState::InProgress,
|
|
298
|
+
IceCandidatePairState::Failed,
|
|
299
|
+
IceCandidatePairState::Succeeded,
|
|
300
|
+
IceCandidatePairState::Cancelled,
|
|
301
|
+
]
|
|
302
|
+
.map(pair_state);
|
|
303
|
+
assert_eq!(
|
|
304
|
+
states,
|
|
305
|
+
["waiting", "in-progress", "failed", "succeeded", "cancelled"]
|
|
306
|
+
);
|
|
307
|
+
let types = [
|
|
308
|
+
IceCandidateType::Host,
|
|
309
|
+
IceCandidateType::Srflx,
|
|
310
|
+
IceCandidateType::Prflx,
|
|
311
|
+
IceCandidateType::Relay,
|
|
312
|
+
IceCandidateType::Unknown,
|
|
313
|
+
]
|
|
314
|
+
.map(candidate_type);
|
|
315
|
+
assert_eq!(types, ["host", "srflx", "prflx", "relay", "unknown"]);
|
|
316
|
+
let protocols = [
|
|
317
|
+
RelayProtocol::Udp,
|
|
318
|
+
RelayProtocol::Tcp,
|
|
319
|
+
RelayProtocol::Tls,
|
|
320
|
+
RelayProtocol::NotRelayed,
|
|
321
|
+
]
|
|
322
|
+
.map(relay_protocol);
|
|
323
|
+
assert_eq!(protocols, ["udp", "tcp", "tls", ""]);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
#[test]
|
|
327
|
+
fn a_snapshot_has_the_keys_the_host_parses() {
|
|
328
|
+
let snapshot = MediaSnapshot {
|
|
329
|
+
closed: false,
|
|
330
|
+
queued_control: 1,
|
|
331
|
+
queued_video: 2,
|
|
332
|
+
queued_audio: 3,
|
|
333
|
+
queued_bytes: 4,
|
|
334
|
+
dropped_video: DecimalU64(BEYOND_DOUBLES),
|
|
335
|
+
dropped_audio: DecimalU64(5),
|
|
336
|
+
delivered_video: DecimalU64(6),
|
|
337
|
+
delivered_audio: DecimalU64(7),
|
|
338
|
+
pending_requests: 0,
|
|
339
|
+
};
|
|
340
|
+
assert_eq!(
|
|
341
|
+
serde_json::to_value(&snapshot).unwrap(),
|
|
342
|
+
json!({
|
|
343
|
+
"closed": false,
|
|
344
|
+
"queuedControl": 1,
|
|
345
|
+
"queuedVideo": 2,
|
|
346
|
+
"queuedAudio": 3,
|
|
347
|
+
"queuedBytes": 4,
|
|
348
|
+
"droppedVideo": "9007199254740993",
|
|
349
|
+
"droppedAudio": "5",
|
|
350
|
+
"deliveredVideo": "6",
|
|
351
|
+
"deliveredAudio": "7",
|
|
352
|
+
"pendingRequests": 0,
|
|
353
|
+
})
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
//! The JSON the bridge exchanges with its host: call requests and responses,
|
|
2
|
+
//! and the headers of transport events.
|
|
3
|
+
|
|
4
|
+
mod event;
|
|
5
|
+
mod request;
|
|
6
|
+
mod stats;
|
|
7
|
+
|
|
8
|
+
pub(crate) use event::{Event, LocalCandidate, connection_state};
|
|
9
|
+
pub(crate) use request::{
|
|
10
|
+
BitrateRequest, Direction, DirectionRequest, Mapping, PrepareRequest, PrepareResponse,
|
|
11
|
+
TrackKind,
|
|
12
|
+
};
|
|
13
|
+
pub(crate) use stats::{MediaSnapshot, stats_json};
|
|
14
|
+
|
|
15
|
+
use crate::error::{BridgeError, FailureClass};
|
|
16
|
+
use serde::de::DeserializeOwned;
|
|
17
|
+
use serde::{Serialize, Serializer};
|
|
18
|
+
|
|
19
|
+
/// A `u64` for the JavaScript host, which reads JSON numbers as doubles and
|
|
20
|
+
/// would round counters past 2^53, so it travels as a decimal string.
|
|
21
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
22
|
+
pub(crate) struct DecimalU64(pub(crate) u64);
|
|
23
|
+
|
|
24
|
+
impl Serialize for DecimalU64 {
|
|
25
|
+
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
|
26
|
+
serializer.collect_str(&self.0)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// Decode a JSON request. Any failure is invalid input.
|
|
31
|
+
pub(crate) fn decode<T: DeserializeOwned>(request: &[u8]) -> Result<T, BridgeError> {
|
|
32
|
+
serde_json::from_slice(request)
|
|
33
|
+
.map_err(|error| BridgeError::invalid(format!("invalid JSON: {error}")))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// Encode a JSON response or event header.
|
|
37
|
+
pub(crate) fn encode(value: &impl Serialize) -> Result<Vec<u8>, BridgeError> {
|
|
38
|
+
serde_json::to_vec(value)
|
|
39
|
+
.map_err(|error| BridgeError::new(FailureClass::Native, format!("serialize JSON: {error}")))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// The response of a call that returns nothing.
|
|
43
|
+
pub(crate) fn empty_response() -> Vec<u8> {
|
|
44
|
+
b"{}".to_vec()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#[cfg(test)]
|
|
48
|
+
mod tests {
|
|
49
|
+
use super::*;
|
|
50
|
+
use serde_json::json;
|
|
51
|
+
|
|
52
|
+
#[test]
|
|
53
|
+
fn counters_beyond_double_precision_travel_as_exact_decimal_strings() {
|
|
54
|
+
let counters = [
|
|
55
|
+
DecimalU64(0),
|
|
56
|
+
DecimalU64(9_007_199_254_740_993),
|
|
57
|
+
DecimalU64(u64::MAX),
|
|
58
|
+
];
|
|
59
|
+
assert_eq!(
|
|
60
|
+
serde_json::to_value(counters).unwrap(),
|
|
61
|
+
json!(["0", "9007199254740993", "18446744073709551615"])
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#[test]
|
|
66
|
+
fn a_malformed_request_is_invalid_input() {
|
|
67
|
+
let error = decode::<Vec<u8>>(b"{").unwrap_err();
|
|
68
|
+
assert_eq!(error.class, FailureClass::InvalidInput);
|
|
69
|
+
assert!(error.message.starts_with("invalid JSON: "), "{error}");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
//! Admission of libwebrtc callbacks, fenced by close and awaited by shutdown.
|
|
2
|
+
|
|
3
|
+
use super::lock;
|
|
4
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
|
5
|
+
use std::sync::{Condvar, Mutex, PoisonError};
|
|
6
|
+
|
|
7
|
+
/// Admits libwebrtc callbacks until closed, and counts the admitted ones so
|
|
8
|
+
/// shutdown can wait for every callback that is still running.
|
|
9
|
+
pub(crate) struct CallbackGate {
|
|
10
|
+
open: AtomicBool,
|
|
11
|
+
running: Mutex<usize>,
|
|
12
|
+
idle: Condvar,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
impl CallbackGate {
|
|
16
|
+
/// An open gate.
|
|
17
|
+
pub(crate) fn new() -> Self {
|
|
18
|
+
Self {
|
|
19
|
+
open: AtomicBool::new(true),
|
|
20
|
+
running: Mutex::new(0),
|
|
21
|
+
idle: Condvar::new(),
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// Whether the gate still admits callbacks and host calls.
|
|
26
|
+
pub(crate) fn is_open(&self) -> bool {
|
|
27
|
+
self.open.load(Ordering::Acquire)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// Admit one callback, or refuse it once the gate has closed. The callback
|
|
31
|
+
/// counts as running until the guard drops.
|
|
32
|
+
pub(crate) fn enter(&self) -> Option<CallbackGuard<'_>> {
|
|
33
|
+
if !self.is_open() {
|
|
34
|
+
return None;
|
|
35
|
+
}
|
|
36
|
+
let mut running = lock(&self.running);
|
|
37
|
+
// `wait_idle` takes this lock after `close`, so re-checking under it
|
|
38
|
+
// means a callback is either refused or counted before anyone waits.
|
|
39
|
+
if !self.is_open() {
|
|
40
|
+
return None;
|
|
41
|
+
}
|
|
42
|
+
*running += 1;
|
|
43
|
+
Some(CallbackGuard { gate: self })
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// Stop admitting callbacks. Callbacks already admitted keep running.
|
|
47
|
+
pub(crate) fn close(&self) {
|
|
48
|
+
self.open.store(false, Ordering::Release);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// Block until no admitted callback is running. Called after [`close`],
|
|
52
|
+
/// it returns once no callback can run again.
|
|
53
|
+
///
|
|
54
|
+
/// [`close`]: Self::close
|
|
55
|
+
pub(crate) fn wait_idle(&self) {
|
|
56
|
+
let _idle = self
|
|
57
|
+
.idle
|
|
58
|
+
.wait_while(lock(&self.running), |running| *running > 0)
|
|
59
|
+
.unwrap_or_else(PoisonError::into_inner);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
impl Default for CallbackGate {
|
|
64
|
+
fn default() -> Self {
|
|
65
|
+
Self::new()
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// One admitted callback. Dropping it lets a waiting shutdown proceed.
|
|
70
|
+
pub(crate) struct CallbackGuard<'gate> {
|
|
71
|
+
gate: &'gate CallbackGate,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
impl Drop for CallbackGuard<'_> {
|
|
75
|
+
fn drop(&mut self) {
|
|
76
|
+
let mut running = lock(&self.gate.running);
|
|
77
|
+
*running -= 1;
|
|
78
|
+
if *running == 0 {
|
|
79
|
+
self.gate.idle.notify_all();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#[cfg(test)]
|
|
85
|
+
mod tests {
|
|
86
|
+
use super::*;
|
|
87
|
+
use crate::test_support::{Defer, wait_until};
|
|
88
|
+
use std::sync::atomic::AtomicUsize;
|
|
89
|
+
use std::thread;
|
|
90
|
+
use std::time::Duration;
|
|
91
|
+
|
|
92
|
+
#[test]
|
|
93
|
+
fn close_refuses_new_callbacks_and_wait_idle_waits_for_admitted_ones() {
|
|
94
|
+
let gate = CallbackGate::new();
|
|
95
|
+
let guard = gate.enter().expect("an open gate admits");
|
|
96
|
+
gate.close();
|
|
97
|
+
assert!(!gate.is_open());
|
|
98
|
+
assert!(gate.enter().is_none(), "a closed gate must refuse");
|
|
99
|
+
|
|
100
|
+
thread::scope(|scope| {
|
|
101
|
+
let waiter = scope.spawn(|| gate.wait_idle());
|
|
102
|
+
thread::sleep(Duration::from_millis(10));
|
|
103
|
+
assert!(
|
|
104
|
+
!waiter.is_finished(),
|
|
105
|
+
"wait_idle returned while a callback was running"
|
|
106
|
+
);
|
|
107
|
+
drop(guard);
|
|
108
|
+
waiter.join().unwrap();
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
#[test]
|
|
113
|
+
fn wait_idle_returns_at_once_when_nothing_was_admitted() {
|
|
114
|
+
let gate = CallbackGate::default();
|
|
115
|
+
gate.close();
|
|
116
|
+
gate.wait_idle();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
#[test]
|
|
120
|
+
fn no_callback_runs_once_close_and_wait_idle_return() {
|
|
121
|
+
let gate = CallbackGate::new();
|
|
122
|
+
let running = AtomicUsize::new(0);
|
|
123
|
+
let admitted = AtomicUsize::new(0);
|
|
124
|
+
let stop = AtomicBool::new(false);
|
|
125
|
+
thread::scope(|scope| {
|
|
126
|
+
let _stop_callers = Defer(|| stop.store(true, Ordering::Relaxed));
|
|
127
|
+
for _ in 0..4 {
|
|
128
|
+
scope.spawn(|| {
|
|
129
|
+
while !stop.load(Ordering::Relaxed) {
|
|
130
|
+
if let Some(_guard) = gate.enter() {
|
|
131
|
+
running.fetch_add(1, Ordering::SeqCst);
|
|
132
|
+
admitted.fetch_add(1, Ordering::Relaxed);
|
|
133
|
+
thread::yield_now();
|
|
134
|
+
running.fetch_sub(1, Ordering::SeqCst);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
wait_until("admitted callbacks", Duration::from_secs(5), || {
|
|
140
|
+
admitted.load(Ordering::Relaxed) >= 1_000
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
gate.close();
|
|
144
|
+
gate.wait_idle();
|
|
145
|
+
assert_eq!(
|
|
146
|
+
running.load(Ordering::SeqCst),
|
|
147
|
+
0,
|
|
148
|
+
"a callback outlived wait_idle"
|
|
149
|
+
);
|
|
150
|
+
let admitted_at_close = admitted.load(Ordering::SeqCst);
|
|
151
|
+
thread::sleep(Duration::from_millis(5));
|
|
152
|
+
assert_eq!(
|
|
153
|
+
admitted.load(Ordering::SeqCst),
|
|
154
|
+
admitted_at_close,
|
|
155
|
+
"a callback was admitted after close"
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|