Skip to content

Commit 7d57f63

Browse files
committed
fix: keep SSE stream alive across CarPlay page refreshes
Three bugs fixed: 1. sitemapChanged and widget-notFound called startStreaming(), which cancelled and rebuilt the entire SSE stack (NetworkTracker reset + new subscription). The onTermination handler on the old continuation could fire while the new task was still doing network round-trips, causing SitemapEventStream.cleanupContinuation to cancel listenTask and create a gap in event coverage. Replace with schedulePageRefresh(), which re-fetches only the page without touching the SSE stream. 2. No page refresh on SSE reconnect: if SITEMAP_CHANGED was missed during a disconnect the page stayed permanently stale. Add a needsRefreshOnReconnect flag (matching SitemapPageViewModel) that triggers schedulePageRefresh() on the next .connected event. 3. Calling NetworkTracker.shared.startTracking() on every restart (sitemapChanged, notFound) could reset the shared tracker's active connection and briefly break the main app's SSE. Now startTracking is only called on initial connect or sitemap-selection change. currentPage and currentService are now stored properties so handleSseMessage and refreshPage can read them without being threaded through every function parameter. Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
1 parent 796b65d commit 7d57f63

1 file changed

Lines changed: 55 additions & 11 deletions

File tree

openHAB/CarPlaySceneDelegate.swift

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,13 @@ private var carPlayMaxItems: Int {
2121
final class CarPlaySceneDelegate: UIResponder, CPTemplateApplicationSceneDelegate {
2222
private var interfaceController: CPInterfaceController?
2323
private var streamTask: Task<Void, Never>?
24+
private var refreshTask: Task<Void, Never>?
2425
private var preferencesTask: Task<Void, Never>?
2526
private let sitemapEventStream = SitemapEventStream()
2627
private var currentGridTemplate: CPGridTemplate?
28+
// Retained across SSE restarts so page refreshes don't need a full stream teardown.
29+
private var currentPage: OpenHABPage?
30+
private var currentService: OpenAPIService?
2731

2832
func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene,
2933
didConnect interfaceController: CPInterfaceController) {
@@ -38,11 +42,15 @@ final class CarPlaySceneDelegate: UIResponder, CPTemplateApplicationSceneDelegat
3842
didDisconnectInterfaceController interfaceController: CPInterfaceController) {
3943
streamTask?.cancel()
4044
streamTask = nil
45+
refreshTask?.cancel()
46+
refreshTask = nil
4147
preferencesTask?.cancel()
4248
preferencesTask = nil
4349
Task { await sitemapEventStream.stop() }
4450
self.interfaceController = nil
4551
currentGridTemplate = nil
52+
currentPage = nil
53+
currentService = nil
4654
Logger.carPlay.info("CarPlay scene disconnected")
4755
}
4856

@@ -63,6 +71,7 @@ final class CarPlaySceneDelegate: UIResponder, CPTemplateApplicationSceneDelegat
6371
let newSitemap = Preferences.shared.currentHomePreferences.sitemapForCarPlay
6472
guard newSitemap != lastSitemap else { continue }
6573
lastSitemap = newSitemap
74+
// Sitemap selection changed — full restart needed (different page/subscription).
6675
startStreaming()
6776
}
6877
}
@@ -90,17 +99,17 @@ final class CarPlaySceneDelegate: UIResponder, CPTemplateApplicationSceneDelegat
9099
}
91100
do {
92101
let service = try OpenAPIService(connectionConfiguration: connection.configuration)
102+
currentService = service
93103

94-
// Initial fetch to populate the template and learn the pageId
95104
guard let page = try await service.pollDataForPage(
96105
sitemapname: sitemapName, pageId: "", longPolling: false
97106
) else { return }
107+
currentPage = page
98108
updateTemplate(page: page, service: service)
99109

100-
// Prefer SSE; fall back to long-poll on older servers
101110
let serverProps = try? await service.getRoot()
102111
if serverProps?.hasSseSupport() == true {
103-
await runSSE(page: page, sitemapName: sitemapName, connection: connection, service: service)
112+
await runSSE(sitemapName: sitemapName, connection: connection)
104113
} else {
105114
await runLongPoll(sitemapName: sitemapName, pageId: page.pageId, service: service)
106115
}
@@ -110,40 +119,50 @@ final class CarPlaySceneDelegate: UIResponder, CPTemplateApplicationSceneDelegat
110119
}
111120

112121
@MainActor
113-
private func runSSE(page: OpenHABPage, sitemapName: String, connection: ConnectionInfo, service: OpenAPIService) async {
122+
private func runSSE(sitemapName: String, connection: ConnectionInfo) async {
114123
await sitemapEventStream.startMonitoringNetworkIfNeeded(initialConnection: connection)
115-
let pageId = page.pageId.isEmpty ? sitemapName : page.pageId
124+
let pageId = currentPage.map { $0.pageId.isEmpty ? sitemapName : $0.pageId } ?? sitemapName
116125
let stream = await sitemapEventStream.stream(sitemap: sitemapName, pageId: pageId)
117126
Logger.carPlay.info("CarPlay SSE starting for \(sitemapName)/\(pageId)")
118127

128+
// Refresh on reconnect so structural changes missed during a disconnect are caught.
129+
var needsRefreshOnReconnect = false
130+
119131
for await msg in stream {
120132
guard !Task.isCancelled else { break }
121133
switch msg {
122134
case .connected:
123135
Logger.carPlay.info("CarPlay SSE connected")
136+
if needsRefreshOnReconnect {
137+
needsRefreshOnReconnect = false
138+
schedulePageRefresh(sitemapName: sitemapName)
139+
}
124140
case let .disconnected(error):
141+
needsRefreshOnReconnect = true
125142
if let error { Logger.carPlay.warning("CarPlay SSE disconnected: \(error)") }
126143
case let .event(message):
127-
handleSseMessage(message, page: page, service: service)
144+
handleSseMessage(message, sitemapName: sitemapName)
128145
}
129146
}
130147
}
131148

132149
@MainActor
133-
private func handleSseMessage(_ message: SitemapEventMessage, page: OpenHABPage, service: OpenAPIService) {
150+
private func handleSseMessage(_ message: SitemapEventMessage, sitemapName: String) {
134151
switch message {
135152
case .alive:
136153
break
137154
case .sitemapChanged:
138-
Logger.carPlay.info("CarPlay SSE: sitemap changed, reloading")
139-
startStreaming()
155+
Logger.carPlay.info("CarPlay SSE: sitemap changed, refreshing page")
156+
// Refresh page content only — do not restart the SSE stream.
157+
schedulePageRefresh(sitemapName: sitemapName)
140158
case let .widget(event):
159+
guard let page = currentPage, let service = currentService else { return }
141160
switch page.apply(event: event) {
142161
case .applied:
143162
updateTemplate(page: page, service: service)
144163
case .requiresPageReload, .notFound:
145-
Logger.carPlay.info("CarPlay SSE: widget requires reload")
146-
startStreaming()
164+
Logger.carPlay.info("CarPlay SSE: widget \(event.widgetId ?? "") requires reload")
165+
schedulePageRefresh(sitemapName: sitemapName)
147166
case .unchanged:
148167
break
149168
}
@@ -152,13 +171,38 @@ final class CarPlaySceneDelegate: UIResponder, CPTemplateApplicationSceneDelegat
152171
}
153172
}
154173

174+
/// Refreshes `currentPage` from the server and updates the template without restarting the SSE stream.
175+
/// Cancels any in-flight refresh so rapid events coalesce into a single fetch.
176+
private func schedulePageRefresh(sitemapName: String) {
177+
refreshTask?.cancel()
178+
refreshTask = Task { [weak self] in
179+
guard let self, !Task.isCancelled else { return }
180+
await refreshPage(sitemapName: sitemapName)
181+
}
182+
}
183+
184+
@MainActor
185+
private func refreshPage(sitemapName: String) async {
186+
guard let service = currentService else { return }
187+
do {
188+
guard let page = try await service.pollDataForPage(
189+
sitemapname: sitemapName, pageId: "", longPolling: false
190+
) else { return }
191+
currentPage = page
192+
updateTemplate(page: page, service: service)
193+
} catch {
194+
Logger.carPlay.error("CarPlay page refresh error: \(error)")
195+
}
196+
}
197+
155198
@MainActor
156199
private func runLongPoll(sitemapName: String, pageId: String, service: OpenAPIService) async {
157200
Logger.carPlay.info("CarPlay using long-poll for \(sitemapName)")
158201
do {
159202
for try await event in SitemapPageLoader.stream(sitemapName: sitemapName, pageId: pageId, service: service) {
160203
guard !Task.isCancelled else { break }
161204
if case let .longPoll(page, _) = event {
205+
currentPage = page
162206
updateTemplate(page: page, service: service)
163207
}
164208
}

0 commit comments

Comments
 (0)