Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/zero-rust-hidden-delay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'zero-rust': minor
---

`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`.
14 changes: 8 additions & 6 deletions rust/PARITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,14 @@ factory.
| `slowMaterializeThreshold` | `slow_materialize_threshold_ms` (5,000, as in JS) |
| `maxRecentQueries` (deprecated) | not ported: JS keeps no recent queries by default either |

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

### The client

Expand Down
7 changes: 5 additions & 2 deletions rust/zero-client/src/client/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,10 @@ pub struct ZeroOptions {
/// Where messages are logged. Defaults to [`StderrLogSink`].
pub log_sink: Option<Rc<dyn LogSink>>,
/// How long the app is hidden before the client disconnects
/// (`hiddenTabDisconnectDelay`); see [`Zero::set_visible`].
/// (`hiddenTabDisconnectDelay`); see [`Zero::set_visible`]. 5 minutes
/// by default, as in JS. A mobile app, which the system suspends soon
/// after it goes to the background, can set 0 to disconnect as soon as
/// it is hidden.
///
/// [`Zero::set_visible`]: crate::client::Zero::set_visible
pub hidden_disconnect_delay_ms: f64,
Expand Down Expand Up @@ -249,7 +252,7 @@ impl Default for ZeroOptions {
on_storage_failure: None,
log_level: LogLevel::Warn,
log_sink: None,
hidden_disconnect_delay_ms: 0.0,
hidden_disconnect_delay_ms: 300_000.0,
get_traceparent: None,
batch_view_updates: None,
slow_materialize_threshold_ms: 5_000.0,
Expand Down
71 changes: 71 additions & 0 deletions rust/zero-client/tests/visibility.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
//! `ZeroOptions::default()` waits JS's 5 minutes (`hiddenTabDisconnectDelay`)
//! after `set_visible(false)` before it disconnects. The conformance vectors
//! set the delay themselves, so they don't check the default.

use std::rc::Rc;
use std::sync::Arc;
use zero_client::client::{ConnectionState, Zero, ZeroOptions};
use zero_client::json;
use zero_client::schema::Schema;
use zero_client::testing::{ManualScheduler, MockSocketFactory, RecordingLogSink};

#[test]
fn a_hidden_client_disconnects_after_five_minutes_by_default() {
let schema = Schema::from_json(
&json::parse(
r#"{"tables": {"issue": {"name": "issue", "primaryKey": ["id"],
"columns": {"id": {"type": "string"}}}}}"#,
)
.unwrap(),
)
.unwrap();
let scheduler = ManualScheduler::new(1_700_000_000_000.0);
let factory = MockSocketFactory::new();
let options = ZeroOptions {
cache_url: Some("http://zero.test".into()),
user_id: Some("u1".into()),
log_sink: Some(Rc::new(RecordingLogSink::default())),
..ZeroOptions::default()
};
let zero: Zero = Zero::new(
Arc::new(schema),
(),
vec![],
options,
scheduler.clone(),
Some(factory.clone()),
)
.unwrap();
let settle = || {
zero.dispatch(|| {});
scheduler.run_due_timers();
};
settle();
let socket = factory.sockets().last().cloned().expect("a socket");
socket.receive_text(r#"["connected",{"wsid":"w1"}]"#);
settle();
assert_eq!(zero.connection().state(), ConnectionState::Connected);

zero.set_visible(false);
settle();
// Answer pings, so the connection stays up while the app is hidden.
let mut waited = 0.0;
while waited < 299_000.0 {
scheduler.advance(1_000.0);
waited += 1_000.0;
socket.receive_text(r#"["pong",{}]"#);
settle();
}
assert_eq!(zero.connection().state(), ConnectionState::Connected);

scheduler.advance(1_000.0);
settle();
assert!(
matches!(
zero.connection().state(),
ConnectionState::Disconnected { .. }
),
"{:?}",
zero.connection().state()
);
}
Loading