From 09db45c2467b82e5c2f8e73d15ac672e71ae252d Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Fri, 9 Aug 2024 14:19:56 +0200 Subject: [PATCH] Make sure configure() is called in the correct order --- .../Notifications/NotificationHandler.swift | 3 + .../Dependencies/DependencyManager.swift | 46 +++++++++++++ .../Property/DependencyCollection.swift | 6 ++ .../Property/DependencyContext.swift | 4 ++ .../Property/DependencyDeclaration.swift | 5 ++ .../Property/DependencyPropertyWrapper.swift | 4 ++ .../DependenciesTests/DependencyTests.swift | 69 +++++++++++++++++++ 7 files changed, 137 insertions(+) diff --git a/Sources/Spezi/Capabilities/Notifications/NotificationHandler.swift b/Sources/Spezi/Capabilities/Notifications/NotificationHandler.swift index 2ae91a60..128d2cc0 100644 --- a/Sources/Spezi/Capabilities/Notifications/NotificationHandler.swift +++ b/Sources/Spezi/Capabilities/Notifications/NotificationHandler.swift @@ -22,6 +22,7 @@ public protocol NotificationHandler { /// - Note: Notification Actions are not supported on `tvOS`. /// /// - Parameter response: The user's response to the notification. + @MainActor func handleNotificationAction(_ response: UNNotificationResponse) async #endif @@ -35,6 +36,7 @@ public protocol NotificationHandler { /// /// - Parameter notification: The notification that is about to be delivered. /// - Returns: The option for notifying the user. Use `[]` to silence the notification. + @MainActor func receiveIncomingNotification(_ notification: UNNotification) async -> UNNotificationPresentationOptions? #if !os(macOS) @@ -51,6 +53,7 @@ public protocol NotificationHandler { /// /// - Parameter remoteNotification: The data of the notification payload. /// - Returns: Return the respective ``BackgroundFetchResult``. + @MainActor func receiveRemoteNotification(_ remoteNotification: [AnyHashable: Any]) async -> BackgroundFetchResult #else /// Handle remote notification when the app is running in background. diff --git a/Sources/Spezi/Dependencies/DependencyManager.swift b/Sources/Spezi/Dependencies/DependencyManager.swift index cdd36ee6..b821b63b 100644 --- a/Sources/Spezi/Dependencies/DependencyManager.swift +++ b/Sources/Spezi/Dependencies/DependencyManager.swift @@ -14,6 +14,8 @@ import XCTRuntimeAssertions public class DependencyManager: Sendable { /// Collection of already initialized modules. private let existingModules: [any Module] + /// We track the top level module instances to resolve the order for initialization. + private let originalModules: [any Module] /// Collection of initialized Modules. /// @@ -34,6 +36,9 @@ public class DependencyManager: Sendable { private var currentPushedModule: ModuleReference? private var searchStacks: [ModuleReference: [any Module.Type]] = [:] + private var nextTypeOrderIndex: UInt64 = 0 + private var moduleTypeOrder: [ObjectIdentifier: UInt64] = [:] + /// A ``DependencyManager`` in Spezi is used to gather information about modules with dependencies. /// @@ -46,6 +51,7 @@ public class DependencyManager: Sendable { self.modulesWithDependencies = modules.filter { !$0.dependencyDeclarations.isEmpty } + self.originalModules = modules self.existingModules = existingModules } @@ -61,6 +67,46 @@ public class DependencyManager: Sendable { injectDependencies() assert(searchStacks.isEmpty, "`searchStacks` are not getting cleaned up!") assert(currentPushedModule == nil, "`currentPushedModule` is never reset!") + assert(modulesWithDependencies.isEmpty, "modulesWithDependencies has remaining entries \(modulesWithDependencies)") + + buildTypeOrder() + + initializedModules.sort { lhs, rhs in + retrieveTypeOrder(for: lhs) < retrieveTypeOrder(for: rhs) + } + } + + private func buildTypeOrder() { + // when this method is called, we already know there is no cycle + + func nextEntry(for module: any Module.Type) { + let id = ObjectIdentifier(module) + guard moduleTypeOrder[id] == nil else { + return // already tracked + } + moduleTypeOrder[id] = nextTypeOrderIndex + nextTypeOrderIndex += 1 + } + + func depthFirstSearch(for module: any Module) { + for declaration in module.dependencyDeclarations { + for dependency in declaration.unsafeInjectedModules { + depthFirstSearch(for: dependency) + } + } + nextEntry(for: type(of: module)) + } + + for module in originalModules { + depthFirstSearch(for: module) + } + } + + private func retrieveTypeOrder(for module: any Module) -> UInt64 { + guard let order = moduleTypeOrder[ObjectIdentifier(type(of: module))] else { + preconditionFailure("Failed to retrieve module order index for module of type \(type(of: module))") + } + return order } private func injectDependencies() { diff --git a/Sources/Spezi/Dependencies/Property/DependencyCollection.swift b/Sources/Spezi/Dependencies/Property/DependencyCollection.swift index cdf10498..16cca771 100644 --- a/Sources/Spezi/Dependencies/Property/DependencyCollection.swift +++ b/Sources/Spezi/Dependencies/Property/DependencyCollection.swift @@ -108,6 +108,12 @@ public struct DependencyCollection { extension DependencyCollection: DependencyDeclaration { + var unsafeInjectedModules: [any Module] { + entries.flatMap { entry in + entry.unsafeInjectedModules + } + } + func dependencyRelation(to module: DependencyReference) -> DependencyRelation { let relations = entries.map { $0.dependencyRelation(to: module) } diff --git a/Sources/Spezi/Dependencies/Property/DependencyContext.swift b/Sources/Spezi/Dependencies/Property/DependencyContext.swift index 79087ec1..041ff82d 100644 --- a/Sources/Spezi/Dependencies/Property/DependencyContext.swift +++ b/Sources/Spezi/Dependencies/Property/DependencyContext.swift @@ -70,6 +70,10 @@ class DependencyContext: AnyDependencyContext { return nil } + var unsafeInjectedModules: [any Module] { + injectedDependency?.element.map { [$0] } ?? [] + } + init(for dependency: Dependency.Type, type: DependencyType, defaultValue: (() -> Dependency)? = nil) { self.type = type self.defaultValue = defaultValue diff --git a/Sources/Spezi/Dependencies/Property/DependencyDeclaration.swift b/Sources/Spezi/Dependencies/Property/DependencyDeclaration.swift index 0cc6f063..13a942f8 100644 --- a/Sources/Spezi/Dependencies/Property/DependencyDeclaration.swift +++ b/Sources/Spezi/Dependencies/Property/DependencyDeclaration.swift @@ -21,6 +21,11 @@ enum DependencyRelation: Hashable { /// /// This protocol allows to communicate dependency requirements of a ``Module`` to the ``DependencyManager``. protocol DependencyDeclaration { + /// Directly access the injected dependency of the dependency declaration. + /// + /// Unsafe, as there are not extra checks made to guarantee a safe value. + var unsafeInjectedModules: [any Module] { get } + /// Request from the ``DependencyManager`` to collect all dependencies. Mark required by calling `DependencyManager/require(_:defaultValue:)`. @MainActor func collect(into dependencyManager: DependencyManager) diff --git a/Sources/Spezi/Dependencies/Property/DependencyPropertyWrapper.swift b/Sources/Spezi/Dependencies/Property/DependencyPropertyWrapper.swift index d4de246d..268975b2 100644 --- a/Sources/Spezi/Dependencies/Property/DependencyPropertyWrapper.swift +++ b/Sources/Spezi/Dependencies/Property/DependencyPropertyWrapper.swift @@ -110,6 +110,10 @@ extension _DependencyPropertyWrapper: SpeziPropertyWrapper { extension _DependencyPropertyWrapper: DependencyDeclaration { + var unsafeInjectedModules: [any Module] { + dependencies.unsafeInjectedModules + } + func dependencyRelation(to module: DependencyReference) -> DependencyRelation { dependencies.dependencyRelation(to: module) } diff --git a/Tests/SpeziTests/DependenciesTests/DependencyTests.swift b/Tests/SpeziTests/DependenciesTests/DependencyTests.swift index 00011f69..66974574 100644 --- a/Tests/SpeziTests/DependenciesTests/DependencyTests.swift +++ b/Tests/SpeziTests/DependenciesTests/DependencyTests.swift @@ -661,6 +661,75 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le } } + @MainActor + func testConfigureCallOrder() throws { + class Order: Module, DefaultInitializable { + @MainActor + var order: [Int] = [] + + required init() {} + } + + class ModuleA: Module { + @Dependency(Order.self) + private var order + + func configure() { + order.order.append(1) + } + } + + class ModuleB: Module { + @Dependency(Order.self) + private var order + + @Dependency(ModuleA.self) + private var module = ModuleA() + + func configure() { + order.order.append(2) + } + } + + class ModuleC: Module { + @Dependency(Order.self) + private var order + + @Dependency(ModuleA.self) + private var moduleA + @Dependency(ModuleB.self) + private var moduleB + + func configure() { + order.order.append(3) + } + } + + class ModuleD: Module { + @Dependency(Order.self) + private var order + + @Dependency(ModuleC.self) + private var module + + func configure() { + order.order.append(4) + } + } + + let spezi = Spezi(standard: DefaultStandard(), modules: [ + ModuleC(), + ModuleB(), + ModuleD(), + ModuleD() + ]) + + let modules = spezi.modules + let order: Order = try getModule(in: modules) + + XCTAssertEqual(order.order, [1, 2, 3, 4, 4]) + } + @available(*, deprecated, message: "Propagate deprecation warning") @MainActor func testDeprecatedInits() throws {