CallKit/PushKit интеграция + фикс PacketPushNotification (tokenType, deviceId)
This commit is contained in:
@@ -98,6 +98,12 @@ final class ProtocolManager: @unchecked Sendable {
|
||||
// Saved credentials for auto-reconnect
|
||||
private var savedPublicKey: String?
|
||||
private var savedPrivateHash: String?
|
||||
/// Pre-built handshake packet for instant send on socket open.
|
||||
/// Built once in connect() on MainActor (safe UIDevice access), reused across reconnects.
|
||||
private var cachedHandshakeData: Data?
|
||||
/// Timestamp of last successful authentication — used to decide whether to reset backoff.
|
||||
/// If connection was short-lived (<10s), don't reset backoff counter (server RST loop).
|
||||
private var lastAuthenticatedTime: CFAbsoluteTime = 0
|
||||
|
||||
var publicKey: String? { savedPublicKey }
|
||||
var privateHash: String? { savedPrivateHash }
|
||||
@@ -118,6 +124,7 @@ final class ProtocolManager: @unchecked Sendable {
|
||||
|
||||
savedPublicKey = publicKey
|
||||
savedPrivateHash = privateKeyHash
|
||||
cachedHandshakeData = buildHandshakeData()
|
||||
|
||||
switch connectionState {
|
||||
case .authenticated, .handshaking, .deviceVerificationRequired:
|
||||
@@ -163,6 +170,8 @@ final class ProtocolManager: @unchecked Sendable {
|
||||
connectionState = .disconnected
|
||||
savedPublicKey = nil
|
||||
savedPrivateHash = nil
|
||||
cachedHandshakeData = nil
|
||||
lastAuthenticatedTime = 0
|
||||
Task { @MainActor in
|
||||
TransportManager.shared.reset()
|
||||
}
|
||||
@@ -194,6 +203,8 @@ final class ProtocolManager: @unchecked Sendable {
|
||||
handshakeComplete = false
|
||||
heartbeatTask?.cancel()
|
||||
searchRouter.resetPending()
|
||||
// User-initiated foreground — allow fast retry on next disconnect.
|
||||
lastAuthenticatedTime = 0
|
||||
connectionState = .connecting
|
||||
client.forceReconnect()
|
||||
}
|
||||
@@ -419,8 +430,16 @@ final class ProtocolManager: @unchecked Sendable {
|
||||
self.connectionState = .connected
|
||||
}
|
||||
|
||||
// Auto-handshake with saved credentials
|
||||
if let pk = savedPublicKey, let hash = savedPrivateHash {
|
||||
// Send pre-built handshake immediately — no packet construction on critical path.
|
||||
if let data = cachedHandshakeData {
|
||||
Self.logger.info("⚡ Sending pre-built handshake packet")
|
||||
Task { @MainActor in
|
||||
self.connectionState = .handshaking
|
||||
}
|
||||
client.send(data)
|
||||
startHandshakeTimeout()
|
||||
} else if let pk = savedPublicKey, let hash = savedPrivateHash {
|
||||
// Fallback: build handshake on the fly
|
||||
startHandshake(publicKey: pk, privateHash: hash)
|
||||
}
|
||||
}
|
||||
@@ -468,6 +487,27 @@ final class ProtocolManager: @unchecked Sendable {
|
||||
|
||||
// MARK: - Handshake
|
||||
|
||||
/// Build serialized handshake packet from saved credentials.
|
||||
/// Called from MainActor context — safe to access UIDevice.
|
||||
private func buildHandshakeData() -> Data? {
|
||||
guard let pk = savedPublicKey, let hash = savedPrivateHash else { return nil }
|
||||
let device = HandshakeDevice(
|
||||
deviceId: DeviceIdentityManager.shared.currentDeviceId(),
|
||||
deviceName: UIDevice.current.name,
|
||||
deviceOs: "iOS \(UIDevice.current.systemVersion)"
|
||||
)
|
||||
let handshake = PacketHandshake(
|
||||
privateKey: hash,
|
||||
publicKey: pk,
|
||||
protocolVersion: 1,
|
||||
heartbeatInterval: 15,
|
||||
device: device,
|
||||
handshakeState: .needDeviceVerification
|
||||
)
|
||||
return PacketRegistry.encode(handshake)
|
||||
}
|
||||
|
||||
/// Fallback handshake — builds packet on the fly when cached data is unavailable.
|
||||
private func startHandshake(publicKey: String, privateHash: String) {
|
||||
Self.logger.info("Starting handshake for \(publicKey.prefix(20))...")
|
||||
|
||||
@@ -491,24 +531,25 @@ final class ProtocolManager: @unchecked Sendable {
|
||||
)
|
||||
|
||||
sendPacketDirect(handshake)
|
||||
startHandshakeTimeout()
|
||||
}
|
||||
|
||||
// Timeout — force reconnect instead of permanent disconnect.
|
||||
// `client.disconnect()` sets `isManuallyClosed = true` which kills all
|
||||
// future reconnection attempts. Use `forceReconnect()` to retry.
|
||||
private func startHandshakeTimeout() {
|
||||
// 5s is generous for a single packet round-trip. Faster detection
|
||||
// means faster recovery via instant first retry (0ms backoff).
|
||||
handshakeTimeoutTask?.cancel()
|
||||
handshakeTimeoutTask = Task { [weak self] in
|
||||
do {
|
||||
try await Task.sleep(nanoseconds: 10_000_000_000)
|
||||
try await Task.sleep(nanoseconds: 5_000_000_000)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
guard let self, !Task.isCancelled else { return }
|
||||
if !self.handshakeComplete {
|
||||
Self.logger.error("Handshake timeout — forcing reconnect")
|
||||
Self.logger.error("Handshake timeout (5s) — forcing reconnect")
|
||||
self.handshakeComplete = false
|
||||
self.heartbeatTask?.cancel()
|
||||
Task { @MainActor in
|
||||
// Guard: only downgrade to .connecting if reconnect hasn't already progressed.
|
||||
let s = self.connectionState
|
||||
if s != .authenticated && s != .handshaking && s != .connected {
|
||||
self.connectionState = .connecting
|
||||
@@ -719,8 +760,17 @@ final class ProtocolManager: @unchecked Sendable {
|
||||
switch packet.handshakeState {
|
||||
case .completed:
|
||||
handshakeComplete = true
|
||||
// Android parity: reset backoff counter on successful authentication.
|
||||
client.resetReconnectAttempts()
|
||||
// Reset backoff only if previous connection was stable (>10s).
|
||||
// Prevents tight reconnect loop when server/proxy RSTs connections
|
||||
// shortly after sync. Without this, resetReconnectAttempts on every auth
|
||||
// means backoff always starts at 1s (attempt #1) = infinite 1s loop.
|
||||
let connectionAge = CFAbsoluteTimeGetCurrent() - lastAuthenticatedTime
|
||||
if lastAuthenticatedTime == 0 || connectionAge > 10 {
|
||||
client.resetReconnectAttempts()
|
||||
} else {
|
||||
Self.logger.info("Short-lived connection (\(Int(connectionAge))s) — keeping backoff counter")
|
||||
}
|
||||
lastAuthenticatedTime = CFAbsoluteTimeGetCurrent()
|
||||
Self.logger.info("Handshake completed. Protocol v\(packet.protocolVersion), heartbeat \(packet.heartbeatInterval)s")
|
||||
|
||||
flushPacketQueue()
|
||||
@@ -758,13 +808,20 @@ final class ProtocolManager: @unchecked Sendable {
|
||||
|
||||
private func startHeartbeat(interval: Int) {
|
||||
heartbeatTask?.cancel()
|
||||
// Android parity: heartbeat at 1/3 the server-specified interval (more aggressive keep-alive).
|
||||
let intervalNs = UInt64(interval) * 1_000_000_000 / 3
|
||||
// Send heartbeat every 5 seconds — aggressive keep-alive to prevent
|
||||
// server/proxy idle timeouts. Server timeout is heartbeat*2 = 60s,
|
||||
// so 5s gives 12× safety margin.
|
||||
let intervalNs: UInt64 = 5_000_000_000
|
||||
|
||||
// Send first heartbeat SYNCHRONOUSLY on current thread (URLSession delegate queue).
|
||||
// This bypasses the connectionState race: startHeartbeat() is called BEFORE
|
||||
// the MainActor task sets .authenticated, so sendHeartbeat()'s guard would
|
||||
// skip the first heartbeat. Direct sendText avoids this.
|
||||
if client.isConnected {
|
||||
client.sendText("heartbeat")
|
||||
}
|
||||
|
||||
heartbeatTask = Task { [weak self] in
|
||||
// Send first heartbeat immediately
|
||||
self?.sendHeartbeat()
|
||||
|
||||
while !Task.isCancelled {
|
||||
try? await Task.sleep(nanoseconds: intervalNs)
|
||||
guard !Task.isCancelled else { break }
|
||||
@@ -773,10 +830,11 @@ final class ProtocolManager: @unchecked Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
/// Android parity: send heartbeat and trigger disconnect on failure.
|
||||
/// Send heartbeat and trigger disconnect on failure.
|
||||
private func sendHeartbeat() {
|
||||
let state = connectionState
|
||||
guard state == .authenticated || state == .deviceVerificationRequired else { return }
|
||||
// Allow heartbeat when handshake is complete (covers the gap before
|
||||
// MainActor sets .authenticated) or in device verification.
|
||||
guard handshakeComplete || connectionState == .deviceVerificationRequired else { return }
|
||||
guard client.isConnected else {
|
||||
Self.logger.warning("💔 Heartbeat failed: socket not connected — triggering reconnect")
|
||||
handleHeartbeatFailure()
|
||||
|
||||
Reference in New Issue
Block a user