diff --git a/docs/macos.md b/docs/macos.md index 4c4e476..2de257c 100644 --- a/docs/macos.md +++ b/docs/macos.md @@ -74,7 +74,7 @@ API calls go through the generated `Client` over `URLSessionTransport`. A middle `EventStream` is a hand-written SSE client over `URLSession.bytes(for:)`. It splits lines itself, because `AsyncBytes.lines` drops the blank lines that end SSE events, and parses off the main actor, handing each frame to it. It handles `id`, `event: ping`, `event: ready` and `data`: it tracks the last seq, reconnects from it after a stale threshold (40 s, checked every 5 s) and a retry delay (2 s), when the network comes back (`NWPathMonitor`), and when the app becomes active with a stale stream. An event whose data doesn't decode is logged and skipped. There are two streams, global (after the bootstrap `last_event_seq`) and lead (`?lead=true`, after the last event of the lead's first page), and only the global one drives the connection dot. -Startup: `GET /api/admin/status`, then me, projects, inbox, lead sessions and summary reads in parallel, then drafts, focus, the lead stream, the window, the global stream. A failure before the window shows replaces it with `byre failed to start: `; connecting to the same URL again from New Server Window restarts it, and Forget Server leaves that screen. `handleEvent` calls `notifyInbox` on `inbox.created` and `notifyTurn` on `turn.ended`. +Startup: `GET /api/admin/status`, then me, projects, inbox, lead sessions and summary reads in parallel, then drafts, focus, the lead stream, the window, the global stream. A failure before the window shows replaces it with `byre failed to start: ` and the whole startup is tried again every 2 s until it succeeds, so a server restarting (the app's own kickstart after an update, for one) heals by itself; Forget Server leaves that screen. `handleEvent` calls `notifyInbox` on `inbox.created` and `notifyTurn` on `turn.ended`. The app logs through `os.Logger` with subsystem `run.fc.byre.mac`: `log show --last 10m --predicate 'subsystem == "run.fc.byre.mac"'` prints it. diff --git a/mac/Sources/Byre/Startup.swift b/mac/Sources/Byre/Startup.swift index 2f63229..e191e6b 100644 --- a/mac/Sources/Byre/Startup.swift +++ b/mac/Sources/Byre/Startup.swift @@ -61,9 +61,6 @@ final class Servers { } func add(_ origin: URL) { - if let connection = connections[origin.absoluteString], case .failed = connection.phase { - connections[origin.absoluteString] = nil - } if record(origin.absoluteString) == nil { let used = Set(entries.map(\.record.color)) let color = taskPalette.indices.first { !used.contains($0) } ?? entries.count % taskPalette.count @@ -140,6 +137,12 @@ final class Connection { } private func start() async { + while !stopped, !(await attempt()) { + try? await Task.sleep(for: .seconds(2)) + } + } + + private func attempt() async -> Bool { let store = Store(api: API(origin: origin)) do { let status = try await store.api.client.getStatus().ok.body.json @@ -153,7 +156,7 @@ final class Connection { try await store.loadFocus() guard !stopped else { await store.stop() - return + return true } Task { await store.startLeadStream() @@ -162,11 +165,13 @@ final class Connection { store.startGlobalStream(after: status.lastEventSeq) store.remote.start() Notifier.shared.deliver() + return true } catch { await store.stop() if !stopped { phase = .failed("byre failed to start: \(errorText(error))") } + return false } } }