diff --git a/NSE/Sources/NotificationServiceExtension.swift b/NSE/Sources/NotificationServiceExtension.swift index baac463055..11cd835361 100644 --- a/NSE/Sources/NotificationServiceExtension.swift +++ b/NSE/Sources/NotificationServiceExtension.swift @@ -49,7 +49,11 @@ private let keychainController = KeychainController(service: .sessions, class NotificationServiceExtension: UNNotificationServiceExtension { private var handler: ((UNNotificationContent) -> Void)? private var modifiedContent: UNMutableNotificationContent? - + + // Used to create one single UserSession across process/instances/runs + private static let serialQueue = DispatchQueue(label: "io.element.elementx.nse") + private static var userSession: NSEUserSession? + override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { guard !DataProtectionManager.isDeviceLockedAfterReboot(containerURL: URL.appGroupContainerDirectory), @@ -73,6 +77,24 @@ class NotificationServiceExtension: UNNotificationServiceExtension { MXLog.info("\(tag) #########################################") NSELogger.logMemory(with: tag) MXLog.info("\(tag) Payload came: \(request.content.userInfo)") + + Self.serialQueue.sync { + if Self.userSession == nil { + // This function might be run concurrently and from different processes + // It's imperative that we create **at most** one UserSession/Client per process + Task.synchronous { + do { + Self.userSession = try await NSEUserSession(credentials: credentials, clientSessionDelegate: keychainController) + } catch { + MXLog.error("Failed creating user session with error: \(error)") + } + } + } + + if Self.userSession == nil { + return discard(unreadCount: request.unreadCount) + } + } Task { await run(with: credentials, @@ -94,12 +116,13 @@ class NotificationServiceExtension: UNNotificationServiceExtension { eventId: String, unreadCount: Int?) async { MXLog.info("\(tag) run with roomId: \(roomId), eventId: \(eventId)") + + guard let userSession = Self.userSession else { + MXLog.error("Invalid NSE User Session, discarding.") + return discard(unreadCount: unreadCount) + } do { - // This function might be run concurrently and from different processes, let the SDK handle race conditions - // on fetching user sessions - let userSession = try await NSEUserSession(credentials: credentials, clientSessionDelegate: keychainController) - guard let itemProxy = await userSession.notificationItemProxy(roomID: roomId, eventID: eventId) else { MXLog.info("\(tag) no notification for the event, discard") return discard(unreadCount: unreadCount) @@ -219,3 +242,20 @@ class NotificationServiceExtension: UNNotificationServiceExtension { return false } } + +// https://stackoverflow.com/a/77300959/730924 +private extension Task where Failure == Error { + /// Performs an async task in a sync context. + /// + /// - Note: This function blocks the thread until the given operation is finished. The caller is responsible for managing multithreading. + static func synchronous(priority: TaskPriority? = nil, operation: @escaping @Sendable () async throws -> Success) { + let semaphore = DispatchSemaphore(value: 0) + + Task(priority: priority) { + defer { semaphore.signal() } + return try await operation() + } + + semaphore.wait() + } +}