mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-26 15:07:39 +00:00
Use modern Swift
This commit is contained in:
parent
831361f5b2
commit
7b02733207
10 changed files with 92 additions and 184 deletions
|
|
@ -219,7 +219,7 @@ struct CastFileGenerator {
|
|||
}
|
||||
|
||||
// Sleep briefly before checking again
|
||||
try? await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds
|
||||
try? await Task.sleep(for: .milliseconds(100))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import Combine
|
||||
import Foundation
|
||||
import Hummingbird
|
||||
import OSLog
|
||||
|
|
@ -13,7 +12,7 @@ import OSLog
|
|||
final class HummingbirdServer: ServerProtocol {
|
||||
private var tunnelServer: TunnelServer?
|
||||
private let logger = Logger(subsystem: "com.steipete.VibeTunnel", category: "HummingbirdServer")
|
||||
private let logSubject = PassthroughSubject<ServerLogEntry, Never>()
|
||||
private var logContinuation: AsyncStream<ServerLogEntry>.Continuation?
|
||||
|
||||
var serverType: ServerMode { .hummingbird }
|
||||
|
||||
|
|
@ -32,8 +31,14 @@ final class HummingbirdServer: ServerProtocol {
|
|||
}
|
||||
}
|
||||
|
||||
var logPublisher: AnyPublisher<ServerLogEntry, Never> {
|
||||
logSubject.eraseToAnyPublisher()
|
||||
let logStream: AsyncStream<ServerLogEntry>
|
||||
|
||||
init() {
|
||||
var localContinuation: AsyncStream<ServerLogEntry>.Continuation?
|
||||
self.logStream = AsyncStream { continuation in
|
||||
localContinuation = continuation
|
||||
}
|
||||
self.logContinuation = localContinuation
|
||||
}
|
||||
|
||||
func start() async throws {
|
||||
|
|
@ -43,7 +48,7 @@ final class HummingbirdServer: ServerProtocol {
|
|||
}
|
||||
|
||||
logger.info("Starting Hummingbird server on port \(self.port)")
|
||||
logSubject.send(ServerLogEntry(
|
||||
logContinuation?.yield(ServerLogEntry(
|
||||
level: .info,
|
||||
message: "Initializing Hummingbird server...",
|
||||
source: .hummingbird
|
||||
|
|
@ -58,10 +63,10 @@ final class HummingbirdServer: ServerProtocol {
|
|||
try await server.start()
|
||||
|
||||
logger.info("Hummingbird server started successfully")
|
||||
logSubject.send(ServerLogEntry(level: .info, message: "Hummingbird server is ready", source: .hummingbird))
|
||||
logContinuation?.yield(ServerLogEntry(level: .info, message: "Hummingbird server is ready", source: .hummingbird))
|
||||
} catch {
|
||||
logger.error("Failed to start Hummingbird server: \(error.localizedDescription)")
|
||||
logSubject.send(ServerLogEntry(
|
||||
logContinuation?.yield(ServerLogEntry(
|
||||
level: .error,
|
||||
message: "Failed to start: \(error.localizedDescription)",
|
||||
source: .hummingbird
|
||||
|
|
@ -77,7 +82,7 @@ final class HummingbirdServer: ServerProtocol {
|
|||
}
|
||||
|
||||
logger.info("Stopping Hummingbird server")
|
||||
logSubject.send(ServerLogEntry(
|
||||
logContinuation?.yield(ServerLogEntry(
|
||||
level: .info,
|
||||
message: "Shutting down Hummingbird server...",
|
||||
source: .hummingbird
|
||||
|
|
@ -88,14 +93,14 @@ final class HummingbirdServer: ServerProtocol {
|
|||
tunnelServer = nil
|
||||
|
||||
logger.info("Hummingbird server stopped")
|
||||
logSubject.send(ServerLogEntry(
|
||||
logContinuation?.yield(ServerLogEntry(
|
||||
level: .info,
|
||||
message: "Hummingbird server shutdown complete",
|
||||
source: .hummingbird
|
||||
))
|
||||
} catch {
|
||||
logger.error("Error stopping Hummingbird server: \(error.localizedDescription)")
|
||||
logSubject.send(ServerLogEntry(
|
||||
logContinuation?.yield(ServerLogEntry(
|
||||
level: .error,
|
||||
message: "Error stopping: \(error.localizedDescription)",
|
||||
source: .hummingbird
|
||||
|
|
@ -105,7 +110,7 @@ final class HummingbirdServer: ServerProtocol {
|
|||
|
||||
func restart() async throws {
|
||||
logger.info("Restarting Hummingbird server")
|
||||
logSubject.send(ServerLogEntry(level: .info, message: "Restarting server", source: .hummingbird))
|
||||
logContinuation?.yield(ServerLogEntry(level: .info, message: "Restarting server", source: .hummingbird))
|
||||
|
||||
await stop()
|
||||
try await start()
|
||||
|
|
|
|||
|
|
@ -298,7 +298,7 @@ final class NgrokService: NgrokTunnelProtocol {
|
|||
}
|
||||
|
||||
group.addTask {
|
||||
try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
|
||||
try await Task.sleep(for: .seconds(seconds))
|
||||
throw NgrokError.networkError("Operation timed out")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import Combine
|
||||
import Foundation
|
||||
import OSLog
|
||||
|
||||
|
|
@ -27,7 +26,7 @@ final class RustServer: ServerProtocol {
|
|||
private var errorTask: Task<Void, Never>?
|
||||
|
||||
private let logger = Logger(subsystem: "com.steipete.VibeTunnel", category: "RustServer")
|
||||
private let logSubject = PassthroughSubject<ServerLogEntry, Never>()
|
||||
private var logContinuation: AsyncStream<ServerLogEntry>.Continuation?
|
||||
private let processQueue = DispatchQueue(label: "com.steipete.VibeTunnel.RustServer", qos: .userInitiated)
|
||||
|
||||
/// Actor to handle process operations on background thread.
|
||||
|
|
@ -89,8 +88,14 @@ final class RustServer: ServerProtocol {
|
|||
}
|
||||
}
|
||||
|
||||
var logPublisher: AnyPublisher<ServerLogEntry, Never> {
|
||||
logSubject.eraseToAnyPublisher()
|
||||
let logStream: AsyncStream<ServerLogEntry>
|
||||
|
||||
init() {
|
||||
var localContinuation: AsyncStream<ServerLogEntry>.Continuation?
|
||||
self.logStream = AsyncStream { continuation in
|
||||
localContinuation = continuation
|
||||
}
|
||||
self.logContinuation = localContinuation
|
||||
}
|
||||
|
||||
func start() async throws {
|
||||
|
|
@ -102,19 +107,19 @@ final class RustServer: ServerProtocol {
|
|||
guard !port.isEmpty else {
|
||||
let error = RustServerError.invalidPort
|
||||
logger.error("Port not configured")
|
||||
logSubject.send(ServerLogEntry(level: .error, message: error.localizedDescription, source: .rust))
|
||||
logContinuation?.yield(ServerLogEntry(level: .error, message: error.localizedDescription, source: .rust))
|
||||
throw error
|
||||
}
|
||||
|
||||
logger.info("Starting Rust tty-fwd server on port \(self.port)")
|
||||
logSubject.send(ServerLogEntry(level: .info, message: "Initializing Rust tty-fwd server...", source: .rust))
|
||||
logContinuation?.yield(ServerLogEntry(level: .info, message: "Initializing Rust tty-fwd server...", source: .rust))
|
||||
|
||||
// Get the tty-fwd binary path
|
||||
let binaryPath = Bundle.main.path(forResource: "tty-fwd", ofType: nil)
|
||||
guard let binaryPath else {
|
||||
let error = RustServerError.binaryNotFound
|
||||
logger.error("tty-fwd binary not found in bundle")
|
||||
logSubject.send(ServerLogEntry(level: .error, message: error.localizedDescription, source: .rust))
|
||||
logContinuation?.yield(ServerLogEntry(level: .error, message: error.localizedDescription, source: .rust))
|
||||
throw error
|
||||
}
|
||||
|
||||
|
|
@ -217,7 +222,7 @@ final class RustServer: ServerProtocol {
|
|||
let errorData = stderrPipe.fileHandleForReading.availableData
|
||||
if !errorData.isEmpty, let errorOutput = String(data: errorData, encoding: .utf8) {
|
||||
logger.error("Process stderr: \(errorOutput)")
|
||||
logSubject.send(ServerLogEntry(
|
||||
logContinuation?.yield(ServerLogEntry(
|
||||
level: .error,
|
||||
message: "Process error: \(errorOutput)",
|
||||
source: .rust
|
||||
|
|
@ -229,15 +234,15 @@ final class RustServer: ServerProtocol {
|
|||
}
|
||||
|
||||
logger.info("Rust server process started, performing health check...")
|
||||
logSubject.send(ServerLogEntry(level: .info, message: "Performing health check...", source: .rust))
|
||||
logContinuation?.yield(ServerLogEntry(level: .info, message: "Performing health check...", source: .rust))
|
||||
|
||||
// Perform health check to ensure server is actually responding
|
||||
let isHealthy = await performHealthCheck(maxAttempts: 10, delaySeconds: 0.5)
|
||||
|
||||
if isHealthy {
|
||||
logger.info("Rust server started successfully and is responding")
|
||||
logSubject.send(ServerLogEntry(level: .info, message: "Health check passed ✓", source: .rust))
|
||||
logSubject.send(ServerLogEntry(level: .info, message: "Rust tty-fwd server is ready", source: .rust))
|
||||
logContinuation?.yield(ServerLogEntry(level: .info, message: "Health check passed ✓", source: .rust))
|
||||
logContinuation?.yield(ServerLogEntry(level: .info, message: "Rust tty-fwd server is ready", source: .rust))
|
||||
|
||||
// Monitor process termination with task context
|
||||
Task {
|
||||
|
|
@ -250,7 +255,7 @@ final class RustServer: ServerProtocol {
|
|||
} else {
|
||||
// Server process is running but not responding
|
||||
logger.error("Rust server process started but is not responding to health checks")
|
||||
logSubject.send(ServerLogEntry(
|
||||
logContinuation?.yield(ServerLogEntry(
|
||||
level: .error,
|
||||
message: "Health check failed - server not responding",
|
||||
source: .rust
|
||||
|
|
@ -268,7 +273,7 @@ final class RustServer: ServerProtocol {
|
|||
} catch {
|
||||
isRunning = false
|
||||
logger.error("Failed to start Rust server: \(error.localizedDescription)")
|
||||
logSubject.send(ServerLogEntry(
|
||||
logContinuation?.yield(ServerLogEntry(
|
||||
level: .error,
|
||||
message: "Failed to start: \(error.localizedDescription)",
|
||||
source: .rust
|
||||
|
|
@ -284,7 +289,7 @@ final class RustServer: ServerProtocol {
|
|||
}
|
||||
|
||||
logger.info("Stopping Rust server")
|
||||
logSubject.send(ServerLogEntry(level: .info, message: "Shutting down Rust tty-fwd server...", source: .rust))
|
||||
logContinuation?.yield(ServerLogEntry(level: .info, message: "Shutting down Rust tty-fwd server...", source: .rust))
|
||||
|
||||
// Cancel output monitoring tasks
|
||||
outputTask?.cancel()
|
||||
|
|
@ -302,7 +307,7 @@ final class RustServer: ServerProtocol {
|
|||
// Force kill if termination timeout
|
||||
process.interrupt()
|
||||
logger.warning("Force killed Rust server after timeout")
|
||||
logSubject.send(ServerLogEntry(
|
||||
logContinuation?.yield(ServerLogEntry(
|
||||
level: .warning,
|
||||
message: "Force killed server after timeout",
|
||||
source: .rust
|
||||
|
|
@ -318,12 +323,12 @@ final class RustServer: ServerProtocol {
|
|||
isRunning = false
|
||||
|
||||
logger.info("Rust server stopped")
|
||||
logSubject.send(ServerLogEntry(level: .info, message: "Rust tty-fwd server shutdown complete", source: .rust))
|
||||
logContinuation?.yield(ServerLogEntry(level: .info, message: "Rust tty-fwd server shutdown complete", source: .rust))
|
||||
}
|
||||
|
||||
func restart() async throws {
|
||||
logger.info("Restarting Rust server")
|
||||
logSubject.send(ServerLogEntry(level: .info, message: "Restarting server", source: .rust))
|
||||
logContinuation?.yield(ServerLogEntry(level: .info, message: "Restarting server", source: .rust))
|
||||
|
||||
await stop()
|
||||
try await start()
|
||||
|
|
@ -342,7 +347,7 @@ final class RustServer: ServerProtocol {
|
|||
var request = URLRequest(url: healthURL)
|
||||
request.timeoutInterval = 2.0
|
||||
|
||||
logSubject.send(ServerLogEntry(
|
||||
logContinuation?.yield(ServerLogEntry(
|
||||
level: .debug,
|
||||
message: "Health check attempt \(attempt)/\(maxAttempts)...",
|
||||
source: .rust
|
||||
|
|
@ -357,7 +362,7 @@ final class RustServer: ServerProtocol {
|
|||
} catch {
|
||||
logger.debug("Health check attempt \(attempt) failed: \(error.localizedDescription)")
|
||||
if attempt == maxAttempts {
|
||||
logSubject.send(ServerLogEntry(
|
||||
logContinuation?.yield(ServerLogEntry(
|
||||
level: .warning,
|
||||
message: "Health check failed after \(maxAttempts) attempts",
|
||||
source: .rust
|
||||
|
|
@ -403,7 +408,7 @@ final class RustServer: ServerProtocol {
|
|||
Task { @MainActor [weak self] in
|
||||
guard let self else { return }
|
||||
let level = self.detectLogLevel(from: line)
|
||||
self.logSubject.send(ServerLogEntry(level: level, message: line, source: .rust))
|
||||
self.logContinuation?.yield(ServerLogEntry(level: level, message: line, source: .rust))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -437,7 +442,7 @@ final class RustServer: ServerProtocol {
|
|||
}
|
||||
Task { @MainActor [weak self] in
|
||||
guard let self else { return }
|
||||
self.logSubject.send(ServerLogEntry(
|
||||
self.logContinuation?.yield(ServerLogEntry(
|
||||
level: .error,
|
||||
message: line,
|
||||
source: .rust
|
||||
|
|
@ -464,7 +469,7 @@ final class RustServer: ServerProtocol {
|
|||
// Unexpected termination
|
||||
let exitCode = process.terminationStatus
|
||||
self.logger.error("Rust server terminated unexpectedly with exit code: \(exitCode)")
|
||||
self.logSubject.send(ServerLogEntry(
|
||||
self.logContinuation?.yield(ServerLogEntry(
|
||||
level: .error,
|
||||
message: "Server terminated unexpectedly with exit code: \(exitCode)",
|
||||
source: .rust
|
||||
|
|
@ -477,7 +482,7 @@ final class RustServer: ServerProtocol {
|
|||
try? await Task.sleep(for: .seconds(2))
|
||||
if self.process == nil { // Only restart if not manually stopped
|
||||
self.logger.info("Auto-restarting Rust server after crash")
|
||||
self.logSubject.send(ServerLogEntry(
|
||||
self.logContinuation?.yield(ServerLogEntry(
|
||||
level: .info,
|
||||
message: "Auto-restarting server after crash",
|
||||
source: .rust
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import Combine
|
||||
import Foundation
|
||||
import Observation
|
||||
import OSLog
|
||||
|
|
@ -52,45 +51,33 @@ class ServerManager {
|
|||
private(set) var lastError: Error?
|
||||
|
||||
private let logger = Logger(subsystem: "com.steipete.VibeTunnel", category: "ServerManager")
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
private let logSubject = PassthroughSubject<ServerLogEntry, Never>()
|
||||
private var logContinuation: AsyncStream<ServerLogEntry>.Continuation?
|
||||
private var serverLogTask: Task<Void, Never>?
|
||||
private(set) var logStream: AsyncStream<ServerLogEntry>!
|
||||
|
||||
var serverMode: ServerMode {
|
||||
get { ServerMode(rawValue: serverModeString) ?? .rust }
|
||||
set { serverModeString = newValue.rawValue }
|
||||
}
|
||||
|
||||
var logPublisher: AnyPublisher<ServerLogEntry, Never> {
|
||||
logSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
/// Modern async stream for logs
|
||||
var logStream: AsyncStream<ServerLogEntry> {
|
||||
AsyncStream { continuation in
|
||||
// Use logPublisher directly without storing the cancellable
|
||||
Task { @MainActor in
|
||||
for await entry in logPublisher.values {
|
||||
continuation.yield(entry)
|
||||
}
|
||||
continuation.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private init() {
|
||||
setupLogStream()
|
||||
setupObservers()
|
||||
}
|
||||
|
||||
private func setupLogStream() {
|
||||
logStream = AsyncStream { continuation in
|
||||
self.logContinuation = continuation
|
||||
}
|
||||
}
|
||||
|
||||
private func setupObservers() {
|
||||
// Watch for server mode changes when the value actually changes
|
||||
// Since we're using @AppStorage, we need to observe changes differently
|
||||
NotificationCenter.default.publisher(for: UserDefaults.didChangeNotification)
|
||||
.sink { [weak self] _ in
|
||||
Task { @MainActor [weak self] in
|
||||
await self?.handleServerModeChange()
|
||||
}
|
||||
Task { @MainActor in
|
||||
for await _ in NotificationCenter.default.notifications(named: UserDefaults.didChangeNotification) {
|
||||
await handleServerModeChange()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
||||
|
||||
/// Start the server with current configuration
|
||||
|
|
@ -105,7 +92,7 @@ class ServerManager {
|
|||
ServerMonitor.shared.isServerRunning = true
|
||||
|
||||
// Log for clarity
|
||||
logSubject.send(ServerLogEntry(
|
||||
logContinuation?.yield(ServerLogEntry(
|
||||
level: .info,
|
||||
message: "\(serverMode.displayName) server already running on port \(port)",
|
||||
source: serverMode
|
||||
|
|
@ -114,7 +101,7 @@ class ServerManager {
|
|||
}
|
||||
|
||||
// Log that we're starting a server
|
||||
logSubject.send(ServerLogEntry(
|
||||
logContinuation?.yield(ServerLogEntry(
|
||||
level: .info,
|
||||
message: "Starting \(serverMode.displayName) server on port \(port)...",
|
||||
source: serverMode
|
||||
|
|
@ -125,11 +112,11 @@ class ServerManager {
|
|||
server.port = port
|
||||
|
||||
// Subscribe to server logs
|
||||
server.logPublisher
|
||||
.sink { [weak self] entry in
|
||||
self?.logSubject.send(entry)
|
||||
serverLogTask = Task { [weak self] in
|
||||
for await entry in server.logStream {
|
||||
self?.logContinuation?.yield(entry)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
try await server.start()
|
||||
|
||||
|
|
@ -146,7 +133,7 @@ class ServerManager {
|
|||
await triggerInitialCleanup()
|
||||
} catch {
|
||||
logger.error("Failed to start server: \(error.localizedDescription)")
|
||||
logSubject.send(ServerLogEntry(
|
||||
logContinuation?.yield(ServerLogEntry(
|
||||
level: .error,
|
||||
message: "Failed to start \(serverMode.displayName) server: \(error.localizedDescription)",
|
||||
source: serverMode
|
||||
|
|
@ -176,18 +163,20 @@ class ServerManager {
|
|||
logger.info("Stopping \(serverType.displayName) server")
|
||||
|
||||
// Log that we're stopping the server
|
||||
logSubject.send(ServerLogEntry(
|
||||
logContinuation?.yield(ServerLogEntry(
|
||||
level: .info,
|
||||
message: "Stopping \(serverType.displayName) server...",
|
||||
source: serverType
|
||||
))
|
||||
|
||||
await server.stop()
|
||||
serverLogTask?.cancel()
|
||||
serverLogTask = nil
|
||||
currentServer = nil
|
||||
isRunning = false
|
||||
|
||||
// Log that the server has stopped
|
||||
logSubject.send(ServerLogEntry(
|
||||
logContinuation?.yield(ServerLogEntry(
|
||||
level: .info,
|
||||
message: "\(serverType.displayName) server stopped",
|
||||
source: serverType
|
||||
|
|
@ -207,7 +196,7 @@ class ServerManager {
|
|||
defer { isRestarting = false }
|
||||
|
||||
// Log that we're restarting
|
||||
logSubject.send(ServerLogEntry(
|
||||
logContinuation?.yield(ServerLogEntry(
|
||||
level: .info,
|
||||
message: "Restarting server...",
|
||||
source: serverMode
|
||||
|
|
@ -228,17 +217,17 @@ class ServerManager {
|
|||
logger.info("Switching from \(oldMode.displayName) to \(mode.displayName)")
|
||||
|
||||
// Log the mode switch with a clear separator
|
||||
logSubject.send(ServerLogEntry(
|
||||
logContinuation?.yield(ServerLogEntry(
|
||||
level: .info,
|
||||
message: "════════════════════════════════════════════════════════",
|
||||
source: oldMode
|
||||
))
|
||||
logSubject.send(ServerLogEntry(
|
||||
logContinuation?.yield(ServerLogEntry(
|
||||
level: .info,
|
||||
message: "Switching server mode: \(oldMode.displayName) → \(mode.displayName)",
|
||||
source: oldMode
|
||||
))
|
||||
logSubject.send(ServerLogEntry(
|
||||
logContinuation?.yield(ServerLogEntry(
|
||||
level: .info,
|
||||
message: "════════════════════════════════════════════════════════",
|
||||
source: oldMode
|
||||
|
|
@ -259,17 +248,17 @@ class ServerManager {
|
|||
await start()
|
||||
|
||||
// Log completion
|
||||
logSubject.send(ServerLogEntry(
|
||||
logContinuation?.yield(ServerLogEntry(
|
||||
level: .info,
|
||||
message: "════════════════════════════════════════════════════════",
|
||||
source: mode
|
||||
))
|
||||
logSubject.send(ServerLogEntry(
|
||||
logContinuation?.yield(ServerLogEntry(
|
||||
level: .info,
|
||||
message: "Server mode switch completed successfully",
|
||||
source: mode
|
||||
))
|
||||
logSubject.send(ServerLogEntry(
|
||||
logContinuation?.yield(ServerLogEntry(
|
||||
level: .info,
|
||||
message: "════════════════════════════════════════════════════════",
|
||||
source: mode
|
||||
|
|
@ -326,14 +315,14 @@ class ServerManager {
|
|||
let cleanedCount = jsonData["cleaned_count"] as? Int
|
||||
{
|
||||
logger.info("Initial cleanup completed: cleaned \(cleanedCount) exited sessions")
|
||||
logSubject.send(ServerLogEntry(
|
||||
logContinuation?.yield(ServerLogEntry(
|
||||
level: .info,
|
||||
message: "Cleaned up \(cleanedCount) exited sessions on startup",
|
||||
source: serverMode
|
||||
))
|
||||
} else {
|
||||
logger.info("Initial cleanup completed successfully")
|
||||
logSubject.send(ServerLogEntry(
|
||||
logContinuation?.yield(ServerLogEntry(
|
||||
level: .info,
|
||||
message: "Cleaned up exited sessions on startup",
|
||||
source: serverMode
|
||||
|
|
@ -346,7 +335,7 @@ class ServerManager {
|
|||
} catch {
|
||||
// Log the error but don't fail startup
|
||||
logger.warning("Failed to trigger initial cleanup: \(error.localizedDescription)")
|
||||
logSubject.send(ServerLogEntry(
|
||||
logContinuation?.yield(ServerLogEntry(
|
||||
level: .warning,
|
||||
message: "Could not clean up old sessions: \(error.localizedDescription)",
|
||||
source: serverMode
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import Combine
|
||||
import Foundation
|
||||
|
||||
/// Common interface for server implementations.
|
||||
|
|
@ -26,8 +25,8 @@ protocol ServerProtocol: AnyObject {
|
|||
/// Restart the server
|
||||
func restart() async throws
|
||||
|
||||
/// Publisher for streaming log messages
|
||||
var logPublisher: AnyPublisher<ServerLogEntry, Never> { get }
|
||||
/// Stream for receiving log messages
|
||||
var logStream: AsyncStream<ServerLogEntry> { get }
|
||||
}
|
||||
|
||||
/// Server mode options.
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ actor TerminalManager {
|
|||
}
|
||||
|
||||
group.addTask {
|
||||
try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
|
||||
try await Task.sleep(for: .seconds(seconds))
|
||||
throw TunnelError.timeout
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -395,7 +395,7 @@ public final class TunnelServer {
|
|||
// Wait for the server to actually start listening
|
||||
var serverStarted = false
|
||||
for _ in 0..<10 { // Try for up to 1 second
|
||||
try await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
|
||||
// Check if the server is actually listening
|
||||
if await isServerListening(on: port) {
|
||||
|
|
@ -769,7 +769,7 @@ public final class TunnelServer {
|
|||
// Wait up to 3 seconds for session ID
|
||||
let maxAttempts = 30
|
||||
for _ in 0..<maxAttempts {
|
||||
try await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
|
||||
let moreData = outputPipe.fileHandleForReading.availableData
|
||||
if !moreData.isEmpty {
|
||||
|
|
@ -975,7 +975,7 @@ public final class TunnelServer {
|
|||
// Send heartbeat every 15 seconds to keep connection alive
|
||||
while !Task.isCancelled {
|
||||
do {
|
||||
try await Task.sleep(nanoseconds: 15_000_000_000) // 15 seconds
|
||||
try await Task.sleep(for: .seconds(15))
|
||||
|
||||
// Send SSE comment as heartbeat (comments start with ':')
|
||||
var heartbeat = ByteBuffer()
|
||||
|
|
|
|||
|
|
@ -219,8 +219,11 @@ struct DebugSettingsView: View {
|
|||
UserDefaults.standard.synchronize()
|
||||
|
||||
// Quit the app after a short delay to ensure the purge completes
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
NSApplication.shared.terminate(nil)
|
||||
Task {
|
||||
try? await Task.sleep(for: .milliseconds(500))
|
||||
await MainActor.run {
|
||||
NSApplication.shared.terminate(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,93 +0,0 @@
|
|||
# Modern Swift Refactoring Summary
|
||||
|
||||
This document summarizes the modernization changes made to the VibeTunnel codebase to align with modern Swift best practices as outlined in `modern-swift.md`.
|
||||
|
||||
## Key Changes
|
||||
|
||||
### 1. Converted @ObservableObject to @Observable
|
||||
|
||||
- **SessionMonitor.swift**: Converted from `@ObservableObject` with `@Published` properties to `@Observable` class
|
||||
- Removed `import Combine`
|
||||
- Replaced `@Published` with regular properties
|
||||
- Changed from `ObservableObject` to `@Observable`
|
||||
|
||||
- **TunnelServerDemo.swift**: Converted from `@ObservableObject` to `@Observable`
|
||||
- Removed `import Combine`
|
||||
- Simplified property declarations
|
||||
|
||||
- **SparkleViewModel**: Converted stub implementation to use `@Observable`
|
||||
|
||||
### 2. Replaced Combine with Async/Await
|
||||
|
||||
- **SessionMonitor.swift**:
|
||||
- Replaced `Timer` with `Task` for periodic monitoring
|
||||
- Used `Task.sleep(for:)` instead of Timer callbacks
|
||||
- Eliminated nested `Task { }` blocks
|
||||
|
||||
- **TunnelClient.swift**:
|
||||
- Removed `PassthroughSubject` from WebSocket implementation
|
||||
- Replaced with `AsyncStream<WSMessage>` for message handling
|
||||
- Updated delegate methods to use `continuation.yield()` instead of `subject.send()`
|
||||
|
||||
### 3. Simplified State Management
|
||||
|
||||
- **VibeTunnelApp.swift**:
|
||||
- Changed `@StateObject` to `@State` for SessionMonitor
|
||||
- Updated `.environmentObject()` to `.environment()` for modern environment injection
|
||||
|
||||
- **MenuBarView.swift**:
|
||||
- Changed `@EnvironmentObject` to `@Environment(SessionMonitor.self)`
|
||||
|
||||
- **SettingsView.swift**:
|
||||
- Removed `ServerObserver` ViewModel completely
|
||||
- Moved server state directly into view as `@State private var httpServer`
|
||||
- Simplified server state access with computed properties
|
||||
|
||||
### 4. Modernized Async Operations
|
||||
|
||||
- **VibeTunnelApp.swift**:
|
||||
- Replaced `DispatchQueue.main.asyncAfter` with `Task.sleep(for:)`
|
||||
- Updated all `Task.sleep(nanoseconds:)` to `Task.sleep(for:)` with Duration
|
||||
|
||||
- **SettingsView.swift**:
|
||||
- Replaced `.onAppear` with `.task` for async initialization
|
||||
- Modernized Task.sleep usage throughout
|
||||
|
||||
### 5. Removed Unnecessary Abstractions
|
||||
|
||||
- Eliminated `ServerObserver` class - moved logic directly into views
|
||||
- Removed Combine imports where no longer needed
|
||||
- Simplified state ownership by keeping it close to where it's used
|
||||
|
||||
### 6. Updated Error Handling
|
||||
|
||||
- Maintained proper async/await error handling with try/catch
|
||||
- Removed completion handler patterns where applicable
|
||||
- Simplified error state management
|
||||
|
||||
## Benefits Achieved
|
||||
|
||||
1. **Reduced Dependencies**: Removed Combine dependency from most files
|
||||
2. **Simpler Code**: Eliminated unnecessary ViewModels and abstractions
|
||||
3. **Better Performance**: Native SwiftUI state management with @Observable
|
||||
4. **Modern Patterns**: Consistent use of async/await throughout
|
||||
5. **Cleaner Architecture**: State lives closer to where it's used
|
||||
|
||||
## Migration Notes
|
||||
|
||||
- All @Observable classes require iOS 17+ / macOS 14+
|
||||
- AsyncStream provides a more natural API than Combine subjects
|
||||
- Task-based monitoring is more efficient than Timer-based
|
||||
- SwiftUI's built-in state management eliminates need for custom ViewModels
|
||||
|
||||
## Files Modified
|
||||
|
||||
1. `/VibeTunnel/Core/Services/SessionMonitor.swift`
|
||||
2. `/VibeTunnel/Core/Services/TunnelClient.swift`
|
||||
3. `/VibeTunnel/Core/Services/TunnelServerDemo.swift`
|
||||
4. `/VibeTunnel/Core/Services/SparkleUpdaterManager.swift`
|
||||
5. `/VibeTunnel/VibeTunnelApp.swift`
|
||||
6. `/VibeTunnel/Views/MenuBarView.swift`
|
||||
7. `/VibeTunnel/SettingsView.swift`
|
||||
|
||||
All changes follow the principles outlined in the Modern Swift guidelines, embracing SwiftUI's native patterns and avoiding unnecessary complexity.
|
||||
Loading…
Reference in a new issue