diff --git a/templates/apple/Sources/Services/Push.swift.twig b/templates/apple/Sources/Services/Push.swift.twig index 5d7eadcd7a..d680f215cb 100644 --- a/templates/apple/Sources/Services/Push.swift.twig +++ b/templates/apple/Sources/Services/Push.swift.twig @@ -951,13 +951,15 @@ open class Push: Service { } // Run a fire-and-forget broker call (an unsubscribe or re-SUBSCRIBE nobody awaits) and - // report its failure. Failures after a deliberate close() are its own teardown, not errors. + // report its failure. Failures after a deliberate close() are its own teardown, not errors, + // even once a later subscribe() has reopened the connection and cleared `closing`. private func inBackground(_ operation: @escaping () async throws -> Void) { + let generation = sync.sync { closeGeneration } Task { [weak self] in do { try await operation() } catch { - guard let self, !self.sync.sync(execute: { self.closing }) else { return } + guard let self, !self.sync.sync(execute: { self.closing || self.closeGeneration != generation }) else { return } self.surface(Push.describe(error)) } } diff --git a/tests/e2e/languages/apple/Tests.swift b/tests/e2e/languages/apple/Tests.swift index f3c18c6db5..c094692b45 100644 --- a/tests/e2e/languages/apple/Tests.swift +++ b/tests/e2e/languages/apple/Tests.swift @@ -754,21 +754,33 @@ class Tests: XCTestCase { .setPushEndpoint("mqtt://mqtt:1883") .setSession(e2eSession) ) + // Dropping the last subscription closes the connection while its UNSUBSCRIBE is in flight, + // and reopening at once clears `closing` before that UNSUBSCRIBE fails. The failure is the + // closed connection's teardown, so it must not reach onError (or stderr without one). + let callbackErrors = ErrorCollector() + _ = callbackPush.onError { error in + callbackErrors.record((error as? AppwriteError)?.message ?? "\(error)") + } let handle = SubscriptionBox() let closedFromCallback = ErrorCollector() + let reopenedFromCallback = ErrorCollector() handle.subscription = try? await callbackPush.subscribe("e2e-push") { _ in handle.subscription?.unsubscribe() closedFromCallback.record("closed") + Task { + reopenedFromCallback.record(await outcome { _ = try await callbackPush.subscribe("e2e-switch") { _ in } }) + } } - for _ in 0..<50 where closedFromCallback.message == nil { + for _ in 0..<50 where reopenedFromCallback.message == nil { try? await Task.sleep(nanoseconds: 100_000_000) } - let reopened = await outcome { _ = try await callbackPush.subscribe("e2e-switch") { _ in } } + try? await Task.sleep(nanoseconds: 1_000_000_000) + let reopened = reopenedFromCallback.message ?? "timeout" callbackPush.close() print( - closedFromCallback.message == "closed" && reopened.isEmpty + closedFromCallback.message == "closed" && reopened.isEmpty && callbackErrors.message == nil ? "Push close from callback:passed" - : "Push close from callback:failed (callback: \(closedFromCallback.message ?? "none"), reopened: \(reopened))" + : "Push close from callback:failed (callback: \(closedFromCallback.message ?? "none"), reopened: \(reopened), error: \(callbackErrors.message ?? "none"))" ) }