murky problem again

This commit is contained in:
Ian Gulliver
2026-10-18 21:54:54 -07:00
parent 41490b2251
commit d02f4c7280
2 changed files with 10 additions and 5 deletions
+1 -1
View File
@@ -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. `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: <error>`; 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: <error>` 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. The app logs through `os.Logger` with subsystem `run.fc.byre.mac`: `log show --last 10m --predicate 'subsystem == "run.fc.byre.mac"'` prints it.
+9 -4
View File
@@ -61,9 +61,6 @@ final class Servers {
} }
func add(_ origin: URL) { func add(_ origin: URL) {
if let connection = connections[origin.absoluteString], case .failed = connection.phase {
connections[origin.absoluteString] = nil
}
if record(origin.absoluteString) == nil { if record(origin.absoluteString) == nil {
let used = Set(entries.map(\.record.color)) let used = Set(entries.map(\.record.color))
let color = taskPalette.indices.first { !used.contains($0) } ?? entries.count % taskPalette.count let color = taskPalette.indices.first { !used.contains($0) } ?? entries.count % taskPalette.count
@@ -140,6 +137,12 @@ final class Connection {
} }
private func start() async { 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)) let store = Store(api: API(origin: origin))
do { do {
let status = try await store.api.client.getStatus().ok.body.json let status = try await store.api.client.getStatus().ok.body.json
@@ -153,7 +156,7 @@ final class Connection {
try await store.loadFocus() try await store.loadFocus()
guard !stopped else { guard !stopped else {
await store.stop() await store.stop()
return return true
} }
Task { Task {
await store.startLeadStream() await store.startLeadStream()
@@ -162,11 +165,13 @@ final class Connection {
store.startGlobalStream(after: status.lastEventSeq) store.startGlobalStream(after: status.lastEventSeq)
store.remote.start() store.remote.start()
Notifier.shared.deliver() Notifier.shared.deliver()
return true
} catch { } catch {
await store.stop() await store.stop()
if !stopped { if !stopped {
phase = .failed("byre failed to start: \(errorText(error))") phase = .failed("byre failed to start: \(errorText(error))")
} }
return false
} }
} }
} }