-
Notifications
You must be signed in to change notification settings - Fork 15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
refactor: implement new NSE mechanism and pull pending events - WPB-10219 #2287
base: develop
Are you sure you want to change the base?
Changes from all commits
1203631
35ffc9d
6345728
c43bad5
06193e3
d23a682
438d5c8
aff095f
8497f0e
e36dd0b
56aa464
e8c164f
081543b
a2e720e
4ad3c72
8d3a510
dba88d3
9378165
29d5784
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
// | ||
// Wire | ||
// Copyright (C) 2025 Wire Swiss GmbH | ||
// | ||
// This program is free software: you can redistribute it and/or modify | ||
// it under the terms of the GNU General Public License as published by | ||
// the Free Software Foundation, either version 3 of the License, or | ||
// (at your option) any later version. | ||
// | ||
// This program is distributed in the hope that it will be useful, | ||
// but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
// GNU General Public License for more details. | ||
// | ||
// You should have received a copy of the GNU General Public License | ||
// along with this program. If not, see http://www.gnu.org/licenses/. | ||
// | ||
|
||
import WireAPI | ||
import WireDataModel | ||
import WireFoundation | ||
|
||
public final class Assembly { | ||
|
||
private let userID: UUID | ||
private let clientID: String | ||
private let context: NSManagedObjectContext | ||
private let sharedUserDefaults: UserDefaults | ||
private let proteusService: any ProteusServiceInterface | ||
private let apiService: any APIServiceProtocol | ||
private let apiVersion: WireAPI.APIVersion | ||
private let pushChannel: any PushChannelProtocol | ||
private let backendEnvironmentProvider: BackendEnvironmentProvider | ||
|
||
init( | ||
userID: UUID, | ||
clientID: String, | ||
context: NSManagedObjectContext, | ||
sharedUserDefaults: UserDefaults, | ||
proteusService: any ProteusServiceInterface, | ||
apiService: any APIServiceProtocol, | ||
apiVersion: WireAPI.APIVersion, | ||
pushChannel: any PushChannelProtocol, | ||
backendEnvironmentProvider: BackendEnvironmentProvider | ||
) { | ||
self.userID = userID | ||
self.clientID = clientID | ||
self.context = context | ||
self.sharedUserDefaults = sharedUserDefaults | ||
self.proteusService = proteusService | ||
self.apiService = apiService | ||
self.apiVersion = apiVersion | ||
self.pushChannel = pushChannel | ||
self.backendEnvironmentProvider = backendEnvironmentProvider | ||
|
||
registerNotificationServiceDependencies() | ||
} | ||
|
||
// MARK: - API Init | ||
|
||
private lazy var updateEventsAPI = UpdateEventsAPIBuilder( | ||
apiService: apiService | ||
).makeAPI(for: apiVersion) | ||
|
||
// MARK: - Repositories and local stores Init | ||
|
||
private lazy var userLocalStore = UserLocalStore(context: context) | ||
|
||
private lazy var updateEventsLocalStore = UpdateEventsLocalStore( | ||
context: context, | ||
userID: userID, | ||
sharedUserDefaults: sharedUserDefaults | ||
) | ||
|
||
} | ||
|
||
extension Assembly { | ||
|
||
/// Register some domain dependencies to be resolved by the `NotificationService`. | ||
/// Since `NotificationService` is not initializable, the injector provides a lightweight dependency injection | ||
/// mechanism to retrieve some already initialized dependencies that the notification service requires. | ||
|
||
private func registerNotificationServiceDependencies() { | ||
Injector.register(UserLocalStoreProtocol.self) { | ||
self.userLocalStore | ||
} | ||
|
||
Injector.register(UpdateEventsAPI.self) { | ||
self.updateEventsAPI | ||
} | ||
|
||
Injector.register(PushChannelProtocol.self) { | ||
self.pushChannel | ||
} | ||
|
||
Injector.register(BackendEnvironmentProvider.self) { | ||
self.backendEnvironmentProvider | ||
} | ||
|
||
Injector.register(UpdateEventsLocalStoreProtocol.self) { | ||
self.updateEventsLocalStore | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,194 @@ | ||
// | ||
// Wire | ||
// Copyright (C) 2025 Wire Swiss GmbH | ||
// | ||
// This program is free software: you can redistribute it and/or modify | ||
// it under the terms of the GNU General Public License as published by | ||
// the Free Software Foundation, either version 3 of the License, or | ||
// (at your option) any later version. | ||
// | ||
// This program is distributed in the hope that it will be useful, | ||
// but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
// GNU General Public License for more details. | ||
// | ||
// You should have received a copy of the GNU General Public License | ||
// along with this program. If not, see http://www.gnu.org/licenses/. | ||
// | ||
|
||
import Foundation | ||
|
||
protocol OptionalProtocol { | ||
static var wrappedType: Any.Type { get } | ||
} | ||
|
||
extension Optional: OptionalProtocol { | ||
public static var wrappedType: Any.Type { | ||
Wrapped.self | ||
} | ||
} | ||
|
||
enum Injector { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I feel like introducing a dependency injection system is out of the scope of this task, or is there a specific reason to include it here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It was out of the scope but I also felt like we kind of needed to introduce such mechanism to easily resolve the dependencies required by the NotificationSession object and avoid initializing the same objects over and over again. For instance, to initialize an So I figured: let's introduce a lightweight DI mechanism and use our entry point to register the dependencies we need so we don't have to do that work over and over again in the NotificationService. also mentioning @netbe because it is related to the same topic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jullianm Ok, I think it would be better to create a dedicated PR for the Injector and to present to the rest of the team before going further with this DI mechnism. |
||
private nonisolated(unsafe) static var services: [ServiceKey: ServiceEntryProtocol] = [:] | ||
|
||
// MARK: - Register | ||
|
||
static func register<Service>( | ||
_ serviceType: Service.Type, | ||
factory: @escaping () -> Service | ||
) { | ||
_register(serviceType, factory: factory) | ||
} | ||
|
||
static func register<Service, Arg1>( | ||
_ serviceType: Service.Type, | ||
factory: @escaping (Arg1) -> Service | ||
) { | ||
_register(serviceType, factory: factory) | ||
} | ||
|
||
static func register<Service, Arg1, Arg2>( | ||
_ serviceType: Service.Type, | ||
factory: @escaping (Arg1, Arg2) -> Service | ||
) { | ||
_register(serviceType, factory: factory) | ||
} | ||
|
||
static func register<Service, Arg1, Arg2, Arg3>( | ||
_ serviceType: Service.Type, | ||
factory: @escaping (Arg1, Arg2, Arg3) -> Service | ||
) { | ||
_register(serviceType, factory: factory) | ||
} | ||
|
||
static func register<Service, Arg1, Arg2, Arg3, Arg4>( | ||
_ serviceType: Service.Type, | ||
factory: @escaping (Arg1, Arg2, Arg3, Arg4) -> Service | ||
) { | ||
_register(serviceType, factory: factory) | ||
} | ||
|
||
static func register<Service, Arg1, Arg2, Arg3, Arg4, Arg5>( | ||
_ serviceType: Service.Type, | ||
factory: @escaping (Arg1, Arg2, Arg3, Arg4, Arg5) -> Service | ||
) { | ||
_register(serviceType, factory: factory) | ||
} | ||
|
||
static func register<Service, Arg1, Arg2, Arg3, Arg4, Arg5, Arg6>( | ||
_ serviceType: Service.Type, | ||
factory: @escaping (Arg1, Arg2, Arg3, Arg4, Arg5, Arg6) -> Service | ||
) { | ||
_register(serviceType, factory: factory) | ||
} | ||
Comment on lines
+71
to
+83
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question: in what case do we need multiple arguments? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When we want to resolve a given dependency with a set of arguments that we only have access to at some point in the app lifecycle. We don't access the concrete type directly, we'll just provide the arguments it needs using the (public) protocol it conforms to. |
||
|
||
private static func _register<Service, Arguments>( | ||
_ serviceType: Service.Type, | ||
factory: @escaping (Arguments) -> Any | ||
) { | ||
|
||
let key = ServiceKey(serviceType: Service.self, argumentsType: Arguments.self) | ||
|
||
let entry = ServiceEntry( | ||
serviceType: serviceType, | ||
argumentsType: Arguments.self, | ||
factory: factory | ||
) | ||
services[key] = entry | ||
} | ||
|
||
// MARK: - Resolve | ||
|
||
static func resolve<Service>() -> Service { | ||
typealias FactoryType = (()) -> Any | ||
return _genericResolve(serviceType: Service.self) { (factory: FactoryType) in | ||
factory(()) | ||
} | ||
} | ||
|
||
static func resolve<Service, Arg1>( | ||
argument: Arg1 | ||
) -> Service { | ||
typealias FactoryType = (Arg1) -> Any | ||
return _genericResolve(serviceType: Service.self) { (factory: FactoryType) in | ||
factory(argument) | ||
} | ||
} | ||
|
||
static func resolve<Service, Arg1, Arg2>( | ||
arguments arg1: Arg1, _ arg2: Arg2 | ||
) -> Service { | ||
typealias FactoryType = ((Arg1, Arg2)) -> Any | ||
return _genericResolve(serviceType: Service.self) { (factory: FactoryType) in | ||
factory((arg1, arg2)) | ||
} | ||
} | ||
|
||
static func resolve<Service, Arg1, Arg2, Arg3>( | ||
arguments arg1: Arg1, _ arg2: Arg2, _ arg3: Arg3 | ||
) -> Service { | ||
typealias FactoryType = ((Arg1, Arg2, Arg3)) -> Any | ||
return _genericResolve(serviceType: Service.self) { (factory: FactoryType) in | ||
factory((arg1, arg2, arg3)) | ||
} | ||
} | ||
|
||
static func resolve<Service, Arg1, Arg2, Arg3, Arg4>( | ||
arguments arg1: Arg1, _ arg2: Arg2, _ arg3: Arg3, _ arg4: Arg4 | ||
) -> Service { | ||
typealias FactoryType = ((Arg1, Arg2, Arg3, Arg4)) -> Any | ||
return _genericResolve(serviceType: Service.self) { (factory: FactoryType) in | ||
factory((arg1, arg2, arg3, arg4)) | ||
} | ||
} | ||
|
||
static func resolve<Service, Arg1, Arg2, Arg3, Arg4, Arg5>( | ||
arguments arg1: Arg1, _ arg2: Arg2, _ arg3: Arg3, _ arg4: Arg4, _ arg5: Arg5 | ||
) -> Service { | ||
typealias FactoryType = ((Arg1, Arg2, Arg3, Arg4, Arg5)) -> Any | ||
return _genericResolve(serviceType: Service.self) { (factory: FactoryType) in | ||
factory((arg1, arg2, arg3, arg4, arg5)) | ||
} | ||
} | ||
|
||
static func resolve<Service, Arg1, Arg2, Arg3, Arg4, Arg5, Arg6>( | ||
arguments arg1: Arg1, _ arg2: Arg2, _ arg3: Arg3, _ arg4: Arg4, _ arg5: Arg5, _ arg6: Arg6 | ||
) -> Service { | ||
typealias FactoryType = ((Arg1, Arg2, Arg3, Arg4, Arg5, Arg6)) -> Any | ||
return _genericResolve(serviceType: Service.self) { (factory: FactoryType) in | ||
factory((arg1, arg2, arg3, arg4, arg5, arg6)) | ||
} | ||
} | ||
|
||
static func _genericResolve<Service, Arguments>( | ||
serviceType: Service.Type, | ||
invoker: @escaping ((Arguments) -> Any) -> Any | ||
) -> Service { | ||
var resolvedInstance: Service? | ||
let type: Any.Type = if let optionalType = Service.self as? OptionalProtocol.Type { | ||
optionalType.wrappedType | ||
} else { | ||
Service.self | ||
} | ||
|
||
let key = ServiceKey(serviceType: type, argumentsType: Arguments.self) | ||
|
||
if let entry = services[key] { | ||
resolvedInstance = resolve(entry: entry, invoker: invoker) | ||
} | ||
|
||
if let resolvedInstance { | ||
return resolvedInstance | ||
} else { | ||
fatalError("You need to register concrete type for \(Service.self)") | ||
} | ||
} | ||
|
||
private static func resolve<Service, Factory>( | ||
entry: ServiceEntryProtocol, | ||
invoker: (Factory) -> Any | ||
) -> Service? { | ||
let resolvedInstance = invoker(entry.factory as! Factory) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why do we need to cast here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. because the underlying type of entry.factory is Any so we need to force cast to deal with a same expected generic type ( There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok I feel like a tuple call will help me better understand it, I wonder if we can avoid casting using generic |
||
return resolvedInstance as? Service | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
// | ||
// Wire | ||
// Copyright (C) 2025 Wire Swiss GmbH | ||
// | ||
// This program is free software: you can redistribute it and/or modify | ||
// it under the terms of the GNU General Public License as published by | ||
// the Free Software Foundation, either version 3 of the License, or | ||
// (at your option) any later version. | ||
// | ||
// This program is distributed in the hope that it will be useful, | ||
// but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
// GNU General Public License for more details. | ||
// | ||
// You should have received a copy of the GNU General Public License | ||
// along with this program. If not, see http://www.gnu.org/licenses/. | ||
// | ||
|
||
import Foundation | ||
|
||
typealias FunctionType = Any | ||
|
||
protocol ServiceEntryProtocol: AnyObject { | ||
var factory: FunctionType { get } | ||
var serviceType: Any.Type { get } | ||
} | ||
|
||
final class ServiceEntry<Service>: ServiceEntryProtocol { | ||
|
||
// MARK: - Properties | ||
|
||
let serviceType: Any.Type | ||
let argumentsType: Any.Type | ||
let factory: FunctionType | ||
|
||
// MARK: - Object lifecycle | ||
|
||
init(serviceType: Service.Type, argumentsType: Any.Type, factory: FunctionType) { | ||
self.serviceType = serviceType | ||
self.argumentsType = argumentsType | ||
self.factory = factory | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So just for my understanding, when and where would we initialize the Assembly.
One thing to have in mind there can be multiple process of NSE or iOS can reuse the same process to process different notifications payload
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what I had in mind is that the assembly is the entry point of WireDomain so when the app starts, we use that entry point to setup all of our dependencies, we also take that occasion to register (using the lightweight DI mechanism
Injector
) some of the dependencies the notification service needs so we don't have to initialize them again every time we receive a new notification.For instance, the
NotificationSession
is initialized everytime we receive a new notification, given some of these dependencies were already registered before, we can just resolve them and only pass variable userID which comes from the current notification payload :also mentioning @johnxnguyen because it is related
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sounds good
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@netbe, you, and I are all facing the question of how to set up the dependency graph, and we all have various solutions. I propose we meet to find a common solution.