Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions templates/apple/Sources/Services/Push.swift.twig
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
Expand Down
20 changes: 16 additions & 4 deletions tests/e2e/languages/apple/Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
abnegate marked this conversation as resolved.
? "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"))"
)
}

Expand Down
Loading