From 53304f5de35c9bf95506d547304afd9c40544797 Mon Sep 17 00:00:00 2001 From: AnemoFlower Date: Sat, 28 Feb 2026 12:39:40 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E7=BB=93?= =?UTF-8?q?=E6=9E=84=E4=BD=93=20PlayerInfo=EF=BC=8C=E5=87=8F=E5=B0=91=20Cl?= =?UTF-8?q?ient/Server=20=E6=9E=84=E9=80=A0=E5=87=BD=E6=95=B0=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E6=95=B0=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Client/ScaffoldingClient.swift | 22 +++++++++------ .../{Room => Models}/Member.swift | 0 .../SwiftScaffolding/Models/PlayerInfo.swift | 18 ++++++++++++ .../{Room => Models}/Room.swift | 1 + .../Server/ScaffoldingServer.swift | 28 ++++++++++++++----- .../{Util => Utils}/ByteBuffer.swift | 0 .../{Util => Utils}/ConnectionUtil.swift | 0 .../{Util => Utils}/Logger.swift | 0 .../{Util => Utils}/Once.swift | 0 .../{Util => Utils}/RoomCode.swift | 0 10 files changed, 53 insertions(+), 16 deletions(-) rename Sources/SwiftScaffolding/{Room => Models}/Member.swift (100%) create mode 100644 Sources/SwiftScaffolding/Models/PlayerInfo.swift rename Sources/SwiftScaffolding/{Room => Models}/Room.swift (98%) rename Sources/SwiftScaffolding/{Util => Utils}/ByteBuffer.swift (100%) rename Sources/SwiftScaffolding/{Util => Utils}/ConnectionUtil.swift (100%) rename Sources/SwiftScaffolding/{Util => Utils}/Logger.swift (100%) rename Sources/SwiftScaffolding/{Util => Utils}/Once.swift (100%) rename Sources/SwiftScaffolding/{Util => Utils}/RoomCode.swift (100%) diff --git a/Sources/SwiftScaffolding/Client/ScaffoldingClient.swift b/Sources/SwiftScaffolding/Client/ScaffoldingClient.swift index ff868ad..a9c306c 100644 --- a/Sources/SwiftScaffolding/Client/ScaffoldingClient.swift +++ b/Sources/SwiftScaffolding/Client/ScaffoldingClient.swift @@ -30,18 +30,13 @@ public final class ScaffoldingClient { /// /// - Parameters: /// - easyTier: 使用的 EasyTier。 - /// - playerName: 玩家名。 - /// - vendor: 联机客户端信息。 - public init( - easyTier: EasyTier, - playerName: String, - vendor: String - ) { + /// - playerInfo: 玩家信息。 + public init(easyTier: EasyTier, playerInfo: PlayerInfo) { self.easyTier = easyTier self.player = .init( - name: playerName, + name: playerInfo.name, machineID: Scaffolding.getMachineID(), - vendor: vendor, + vendor: playerInfo.vendor, kind: .guest ) @@ -52,6 +47,15 @@ public final class ScaffoldingClient { self.protocols = RequestHandler().protocols() } + // 旧格式支持 + public convenience init( + easyTier: EasyTier, + playerName: String, + vendor: String + ) { + self.init(easyTier: easyTier, playerInfo: .init(name: playerName, vendor: vendor)) + } + /// 连接到房间。 /// /// 该方法返回后,必须每隔 5s 调用一次 `heartbeat()` 方法。 diff --git a/Sources/SwiftScaffolding/Room/Member.swift b/Sources/SwiftScaffolding/Models/Member.swift similarity index 100% rename from Sources/SwiftScaffolding/Room/Member.swift rename to Sources/SwiftScaffolding/Models/Member.swift diff --git a/Sources/SwiftScaffolding/Models/PlayerInfo.swift b/Sources/SwiftScaffolding/Models/PlayerInfo.swift new file mode 100644 index 0000000..cd3ed21 --- /dev/null +++ b/Sources/SwiftScaffolding/Models/PlayerInfo.swift @@ -0,0 +1,18 @@ +// +// PlayerInfo.swift +// SwiftScaffolding +// +// Created by AnemoFlower on 2026/2/28. +// + +import Foundation + +public struct PlayerInfo { + public let name: String + public let vendor: String + + public init(name: String, vendor: String) { + self.name = name + self.vendor = vendor + } +} diff --git a/Sources/SwiftScaffolding/Room/Room.swift b/Sources/SwiftScaffolding/Models/Room.swift similarity index 98% rename from Sources/SwiftScaffolding/Room/Room.swift rename to Sources/SwiftScaffolding/Models/Room.swift index 2b86593..1922aad 100644 --- a/Sources/SwiftScaffolding/Room/Room.swift +++ b/Sources/SwiftScaffolding/Models/Room.swift @@ -11,6 +11,7 @@ import Network public final class Room: ObservableObject { /// 房客列表。 @Published public internal(set) var members: [Member] = [] + /// Minecraft 服务器端口。 public internal(set) var serverPort: UInt16 diff --git a/Sources/SwiftScaffolding/Server/ScaffoldingServer.swift b/Sources/SwiftScaffolding/Server/ScaffoldingServer.swift index 28ff1f0..6c3ee49 100644 --- a/Sources/SwiftScaffolding/Server/ScaffoldingServer.swift +++ b/Sources/SwiftScaffolding/Server/ScaffoldingServer.swift @@ -31,18 +31,21 @@ public final class ScaffoldingServer { /// - Parameters: /// - easyTier: 使用的 EasyTier。 /// - roomCode: 房间码。若不合法,将在 `createRoom()` 中抛出 `RoomError.invalidRoomCode` 错误。 - /// - playerName: 玩家名。 - /// - vendor: 联机客户端信息。 /// - serverPort: Minecraft 服务器端口号。 + /// - hostInfo: 房主信息。 public init( easyTier: EasyTier, roomCode: String, - playerName: String, - vendor: String, - serverPort: UInt16 + serverPort: UInt16, + hostInfo: PlayerInfo ) { self.room = Room( - members: [.init(name: playerName, machineID: Scaffolding.getMachineID(forHost: true), vendor: vendor, kind: .host)], + members: [.init( + name: hostInfo.name, + machineID: Scaffolding.getMachineID(forHost: true), + vendor: hostInfo.vendor, + kind: .host + )], serverPort: serverPort ) self.easyTier = easyTier @@ -55,10 +58,21 @@ public final class ScaffoldingServer { self.handler.server = self } + // 旧格式支持 + public convenience init( + easyTier: EasyTier, + roomCode: String, + playerName: String, + vendor: String, + serverPort: UInt16 + ) { + self.init(easyTier: easyTier, roomCode: roomCode, serverPort: serverPort, hostInfo: .init(name: playerName, vendor: vendor)) + } + /// 启动联机中心监听器。 /// /// 默认会在 `13452` 端口监听。若该端口被占用,会重新申请一个端口。 - /// - Returns: 联机中心端口号。 + /// - Returns: 联机中心实际端口号。 @discardableResult public func startListener() async throws -> UInt16 { let port: UInt16 = try ConnectionUtil.getPort(13452) diff --git a/Sources/SwiftScaffolding/Util/ByteBuffer.swift b/Sources/SwiftScaffolding/Utils/ByteBuffer.swift similarity index 100% rename from Sources/SwiftScaffolding/Util/ByteBuffer.swift rename to Sources/SwiftScaffolding/Utils/ByteBuffer.swift diff --git a/Sources/SwiftScaffolding/Util/ConnectionUtil.swift b/Sources/SwiftScaffolding/Utils/ConnectionUtil.swift similarity index 100% rename from Sources/SwiftScaffolding/Util/ConnectionUtil.swift rename to Sources/SwiftScaffolding/Utils/ConnectionUtil.swift diff --git a/Sources/SwiftScaffolding/Util/Logger.swift b/Sources/SwiftScaffolding/Utils/Logger.swift similarity index 100% rename from Sources/SwiftScaffolding/Util/Logger.swift rename to Sources/SwiftScaffolding/Utils/Logger.swift diff --git a/Sources/SwiftScaffolding/Util/Once.swift b/Sources/SwiftScaffolding/Utils/Once.swift similarity index 100% rename from Sources/SwiftScaffolding/Util/Once.swift rename to Sources/SwiftScaffolding/Utils/Once.swift diff --git a/Sources/SwiftScaffolding/Util/RoomCode.swift b/Sources/SwiftScaffolding/Utils/RoomCode.swift similarity index 100% rename from Sources/SwiftScaffolding/Util/RoomCode.swift rename to Sources/SwiftScaffolding/Utils/RoomCode.swift From 3d76f68f8fecdb607e6af139dbac8e236dab195b Mon Sep 17 00:00:00 2001 From: AnemoFlower Date: Sat, 28 Feb 2026 13:10:52 +0800 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=E8=87=AA=E5=8A=A8=E5=BF=83?= =?UTF-8?q?=E8=B7=B3=E4=BB=BB=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Client/ScaffoldingClient.swift | 33 +++++++++++++++++++ Sources/SwiftScaffolding/Errors.swift | 7 ++++ .../Server/ScaffoldingServer.swift | 1 + .../en.lproj/Localizable.strings | 1 + .../zh-Hans.lproj/Localizable.strings | 1 + 5 files changed, 43 insertions(+) diff --git a/Sources/SwiftScaffolding/Client/ScaffoldingClient.swift b/Sources/SwiftScaffolding/Client/ScaffoldingClient.swift index a9c306c..1f3481a 100644 --- a/Sources/SwiftScaffolding/Client/ScaffoldingClient.swift +++ b/Sources/SwiftScaffolding/Client/ScaffoldingClient.swift @@ -19,9 +19,11 @@ public final class ScaffoldingClient { private var connection: NWConnection! private var serverNodeIp: String! private var protocols: [String] + private var heartbeatTask: Task? deinit { Logger.debug("ScaffoldingClient is being deallocated") + heartbeatTask?.cancel() connection?.cancel() easyTier.terminate() } @@ -48,6 +50,7 @@ public final class ScaffoldingClient { } // 旧格式支持 + @available(*, deprecated, renamed: "ScaffoldingClient.init(easyTier:playerInfo:)", message: "") public convenience init( easyTier: EasyTier, playerName: String, @@ -64,6 +67,9 @@ public final class ScaffoldingClient { /// - roomCode: 房间码。 /// - checkServer: 是否检查联机中心返回的 Minecraft 服务器端口号。 public func connect(to roomCode: String, checkServer: Bool = true, terminationHandler: ((Process) -> Void)? = nil) async throws { + guard connection == nil else { + throw ConnectionError.alreadyConnected + } guard RoomCode.isValid(code: roomCode) else { throw RoomError.invalidRoomCode } @@ -164,6 +170,33 @@ public final class ScaffoldingClient { } } + /// 启动自动心跳任务。 + /// + /// 如果已有一个任务正在运行,该方法会直接返回。 + /// - Throws: `ScaffoldingClient` 状态异常。 + public func startHeartbeatTask() throws { + guard connection != nil, room != nil else { + throw ConnectionError.missingConnection + } + if heartbeatTask != nil { + return + } + heartbeatTask = Task { [weak self] in + do { + while !Task.isCancelled { + guard let self else { break } + try await Task.sleep(nanoseconds: 5 * 1_000_000_000) + try Task.checkCancellation() + try await self.heartbeat() + } + } catch is CancellationError { + } catch { + Logger.error("Heartbeat task failed: \(error.localizedDescription)") + throw error + } + } + } + /// 退出房间并关闭连接。 public func stop() { Logger.info("Stopping scaffolding client") diff --git a/Sources/SwiftScaffolding/Errors.swift b/Sources/SwiftScaffolding/Errors.swift index 3d1c6d0..f7a1f28 100644 --- a/Sources/SwiftScaffolding/Errors.swift +++ b/Sources/SwiftScaffolding/Errors.swift @@ -16,6 +16,7 @@ public enum ConnectionError: LocalizedError, Equatable { case failedToAllocatePort case notEnoughBytes case failedToDecodeString + case alreadyConnected public var errorDescription: String? { switch self { @@ -61,6 +62,12 @@ public enum ConnectionError: LocalizedError, Equatable { bundle: Bundle.module, comment: "解析 UTF-8 字符串失败" ) + case .alreadyConnected: + return NSLocalizedString( + "ConnectionError.alreadyConnected", + bundle: Bundle.module, + comment: "已有一个已建立的连接" + ) } } } diff --git a/Sources/SwiftScaffolding/Server/ScaffoldingServer.swift b/Sources/SwiftScaffolding/Server/ScaffoldingServer.swift index 6c3ee49..08440f8 100644 --- a/Sources/SwiftScaffolding/Server/ScaffoldingServer.swift +++ b/Sources/SwiftScaffolding/Server/ScaffoldingServer.swift @@ -59,6 +59,7 @@ public final class ScaffoldingServer { } // 旧格式支持 + @available(*, deprecated, renamed: "ScaffoldingServer.init(easyTier:roomCode:serverPort:hostInfo:)", message: "") public convenience init( easyTier: EasyTier, roomCode: String, diff --git a/Sources/SwiftScaffolding/en.lproj/Localizable.strings b/Sources/SwiftScaffolding/en.lproj/Localizable.strings index 7ab8f6f..fc32695 100644 --- a/Sources/SwiftScaffolding/en.lproj/Localizable.strings +++ b/Sources/SwiftScaffolding/en.lproj/Localizable.strings @@ -13,6 +13,7 @@ "ConnectionError.failedToAllocatePort" = "Failed to allocate port."; "ConnectionError.notEnoughBytes" = "The peer did not send enough data."; "ConnectionError.failedToDecodeString" = "Failed to decode string."; +"ConnectionError.alreadyConnected" = "There is already an established connection."; "RoomError.invalidRoomCode" = "The room code was invalid."; "RoomError.roomClosed" = "The room has been closed, or the network is unstable."; diff --git a/Sources/SwiftScaffolding/zh-Hans.lproj/Localizable.strings b/Sources/SwiftScaffolding/zh-Hans.lproj/Localizable.strings index 7e963b4..459cee2 100644 --- a/Sources/SwiftScaffolding/zh-Hans.lproj/Localizable.strings +++ b/Sources/SwiftScaffolding/zh-Hans.lproj/Localizable.strings @@ -13,6 +13,7 @@ "ConnectionError.failedToAllocatePort" = "端口分配失败。"; "ConnectionError.notEnoughBytes" = "对端未发送足够长的数据。"; "ConnectionError.failedToDecodeString" = "解析字符串失败。"; +"ConnectionError.alreadyConnected" = "已有一个已建立的连接。"; "RoomError.invalidRoomCode" = "无效的房间码。"; "RoomError.roomClosed" = "房间已被关闭,或者网络不稳定。"; From fed49a55662ee13f49d10b64915c4bb1dc339c15 Mon Sep 17 00:00:00 2001 From: AnemoFlower Date: Sat, 28 Feb 2026 16:05:27 +0800 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E4=BC=A0?= =?UTF-8?q?=E5=85=A5=E5=BF=83=E8=B7=B3=E5=A4=B1=E8=B4=A5=E5=9B=9E=E8=B0=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SwiftScaffolding/Client/ScaffoldingClient.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/SwiftScaffolding/Client/ScaffoldingClient.swift b/Sources/SwiftScaffolding/Client/ScaffoldingClient.swift index 1f3481a..585fba2 100644 --- a/Sources/SwiftScaffolding/Client/ScaffoldingClient.swift +++ b/Sources/SwiftScaffolding/Client/ScaffoldingClient.swift @@ -19,13 +19,11 @@ public final class ScaffoldingClient { private var connection: NWConnection! private var serverNodeIp: String! private var protocols: [String] - private var heartbeatTask: Task? + private var heartbeatTask: Task? deinit { Logger.debug("ScaffoldingClient is being deallocated") - heartbeatTask?.cancel() - connection?.cancel() - easyTier.terminate() + stop() } /// 使用指定的 EasyTier 创建连接到指定房间的 `ScaffoldingClient`。 @@ -174,7 +172,7 @@ public final class ScaffoldingClient { /// /// 如果已有一个任务正在运行,该方法会直接返回。 /// - Throws: `ScaffoldingClient` 状态异常。 - public func startHeartbeatTask() throws { + public func startHeartbeatTask(failureHandler: ((Error) async -> Void)?) throws { guard connection != nil, room != nil else { throw ConnectionError.missingConnection } @@ -192,7 +190,7 @@ public final class ScaffoldingClient { } catch is CancellationError { } catch { Logger.error("Heartbeat task failed: \(error.localizedDescription)") - throw error + await failureHandler?(error) } } } @@ -201,6 +199,8 @@ public final class ScaffoldingClient { public func stop() { Logger.info("Stopping scaffolding client") easyTier.terminate() + heartbeatTask?.cancel() + heartbeatTask = nil connection?.cancel() connection = nil } From 8e632375181acb3808d9fbfbd8fcebdda53d0ace Mon Sep 17 00:00:00 2001 From: AnemoFlower Date: Sat, 28 Feb 2026 16:17:12 +0800 Subject: [PATCH 4/5] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=20RequestHandler?= =?UTF-8?q?=EF=BC=8C=E9=81=BF=E5=85=8D=E5=8F=AF=E8=83=BD=E7=9A=84=E5=86=85?= =?UTF-8?q?=E5=AD=98=E6=B3=84=E6=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Server/RequestHandler.swift | 111 +++++++++--------- .../Server/ScaffoldingServer.swift | 1 - 2 files changed, 58 insertions(+), 54 deletions(-) diff --git a/Sources/SwiftScaffolding/Server/RequestHandler.swift b/Sources/SwiftScaffolding/Server/RequestHandler.swift index 64de81c..b784963 100644 --- a/Sources/SwiftScaffolding/Server/RequestHandler.swift +++ b/Sources/SwiftScaffolding/Server/RequestHandler.swift @@ -10,7 +10,7 @@ import SwiftyJSON import Network public class RequestHandler { - var server: ScaffoldingServer! + weak var server: ScaffoldingServer? private var handlers: [String: (Sender, ByteBuffer) throws -> Scaffolding.Response] = [:] internal init() { @@ -35,17 +35,13 @@ public class RequestHandler { return Array(handlers.keys) } - internal func destroy() { - handlers.removeAll() - server = nil - } - internal func handleRequest( from connection: NWConnection, type: String, requestBody: ByteBuffer, responseBuffer: ByteBuffer ) throws { + guard let server else { return } guard let handler = handlers[type] else { Logger.warn("Unknown request: \(type)") responseBuffer.writeUInt8(255) @@ -66,62 +62,71 @@ public class RequestHandler { } private func registerHandlers() { - registerHandler(for: "c:ping") { sender, requestBody in - return .init(status: 0, data: requestBody.data) - } + registerHandler(for: "c:ping", handler: handlePing(sender:requestBody:)) + registerHandler(for: "c:protocols", handler: handleProtocols(sender:requestBody:)) + registerHandler(for: "c:server_port", handler: handleServerPort(sender:requestBody:)) + registerHandler(for: "c:player_ping", handler: handlePlayerPing(sender:requestBody:)) + registerHandler(for: "c:player_profiles_list", handler: handlePlayerList(sender:requestBody:)) + } + + private func handlePing(sender: Sender, requestBody: ByteBuffer) throws -> Scaffolding.Response { + return .init(status: 0, data: requestBody.data) + } + + private func handleProtocols(sender: Sender, requestBody: ByteBuffer) throws -> Scaffolding.Response { + let protocols: String = Array(self.handlers.keys).joined(separator: "\0") + return .init(status: 0) { $0.writeString(protocols) } + } + + private func handleServerPort(sender: Sender, requestBody: ByteBuffer) throws -> Scaffolding.Response { + guard let server else { return .init(status: 255, data: .init()) } + return .init(status: 0) { $0.writeUInt16(server.room.serverPort) } + } + + private func handlePlayerPing(sender: Sender, requestBody: ByteBuffer) throws -> Scaffolding.Response { + guard let server else { return .init(status: 255, data: .init()) } + let connection: NWConnection = sender.connection + let rawMember: Member = try server.decoder.decode(Member.self, from: requestBody.data) - registerHandler(for: "c:protocols") { sender, requestBody in - let protocols: String = Array(self.handlers.keys).joined(separator: "\0") - return .init(status: 0) { $0.writeString(protocols) } - } + let member: Member = .init( + name: rawMember.name, + machineID: rawMember.machineId, + vendor: rawMember.vendor, + kind: .guest + ) + + let identifier: ObjectIdentifier = .init(connection) - registerHandler(for: "c:server_port") { sender, requestBody in - return .init(status: 0) { $0.writeUInt16(self.server.room.serverPort) } + if server.machineIdMap[identifier] == nil + && server.machineIdMap.values.contains(member.machineId) { + Logger.warn("Detected a machine_id collision") + throw RoomError.playerInfoMismatch + } + if let machineId = server.machineIdMap[identifier], machineId != member.machineId { + Logger.warn("machine_id mismatch detected") + throw RoomError.playerInfoMismatch } - registerHandler(for: "c:player_ping") { sender, requestBody in - let connection: NWConnection = sender.connection - let rawMember: Member = try self.server.decoder.decode(Member.self, from: requestBody.data) - - let member: Member = .init( - name: rawMember.name, - machineID: rawMember.machineId, - vendor: rawMember.vendor, - kind: .guest - ) - - let identifier: ObjectIdentifier = .init(connection) - - if self.server.machineIdMap[identifier] == nil - && self.server.machineIdMap.values.contains(member.machineId) { - Logger.warn("Detected a machine_id collision") - throw RoomError.playerInfoMismatch - } - if let machineId = self.server.machineIdMap[identifier], machineId != member.machineId { - Logger.warn("machine_id mismatch detected") + server.machineIdMap[identifier] = member.machineId + + if let storedMember: Member = server.room.members.first(where: { $0.machineId == member.machineId }) { + if storedMember != member { + Logger.warn("Member info mismatch for \(storedMember.name)") throw RoomError.playerInfoMismatch } - - self.server.machineIdMap[identifier] = member.machineId - - if let storedMember: Member = self.server.room.members.first(where: { $0.machineId == member.machineId }) { - if storedMember != member { - Logger.warn("Member info mismatch for \(storedMember.name)") - throw RoomError.playerInfoMismatch - } - } else { - Logger.info("Received player info from \(connection.endpoint.debugDescription): { \"name\": \"\(member.name)\", \"vendor\": \"\(member.vendor)\", \"machine_id\": \"\(member.machineId)\"}") - DispatchQueue.main.async { - self.server.room.members.append(member) - } + } else { + Logger.info("Received player info from \(connection.endpoint.debugDescription): { \"name\": \"\(member.name)\", \"vendor\": \"\(member.vendor)\", \"machine_id\": \"\(member.machineId)\"}") + DispatchQueue.main.async { + server.room.members.append(member) } - - return .init(status: 0, data: Data()) } - registerHandler(for: "c:player_profiles_list") { sender, requestBody in - return .init(status: 0, data: try self.server.encoder.encode(self.server.room.members)) - } + return .init(status: 0, data: Data()) + } + + private func handlePlayerList(sender: Sender, requestBody: ByteBuffer) throws -> Scaffolding.Response { + guard let server else { return .init(status: 255, data: .init()) } + return .init(status: 0, data: try server.encoder.encode(server.room.members)) } public struct Sender { diff --git a/Sources/SwiftScaffolding/Server/ScaffoldingServer.swift b/Sources/SwiftScaffolding/Server/ScaffoldingServer.swift index 08440f8..5b3fbba 100644 --- a/Sources/SwiftScaffolding/Server/ScaffoldingServer.swift +++ b/Sources/SwiftScaffolding/Server/ScaffoldingServer.swift @@ -167,7 +167,6 @@ public final class ScaffoldingServer { } } connections = [] - handler.destroy() } From 078a6f4a897121d4d01fa915b4e0d12329c22e18 Mon Sep 17 00:00:00 2001 From: AnemoFlower Date: Sat, 28 Feb 2026 16:29:01 +0800 Subject: [PATCH 5/5] fix --- Sources/SwiftScaffolding/Client/ScaffoldingClient.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SwiftScaffolding/Client/ScaffoldingClient.swift b/Sources/SwiftScaffolding/Client/ScaffoldingClient.swift index 585fba2..b333da3 100644 --- a/Sources/SwiftScaffolding/Client/ScaffoldingClient.swift +++ b/Sources/SwiftScaffolding/Client/ScaffoldingClient.swift @@ -176,7 +176,7 @@ public final class ScaffoldingClient { guard connection != nil, room != nil else { throw ConnectionError.missingConnection } - if heartbeatTask != nil { + if let heartbeatTask, !heartbeatTask.isCancelled { return } heartbeatTask = Task { [weak self] in