offline_protocol_reliability/constants.rs
1//! Reliability layer constants.
2
3/// Default ACK timeout in milliseconds.
4pub const DEFAULT_ACK_TIMEOUT_MS: u64 = 10000;
5
6/// Maximum number of pending ACKs to track.
7pub const DEFAULT_MAX_PENDING_ACKS: usize = 1000;
8
9/// Default maximum number of retries per message.
10pub const DEFAULT_MAX_RETRIES: u32 = 10;
11
12/// Initial retry delay in milliseconds.
13pub const DEFAULT_INITIAL_DELAY_MS: u64 = 1000;
14
15/// Maximum retry delay in milliseconds.
16///
17/// 5 minutes. Delivery latency does not ride on this ceiling: outbox
18/// flushes (start, transport reconnect, peer rediscovery, session
19/// establishment) bypass backoff timers entirely. The periodic retry is a
20/// safety net, and a lower ceiling only multiplies futile send attempts —
21/// and per-failure `MessageRetrying` events — while a peer stays offline
22/// within the 7-day outbox window.
23pub const DEFAULT_MAX_DELAY_MS: u64 = 300_000;
24
25/// Backoff multiplier for exponential backoff.
26pub const DEFAULT_BACKOFF_MULTIPLIER: f32 = 2.0;
27
28/// Maximum lifetime for messages in outbox (milliseconds).
29///
30/// 7 days, matching the app-layer presence-flush window: a peer is
31/// considered recoverable for up to 7 days offline, so queued messages
32/// must survive at least that long before the outbox gives up on them.
33pub const DEFAULT_OUTBOX_LIFETIME_MS: u64 = 604_800_000;
34
35/// How long one message id may be re-sent after its first send: 7 days.
36///
37/// The one window both ends of a delivery agree on. A sender drops an outbox
38/// entry the relay has pushed once it is this old, measured from its first
39/// send and never re-granted; a receiver's deduplicator remembers ids this
40/// long, so every replay inside the window is answered as a duplicate (with
41/// a re-ACK) instead of reaching a ratchet whose generation is already spent.
42///
43/// Pairs with the relay's push-claim TTL (one visible notification per
44/// message id) and its mailbox retention, both 7 days: past this age the
45/// relay would push the id again and its mailbox has already let it go.
46/// Fixed rather than read from `outbox_max_lifetime_ms`, because those relay
47/// windows do not move with an app's configuration.
48pub const REDELIVERY_WINDOW_MS: u64 = DEFAULT_OUTBOX_LIFETIME_MS;
49
50/// Maximum lifetime for messages waiting on MLS session establishment.
51///
52/// This is deliberately finite so an unresolved or permanently invalid
53/// recipient cannot remain in durable `pending_messages` forever.
54pub const DEFAULT_PENDING_MESSAGE_LIFETIME_MS: u64 = 604_800_000;
55
56/// Estimated size of an ACK entry in bytes (message ID + metadata).
57pub const ESTIMATED_ACK_SIZE_BYTES: usize = 40;
58
59/// Network size threshold below which all ACKs are relayed.
60pub const SMALL_NETWORK_RELAY_THRESHOLD: usize = 5;
61
62/// Density factor numerator for relay probability calculation.
63pub const RELAY_DENSITY_FACTOR_NUMERATOR: f32 = 5.0;
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68
69 /// The RN native bridges rebuild the whole `RetryConfig` from JSON in
70 /// `updateRetryConfig`, filling absent fields from hardcoded fallbacks.
71 /// A fallback that drifts from the Rust defaults silently overrides the
72 /// SDK default for every field the app didn't set (this happened once:
73 /// `maxRetries` 3 vs 10). This test pins the bridge literals to the
74 /// constants above; when a default changes here, it fails until both
75 /// bridge files are updated.
76 #[test]
77 fn rn_bridge_retry_fallbacks_match_rust_defaults() {
78 let rn_root =
79 std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../bindings/react-native");
80 let kotlin_path =
81 rn_root.join("android/src/main/java/com/offlineprotocol/ProtocolEngineHostBridge.kt");
82 let swift_path = rn_root.join("ios/ProtocolEngineHostBridge.swift");
83
84 // The guard only applies in the repo checkout; skip when the
85 // bindings tree isn't present (e.g. a vendored crate).
86 let (Ok(kotlin), Ok(swift)) = (
87 std::fs::read_to_string(&kotlin_path),
88 std::fs::read_to_string(&swift_path),
89 ) else {
90 eprintln!("bindings tree not present, skipping RN fallback drift check");
91 return;
92 };
93
94 let ack_expected = [
95 (
96 format!("json.optLong(\"defaultTimeoutMs\", {DEFAULT_ACK_TIMEOUT_MS})"),
97 format!(
98 r#"unsigned64(j, "defaultTimeoutMs", {})"#,
99 format_underscored(DEFAULT_ACK_TIMEOUT_MS)
100 ),
101 ),
102 (
103 format!("json.optLong(\"maxPendingAcks\", {DEFAULT_MAX_PENDING_ACKS})"),
104 format!(
105 r#"unsigned64(j, "maxPendingAcks", {})"#,
106 format_underscored(DEFAULT_MAX_PENDING_ACKS as u64)
107 ),
108 ),
109 ];
110 for (kotlin_fallback, swift_fallback) in &ack_expected {
111 assert!(
112 kotlin.contains(kotlin_fallback),
113 "RN Android bridge ACK fallback drifted from the Rust default: \
114 expected `{kotlin_fallback}` in {}",
115 kotlin_path.display()
116 );
117 assert!(
118 swift.contains(swift_fallback),
119 "RN iOS bridge ACK fallback drifted from the Rust default: \
120 expected `{swift_fallback}` in {}",
121 swift_path.display()
122 );
123 }
124
125 let kotlin_expected = [
126 format!("json.optInt(\"maxRetries\", {DEFAULT_MAX_RETRIES})"),
127 format!("json.optLong(\"initialDelayMs\", {DEFAULT_INITIAL_DELAY_MS})"),
128 format!("json.optLong(\"maxDelayMs\", {DEFAULT_MAX_DELAY_MS})"),
129 format!("json.optDouble(\"backoffMultiplier\", {DEFAULT_BACKOFF_MULTIPLIER:.1})"),
130 format!("json.optLong(\"outboxMaxLifetimeMs\", {DEFAULT_OUTBOX_LIFETIME_MS})"),
131 format!(
132 "json.optLong(\"pendingMessageMaxLifetimeMs\", {DEFAULT_PENDING_MESSAGE_LIFETIME_MS})"
133 ),
134 ];
135 for expected in &kotlin_expected {
136 assert!(
137 kotlin.contains(expected),
138 "RN Android bridge retry fallback drifted from the Rust default: \
139 expected `{expected}` in {}",
140 kotlin_path.display()
141 );
142 }
143
144 let swift_expected = [
145 format!(r#"unsigned32(j, "maxRetries", {DEFAULT_MAX_RETRIES})"#),
146 format!(
147 r#"unsigned64(j, "initialDelayMs", {})"#,
148 format_underscored(DEFAULT_INITIAL_DELAY_MS)
149 ),
150 format!(
151 r#"unsigned64(j, "maxDelayMs", {})"#,
152 format_underscored(DEFAULT_MAX_DELAY_MS)
153 ),
154 format!(r#"number(j, "backoffMultiplier") ?? {DEFAULT_BACKOFF_MULTIPLIER:.1}"#),
155 format!(
156 r#"unsigned64(j, "outboxMaxLifetimeMs", {})"#,
157 format_underscored(DEFAULT_OUTBOX_LIFETIME_MS)
158 ),
159 format!(
160 r#"unsigned64(j, "pendingMessageMaxLifetimeMs", {})"#,
161 format_underscored(DEFAULT_PENDING_MESSAGE_LIFETIME_MS)
162 ),
163 ];
164 for expected in &swift_expected {
165 assert!(
166 swift.contains(expected),
167 "RN iOS bridge retry fallback drifted from the Rust default: \
168 expected `{expected}` in {}",
169 swift_path.display()
170 );
171 }
172 }
173
174 /// Renders `86400000` as `86_400_000`, the form Swift bridge literals use.
175 fn format_underscored(value: u64) -> String {
176 let digits = value.to_string();
177 let mut out = String::new();
178 for (i, c) in digits.chars().enumerate() {
179 if i > 0 && (digits.len() - i) % 3 == 0 {
180 out.push('_');
181 }
182 out.push(c);
183 }
184 out
185 }
186}