Split receive and acknowledgement of a notification
Dadoum

Dadoum commited on 2026-09-28 17:49:52
Showing 2 changed files, with 22 additions and 15 deletions.

... ...
@@ -17,7 +17,8 @@ let device_data = grandslam::DeviceData {
17 17
 connection.set_topics(&["com.apple.idmsauth"]).await?;
18 18
 loop {
19 19
     let notification = connection.receive().await?;
20
-    // Interpret notification.payload according to its topic.
20
+    // Accept or store notification.payload according to its topic.
21
+    connection.acknowledge(&notification).await?;
21 22
 }
22 23
 ```
23 24
 
... ...
@@ -39,13 +40,15 @@ signer uses embedded constants and Rust cryptography.
39 40
 
40 41
 ## Connection lifetime and persistence
41 42
 
42
-`receive` handles notification acknowledgements and ping/pong traffic. Keep it
43
-running while expecting pushes. There are no hidden background tasks or retry
43
+`receive` returns notifications and handles ping/pong traffic. Call `acknowledge`
44
+after accepting or storing each notification; it borrows the notification, so
45
+an acknowledgement error leaves it available to the caller. Keep calling
46
+`receive` while expecting pushes. There are no hidden background tasks or retry
44 47
 loops. Call `reconnect` after transport failure; it reuses the identity/token
45 48
 and restores the enabled topics. Use `close` to shut down deliberately. A
46 49
 token's existence does not mean a connection is currently reachable.
47
-If cancelling `receive` during an acknowledgement or keepalive write, reconnect
48
-before continuing.
50
+If cancelling `acknowledge` during its write, or `receive` during a keepalive
51
+write, reconnect before continuing.
49 52
 
50 53
 Persist the device certificate, private key, and latest token together.
51 54
 `PushIdentity::certificate` returns DER bytes; `private_key` exposes the RSA key
... ...
@@ -186,8 +186,9 @@ impl PushConnection {
186 186
         Ok(())
187 187
     }
188 188
 
189
-    /// Receives and acknowledges the next notification, driving keepalive while
190
-    /// waiting. Reconnect after errors or cancellation during a protocol write.
189
+    /// Receives the next notification, driving keepalive while waiting.
190
+    /// Call `acknowledge` once the notification is accepted or stored.
191
+    /// Reconnect after errors or cancellation during a keepalive write.
191 192
     pub async fn receive(&mut self) -> Result<Notification, CourierError> {
192 193
         loop {
193 194
             let deadline = self.pong_deadline.unwrap_or(self.next_ping);
... ...
@@ -216,14 +217,6 @@ impl PushConnection {
216 217
                     if token != self.token.as_bytes() {
217 218
                         return Err(CourierError::TokenMismatch);
218 219
                     }
219
-                    // Validate the payload before acknowledging it.
220
-                    frame.required(3)?;
221
-                    Message::Ack {
222
-                        token: &self.token,
223
-                        id,
224
-                    }
225
-                    .write(&mut self.stream)
226
-                    .await?;
227 220
                     return Ok(Notification {
228 221
                         id,
229 222
                         topic,
... ...
@@ -240,6 +233,17 @@ impl PushConnection {
240 233
         }
241 234
     }
242 235
 
236
+    /// Acknowledges receipt of a notification returned by this connection.
237
+    /// Reconnect after errors or cancellation during the write.
238
+    pub async fn acknowledge(&mut self, notification: &Notification) -> Result<(), CourierError> {
239
+        Message::Ack {
240
+            token: &self.token,
241
+            id: notification.id,
242
+        }
243
+        .write(&mut self.stream)
244
+        .await
245
+    }
246
+
243 247
     pub async fn close(mut self) -> io::Result<()> {
244 248
         timeout(TIMEOUT, self.stream.shutdown())
245 249
             .await
246 250