Skip to content

Commit d1b27f8

Browse files
tmmampagent
andauthored
feat(zero-rust): wait JS's 5 minutes before a hidden client disconnects (#214)
ZeroOptions::hidden_disconnect_delay_ms now defaults to 300,000 ms, as JS's hiddenTabDisconnectDelay does, instead of 0. Memory stays the default storage, documented in PARITY.md as deliberate. Amp-Thread-ID: https://ampcode.com/threads/T-db04f816-97ed-4bc9-b19e-9ee68611a0ab Co-authored-by: Amp <amp@ampcode.com>
1 parent 743269f commit d1b27f8

4 files changed

Lines changed: 89 additions & 8 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'zero-rust': minor
3+
---
4+
5+
`ZeroOptions::hidden_disconnect_delay_ms` now defaults to 5 minutes, as JS's `hiddenTabDisconnectDelay` does, instead of 0: after `Zero::set_visible(false)`, the client stays connected for 5 minutes before it disconnects. An app that wants the old behavior, such as a mobile app the system suspends in the background, sets `hidden_disconnect_delay_ms: 0.0`.

‎rust/PARITY.md‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,12 +78,14 @@ factory.
7878
| `slowMaterializeThreshold` | `slow_materialize_threshold_ms` (5,000, as in JS) |
7979
| `maxRecentQueries` (deprecated) | not ported: JS keeps no recent queries by default either |
8080

81-
Two defaults differ from JS. `storage` is `ZeroStorage::Memory`, since
82-
SQLite is an opt-in feature, where JS keeps its data in IndexedDB.
83-
`hidden_disconnect_delay_ms` is 0 where JS waits 5 minutes, as in Swift:
84-
a native app is suspended soon after it goes to the background, so the
85-
client disconnects as soon as the app says it is hidden. A desktop app
86-
whose window can stay hidden for long can set JS's 300,000.
81+
One default differs from JS, on purpose: `storage` is
82+
`ZeroStorage::Memory`, where JS keeps its data in IndexedDB. SQLite
83+
storage is the opt-in `sqlite` feature, so the default build has no
84+
third-party dependencies, and an app chooses where its replica lives
85+
(`ZeroStorage::Sqlite { directory }`); an app that wants its data across
86+
restarts sets it. `hidden_disconnect_delay_ms` is JS's 5 minutes; a mobile
87+
app, which the system suspends soon after it goes to the background, can
88+
set 0 to disconnect as soon as it says it is hidden, as Swift does.
8789

8890
### The client
8991

‎rust/zero-client/src/client/engine.rs‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,10 @@ pub struct ZeroOptions {
207207
/// Where messages are logged. Defaults to [`StderrLogSink`].
208208
pub log_sink: Option<Rc<dyn LogSink>>,
209209
/// How long the app is hidden before the client disconnects
210-
/// (`hiddenTabDisconnectDelay`); see [`Zero::set_visible`].
210+
/// (`hiddenTabDisconnectDelay`); see [`Zero::set_visible`]. 5 minutes
211+
/// by default, as in JS. A mobile app, which the system suspends soon
212+
/// after it goes to the background, can set 0 to disconnect as soon as
213+
/// it is hidden.
211214
///
212215
/// [`Zero::set_visible`]: crate::client::Zero::set_visible
213216
pub hidden_disconnect_delay_ms: f64,
@@ -249,7 +252,7 @@ impl Default for ZeroOptions {
249252
on_storage_failure: None,
250253
log_level: LogLevel::Warn,
251254
log_sink: None,
252-
hidden_disconnect_delay_ms: 0.0,
255+
hidden_disconnect_delay_ms: 300_000.0,
253256
get_traceparent: None,
254257
batch_view_updates: None,
255258
slow_materialize_threshold_ms: 5_000.0,
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
//! `ZeroOptions::default()` waits JS's 5 minutes (`hiddenTabDisconnectDelay`)
2+
//! after `set_visible(false)` before it disconnects. The conformance vectors
3+
//! set the delay themselves, so they don't check the default.
4+
5+
use std::rc::Rc;
6+
use std::sync::Arc;
7+
use zero_client::client::{ConnectionState, Zero, ZeroOptions};
8+
use zero_client::json;
9+
use zero_client::schema::Schema;
10+
use zero_client::testing::{ManualScheduler, MockSocketFactory, RecordingLogSink};
11+
12+
#[test]
13+
fn a_hidden_client_disconnects_after_five_minutes_by_default() {
14+
let schema = Schema::from_json(
15+
&json::parse(
16+
r#"{"tables": {"issue": {"name": "issue", "primaryKey": ["id"],
17+
"columns": {"id": {"type": "string"}}}}}"#,
18+
)
19+
.unwrap(),
20+
)
21+
.unwrap();
22+
let scheduler = ManualScheduler::new(1_700_000_000_000.0);
23+
let factory = MockSocketFactory::new();
24+
let options = ZeroOptions {
25+
cache_url: Some("http://zero.test".into()),
26+
user_id: Some("u1".into()),
27+
log_sink: Some(Rc::new(RecordingLogSink::default())),
28+
..ZeroOptions::default()
29+
};
30+
let zero: Zero = Zero::new(
31+
Arc::new(schema),
32+
(),
33+
vec![],
34+
options,
35+
scheduler.clone(),
36+
Some(factory.clone()),
37+
)
38+
.unwrap();
39+
let settle = || {
40+
zero.dispatch(|| {});
41+
scheduler.run_due_timers();
42+
};
43+
settle();
44+
let socket = factory.sockets().last().cloned().expect("a socket");
45+
socket.receive_text(r#"["connected",{"wsid":"w1"}]"#);
46+
settle();
47+
assert_eq!(zero.connection().state(), ConnectionState::Connected);
48+
49+
zero.set_visible(false);
50+
settle();
51+
// Answer pings, so the connection stays up while the app is hidden.
52+
let mut waited = 0.0;
53+
while waited < 299_000.0 {
54+
scheduler.advance(1_000.0);
55+
waited += 1_000.0;
56+
socket.receive_text(r#"["pong",{}]"#);
57+
settle();
58+
}
59+
assert_eq!(zero.connection().state(), ConnectionState::Connected);
60+
61+
scheduler.advance(1_000.0);
62+
settle();
63+
assert!(
64+
matches!(
65+
zero.connection().state(),
66+
ConnectionState::Disconnected { .. }
67+
),
68+
"{:?}",
69+
zero.connection().state()
70+
);
71+
}

0 commit comments

Comments
 (0)