firebase_rs_sdk/database/
on_disconnect.rs

1use serde_json::{Map as JsonMap, Value};
2
3use crate::database::api::{normalize_path, pack_with_priority, validate_priority_value};
4use crate::database::error::{invalid_argument, DatabaseResult};
5use crate::database::DatabaseReference;
6
7/// Async handle for scheduling writes that run when the client disconnects.
8///
9/// Mirrors the surface of the JS SDK (`packages/database/src/api/OnDisconnect.ts`).
10/// Operations resolve server value placeholders before being sent to the realtime
11/// backend and require an active WebSocket transport for full server-side
12/// semantics. When the runtime falls back to the HTTP long-poll transport the
13/// commands are queued locally and flushed when `Database::go_offline()` runs,
14/// which preserves graceful shutdowns but cannot detect abrupt connection loss.
15#[derive(Clone, Debug)]
16pub struct OnDisconnect {
17    reference: DatabaseReference,
18}
19
20impl OnDisconnect {
21    pub(crate) fn new(reference: DatabaseReference) -> Self {
22        Self { reference }
23    }
24
25    /// Schedules a write for when the client disconnects.
26    ///
27    /// Mirrors `OnDisconnect.set()` from the JS SDK. The payload is normalised
28    /// using the same server timestamp/increment resolution as immediate writes
29    /// so placeholders resolve against the current backend value.
30    pub async fn set<V>(&self, value: V) -> DatabaseResult<()>
31    where
32        V: Into<Value>,
33    {
34        let resolved = self
35            .reference
36            .resolve_for_current_path(value.into())
37            .await?;
38        self.reference
39            .database()
40            .repo()
41            .on_disconnect_put(self.reference.path_segments(), resolved)
42            .await
43    }
44
45    /// Schedules a write together with its priority for disconnect.
46    pub async fn set_with_priority<V, P>(&self, value: V, priority: P) -> DatabaseResult<()>
47    where
48        V: Into<Value>,
49        P: Into<Value>,
50    {
51        let priority = priority.into();
52        validate_priority_value(&priority)?;
53        if matches!(self.reference.key(), Some(".length" | ".keys")) {
54            return Err(invalid_argument(
55                "set_with_priority failed: read-only child key",
56            ));
57        }
58
59        let resolved = self
60            .reference
61            .resolve_for_current_path(value.into())
62            .await?;
63        let payload = pack_with_priority(resolved, priority);
64        self.reference
65            .database()
66            .repo()
67            .on_disconnect_put(self.reference.path_segments(), payload)
68            .await
69    }
70
71    /// Schedules an update when the client disconnects.
72    pub async fn update(&self, updates: JsonMap<String, Value>) -> DatabaseResult<()> {
73        if updates.is_empty() {
74            return Ok(());
75        }
76
77        let base_path = self.reference.path_segments();
78        let mut payload = JsonMap::with_capacity(updates.len());
79
80        for (key, value) in updates {
81            if key.trim().is_empty() {
82                return Err(invalid_argument("OnDisconnect.update keys cannot be empty"));
83            }
84            let relative_segments = normalize_path(&key)?;
85            if relative_segments.is_empty() {
86                return Err(invalid_argument(
87                    "OnDisconnect.update path cannot reference the current location",
88                ));
89            }
90
91            let mut absolute = base_path.clone();
92            absolute.extend(relative_segments.clone());
93            let resolved = self
94                .reference
95                .resolve_for_absolute_path(&absolute, value)
96                .await?;
97            let canonical = relative_segments.join("/");
98            payload.insert(canonical, resolved);
99        }
100
101        self.reference
102            .database()
103            .repo()
104            .on_disconnect_merge(base_path, Value::Object(payload))
105            .await
106    }
107
108    /// Ensures the value at this location is deleted when the client disconnects.
109    pub async fn remove(&self) -> DatabaseResult<()> {
110        self.set(Value::Null).await
111    }
112
113    /// Cancels all pending on-disconnect operations.
114    pub async fn cancel(&self) -> DatabaseResult<()> {
115        self.reference
116            .database()
117            .repo()
118            .on_disconnect_cancel(self.reference.path_segments())
119            .await
120    }
121}