iOS: streamline notify timeouts

This commit is contained in:
Mariano Belinky
2026-01-31 20:49:16 +01:00
committed by Mariano Belinky
parent 761188cd1d
commit f72ac60b01
3 changed files with 13 additions and 9 deletions

View File

@@ -2,7 +2,6 @@ import AVFoundation
import Contacts
import CoreLocation
import CoreMotion
import Darwin
import EventKit
import Foundation
import OpenClawKit

View File

@@ -1,14 +1,15 @@
import OpenClawKit
import Network
import Observation
import SwiftUI
import UIKit
import UserNotifications
// Wrap errors without pulling non-Sendable types into async notification paths.
private struct NotificationCallError: Error, Sendable {
let message: String
}
// Ensures notification requests return promptly even if the system prompt blocks.
private final class NotificationInvokeLatch<T: Sendable>: @unchecked Sendable {
private let lock = NSLock()
private var continuation: CheckedContinuation<Result<T, NotificationCallError>, Never>?
@@ -1011,7 +1012,12 @@ final class NodeAppModel {
let latch = NotificationInvokeLatch<T>()
var opTask: Task<Void, Never>?
var timeoutTask: Task<Void, Never>?
let result = await withCheckedContinuation { (cont: CheckedContinuation<Result<T, NotificationCallError>, Never>) in
defer {
opTask?.cancel()
timeoutTask?.cancel()
}
let clamped = max(0.0, timeoutSeconds)
return await withCheckedContinuation { (cont: CheckedContinuation<Result<T, NotificationCallError>, Never>) in
latch.setContinuation(cont)
opTask = Task { @MainActor in
do {
@@ -1022,16 +1028,12 @@ final class NodeAppModel {
}
}
timeoutTask = Task.detached {
let clamped = max(0.0, timeoutSeconds)
if clamped > 0 {
try? await Task.sleep(nanoseconds: UInt64(clamped * 1_000_000_000))
}
latch.resume(.failure(NotificationCallError(message: "notification request timed out")))
}
}
opTask?.cancel()
timeoutTask?.cancel()
return result
}
private func handleDeviceInvoke(_ req: BridgeInvokeRequest) async throws -> BridgeInvokeResponse {

View File

@@ -43,6 +43,7 @@ public actor GatewayNodeSession {
return await onInvoke(request)
}
// Use an explicit latch so timeouts win even if onInvoke blocks (e.g., permission prompts).
final class InvokeLatch: @unchecked Sendable {
private let lock = NSLock()
private var continuation: CheckedContinuation<BridgeInvokeResponse, Never>?
@@ -72,6 +73,10 @@ public actor GatewayNodeSession {
let latch = InvokeLatch()
var onInvokeTask: Task<Void, Never>?
var timeoutTask: Task<Void, Never>?
defer {
onInvokeTask?.cancel()
timeoutTask?.cancel()
}
let response = await withCheckedContinuation { (cont: CheckedContinuation<BridgeInvokeResponse, Never>) in
latch.setContinuation(cont)
onInvokeTask = Task.detached {
@@ -90,8 +95,6 @@ public actor GatewayNodeSession {
))
}
}
onInvokeTask?.cancel()
timeoutTask?.cancel()
timeoutLogger.info("node invoke race resolved id=\(request.id, privacy: .public) ok=\(response.ok, privacy: .public)")
return response
}