@@ -119,52 +119,57 @@ public func installApp(from ipaURL: URL) async throws
119119
120120 print ( " Installing app from: \( ipaURL. path) " )
121121
122- return AsyncThrowingStream { continuation in
123- // Keep track of subscriptions & a cancellation token
122+ // === IMPORTANT: explicitly specify element and failure types so the compiler selects
123+ // the continuation-style initializer (the one that passes `continuation` to the closure).
124+ typealias InstallUpdate = ( progress: Double , status: String )
125+ typealias StreamContinuation = AsyncThrowingStream < InstallUpdate , Error > . Continuation
126+
127+ return AsyncThrowingStream < InstallUpdate , Error > { continuation in
124128 var cancellables = Set < AnyCancellable > ( )
125129 var installTask : Task < Void , Never > ?
126130
127- // Ensure cleanup when the stream ends for whatever reason
128- continuation. onTermination = { @Sendable _ in
129- print ( " Install stream terminated — cleaning up. " )
131+ // Explicitly annotate the termination parameter type so compiler is happy
132+ continuation. onTermination = { @Sendable ( reason : StreamContinuation . Termination ) in
133+ print ( " Install stream terminated: \( reason ) " )
130134 cancellables. removeAll ( )
131- // cancel the installation task if still running
135+ // cancel running install task if still active
132136 installTask? . cancel ( )
133137 }
134138
135- // Start the async work on a Task so we can await inside
136139 installTask = Task {
137140 // Start heartbeat to keep connection alive
138141 HeartbeatManager . shared. start ( )
139142
140- // initialize view model the same way UI does (important)
143+ // initialize view model same as UI code (important)
141144 let isIdevice = UserDefaults . standard. integer ( forKey: " Feather.installationMethod " ) == 1
142145 let viewModel = InstallerStatusViewModel ( isIdevice: isIdevice)
143146
144- // Log useful updates to console (debug)
145- viewModel. $status. sink { status in
146- print ( " [Installer] status -> " , status)
147- } . store ( in: & cancellables)
147+ // Debug logging for status changes
148+ viewModel. $status
149+ . sink { status in
150+ print ( " [Installer] status -> " , status)
151+ }
152+ . store ( in: & cancellables)
148153
154+ // Progress stream (combine upload & install progress)
149155 viewModel. $uploadProgress
150156 . combineLatest ( viewModel. $installProgress)
151157 . sink { uploadProgress, installProgress in
152158 let overall = ( uploadProgress + installProgress) / 2.0
153- let status : String
159+ let statusText : String
154160 if uploadProgress < 1.0 {
155- status = " 📤 Uploading... "
161+ statusText = " 📤 Uploading... "
156162 } else if installProgress < 1.0 {
157- status = " 📲 Installing... "
163+ statusText = " 📲 Installing... "
158164 } else {
159- status = " 🏁 Finalizing... "
165+ statusText = " 🏁 Finalizing... "
160166 }
161- // debug
162167 print ( " [Installer] progress upload: \( uploadProgress) install: \( installProgress) overall: \( overall) " )
163- continuation. yield ( ( overall, status ) )
168+ continuation. yield ( ( overall, statusText ) )
164169 }
165170 . store ( in: & cancellables)
166171
167- // Watch for completion via published isCompleted (robust across enum shapes)
172+ // Robust completion detection: watch isCompleted
168173 viewModel. $isCompleted
169174 . sink { completed in
170175 if completed {
@@ -177,21 +182,20 @@ public func installApp(from ipaURL: URL) async throws
177182 . store ( in: & cancellables)
178183
179184 do {
180- // Create the installer tied to the view model
181185 let installer = await InstallationProxy ( viewModel: viewModel)
182186
183- // If you need same behaviour as UI, pass the suspend flag like the UI does:
184- // let suspend = (Bundle.main.bundleIdentifier == someIdentifier) // adapt as needed
185- // try await installer.install(at: ipaURL, suspend: suspend)
186-
187- // For now, call the simpler signature – change to include 'suspend:' if needed
187+ // If your UI calls install(at: suspend:) when updating itself,
188+ // replicate that logic here if you need that behaviour.
188189 try await installer. install ( at: ipaURL)
189190
190- // tiny pause so progress updates propagate
191+ // small delay to let final progress propagate
191192 try await Task . sleep ( nanoseconds: 500_000_000 )
192193
193- print ( " Installation call returned without throwing — waiting for viewModel to report completion. " )
194- // Don't force finish here: wait for the published isCompleted to fire (above)
194+ print ( " Installation call returned — waiting for viewModel to report completion. " )
195+
196+ // Note: we intentionally don't call continuation.finish() here -
197+ // we rely on viewModel.$isCompleted to finish the stream so the
198+ // installer has its normal lifecycle.
195199
196200 } catch {
197201 print ( " [Installer] install threw error -> " , error)
@@ -201,4 +205,3 @@ public func installApp(from ipaURL: URL) async throws
201205 }
202206 }
203207}
204-
0 commit comments