Skip to content
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

Open
wants to merge 19 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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 @@ -20,7 +20,7 @@ import Foundation

/// An event concerning feature configs.

public enum FeatureConfigEvent: Equatable, Codable {
public enum FeatureConfigEvent: Equatable, Codable, Sendable {

/// A feature config was updated.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import Foundation

/// An event concerning federation between domains.

public enum FederationEvent: Equatable, Codable {
public enum FederationEvent: Equatable, Codable, Sendable {

/// Two or more other domains have stopped federating
/// with each other.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import Foundation

/// An event concerning teams.

public enum TeamEvent: Equatable, Codable {
public enum TeamEvent: Equatable, Codable, Sendable {

/// The self team was deleted.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import Foundation

/// An event concerning users.

public enum UserEvent: Equatable, Codable {
public enum UserEvent: Equatable, Codable, Sendable {

/// The self user has added a new client.

Expand Down
104 changes: 104 additions & 0 deletions WireDomain/Sources/WireDomain/Assembly.swift
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()
Copy link
Collaborator

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

Copy link
Contributor Author

@jullianm jullianm Jan 6, 2025

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 :

let updateEventsRepository = UpdateEventsRepository(
            userID: userID,
            selfClientID: selfClientID,
            // these were already initialized, resolving them
            updateEventsAPI: Injector.resolve(),
            pushChannel: Injector.resolve(),
            updateEventDecryptor: Injector.resolve(),
            updateEventsLocalStore: Injector.resolve()
        )

also mentioning @johnxnguyen because it is related

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good

Copy link
Collaborator

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.

}

// 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
}
}
}
194 changes: 194 additions & 0 deletions WireDomain/Sources/WireDomain/Dependency Injection/Injector.swift
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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

@jullianm jullianm Jan 6, 2025

Choose a reason for hiding this comment

The 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 UpdateEventsLocalStore I would need a context, where I would find this context ? This is the kind of question (and work) the notification service (I think) doesn't need to do since we have the entry point that setup of all of WireDomain dependencies and basically does that work already.

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

Copy link
Collaborator

Choose a reason for hiding this comment

The 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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: in what case do we need multiple arguments?

Copy link
Contributor Author

@jullianm jullianm Jan 3, 2025

Choose a reason for hiding this comment

The 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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need to cast here?

Copy link
Contributor Author

@jullianm jullianm Jan 3, 2025

Choose a reason for hiding this comment

The 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 (Factory). Also, at this point the force cast is without any risks because we know we're dealing with a matching entry in the services dict.

Copy link
Collaborator

Choose a reason for hiding this comment

The 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
}
}
Loading
Loading