iOS: streamline notify timeouts
parent
761188cd1d
commit
f72ac60b01
|
|
@ -2,7 +2,6 @@ import AVFoundation
|
||||||
import Contacts
|
import Contacts
|
||||||
import CoreLocation
|
import CoreLocation
|
||||||
import CoreMotion
|
import CoreMotion
|
||||||
import Darwin
|
|
||||||
import EventKit
|
import EventKit
|
||||||
import Foundation
|
import Foundation
|
||||||
import OpenClawKit
|
import OpenClawKit
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
import OpenClawKit
|
import OpenClawKit
|
||||||
import Network
|
|
||||||
import Observation
|
import Observation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UIKit
|
import UIKit
|
||||||
import UserNotifications
|
import UserNotifications
|
||||||
|
|
||||||
|
// Wrap errors without pulling non-Sendable types into async notification paths.
|
||||||
private struct NotificationCallError: Error, Sendable {
|
private struct NotificationCallError: Error, Sendable {
|
||||||
let message: String
|
let message: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensures notification requests return promptly even if the system prompt blocks.
|
||||||
private final class NotificationInvokeLatch<T: Sendable>: @unchecked Sendable {
|
private final class NotificationInvokeLatch<T: Sendable>: @unchecked Sendable {
|
||||||
private let lock = NSLock()
|
private let lock = NSLock()
|
||||||
private var continuation: CheckedContinuation<Result<T, NotificationCallError>, Never>?
|
private var continuation: CheckedContinuation<Result<T, NotificationCallError>, Never>?
|
||||||
|
|
@ -1011,7 +1012,12 @@ final class NodeAppModel {
|
||||||
let latch = NotificationInvokeLatch<T>()
|
let latch = NotificationInvokeLatch<T>()
|
||||||
var opTask: Task<Void, Never>?
|
var opTask: Task<Void, Never>?
|
||||||
var timeoutTask: 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)
|
latch.setContinuation(cont)
|
||||||
opTask = Task { @MainActor in
|
opTask = Task { @MainActor in
|
||||||
do {
|
do {
|
||||||
|
|
@ -1022,16 +1028,12 @@ final class NodeAppModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
timeoutTask = Task.detached {
|
timeoutTask = Task.detached {
|
||||||
let clamped = max(0.0, timeoutSeconds)
|
|
||||||
if clamped > 0 {
|
if clamped > 0 {
|
||||||
try? await Task.sleep(nanoseconds: UInt64(clamped * 1_000_000_000))
|
try? await Task.sleep(nanoseconds: UInt64(clamped * 1_000_000_000))
|
||||||
}
|
}
|
||||||
latch.resume(.failure(NotificationCallError(message: "notification request timed out")))
|
latch.resume(.failure(NotificationCallError(message: "notification request timed out")))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
opTask?.cancel()
|
|
||||||
timeoutTask?.cancel()
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleDeviceInvoke(_ req: BridgeInvokeRequest) async throws -> BridgeInvokeResponse {
|
private func handleDeviceInvoke(_ req: BridgeInvokeRequest) async throws -> BridgeInvokeResponse {
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ public actor GatewayNodeSession {
|
||||||
return await onInvoke(request)
|
return await onInvoke(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use an explicit latch so timeouts win even if onInvoke blocks (e.g., permission prompts).
|
||||||
final class InvokeLatch: @unchecked Sendable {
|
final class InvokeLatch: @unchecked Sendable {
|
||||||
private let lock = NSLock()
|
private let lock = NSLock()
|
||||||
private var continuation: CheckedContinuation<BridgeInvokeResponse, Never>?
|
private var continuation: CheckedContinuation<BridgeInvokeResponse, Never>?
|
||||||
|
|
@ -72,6 +73,10 @@ public actor GatewayNodeSession {
|
||||||
let latch = InvokeLatch()
|
let latch = InvokeLatch()
|
||||||
var onInvokeTask: Task<Void, Never>?
|
var onInvokeTask: Task<Void, Never>?
|
||||||
var timeoutTask: Task<Void, Never>?
|
var timeoutTask: Task<Void, Never>?
|
||||||
|
defer {
|
||||||
|
onInvokeTask?.cancel()
|
||||||
|
timeoutTask?.cancel()
|
||||||
|
}
|
||||||
let response = await withCheckedContinuation { (cont: CheckedContinuation<BridgeInvokeResponse, Never>) in
|
let response = await withCheckedContinuation { (cont: CheckedContinuation<BridgeInvokeResponse, Never>) in
|
||||||
latch.setContinuation(cont)
|
latch.setContinuation(cont)
|
||||||
onInvokeTask = Task.detached {
|
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)")
|
timeoutLogger.info("node invoke race resolved id=\(request.id, privacy: .public) ok=\(response.ok, privacy: .public)")
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue