Skip to content
Open
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
ed08eb9
Delete NotificationService protocol
samwyndham Jun 11, 2026
359d5ca
Remove withExpiringActivity from NSE
samwyndham Jun 11, 2026
25a18c1
Ensure `currentTask` is cancelled when `NSEClientScope.processPayload…
samwyndham Jun 11, 2026
5051808
Delete NSEClientScope task properties
samwyndham Jun 11, 2026
458e058
Make `NSEClientScope` correspond to a single invocation
samwyndham Jun 11, 2026
4c65eb8
Fix warning
samwyndham Jun 11, 2026
def8bc0
Fix compile issue
samwyndham Jun 11, 2026
7f7283a
Rework cancellation of NSE when the service extension will expire.
samwyndham Jun 11, 2026
0928a2b
Introduce `withBackgroundTask(name:executer:operation:)`
samwyndham Jun 15, 2026
9be9bff
Use `withBackgroundTask(name:executer:operation:)` within `SafeCoreCr…
samwyndham Jun 15, 2026
362e392
Use `withBackgroundTask(name:executer:operation:)` instead of `withEx…
samwyndham Jun 15, 2026
f4afc35
Inject `PassthroughTaskExecuter` to the call sites.
samwyndham Jun 15, 2026
dec0703
Delete `BackgroundTaskManager`
samwyndham Jun 15, 2026
47bc05b
Introduce `AppTaskExecuter`
samwyndham Jun 15, 2026
c8fb1b7
Use `AppTaskExecuter` in the main app
samwyndham Jun 15, 2026
34078b0
Keep the share extension process alive while cancelling
samwyndham Jun 16, 2026
891ff0b
Introduce `static Process.registerUnsafeExpiringActivity(_:timeout:pr…
samwyndham Jun 16, 2026
2efdb08
Revert "Introduce `static Process.registerUnsafeExpiringActivity(_:ti…
samwyndham Jun 16, 2026
1be447e
Delete ExpiringActivity
samwyndham Jun 16, 2026
f2d7f18
Rename AppTaskExecuter -> AppBackgroundTaskExecuter
samwyndham Jun 16, 2026
8952061
Small cleanup
samwyndham Jun 22, 2026
1e28ac1
Fix incorrect API
samwyndham Jun 22, 2026
02d155e
Fix endBackgroundTask not being called
samwyndham Jun 22, 2026
423bdc7
Fix incorrect comment
samwyndham Jun 22, 2026
8fafcfe
Lint & format
samwyndham Jun 22, 2026
79e495e
Test `AppBackgroundTaskExecuter`
samwyndham Jun 22, 2026
0dd793c
Lint & format
samwyndham Jun 22, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ public final class ClientSessionComponent {
private let completionHandlers: CompletionHandlers

private let faultyMLSRemovalKeysByDomain: [String: [String]]
private let backgroundTaskExecuter: any BackgroundTaskExecuter

public init(
selfUserID: UUID,
Expand All @@ -88,7 +89,8 @@ public final class ClientSessionComponent {
proteusService: any ProteusServiceInterface,
coreCryptoProvider: any CoreCryptoProviderProtocol,
completionHandlers: CompletionHandlers,
faultyMLSRemovalKeysByDomain: [String: [String]]
faultyMLSRemovalKeysByDomain: [String: [String]],
backgroundTaskExecuter: any BackgroundTaskExecuter
) {
self.selfUserID = selfUserID
self.selfClientID = selfClientID
Expand All @@ -108,6 +110,7 @@ public final class ClientSessionComponent {
self.coreCryptoProvider = coreCryptoProvider
self.completionHandlers = completionHandlers
self.faultyMLSRemovalKeysByDomain = faultyMLSRemovalKeysByDomain
self.backgroundTaskExecuter = backgroundTaskExecuter
}

public private(set) lazy var authenticationManager = AuthenticationManager(
Expand Down Expand Up @@ -411,7 +414,8 @@ public final class ClientSessionComponent {
liveBrokenGroupSubject: liveBrokenGroupSubject,
journal: journal,
mlsGroupRepairAgent: mlsGroupRepairAgent,
earService: earService
earService: earService,
backgroundTaskExecuter: backgroundTaskExecuter
)

public lazy var incrementalSyncV2: IncrementalSyncV2 = if let sharedContainerURL {
Expand All @@ -430,6 +434,7 @@ public final class ClientSessionComponent {
journal: journal,
mlsGroupRepairAgent: mlsGroupRepairAgent,
earService: earService,
backgroundTaskExecuter: backgroundTaskExecuter,
createPushChannelState: { [selfClientID] in
PushChannelState(sharedContainerURL: sharedContainerURL, clientID: selfClientID)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public final class UserSessionComponent {
private let coreCryptoProvider: any CoreCryptoProviderProtocol

private let faultyMLSRemovalKeysByDomain: [String: [String]]
private let backgroundTaskExecuter: any BackgroundTaskExecuter

public init(
currentBuildNumber: String,
Expand All @@ -64,7 +65,8 @@ public final class UserSessionComponent {
mlsDecryptionService: any MLSDecryptionServiceInterface,
proteusService: any ProteusServiceInterface,
coreCryptoProvider: any CoreCryptoProviderProtocol,
faultyMLSRemovalKeysByDomain: [String: [String]]
faultyMLSRemovalKeysByDomain: [String: [String]],
backgroundTaskExecuter: any BackgroundTaskExecuter
) {
self.currentBuildNumber = currentBuildNumber
self.selfUserID = selfUserID
Expand All @@ -84,6 +86,7 @@ public final class UserSessionComponent {
self.coreCryptoProvider = coreCryptoProvider
self.sharedContainerURL = sharedContainerURL
self.faultyMLSRemovalKeysByDomain = faultyMLSRemovalKeysByDomain
self.backgroundTaskExecuter = backgroundTaskExecuter
}

private let cookieStorage: any WireNetwork.CookieStorageProtocol
Expand Down Expand Up @@ -112,7 +115,8 @@ public final class UserSessionComponent {
proteusService: proteusService,
coreCryptoProvider: coreCryptoProvider,
completionHandlers: completionHandlers,
faultyMLSRemovalKeysByDomain: faultyMLSRemovalKeysByDomain
faultyMLSRemovalKeysByDomain: faultyMLSRemovalKeysByDomain,
backgroundTaskExecuter: backgroundTaskExecuter
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ final class NSEClientScope: Component<NSEClientScopeDependency> {

}

private let eventID: UUID
private let contentHandler: (UNNotificationContent) -> Void
private let clientID: String
private let restNetworkService: NetworkService
private let webSocketNetworkService: NetworkService
Expand All @@ -56,12 +58,13 @@ final class NSEClientScope: Component<NSEClientScopeDependency> {
private let isFederationEnabled: Bool
private let coreDataStack: CoreDataStack
private let earService: EARServiceInterface
private let backgroundTaskExecuter: any BackgroundTaskExecuter

private let pushChannelCoordinator: AppExtensionPushChannelCoordinator
private var currentTask: Task<Void, any Error>?
private var monitoringTask: Task<Void, any Error>?

init(
eventID: UUID,
contentHandler: @escaping (UNNotificationContent) -> Void,
parent: any Scope,
clientID: String,
restNetworkService: NetworkService,
Expand All @@ -70,8 +73,11 @@ final class NSEClientScope: Component<NSEClientScopeDependency> {
localDomain: String,
isFederationEnabled: Bool,
coreDataStack: CoreDataStack,
earService: EARServiceInterface
earService: EARServiceInterface,
backgroundTaskExecuter: any BackgroundTaskExecuter
) {
self.eventID = eventID
self.contentHandler = contentHandler
self.clientID = clientID
self.restNetworkService = restNetworkService
self.webSocketNetworkService = webSocketNetworkService
Expand All @@ -81,14 +87,12 @@ final class NSEClientScope: Component<NSEClientScopeDependency> {
self.coreDataStack = coreDataStack
self.pushChannelCoordinator = AppExtensionPushChannelCoordinator(clientID: clientID)
self.earService = earService
self.backgroundTaskExecuter = backgroundTaskExecuter

super.init(parent: parent)
}

func processPayload(
eventID: UUID,
contentHandler: @escaping (UNNotificationContent) -> Void
) async throws {
func processPayload() async throws {
// Pull pending update events.
let eventStream: AsyncStream<[UpdateEvent]>
let publicKeys = try earService.fetchPublicKeys()
Expand All @@ -97,56 +101,57 @@ final class NSEClientScope: Component<NSEClientScopeDependency> {
let (useCase, stream) = syncEventsUseCase()
eventStream = stream

// because we might be interrupted when in background, we wrap the sync in an expiringActivity that will
// cancel the task (not keeping any file lock in suspend mode)
try await withExpiringActivity(reason: "processPayload in NSE") { [weak self] in
guard let self else { return }
// make sure no pushChannel is open
let pushChannelState = PushChannelState(
sharedContainerURL: dependency.appContainerURL,
clientID: clientID
)
// make sure no pushChannel is open
let pushChannelState = PushChannelState(
sharedContainerURL: dependency.appContainerURL,
clientID: clientID
)
do {
try await pushChannelState.markAsOpen()
} catch {
throw Failure.pushChannelAlreadyOpened
}

let currentTask = Task<Void, any Error> {
do {
try await pushChannelState.markAsOpen()
try Task.checkCancellation()
try await useCase.invoke()
} catch {
throw Failure.pushChannelAlreadyOpened
}

monitoringTask = Task { [weak self] in
var request = await self?.pushChannelCoordinator.listenForYieldRequests()
if Task.isCancelled {
return
}
WireLogger.sync.debug("requested to cancel sync", attributes: .incrementalSync, .newNSE)
self?.currentTask?.cancel()
request?.acknowledge()
WireLogger.sync.debug("notified main App to resume sync", attributes: .incrementalSync, .newNSE)
// either we timeout during decrypting/storing events OR an issue
// with the sync. In both cases, we end up with a stream of
// notifications that has not been shown, so we need to continue
// to show them.
WireLogger.sync.warn(
"syncing events via websocket: \(String(describing: error))",
attributes: .incrementalSyncV3, .newNSE
)
await pushChannelState.markAsClosed()
}
}

currentTask = Task {
do {
try Task.checkCancellation()
try await useCase.invoke()
} catch {
// either we timeout during decrypting/storing events OR an issue
// with the sync. In both cases, we end up with a stream of
// notifications that has not been shown, so we need to continue
// to show them.
WireLogger.sync.warn(
"syncing events via websocket: \(String(describing: error))",
attributes: .incrementalSyncV3, .newNSE
)
await pushChannelState.markAsClosed()
}
let monitoringTask = Task<Void, any Error> { [pushChannelCoordinator] in
let request = await pushChannelCoordinator.listenForYieldRequests()
if Task.isCancelled {
return
}
try await currentTask?.value
WireLogger.sync.debug("closing push channel")
await pushChannelState.markAsClosed()
WireLogger.sync.debug("requested to cancel sync", attributes: .incrementalSync, .newNSE)
currentTask.cancel()
request.acknowledge()
WireLogger.sync.debug("notified main App to resume sync", attributes: .incrementalSync, .newNSE)
}

// no need to monitor anymore let's cancel
monitoringTask?.cancel()
try await withTaskCancellationHandler {
try await currentTask.value
} onCancel: {
currentTask.cancel()
}

WireLogger.sync.debug("closing push channel")
await pushChannelState.markAsClosed()

// no need to monitor anymore let's cancel
monitoringTask.cancel()

} else {
eventStream = try await pullEventsUseCase.invoke(publicKeys: publicKeys)
}
Expand Down Expand Up @@ -260,7 +265,7 @@ final class NSEClientScope: Component<NSEClientScopeDependency> {
coreCryptoKeyMigrationManager: coreCryptoMigrationManager,
allowCreation: false,
localDomain: localDomain,
backgroundTaskManager: nil
backgroundTaskExecuter: backgroundTaskExecuter
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ final class NSEFlow: BootstrapComponent {
// TODO: [WPB-19778] use to check app version migration
private let currentAppVersion: String
private var scopesByAccount = [Account: NSEUserScope]()
private let backgroundTaskExecuter = PassthroughTaskExecuter()

init(
currentAppVersion: String,
Expand Down Expand Up @@ -100,7 +101,8 @@ final class NSEFlow: BootstrapComponent {
shared {
NSEUserScope(
parent: self,
account: account
account: account,
backgroundTaskExecuter: backgroundTaskExecuter
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ final class NSEUserScope: Component<NSEUserScopeDependency> {
account.userIdentifier
}

let backgroundTaskExecuter: any BackgroundTaskExecuter

public var userAccountDataURL: URL {
dependency.accountDataURL.appending(path: accountID.uuidString)
}
Expand Down Expand Up @@ -90,9 +92,11 @@ final class NSEUserScope: Component<NSEUserScopeDependency> {

init(
parent: any Scope,
account: Account
account: Account,
backgroundTaskExecuter: any BackgroundTaskExecuter
) {
self.account = account
self.backgroundTaskExecuter = backgroundTaskExecuter
super.init(parent: parent)
}

Expand Down Expand Up @@ -167,6 +171,8 @@ final class NSEUserScope: Component<NSEUserScopeDependency> {

// Continue with client.
let clientScope = clientScope(
eventID: eventID,
contentHandler: contentHandler,
clientID: clientID,
restNetworkService: networkServices.rest,
webSocketNetworkService: networkServices.webSocket,
Expand All @@ -177,10 +183,7 @@ final class NSEUserScope: Component<NSEUserScopeDependency> {
earService: earService
)

try await clientScope.processPayload(
eventID: eventID,
contentHandler: contentHandler
)
try await clientScope.processPayload()
}

private func fetchBackendEnvironment() throws -> BackendEnvironment2? {
Expand Down Expand Up @@ -309,6 +312,8 @@ final class NSEUserScope: Component<NSEUserScopeDependency> {
// MARK: - Children

private func clientScope(
eventID: UUID,
contentHandler: @escaping (UNNotificationContent) -> Void,
clientID: String,
restNetworkService: NetworkService,
webSocketNetworkService: NetworkService,
Expand All @@ -319,6 +324,8 @@ final class NSEUserScope: Component<NSEUserScopeDependency> {
earService: EARServiceInterface
) -> NSEClientScope {
NSEClientScope(
eventID: eventID,
contentHandler: contentHandler,
parent: self,
clientID: clientID,
restNetworkService: restNetworkService,
Expand All @@ -327,7 +334,8 @@ final class NSEUserScope: Component<NSEUserScopeDependency> {
localDomain: localDomain,
isFederationEnabled: isFederationEnabled,
coreDataStack: coreDataStack,
earService: earService
earService: earService,
backgroundTaskExecuter: backgroundTaskExecuter
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import WireUtilitiesPackage
///
/// These sequential steps represents the NSE dependency graph (using Needle).

public final class NotificationServiceExtension: NotificationServiceProtocol {
public final class NotificationServiceExtension {

// MARK: - Properties

Expand Down Expand Up @@ -129,9 +129,13 @@ public final class NotificationServiceExtension: NotificationServiceProtocol {
}
}

public func serviceExtensionTimeWillExpire() {
logger.warn("new notification service will expire", attributes: .newNSE, .safePublic)
public var hasOnGoingTask: Bool {
onGoingTask != nil
}

public func cancel() async {
onGoingTask?.cancel()
await onGoingTask?.value
}
}

Expand Down Expand Up @@ -223,7 +227,7 @@ extension NotificationServiceExtension {

private func logUserError(_ error: NSEUserScope.Failure) {
switch error {
case let .mainAppRequired(message):
case let .mainAppRequired(message, _):
logger.warn(
"Main app required, need to open main app: \(message)",
attributes: .newNSE, .safePublic
Expand Down

This file was deleted.

Loading
Loading