Skip to content

Commit 66efe94

Browse files
committed
Add keyedExecutor
1 parent 6e26c88 commit 66efe94

15 files changed

Lines changed: 398 additions & 139 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import Foundation
2+
import FactoryKit
3+
4+
extension Container {
5+
6+
var keyedExecutor: Factory<KeyedExecutor> {
7+
self {
8+
return KeyedExecutor()
9+
}.shared
10+
}
11+
12+
}

Bluerage/Sources/Kit/Extensions/Logger+Category.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ extension Logger {
99

1010
static let agents = Logger(subsystem: Self.subsystem, category: "agent")
1111

12+
static let auth = Logger(subsystem: Self.subsystem, category: "auth")
13+
1214
static let login = Logger(subsystem: Self.subsystem, category: "login")
1315

1416
static let tools = Logger(subsystem: Self.subsystem, category: "tools")
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import Foundation
2+
3+
private protocol CancellableOperation {
4+
5+
func cancelOperation()
6+
7+
}
8+
9+
extension Task: CancellableOperation {
10+
11+
fileprivate func cancelOperation() {
12+
self.cancel()
13+
}
14+
15+
}
16+
17+
final class KeyedExecutor: @unchecked Sendable {
18+
19+
#if compiler(>=6.0)
20+
typealias ThrowingOperation<Success> = @isolated(any) @Sendable () async throws -> sending Success
21+
typealias Operation<Success> = @isolated(any) @Sendable () async -> sending Success
22+
#else
23+
typealias ThrowingOperation<Success: Sendable> = @Sendable () async throws -> Success
24+
typealias Operation<Success: Sendable> = @Sendable () async -> Success
25+
#endif
26+
27+
private var tasks: [String: CancellableOperation] = [:]
28+
29+
private let lock = NSLock()
30+
31+
@discardableResult
32+
func executeOperation<Success>(for key: String,
33+
@_inheritActorContext operation: @escaping Operation<Success>) async -> Success {
34+
let task = self.lock.withLock {
35+
if let existingTask = self.tasks[key] as? Task<Success, Never> {
36+
return existingTask
37+
}
38+
39+
let newTask = Task<Success, Never> {
40+
return await operation()
41+
}
42+
43+
self.tasks[key] = newTask
44+
45+
return newTask
46+
}
47+
48+
defer {
49+
self.lock.withLock {
50+
self.tasks[key] = nil
51+
}
52+
}
53+
54+
return await task.value
55+
}
56+
57+
@discardableResult
58+
func executeOperation<Success>(for key: String,
59+
@_inheritActorContext operation: @escaping ThrowingOperation<Success>) async throws -> Success {
60+
let task = self.lock.withLock {
61+
if let existingTask = self.tasks[key] as? Task<Success, Error> {
62+
return existingTask
63+
}
64+
65+
let newTask = Task<Success, Error> {
66+
return try await operation()
67+
}
68+
69+
self.tasks[key] = newTask
70+
71+
return newTask
72+
}
73+
74+
defer {
75+
self.lock.withLock {
76+
self.tasks[key] = nil
77+
}
78+
}
79+
80+
return try await task.value
81+
}
82+
83+
func cancelOperation(with key: String) {
84+
self.lock.withLock {
85+
self.tasks[key]?.cancelOperation()
86+
self.tasks[key] = nil
87+
}
88+
}
89+
90+
}

Bluerage/Sources/Kit/Managers/NotificationsManager.swift

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,21 @@ final class NotificationsManager: NSObject, UNUserNotificationCenterDelegate {
1616

1717
@Injected(\.knock) private var knock
1818

19+
@Injected(\.keyedExecutor) private var keyedExecutor
20+
1921
nonisolated override init() {
2022
super.init()
2123

2224
UNUserNotificationCenter.current().delegate = self
2325
}
2426

2527
func pushNotificationTapped(userInfo: [AnyHashable: Any]) {
26-
if let messageId = self.getMessageId(userInfo: userInfo) {
27-
Task {
28-
do {
29-
_ = try await self.knock.updateMessageStatus(messageId: messageId, status: .interacted)
30-
} catch {
31-
Logger.notifications.error("Error sending updating message status for messageId: \(messageId, privacy: .public), error: \(error.localizedDescription, privacy: .public)")
32-
}
33-
}
28+
guard let messageId = self.getMessageId(userInfo: userInfo) else {
29+
return
30+
}
31+
32+
Task {
33+
await self.updateMessageStatus(messageId: messageId, status: .interacted)
3434
}
3535
}
3636

@@ -39,7 +39,9 @@ final class NotificationsManager: NSObject, UNUserNotificationCenterDelegate {
3939
let channelId = await Knock.shared.getPushChannelId()
4040

4141
do {
42-
_ = try await Knock.shared.registerTokenForAPNS(channelId: channelId, token: Self.convertTokenToString(token: deviceToken))
42+
try await self.keyedExecutor.executeOperation(for: "notificationsManager/registerTokenForAPNS/\(channelId ?? "nil")") {
43+
_ = try await Knock.shared.registerTokenForAPNS(channelId: channelId, token: Self.convertTokenToString(token: deviceToken))
44+
}
4345
} catch let error {
4446
Logger.notifications.error("Unable to register for push notification at this time, error: \(error.localizedDescription, privacy: .public)")
4547
}
@@ -52,18 +54,34 @@ final class NotificationsManager: NSObject, UNUserNotificationCenterDelegate {
5254
willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
5355
Logger.notifications.info("pushNotificationDeliveredInForeground")
5456

55-
if let messageId = getMessageId(userInfo: notification.request.content.userInfo) {
56-
self.knock.updateMessageStatus(messageId: messageId, status: .read) { _ in }
57+
let options: UNNotificationPresentationOptions = [.sound, .badge, .banner]
58+
59+
guard let messageId = self.getMessageId(userInfo: notification.request.content.userInfo) else {
60+
return options
5761
}
5862

59-
return [.sound, .badge, .banner]
63+
await self.updateMessageStatus(messageId: messageId, status: .read)
64+
65+
return options
6066
}
6167

6268
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async {
6369
Logger.notifications.info("pushNotificationTapped")
6470

65-
if let messageId = getMessageId(userInfo: response.notification.request.content.userInfo) {
66-
Knock.shared.updateMessageStatus(messageId: messageId, status: .interacted) { _ in }
71+
guard let messageId = self.getMessageId(userInfo: response.notification.request.content.userInfo) else {
72+
return
73+
}
74+
75+
await self.updateMessageStatus(messageId: messageId, status: .interacted)
76+
}
77+
78+
private func updateMessageStatus(messageId: String, status: Knock.KnockMessageStatusUpdateType) async {
79+
do {
80+
try await self.keyedExecutor.executeOperation(for: "notificationsManager/updateMessageStatus/\(status.rawValue)/\(messageId)") {
81+
_ = try await self.knock.updateMessageStatus(messageId: messageId, status: status)
82+
}
83+
} catch {
84+
Logger.notifications.error("Failed to update messageId \(messageId, privacy: .public) to status \(status.rawValue, privacy: .public), error: \(error.localizedDescription, privacy: .public)")
6785
}
6886
}
6987

Bluerage/Sources/Kit/Sessions/AuthSession/AuthSession.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,6 @@ protocol AuthSession {
1313

1414
func signOut() async throws
1515

16+
func deleteAccount() async throws
17+
1618
}

Bluerage/Sources/Kit/Sessions/AuthSession/AuthSessionImpl.swift

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,21 @@ import Foundation
22
import FactoryKit
33
import Combine
44
import ConvexMobile
5+
import OSLog
56

67
final class AuthSessionImpl: AuthSession {
78

9+
enum Error: LocalizedError {
10+
case userInformationIsMissingForDelete
11+
12+
var errorDescription: String? {
13+
switch self {
14+
case .userInformationIsMissingForDelete:
15+
"Delete account failure due to missing user session"
16+
}
17+
}
18+
}
19+
820
private(set) var authState: AuthState {
921
get {
1022
self.authStateSubject.value
@@ -25,6 +37,8 @@ final class AuthSessionImpl: AuthSession {
2537

2638
@Injected(\.env) private var env
2739

40+
@Injected(\.keyedExecutor) private var keyedExecutor
41+
2842
private let authStateSubject: CurrentValueSubject<AuthState, Never>
2943

3044
init() {
@@ -34,7 +48,9 @@ final class AuthSessionImpl: AuthSession {
3448
}
3549

3650
func signInWithApple(idToken: String) async throws {
37-
_ = try await self.convex.login(with: ClerkAuthProvider.AppleLoginParams(idToken: idToken))
51+
try await self.keyedExecutor.executeOperation(for: "authSession/signInWithApple/\(idToken)") {
52+
_ = try await self.convex.login(with: ClerkAuthProvider.AppleLoginParams(idToken: idToken))
53+
}
3854
}
3955

4056
func start() {
@@ -60,12 +76,32 @@ final class AuthSessionImpl: AuthSession {
6076
.store(in: &self.cancellables)
6177

6278
Task {
63-
try await self.convex.loginFromCache()
79+
do {
80+
try await self.keyedExecutor.executeOperation(for: "authSession/loginFromCache") {
81+
try await self.convex.loginFromCache()
82+
}
83+
} catch {
84+
Logger.auth.error("Error logging in from cache: \(error.localizedDescription, privacy: .public)")
85+
}
6486
}
6587
}
6688

89+
@MainActor
6790
func signOut() async throws {
68-
await self.convex.logout()
91+
try await self.keyedExecutor.executeOperation(for: "authSession/signOut") {
92+
try await self.convex.logout()
93+
}
94+
}
95+
96+
@MainActor
97+
func deleteAccount() async throws {
98+
guard let user = self.clerk.user else {
99+
throw Error.userInformationIsMissingForDelete
100+
}
101+
102+
try await self.keyedExecutor.executeOperation(for: "settings/deleteUser") {
103+
try await user.delete()
104+
}
69105
}
70106

71107
}

0 commit comments

Comments
 (0)