mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 01:47:27 +00:00
Revert "iOS: wire node services and tests"
This reverts commit 7b0a0f3dac.
This commit is contained in:
@@ -1,93 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
public enum OpenClawCalendarCommand: String, Codable, Sendable {
|
||||
case events = "calendar.events"
|
||||
case add = "calendar.add"
|
||||
}
|
||||
|
||||
public struct OpenClawCalendarEventsParams: Codable, Sendable, Equatable {
|
||||
public var startISO: String?
|
||||
public var endISO: String?
|
||||
public var limit: Int?
|
||||
|
||||
public init(startISO: String? = nil, endISO: String? = nil, limit: Int? = nil) {
|
||||
self.startISO = startISO
|
||||
self.endISO = endISO
|
||||
self.limit = limit
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawCalendarAddParams: Codable, Sendable, Equatable {
|
||||
public var title: String
|
||||
public var startISO: String
|
||||
public var endISO: String
|
||||
public var isAllDay: Bool?
|
||||
public var location: String?
|
||||
public var notes: String?
|
||||
public var calendarId: String?
|
||||
public var calendarTitle: String?
|
||||
|
||||
public init(
|
||||
title: String,
|
||||
startISO: String,
|
||||
endISO: String,
|
||||
isAllDay: Bool? = nil,
|
||||
location: String? = nil,
|
||||
notes: String? = nil,
|
||||
calendarId: String? = nil,
|
||||
calendarTitle: String? = nil)
|
||||
{
|
||||
self.title = title
|
||||
self.startISO = startISO
|
||||
self.endISO = endISO
|
||||
self.isAllDay = isAllDay
|
||||
self.location = location
|
||||
self.notes = notes
|
||||
self.calendarId = calendarId
|
||||
self.calendarTitle = calendarTitle
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawCalendarEventPayload: Codable, Sendable, Equatable {
|
||||
public var identifier: String
|
||||
public var title: String
|
||||
public var startISO: String
|
||||
public var endISO: String
|
||||
public var isAllDay: Bool
|
||||
public var location: String?
|
||||
public var calendarTitle: String?
|
||||
|
||||
public init(
|
||||
identifier: String,
|
||||
title: String,
|
||||
startISO: String,
|
||||
endISO: String,
|
||||
isAllDay: Bool,
|
||||
location: String? = nil,
|
||||
calendarTitle: String? = nil)
|
||||
{
|
||||
self.identifier = identifier
|
||||
self.title = title
|
||||
self.startISO = startISO
|
||||
self.endISO = endISO
|
||||
self.isAllDay = isAllDay
|
||||
self.location = location
|
||||
self.calendarTitle = calendarTitle
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawCalendarEventsPayload: Codable, Sendable, Equatable {
|
||||
public var events: [OpenClawCalendarEventPayload]
|
||||
|
||||
public init(events: [OpenClawCalendarEventPayload]) {
|
||||
self.events = events
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawCalendarAddPayload: Codable, Sendable, Equatable {
|
||||
public var event: OpenClawCalendarEventPayload
|
||||
|
||||
public init(event: OpenClawCalendarEventPayload) {
|
||||
self.event = event
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,4 @@ public enum OpenClawCapability: String, Codable, Sendable {
|
||||
case screen
|
||||
case voiceWake
|
||||
case location
|
||||
case device
|
||||
case photos
|
||||
case contacts
|
||||
case calendar
|
||||
case reminders
|
||||
case motion
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
public enum OpenClawChatCommand: String, Codable, Sendable {
|
||||
case push = "chat.push"
|
||||
}
|
||||
|
||||
public struct OpenClawChatPushParams: Codable, Sendable, Equatable {
|
||||
public var text: String
|
||||
public var speak: Bool?
|
||||
|
||||
public init(text: String, speak: Bool? = nil) {
|
||||
self.text = text
|
||||
self.speak = speak
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawChatPushPayload: Codable, Sendable, Equatable {
|
||||
public var messageId: String?
|
||||
|
||||
public init(messageId: String? = nil) {
|
||||
self.messageId = messageId
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
public enum OpenClawContactsCommand: String, Codable, Sendable {
|
||||
case search = "contacts.search"
|
||||
case add = "contacts.add"
|
||||
}
|
||||
|
||||
public struct OpenClawContactsSearchParams: Codable, Sendable, Equatable {
|
||||
public var query: String?
|
||||
public var limit: Int?
|
||||
|
||||
public init(query: String? = nil, limit: Int? = nil) {
|
||||
self.query = query
|
||||
self.limit = limit
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawContactsAddParams: Codable, Sendable, Equatable {
|
||||
public var givenName: String?
|
||||
public var familyName: String?
|
||||
public var organizationName: String?
|
||||
public var displayName: String?
|
||||
public var phoneNumbers: [String]?
|
||||
public var emails: [String]?
|
||||
|
||||
public init(
|
||||
givenName: String? = nil,
|
||||
familyName: String? = nil,
|
||||
organizationName: String? = nil,
|
||||
displayName: String? = nil,
|
||||
phoneNumbers: [String]? = nil,
|
||||
emails: [String]? = nil)
|
||||
{
|
||||
self.givenName = givenName
|
||||
self.familyName = familyName
|
||||
self.organizationName = organizationName
|
||||
self.displayName = displayName
|
||||
self.phoneNumbers = phoneNumbers
|
||||
self.emails = emails
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawContactPayload: Codable, Sendable, Equatable {
|
||||
public var identifier: String
|
||||
public var displayName: String
|
||||
public var givenName: String
|
||||
public var familyName: String
|
||||
public var organizationName: String
|
||||
public var phoneNumbers: [String]
|
||||
public var emails: [String]
|
||||
|
||||
public init(
|
||||
identifier: String,
|
||||
displayName: String,
|
||||
givenName: String,
|
||||
familyName: String,
|
||||
organizationName: String,
|
||||
phoneNumbers: [String],
|
||||
emails: [String])
|
||||
{
|
||||
self.identifier = identifier
|
||||
self.displayName = displayName
|
||||
self.givenName = givenName
|
||||
self.familyName = familyName
|
||||
self.organizationName = organizationName
|
||||
self.phoneNumbers = phoneNumbers
|
||||
self.emails = emails
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawContactsSearchPayload: Codable, Sendable, Equatable {
|
||||
public var contacts: [OpenClawContactPayload]
|
||||
|
||||
public init(contacts: [OpenClawContactPayload]) {
|
||||
self.contacts = contacts
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawContactsAddPayload: Codable, Sendable, Equatable {
|
||||
public var contact: OpenClawContactPayload
|
||||
|
||||
public init(contact: OpenClawContactPayload) {
|
||||
self.contact = contact
|
||||
}
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
public enum OpenClawDeviceCommand: String, Codable, Sendable {
|
||||
case status = "device.status"
|
||||
case info = "device.info"
|
||||
}
|
||||
|
||||
public enum OpenClawBatteryState: String, Codable, Sendable {
|
||||
case unknown
|
||||
case unplugged
|
||||
case charging
|
||||
case full
|
||||
}
|
||||
|
||||
public enum OpenClawThermalState: String, Codable, Sendable {
|
||||
case nominal
|
||||
case fair
|
||||
case serious
|
||||
case critical
|
||||
}
|
||||
|
||||
public enum OpenClawNetworkPathStatus: String, Codable, Sendable {
|
||||
case satisfied
|
||||
case unsatisfied
|
||||
case requiresConnection
|
||||
}
|
||||
|
||||
public enum OpenClawNetworkInterfaceType: String, Codable, Sendable {
|
||||
case wifi
|
||||
case cellular
|
||||
case wired
|
||||
case other
|
||||
}
|
||||
|
||||
public struct OpenClawBatteryStatusPayload: Codable, Sendable, Equatable {
|
||||
public var level: Double?
|
||||
public var state: OpenClawBatteryState
|
||||
public var lowPowerModeEnabled: Bool
|
||||
|
||||
public init(level: Double?, state: OpenClawBatteryState, lowPowerModeEnabled: Bool) {
|
||||
self.level = level
|
||||
self.state = state
|
||||
self.lowPowerModeEnabled = lowPowerModeEnabled
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawThermalStatusPayload: Codable, Sendable, Equatable {
|
||||
public var state: OpenClawThermalState
|
||||
|
||||
public init(state: OpenClawThermalState) {
|
||||
self.state = state
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawStorageStatusPayload: Codable, Sendable, Equatable {
|
||||
public var totalBytes: Int64
|
||||
public var freeBytes: Int64
|
||||
public var usedBytes: Int64
|
||||
|
||||
public init(totalBytes: Int64, freeBytes: Int64, usedBytes: Int64) {
|
||||
self.totalBytes = totalBytes
|
||||
self.freeBytes = freeBytes
|
||||
self.usedBytes = usedBytes
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawNetworkStatusPayload: Codable, Sendable, Equatable {
|
||||
public var status: OpenClawNetworkPathStatus
|
||||
public var isExpensive: Bool
|
||||
public var isConstrained: Bool
|
||||
public var interfaces: [OpenClawNetworkInterfaceType]
|
||||
|
||||
public init(
|
||||
status: OpenClawNetworkPathStatus,
|
||||
isExpensive: Bool,
|
||||
isConstrained: Bool,
|
||||
interfaces: [OpenClawNetworkInterfaceType])
|
||||
{
|
||||
self.status = status
|
||||
self.isExpensive = isExpensive
|
||||
self.isConstrained = isConstrained
|
||||
self.interfaces = interfaces
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawDeviceStatusPayload: Codable, Sendable, Equatable {
|
||||
public var battery: OpenClawBatteryStatusPayload
|
||||
public var thermal: OpenClawThermalStatusPayload
|
||||
public var storage: OpenClawStorageStatusPayload
|
||||
public var network: OpenClawNetworkStatusPayload
|
||||
public var uptimeSeconds: Double
|
||||
|
||||
public init(
|
||||
battery: OpenClawBatteryStatusPayload,
|
||||
thermal: OpenClawThermalStatusPayload,
|
||||
storage: OpenClawStorageStatusPayload,
|
||||
network: OpenClawNetworkStatusPayload,
|
||||
uptimeSeconds: Double)
|
||||
{
|
||||
self.battery = battery
|
||||
self.thermal = thermal
|
||||
self.storage = storage
|
||||
self.network = network
|
||||
self.uptimeSeconds = uptimeSeconds
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawDeviceInfoPayload: Codable, Sendable, Equatable {
|
||||
public var deviceName: String
|
||||
public var modelIdentifier: String
|
||||
public var systemName: String
|
||||
public var systemVersion: String
|
||||
public var appVersion: String
|
||||
public var appBuild: String
|
||||
public var locale: String
|
||||
|
||||
public init(
|
||||
deviceName: String,
|
||||
modelIdentifier: String,
|
||||
systemName: String,
|
||||
systemVersion: String,
|
||||
appVersion: String,
|
||||
appBuild: String,
|
||||
locale: String)
|
||||
{
|
||||
self.deviceName = deviceName
|
||||
self.modelIdentifier = modelIdentifier
|
||||
self.systemName = systemName
|
||||
self.systemVersion = systemVersion
|
||||
self.appVersion = appVersion
|
||||
self.appBuild = appBuild
|
||||
self.locale = locale
|
||||
}
|
||||
}
|
||||
@@ -110,13 +110,7 @@ private enum ConnectChallengeError: Error {
|
||||
|
||||
public actor GatewayChannelActor {
|
||||
private let logger = Logger(subsystem: "ai.openclaw", category: "gateway")
|
||||
#if DEBUG
|
||||
private var debugEventLogCount = 0
|
||||
private var debugMessageLogCount = 0
|
||||
private var debugListenLogCount = 0
|
||||
#endif
|
||||
private var task: WebSocketTaskBox?
|
||||
private var listenTask: Task<Void, Never>?
|
||||
private var pending: [String: CheckedContinuation<GatewayFrame, Error>] = [:]
|
||||
private var connected = false
|
||||
private var isConnecting = false
|
||||
@@ -175,9 +169,6 @@ public actor GatewayChannelActor {
|
||||
self.tickTask?.cancel()
|
||||
self.tickTask = nil
|
||||
|
||||
self.listenTask?.cancel()
|
||||
self.listenTask = nil
|
||||
|
||||
self.task?.cancel(with: .goingAway, reason: nil)
|
||||
self.task = nil
|
||||
|
||||
@@ -230,8 +221,6 @@ public actor GatewayChannelActor {
|
||||
self.isConnecting = true
|
||||
defer { self.isConnecting = false }
|
||||
|
||||
self.listenTask?.cancel()
|
||||
self.listenTask = nil
|
||||
self.task?.cancel(with: .goingAway, reason: nil)
|
||||
self.task = self.session.makeWebSocketTask(url: self.url)
|
||||
self.task?.resume()
|
||||
@@ -259,7 +248,6 @@ public actor GatewayChannelActor {
|
||||
throw wrapped
|
||||
}
|
||||
self.listen()
|
||||
self.logger.info("gateway ws listen registered")
|
||||
self.connected = true
|
||||
self.backoffMs = 500
|
||||
self.lastSeq = nil
|
||||
@@ -432,44 +420,24 @@ public actor GatewayChannelActor {
|
||||
}
|
||||
|
||||
private func listen() {
|
||||
#if DEBUG
|
||||
if self.debugListenLogCount < 3 {
|
||||
self.debugListenLogCount += 1
|
||||
self.logger.info("gateway ws listen start")
|
||||
}
|
||||
#endif
|
||||
self.listenTask?.cancel()
|
||||
self.listenTask = Task { [weak self] in
|
||||
self.task?.receive { [weak self] result in
|
||||
guard let self else { return }
|
||||
defer { Task { await self.clearListenTask() } }
|
||||
while !Task.isCancelled {
|
||||
guard let task = await self.currentTask() else { return }
|
||||
do {
|
||||
let msg = try await task.receive()
|
||||
switch result {
|
||||
case let .failure(err):
|
||||
Task { await self.handleReceiveFailure(err) }
|
||||
case let .success(msg):
|
||||
Task {
|
||||
await self.handle(msg)
|
||||
} catch {
|
||||
if Task.isCancelled { return }
|
||||
await self.handleReceiveFailure(error)
|
||||
return
|
||||
await self.listen()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func clearListenTask() {
|
||||
self.listenTask = nil
|
||||
}
|
||||
|
||||
private func currentTask() -> WebSocketTaskBox? {
|
||||
self.task
|
||||
}
|
||||
|
||||
private func handleReceiveFailure(_ err: Error) async {
|
||||
let wrapped = self.wrap(err, context: "gateway receive")
|
||||
self.logger.error("gateway ws receive failed \(wrapped.localizedDescription, privacy: .public)")
|
||||
self.connected = false
|
||||
self.listenTask?.cancel()
|
||||
self.listenTask = nil
|
||||
await self.disconnectHandler?("receive failed: \(wrapped.localizedDescription)")
|
||||
await self.failPending(wrapped)
|
||||
await self.scheduleReconnect()
|
||||
@@ -481,13 +449,6 @@ public actor GatewayChannelActor {
|
||||
case let .string(s): s.data(using: .utf8)
|
||||
@unknown default: nil
|
||||
}
|
||||
#if DEBUG
|
||||
if self.debugMessageLogCount < 8 {
|
||||
self.debugMessageLogCount += 1
|
||||
let size = data?.count ?? 0
|
||||
self.logger.info("gateway ws message received size=\(size, privacy: .public)")
|
||||
}
|
||||
#endif
|
||||
guard let data else { return }
|
||||
guard let frame = try? self.decoder.decode(GatewayFrame.self, from: data) else {
|
||||
self.logger.error("gateway decode failed")
|
||||
@@ -501,13 +462,6 @@ public actor GatewayChannelActor {
|
||||
}
|
||||
case let .event(evt):
|
||||
if evt.event == "connect.challenge" { return }
|
||||
#if DEBUG
|
||||
if self.debugEventLogCount < 12 {
|
||||
self.debugEventLogCount += 1
|
||||
self.logger.info(
|
||||
"gateway event received event=\(evt.event, privacy: .public) payload=\(evt.payload != nil, privacy: .public)")
|
||||
}
|
||||
#endif
|
||||
if let seq = evt.seq {
|
||||
if let last = lastSeq, seq > last + 1 {
|
||||
await self.pushHandler?(.seqGap(expected: last + 1, received: seq))
|
||||
|
||||
@@ -11,7 +11,6 @@ private struct NodeInvokeRequestPayload: Codable, Sendable {
|
||||
var idempotencyKey: String?
|
||||
}
|
||||
|
||||
|
||||
public actor GatewayNodeSession {
|
||||
private let logger = Logger(subsystem: "ai.openclaw", category: "node.gateway")
|
||||
private let decoder = JSONDecoder()
|
||||
@@ -24,78 +23,34 @@ public actor GatewayNodeSession {
|
||||
private var onConnected: (@Sendable () async -> Void)?
|
||||
private var onDisconnected: (@Sendable (String) async -> Void)?
|
||||
private var onInvoke: (@Sendable (BridgeInvokeRequest) async -> BridgeInvokeResponse)?
|
||||
private var hasNotifiedConnected = false
|
||||
private var snapshotReceived = false
|
||||
private var snapshotWaiters: [CheckedContinuation<Bool, Never>] = []
|
||||
|
||||
static func invokeWithTimeout(
|
||||
request: BridgeInvokeRequest,
|
||||
timeoutMs: Int?,
|
||||
onInvoke: @escaping @Sendable (BridgeInvokeRequest) async -> BridgeInvokeResponse
|
||||
) async -> BridgeInvokeResponse {
|
||||
let timeoutLogger = Logger(subsystem: "ai.openclaw", category: "node.gateway")
|
||||
let timeout: Int = {
|
||||
guard let timeoutMs else { return 0 }
|
||||
return max(0, timeoutMs)
|
||||
}()
|
||||
let timeout = max(0, timeoutMs ?? 0)
|
||||
guard timeout > 0 else {
|
||||
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>?
|
||||
private var resumed = false
|
||||
|
||||
func setContinuation(_ continuation: CheckedContinuation<BridgeInvokeResponse, Never>) {
|
||||
self.lock.lock()
|
||||
defer { self.lock.unlock() }
|
||||
self.continuation = continuation
|
||||
}
|
||||
|
||||
func resume(_ response: BridgeInvokeResponse) {
|
||||
let cont: CheckedContinuation<BridgeInvokeResponse, Never>?
|
||||
self.lock.lock()
|
||||
if self.resumed {
|
||||
self.lock.unlock()
|
||||
return
|
||||
}
|
||||
self.resumed = true
|
||||
cont = self.continuation
|
||||
self.continuation = nil
|
||||
self.lock.unlock()
|
||||
cont?.resume(returning: response)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
let result = await onInvoke(request)
|
||||
latch.resume(result)
|
||||
}
|
||||
timeoutTask = Task.detached {
|
||||
return await withTaskGroup(of: BridgeInvokeResponse.self) { group in
|
||||
group.addTask { await onInvoke(request) }
|
||||
group.addTask {
|
||||
try? await Task.sleep(nanoseconds: UInt64(timeout) * 1_000_000)
|
||||
timeoutLogger.info("node invoke timeout fired id=\(request.id, privacy: .public)")
|
||||
latch.resume(BridgeInvokeResponse(
|
||||
return BridgeInvokeResponse(
|
||||
id: request.id,
|
||||
ok: false,
|
||||
error: OpenClawNodeError(
|
||||
code: .unavailable,
|
||||
message: "node invoke timed out")
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
let first = await group.next()!
|
||||
group.cancelAll()
|
||||
return first
|
||||
}
|
||||
timeoutLogger.info("node invoke race resolved id=\(request.id, privacy: .public) ok=\(response.ok, privacy: .public)")
|
||||
return response
|
||||
}
|
||||
private var serverEventSubscribers: [UUID: AsyncStream<EventFrame>.Continuation] = [:]
|
||||
private var canvasHostUrl: String?
|
||||
@@ -123,7 +78,6 @@ public actor GatewayNodeSession {
|
||||
self.onInvoke = onInvoke
|
||||
|
||||
if shouldReconnect {
|
||||
self.resetConnectionState()
|
||||
if let existing = self.channel {
|
||||
await existing.shutdown()
|
||||
}
|
||||
@@ -153,10 +107,7 @@ public actor GatewayNodeSession {
|
||||
|
||||
do {
|
||||
try await channel.connect()
|
||||
let snapshotReady = await self.waitForSnapshot(timeoutMs: 500)
|
||||
if snapshotReady {
|
||||
await self.notifyConnectedIfNeeded()
|
||||
}
|
||||
await onConnected()
|
||||
} catch {
|
||||
await onDisconnected(error.localizedDescription)
|
||||
throw error
|
||||
@@ -169,7 +120,6 @@ public actor GatewayNodeSession {
|
||||
self.activeURL = nil
|
||||
self.activeToken = nil
|
||||
self.activePassword = nil
|
||||
self.resetConnectionState()
|
||||
}
|
||||
|
||||
public func currentCanvasHostUrl() -> String? {
|
||||
@@ -229,8 +179,7 @@ public actor GatewayNodeSession {
|
||||
case let .snapshot(ok):
|
||||
let raw = ok.canvashosturl?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
self.canvasHostUrl = (raw?.isEmpty == false) ? raw : nil
|
||||
self.markSnapshotReceived()
|
||||
await self.notifyConnectedIfNeeded()
|
||||
await self.onConnected?()
|
||||
case let .event(evt):
|
||||
await self.handleEvent(evt)
|
||||
default:
|
||||
@@ -238,98 +187,28 @@ public actor GatewayNodeSession {
|
||||
}
|
||||
}
|
||||
|
||||
private func resetConnectionState() {
|
||||
self.hasNotifiedConnected = false
|
||||
self.snapshotReceived = false
|
||||
if !self.snapshotWaiters.isEmpty {
|
||||
let waiters = self.snapshotWaiters
|
||||
self.snapshotWaiters.removeAll()
|
||||
for waiter in waiters {
|
||||
waiter.resume(returning: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func markSnapshotReceived() {
|
||||
self.snapshotReceived = true
|
||||
if !self.snapshotWaiters.isEmpty {
|
||||
let waiters = self.snapshotWaiters
|
||||
self.snapshotWaiters.removeAll()
|
||||
for waiter in waiters {
|
||||
waiter.resume(returning: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func waitForSnapshot(timeoutMs: Int) async -> Bool {
|
||||
if self.snapshotReceived { return true }
|
||||
let clamped = max(0, timeoutMs)
|
||||
return await withCheckedContinuation { cont in
|
||||
self.snapshotWaiters.append(cont)
|
||||
Task { [weak self] in
|
||||
guard let self else { return }
|
||||
try? await Task.sleep(nanoseconds: UInt64(clamped) * 1_000_000)
|
||||
await self.timeoutSnapshotWaiters()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func timeoutSnapshotWaiters() {
|
||||
guard !self.snapshotReceived else { return }
|
||||
if !self.snapshotWaiters.isEmpty {
|
||||
let waiters = self.snapshotWaiters
|
||||
self.snapshotWaiters.removeAll()
|
||||
for waiter in waiters {
|
||||
waiter.resume(returning: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func notifyConnectedIfNeeded() async {
|
||||
guard !self.hasNotifiedConnected else { return }
|
||||
self.hasNotifiedConnected = true
|
||||
await self.onConnected?()
|
||||
}
|
||||
|
||||
private func handleEvent(_ evt: EventFrame) async {
|
||||
self.broadcastServerEvent(evt)
|
||||
guard evt.event == "node.invoke.request" else { return }
|
||||
self.logger.info("node invoke request received")
|
||||
guard let payload = evt.payload else { return }
|
||||
do {
|
||||
let request = try self.decodeInvokeRequest(from: payload)
|
||||
let timeoutLabel = request.timeoutMs.map(String.init) ?? "none"
|
||||
self.logger.info("node invoke request decoded id=\(request.id, privacy: .public) command=\(request.command, privacy: .public) timeoutMs=\(timeoutLabel, privacy: .public)")
|
||||
let data = try self.encoder.encode(payload)
|
||||
let request = try self.decoder.decode(NodeInvokeRequestPayload.self, from: data)
|
||||
guard let onInvoke else { return }
|
||||
let req = BridgeInvokeRequest(id: request.id, command: request.command, paramsJSON: request.paramsJSON)
|
||||
self.logger.info("node invoke executing id=\(request.id, privacy: .public)")
|
||||
let response = await Self.invokeWithTimeout(
|
||||
request: req,
|
||||
timeoutMs: request.timeoutMs,
|
||||
onInvoke: onInvoke
|
||||
)
|
||||
self.logger.info("node invoke completed id=\(request.id, privacy: .public) ok=\(response.ok, privacy: .public)")
|
||||
await self.sendInvokeResult(request: request, response: response)
|
||||
} catch {
|
||||
self.logger.error("node invoke decode failed: \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
}
|
||||
|
||||
private func decodeInvokeRequest(from payload: OpenClawProtocol.AnyCodable) throws -> NodeInvokeRequestPayload {
|
||||
do {
|
||||
let data = try self.encoder.encode(payload)
|
||||
return try self.decoder.decode(NodeInvokeRequestPayload.self, from: data)
|
||||
} catch {
|
||||
if let raw = payload.value as? String, let data = raw.data(using: .utf8) {
|
||||
return try self.decoder.decode(NodeInvokeRequestPayload.self, from: data)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private func sendInvokeResult(request: NodeInvokeRequestPayload, response: BridgeInvokeResponse) async {
|
||||
guard let channel = self.channel else { return }
|
||||
self.logger.info("node invoke result sending id=\(request.id, privacy: .public) ok=\(response.ok, privacy: .public)")
|
||||
var params: [String: AnyCodable] = [
|
||||
"id": AnyCodable(request.id),
|
||||
"nodeId": AnyCodable(request.nodeId),
|
||||
@@ -347,7 +226,7 @@ public actor GatewayNodeSession {
|
||||
do {
|
||||
try await channel.send(method: "node.invoke.result", params: params)
|
||||
} catch {
|
||||
self.logger.error("node invoke result failed id=\(request.id, privacy: .public) error=\(error.localizedDescription, privacy: .public)")
|
||||
self.logger.error("node invoke result failed: \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -73,11 +73,6 @@ public final class GatewayTLSPinningSession: NSObject, WebSocketSessioning, URLS
|
||||
if let expected {
|
||||
if fingerprint == expected {
|
||||
completionHandler(.useCredential, URLCredential(trust: trust))
|
||||
} else if params.allowTOFU {
|
||||
if let storeKey = params.storeKey {
|
||||
GatewayTLSStore.saveFingerprint(fingerprint, stableID: storeKey)
|
||||
}
|
||||
completionHandler(.useCredential, URLCredential(trust: trust))
|
||||
} else {
|
||||
completionHandler(.cancelAuthenticationChallenge, nil)
|
||||
}
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
public enum OpenClawMotionCommand: String, Codable, Sendable {
|
||||
case activity = "motion.activity"
|
||||
case pedometer = "motion.pedometer"
|
||||
}
|
||||
|
||||
public struct OpenClawMotionActivityParams: Codable, Sendable, Equatable {
|
||||
public var startISO: String?
|
||||
public var endISO: String?
|
||||
public var limit: Int?
|
||||
|
||||
public init(startISO: String? = nil, endISO: String? = nil, limit: Int? = nil) {
|
||||
self.startISO = startISO
|
||||
self.endISO = endISO
|
||||
self.limit = limit
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawMotionActivityEntry: Codable, Sendable, Equatable {
|
||||
public var startISO: String
|
||||
public var endISO: String
|
||||
public var confidence: String
|
||||
public var isWalking: Bool
|
||||
public var isRunning: Bool
|
||||
public var isCycling: Bool
|
||||
public var isAutomotive: Bool
|
||||
public var isStationary: Bool
|
||||
public var isUnknown: Bool
|
||||
|
||||
public init(
|
||||
startISO: String,
|
||||
endISO: String,
|
||||
confidence: String,
|
||||
isWalking: Bool,
|
||||
isRunning: Bool,
|
||||
isCycling: Bool,
|
||||
isAutomotive: Bool,
|
||||
isStationary: Bool,
|
||||
isUnknown: Bool)
|
||||
{
|
||||
self.startISO = startISO
|
||||
self.endISO = endISO
|
||||
self.confidence = confidence
|
||||
self.isWalking = isWalking
|
||||
self.isRunning = isRunning
|
||||
self.isCycling = isCycling
|
||||
self.isAutomotive = isAutomotive
|
||||
self.isStationary = isStationary
|
||||
self.isUnknown = isUnknown
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawMotionActivityPayload: Codable, Sendable, Equatable {
|
||||
public var activities: [OpenClawMotionActivityEntry]
|
||||
|
||||
public init(activities: [OpenClawMotionActivityEntry]) {
|
||||
self.activities = activities
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawPedometerParams: Codable, Sendable, Equatable {
|
||||
public var startISO: String?
|
||||
public var endISO: String?
|
||||
|
||||
public init(startISO: String? = nil, endISO: String? = nil) {
|
||||
self.startISO = startISO
|
||||
self.endISO = endISO
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawPedometerPayload: Codable, Sendable, Equatable {
|
||||
public var startISO: String
|
||||
public var endISO: String
|
||||
public var steps: Int?
|
||||
public var distanceMeters: Double?
|
||||
public var floorsAscended: Int?
|
||||
public var floorsDescended: Int?
|
||||
|
||||
public init(
|
||||
startISO: String,
|
||||
endISO: String,
|
||||
steps: Int?,
|
||||
distanceMeters: Double?,
|
||||
floorsAscended: Int?,
|
||||
floorsDescended: Int?)
|
||||
{
|
||||
self.startISO = startISO
|
||||
self.endISO = endISO
|
||||
self.steps = steps
|
||||
self.distanceMeters = distanceMeters
|
||||
self.floorsAscended = floorsAscended
|
||||
self.floorsDescended = floorsDescended
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
public enum OpenClawPhotosCommand: String, Codable, Sendable {
|
||||
case latest = "photos.latest"
|
||||
}
|
||||
|
||||
public struct OpenClawPhotosLatestParams: Codable, Sendable, Equatable {
|
||||
public var limit: Int?
|
||||
public var maxWidth: Int?
|
||||
public var quality: Double?
|
||||
|
||||
public init(limit: Int? = nil, maxWidth: Int? = nil, quality: Double? = nil) {
|
||||
self.limit = limit
|
||||
self.maxWidth = maxWidth
|
||||
self.quality = quality
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawPhotoPayload: Codable, Sendable, Equatable {
|
||||
public var format: String
|
||||
public var base64: String
|
||||
public var width: Int
|
||||
public var height: Int
|
||||
public var createdAt: String?
|
||||
|
||||
public init(format: String, base64: String, width: Int, height: Int, createdAt: String? = nil) {
|
||||
self.format = format
|
||||
self.base64 = base64
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.createdAt = createdAt
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawPhotosLatestPayload: Codable, Sendable, Equatable {
|
||||
public var photos: [OpenClawPhotoPayload]
|
||||
|
||||
public init(photos: [OpenClawPhotoPayload]) {
|
||||
self.photos = photos
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
public enum OpenClawRemindersCommand: String, Codable, Sendable {
|
||||
case list = "reminders.list"
|
||||
case add = "reminders.add"
|
||||
}
|
||||
|
||||
public enum OpenClawReminderStatusFilter: String, Codable, Sendable {
|
||||
case incomplete
|
||||
case completed
|
||||
case all
|
||||
}
|
||||
|
||||
public struct OpenClawRemindersListParams: Codable, Sendable, Equatable {
|
||||
public var status: OpenClawReminderStatusFilter?
|
||||
public var limit: Int?
|
||||
|
||||
public init(status: OpenClawReminderStatusFilter? = nil, limit: Int? = nil) {
|
||||
self.status = status
|
||||
self.limit = limit
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawRemindersAddParams: Codable, Sendable, Equatable {
|
||||
public var title: String
|
||||
public var dueISO: String?
|
||||
public var notes: String?
|
||||
public var listId: String?
|
||||
public var listName: String?
|
||||
|
||||
public init(
|
||||
title: String,
|
||||
dueISO: String? = nil,
|
||||
notes: String? = nil,
|
||||
listId: String? = nil,
|
||||
listName: String? = nil)
|
||||
{
|
||||
self.title = title
|
||||
self.dueISO = dueISO
|
||||
self.notes = notes
|
||||
self.listId = listId
|
||||
self.listName = listName
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawReminderPayload: Codable, Sendable, Equatable {
|
||||
public var identifier: String
|
||||
public var title: String
|
||||
public var dueISO: String?
|
||||
public var completed: Bool
|
||||
public var listName: String?
|
||||
|
||||
public init(
|
||||
identifier: String,
|
||||
title: String,
|
||||
dueISO: String? = nil,
|
||||
completed: Bool,
|
||||
listName: String? = nil)
|
||||
{
|
||||
self.identifier = identifier
|
||||
self.title = title
|
||||
self.dueISO = dueISO
|
||||
self.completed = completed
|
||||
self.listName = listName
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawRemindersListPayload: Codable, Sendable, Equatable {
|
||||
public var reminders: [OpenClawReminderPayload]
|
||||
|
||||
public init(reminders: [OpenClawReminderPayload]) {
|
||||
self.reminders = reminders
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawRemindersAddPayload: Codable, Sendable, Equatable {
|
||||
public var reminder: OpenClawReminderPayload
|
||||
|
||||
public init(reminder: OpenClawReminderPayload) {
|
||||
self.reminder = reminder
|
||||
}
|
||||
}
|
||||
@@ -123,10 +123,6 @@
|
||||
"screen_record": {
|
||||
"label": "screen record",
|
||||
"detailKeys": ["node", "nodeId", "duration", "durationMs", "fps", "screenIndex"]
|
||||
},
|
||||
"invoke": {
|
||||
"label": "invoke",
|
||||
"detailKeys": ["node", "nodeId", "invokeCommand"]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
public enum OpenClawTalkCommand: String, Codable, Sendable {
|
||||
case pttStart = "talk.ptt.start"
|
||||
case pttStop = "talk.ptt.stop"
|
||||
case pttCancel = "talk.ptt.cancel"
|
||||
case pttOnce = "talk.ptt.once"
|
||||
}
|
||||
|
||||
public struct OpenClawTalkPTTStartPayload: Codable, Sendable, Equatable {
|
||||
public var captureId: String
|
||||
|
||||
public init(captureId: String) {
|
||||
self.captureId = captureId
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawTalkPTTStopPayload: Codable, Sendable, Equatable {
|
||||
public var captureId: String
|
||||
public var transcript: String?
|
||||
public var status: String
|
||||
|
||||
public init(captureId: String, transcript: String?, status: String) {
|
||||
self.captureId = captureId
|
||||
self.transcript = transcript
|
||||
self.status = status
|
||||
}
|
||||
}
|
||||
@@ -1,310 +0,0 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import OpenClawKit
|
||||
import OpenClawProtocol
|
||||
|
||||
@Suite struct GatewayNodeInvokeTests {
|
||||
@Test
|
||||
func nodeInvokeRequestSendsInvokeResult() async throws {
|
||||
let task = TestWebSocketTask()
|
||||
let session = TestWebSocketSession(task: task)
|
||||
|
||||
task.enqueue(Self.makeEventMessage(
|
||||
event: "connect.challenge",
|
||||
payload: ["nonce": "test-nonce"]))
|
||||
|
||||
let tracker = InvokeTracker()
|
||||
let gateway = GatewayNodeSession()
|
||||
try await gateway.connect(
|
||||
url: URL(string: "ws://127.0.0.1:18789")!,
|
||||
token: nil,
|
||||
password: "test-password",
|
||||
connectOptions: GatewayConnectOptions(
|
||||
role: "node",
|
||||
scopes: [],
|
||||
caps: [],
|
||||
commands: ["device.info"],
|
||||
permissions: [:],
|
||||
clientId: "openclaw-ios",
|
||||
clientMode: "node",
|
||||
clientDisplayName: "Test iOS Node"),
|
||||
sessionBox: WebSocketSessionBox(session: session),
|
||||
onConnected: {},
|
||||
onDisconnected: { _ in },
|
||||
onInvoke: { req in
|
||||
await tracker.set(req)
|
||||
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: "{\"ok\":true}")
|
||||
})
|
||||
|
||||
task.enqueue(Self.makeEventMessage(
|
||||
event: "node.invoke.request",
|
||||
payload: [
|
||||
"id": "invoke-1",
|
||||
"nodeId": "node-1",
|
||||
"command": "device.info",
|
||||
"timeoutMs": 15000,
|
||||
"idempotencyKey": "abc123",
|
||||
]))
|
||||
|
||||
let resultFrame = try await waitForSentMethod(
|
||||
task,
|
||||
method: "node.invoke.result",
|
||||
timeoutSeconds: 1.0)
|
||||
|
||||
let sentParams = resultFrame.params?.value as? [String: OpenClawProtocol.AnyCodable]
|
||||
#expect(sentParams?["id"]?.value as? String == "invoke-1")
|
||||
#expect(sentParams?["nodeId"]?.value as? String == "node-1")
|
||||
#expect(sentParams?["ok"]?.value as? Bool == true)
|
||||
|
||||
let captured = await tracker.get()
|
||||
#expect(captured?.command == "device.info")
|
||||
#expect(captured?.id == "invoke-1")
|
||||
}
|
||||
|
||||
@Test
|
||||
func nodeInvokeRequestHandlesStringPayload() async throws {
|
||||
let task = TestWebSocketTask()
|
||||
let session = TestWebSocketSession(task: task)
|
||||
|
||||
task.enqueue(Self.makeEventMessage(
|
||||
event: "connect.challenge",
|
||||
payload: ["nonce": "test-nonce"]))
|
||||
|
||||
let tracker = InvokeTracker()
|
||||
let gateway = GatewayNodeSession()
|
||||
try await gateway.connect(
|
||||
url: URL(string: "ws://127.0.0.1:18789")!,
|
||||
token: nil,
|
||||
password: "test-password",
|
||||
connectOptions: GatewayConnectOptions(
|
||||
role: "node",
|
||||
scopes: [],
|
||||
caps: [],
|
||||
commands: ["device.info"],
|
||||
permissions: [:],
|
||||
clientId: "openclaw-ios",
|
||||
clientMode: "node",
|
||||
clientDisplayName: "Test iOS Node"),
|
||||
sessionBox: WebSocketSessionBox(session: session),
|
||||
onConnected: {},
|
||||
onDisconnected: { _ in },
|
||||
onInvoke: { req in
|
||||
await tracker.set(req)
|
||||
return BridgeInvokeResponse(id: req.id, ok: true)
|
||||
})
|
||||
|
||||
let payload = """
|
||||
{"id":"invoke-2","nodeId":"node-1","command":"device.info"}
|
||||
"""
|
||||
task.enqueue(Self.makeEventMessage(
|
||||
event: "node.invoke.request",
|
||||
payload: payload))
|
||||
|
||||
let resultFrame = try await waitForSentMethod(
|
||||
task,
|
||||
method: "node.invoke.result",
|
||||
timeoutSeconds: 1.0)
|
||||
|
||||
let sentParams = resultFrame.params?.value as? [String: OpenClawProtocol.AnyCodable]
|
||||
#expect(sentParams?["id"]?.value as? String == "invoke-2")
|
||||
#expect(sentParams?["nodeId"]?.value as? String == "node-1")
|
||||
#expect(sentParams?["ok"]?.value as? Bool == true)
|
||||
|
||||
let captured = await tracker.get()
|
||||
#expect(captured?.command == "device.info")
|
||||
#expect(captured?.id == "invoke-2")
|
||||
}
|
||||
}
|
||||
|
||||
private enum TestError: Error {
|
||||
case timeout
|
||||
}
|
||||
|
||||
private func waitForSentMethod(
|
||||
_ task: TestWebSocketTask,
|
||||
method: String,
|
||||
timeoutSeconds: Double
|
||||
) async throws -> RequestFrame {
|
||||
try await AsyncTimeout.withTimeout(
|
||||
seconds: timeoutSeconds,
|
||||
onTimeout: { TestError.timeout },
|
||||
operation: {
|
||||
while true {
|
||||
let frames = task.sentRequests()
|
||||
if let match = frames.first(where: { $0.method == method }) {
|
||||
return match
|
||||
}
|
||||
try? await Task.sleep(nanoseconds: 50_000_000)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private actor InvokeTracker {
|
||||
private var request: BridgeInvokeRequest?
|
||||
|
||||
func set(_ req: BridgeInvokeRequest) {
|
||||
self.request = req
|
||||
}
|
||||
|
||||
func get() -> BridgeInvokeRequest? {
|
||||
self.request
|
||||
}
|
||||
}
|
||||
|
||||
private final class TestWebSocketSession: WebSocketSessioning {
|
||||
private let task: TestWebSocketTask
|
||||
|
||||
init(task: TestWebSocketTask) {
|
||||
self.task = task
|
||||
}
|
||||
|
||||
func makeWebSocketTask(url: URL) -> WebSocketTaskBox {
|
||||
WebSocketTaskBox(task: self.task)
|
||||
}
|
||||
}
|
||||
|
||||
private final class TestWebSocketTask: WebSocketTasking, @unchecked Sendable {
|
||||
private let lock = NSLock()
|
||||
private var _state: URLSessionTask.State = .suspended
|
||||
private var receiveQueue: [URLSessionWebSocketTask.Message] = []
|
||||
private var receiveContinuations: [CheckedContinuation<URLSessionWebSocketTask.Message, Error>] = []
|
||||
private var receiveHandlers: [@Sendable (Result<URLSessionWebSocketTask.Message, Error>) -> Void] = []
|
||||
private var sent: [URLSessionWebSocketTask.Message] = []
|
||||
|
||||
var state: URLSessionTask.State {
|
||||
self.lock.withLock { self._state }
|
||||
}
|
||||
|
||||
func resume() {
|
||||
self.lock.withLock { self._state = .running }
|
||||
}
|
||||
|
||||
func cancel(with closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
|
||||
self.lock.withLock { self._state = .canceling }
|
||||
}
|
||||
|
||||
func send(_ message: URLSessionWebSocketTask.Message) async throws {
|
||||
self.lock.withLock { self.sent.append(message) }
|
||||
guard let frame = Self.decodeRequestFrame(message) else { return }
|
||||
guard frame.method == "connect" else { return }
|
||||
let id = frame.id
|
||||
let response = Self.connectResponse(for: id)
|
||||
self.enqueue(.data(response))
|
||||
}
|
||||
|
||||
func receive() async throws -> URLSessionWebSocketTask.Message {
|
||||
try await withCheckedThrowingContinuation { cont in
|
||||
var next: URLSessionWebSocketTask.Message?
|
||||
self.lock.withLock {
|
||||
if !self.receiveQueue.isEmpty {
|
||||
next = self.receiveQueue.removeFirst()
|
||||
} else {
|
||||
self.receiveContinuations.append(cont)
|
||||
}
|
||||
}
|
||||
if let next { cont.resume(returning: next) }
|
||||
}
|
||||
}
|
||||
|
||||
func receive(completionHandler: @escaping @Sendable (Result<URLSessionWebSocketTask.Message, Error>) -> Void) {
|
||||
var next: URLSessionWebSocketTask.Message?
|
||||
self.lock.withLock {
|
||||
if !self.receiveQueue.isEmpty {
|
||||
next = self.receiveQueue.removeFirst()
|
||||
} else {
|
||||
self.receiveHandlers.append(completionHandler)
|
||||
}
|
||||
}
|
||||
if let next {
|
||||
completionHandler(.success(next))
|
||||
}
|
||||
}
|
||||
|
||||
func enqueue(_ message: URLSessionWebSocketTask.Message) {
|
||||
var handler: (@Sendable (Result<URLSessionWebSocketTask.Message, Error>) -> Void)?
|
||||
var continuation: CheckedContinuation<URLSessionWebSocketTask.Message, Error>?
|
||||
self.lock.withLock {
|
||||
if !self.receiveHandlers.isEmpty {
|
||||
handler = self.receiveHandlers.removeFirst()
|
||||
} else if !self.receiveContinuations.isEmpty {
|
||||
continuation = self.receiveContinuations.removeFirst()
|
||||
} else {
|
||||
self.receiveQueue.append(message)
|
||||
}
|
||||
}
|
||||
if let handler {
|
||||
handler(.success(message))
|
||||
} else if let continuation {
|
||||
continuation.resume(returning: message)
|
||||
}
|
||||
}
|
||||
|
||||
func sentRequests() -> [RequestFrame] {
|
||||
let messages = self.lock.withLock { self.sent }
|
||||
return messages.compactMap(Self.decodeRequestFrame)
|
||||
}
|
||||
|
||||
private static func decodeRequestFrame(_ message: URLSessionWebSocketTask.Message) -> RequestFrame? {
|
||||
let data: Data?
|
||||
switch message {
|
||||
case let .data(raw): data = raw
|
||||
case let .string(text): data = text.data(using: .utf8)
|
||||
@unknown default: data = nil
|
||||
}
|
||||
guard let data else { return nil }
|
||||
return try? JSONDecoder().decode(RequestFrame.self, from: data)
|
||||
}
|
||||
|
||||
private static func connectResponse(for id: String) -> Data {
|
||||
let payload: [String: Any] = [
|
||||
"type": "hello-ok",
|
||||
"protocol": 3,
|
||||
"server": [
|
||||
"version": "dev",
|
||||
"connId": "test-conn",
|
||||
],
|
||||
"features": [
|
||||
"methods": [],
|
||||
"events": [],
|
||||
],
|
||||
"snapshot": [
|
||||
"presence": [],
|
||||
"health": ["ok": true],
|
||||
"stateVersion": ["presence": 0, "health": 0],
|
||||
"uptimeMs": 0,
|
||||
],
|
||||
"policy": [
|
||||
"maxPayload": 1,
|
||||
"maxBufferedBytes": 1,
|
||||
"tickIntervalMs": 1000,
|
||||
],
|
||||
]
|
||||
let frame: [String: Any] = [
|
||||
"type": "res",
|
||||
"id": id,
|
||||
"ok": true,
|
||||
"payload": payload,
|
||||
]
|
||||
return (try? JSONSerialization.data(withJSONObject: frame)) ?? Data()
|
||||
}
|
||||
}
|
||||
|
||||
private extension GatewayNodeInvokeTests {
|
||||
static func makeEventMessage(event: String, payload: Any) -> URLSessionWebSocketTask.Message {
|
||||
let frame: [String: Any] = [
|
||||
"type": "event",
|
||||
"event": event,
|
||||
"payload": payload,
|
||||
]
|
||||
let data = try? JSONSerialization.data(withJSONObject: frame)
|
||||
return .data(data ?? Data())
|
||||
}
|
||||
}
|
||||
|
||||
private extension NSLock {
|
||||
func withLock<T>(_ body: () -> T) -> T {
|
||||
self.lock()
|
||||
defer { self.unlock() }
|
||||
return body()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user