improved disconnection logic (hopefully)

This commit is contained in:
2026-03-25 16:58:39 -04:00
parent eff450e898
commit a1ed1564b9
3 changed files with 49 additions and 32 deletions

View File

@@ -1,15 +1,6 @@
{ {
"originHash" : "4a778111861cbfa51bf1ddb4a59475b19b79249afde443a29b8c9edd66d656a7", "originHash" : "4a778111861cbfa51bf1ddb4a59475b19b79249afde443a29b8c9edd66d656a7",
"pins" : [ "pins" : [
{
"identity" : "swift-dotenv",
"kind" : "remoteSourceControl",
"location" : "https://github.com/thebarndog/swift-dotenv.git",
"state" : {
"revision" : "d13062940a3cdb2b13efa22ecc83b07b21c2e8dc",
"version" : "2.1.0"
}
},
{ {
"identity" : "xmlcoder", "identity" : "xmlcoder",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",

View File

@@ -40,11 +40,10 @@ actor GatewayClient {
self.hbTask = Task.detached { [self] in self.hbTask = Task.detached { [self] in
do { do {
try await Task.sleep(for: .milliseconds(Int.random(in: 0...helloMessage.heartbeat_interval))) try await Task.sleep(for: .milliseconds(Int.random(in: 0...helloMessage.heartbeat_interval)))
try await sendHeartbeat()
while !Task.isCancelled { while !Task.isCancelled {
try await Task.sleep(for: .milliseconds(helloMessage.heartbeat_interval))
try await sendHeartbeat() try await sendHeartbeat()
try await Task.sleep(for: .milliseconds(helloMessage.heartbeat_interval))
} }
} catch { } catch {
print("Heartbeat task canceled") print("Heartbeat task canceled")
@@ -75,36 +74,54 @@ actor GatewayClient {
var gwMessage: GatewayMessage? = nil var gwMessage: GatewayMessage? = nil
let json = JSONDecoder() let json = JSONDecoder()
while gwMessage == nil { while gwMessage == nil {
var wsMessage: URLSessionWebSocketTask.Message? = nil
do { do {
let wsMessage = try await ws.receive() wsMessage = try await ws.receive()
} catch {
print("Error listening to gateway: \(error)")
try await reconnect()
}
guard let wsMessage = wsMessage else { continue }
#if DEBUG #if DEBUG
//print(wsMessage) //print(wsMessage)
#endif #endif
guard case .string(let str) = wsMessage else { throw GatewayError.invalidMessage } guard case .string(let str) = wsMessage else { throw GatewayError.invalidMessage }
strBuffer.append(str) strBuffer.append(str)
do {
gwMessage = try json.decode(GatewayMessage.self, from: Data(strBuffer.utf8)) gwMessage = try json.decode(GatewayMessage.self, from: Data(strBuffer.utf8))
} catch URLError.networkConnectionLost {
self.open = false
try await reconnect()
} catch DecodingError.dataCorrupted { } catch DecodingError.dataCorrupted {
continue continue
} }
} }
guard let gwMessage = gwMessage else { throw GatewayError.invalidMessage } guard let gwMessage = gwMessage else { throw GatewayError.invalidMessage }
sequenceNum = gwMessage.s ?? sequenceNum sequenceNum = gwMessage.s ?? sequenceNum
if gwMessage.d == .heartbeatAck { hbAck = true }
return gwMessage return gwMessage
} }
private func reconnect() async throws { private func reconnect() async throws {
open = false
ws.cancel()
hbTask?.cancel() hbTask?.cancel()
do { do {
try await attemptResume() try await attemptResume()
} catch { } catch {
print(error) print("Error resuming session: \(error)")
while (!open) {
open = false
ws.cancel() ws.cancel()
hbTask?.cancel()
let queryItems = [URLQueryItem(name: "v", value: "10"), URLQueryItem(name: "encoding", value: "json")] let queryItems = [URLQueryItem(name: "v", value: "10"), URLQueryItem(name: "encoding", value: "json")]
ws = URLSession.shared.webSocketTask(with: gatewayURL.appending(queryItems: queryItems)) ws = URLSession.shared.webSocketTask(with: gatewayURL.appending(queryItems: queryItems))
do {
try await openConnection() try await openConnection()
} catch {
print("Error reconnecting: \(error)")
}
if (open) { break }
try await Task.sleep(for: .seconds(5))
}
} }
} }
@@ -141,10 +158,16 @@ actor GatewayClient {
self.open = true self.open = true
} }
var hbAck = false
private func sendHeartbeat() async throws { private func sendHeartbeat() async throws {
guard self.open else { print("conn closed, skipping hb"); return } guard self.open else { print("conn closed, skipping hb"); return }
hbAck = false
let hbMessage = "{\"op\":1,\"d\":\(sequenceNum == nil ? "null" : String(sequenceNum!))}" let hbMessage = "{\"op\":1,\"d\":\(sequenceNum == nil ? "null" : String(sequenceNum!))}"
try await ws.send(.string(hbMessage)) try await ws.send(.string(hbMessage))
try await Task.sleep(for: .seconds(5))
if !hbAck {
ws.cancel(with: .normalClosure, reason: nil)
}
} }
var events: AsyncStream<GatewayPayload> { var events: AsyncStream<GatewayPayload> {

View File

@@ -47,10 +47,11 @@ public struct SessionStartLimit: Codable, Sendable {
public let max_concurrency: Int public let max_concurrency: Int
} }
public enum GatewayPayload: Decodable, Sendable { public enum GatewayPayload: Decodable, Sendable, Equatable {
case hello(GatewayHello) case hello(GatewayHello)
case messageCreate(MessageCreate) case messageCreate(MessageCreate)
case ready(GatewayReady) case ready(GatewayReady)
case heartbeatAck
} }
public struct GatewayMessage: Decodable, Sendable { public struct GatewayMessage: Decodable, Sendable {
@@ -72,6 +73,8 @@ public struct GatewayMessage: Decodable, Sendable {
s = try container.decode(Int?.self, forKey: .s) s = try container.decode(Int?.self, forKey: .s)
t = try container.decode(String?.self, forKey: .t) t = try container.decode(String?.self, forKey: .t)
switch op { switch op {
case 11:
d = .heartbeatAck
case 10: case 10:
let hello = try container.decode(GatewayHello.self, forKey: .d) let hello = try container.decode(GatewayHello.self, forKey: .d)
d = .hello(hello) d = .hello(hello)
@@ -95,12 +98,12 @@ public struct GatewayMessage: Decodable, Sendable {
} }
} }
public struct GatewayReady: Codable, Sendable { public struct GatewayReady: Codable, Sendable, Equatable {
public let session_id: String public let session_id: String
public let resume_gateway_url: URL public let resume_gateway_url: URL
} }
public struct GatewayHello: Codable, Sendable { public struct GatewayHello: Codable, Sendable, Equatable {
public let heartbeat_interval: Int public let heartbeat_interval: Int
} }
@@ -172,7 +175,7 @@ public struct MessageRefrence: Codable, Sendable {
let guild_id: String? let guild_id: String?
} }
public struct MessageCreate: Codable, Sendable { public struct MessageCreate: Codable, Sendable, Equatable {
public let id: String public let id: String
public let channel_id: String public let channel_id: String
public let guild_id: String? public let guild_id: String?
@@ -183,19 +186,19 @@ public struct MessageCreate: Codable, Sendable {
} }
public struct User: Codable, Sendable { public struct User: Codable, Sendable, Equatable {
public let id: String? public let id: String?
public let bot: Bool? public let bot: Bool?
public let global_name: String? public let global_name: String?
public let username: String public let username: String
} }
public struct GuildMember: Codable, Sendable { public struct GuildMember: Codable, Sendable, Equatable {
public let user: User? public let user: User?
public let nick: String? public let nick: String?
} }
public struct MentionUser: Codable, Sendable { public struct MentionUser: Codable, Sendable, Equatable {
public let id: String? public let id: String?
public let bot: Bool? public let bot: Bool?
public let global_name: String? public let global_name: String?