From d87e3d8104a0732c0e294e9ae6354db4a7058800 Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Tue, 23 Jul 2024 10:17:07 +0200 Subject: [PATCH] Allow to load multiple modules of the same type (#110) # Allow to load multiple modules of the same type ## :recycle: Current situation & Problem Currently, Spezi enforces that there is maximum one module of the same type loaded at a time. By allowing the Module system to used much more dynamically via #105, we found that certain types of Modules might exist multiple times in the system (e.g., a Bluetooth device type modeled as a Spezi Module might have two physical devices connected at the same time). This PR makes the necessary infrastructure changes to support loading multiple modules of the same type. A check that the same module can only be loaded once is still in place. Restructuring the `@Dependency` to support multiple modules of the same type is not trivial and will be addressed in a follow-up PR which is tracked in #111. ## :gear: Release Notes * Allow to load multiple modules of the same type. ## :books: Documentation -- ## :white_check_mark: Testing Additional unit testing was added to verify behavior. ## :pencil: Code of Conduct & Contributing Guidelines By submitting creating this pull request, you agree to follow our [Code of Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md): - [x] I agree to follow the [Code of Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md). --- .../Dependencies/DependencyManager.swift | 5 +- .../Property/DependencyCollection.swift | 38 +++++- .../DependencyCollectionBuilder.swift | 2 +- .../Property/DependencyContext.swift | 60 ++++----- .../Property/DependencyDeclaration.swift | 6 +- .../Property/DependencyPropertyWrapper.swift | 6 +- Sources/Spezi/Module/Module.swift | 4 +- Sources/Spezi/Spezi.docc/Module/Module.md | 4 - Sources/Spezi/Spezi.docc/Standard.md | 4 - .../ImplicitlyCreatedModulesKey.swift | 17 +++ .../{ => KnowledgeSources}/SpeziAnchor.swift | 0 .../Spezi/KnowledgeSources/SpeziStorage.swift | 16 +++ .../KnowledgeSources/StoredModulesKey.swift | 83 ++++++++++++ Sources/Spezi/Spezi/Spezi.swift | 124 +++++++----------- .../Spezi/Utilities/DynamicReference.swift | 40 ++++++ .../DependenciesTests/DependencyTests.swift | 69 ++++++++++ 16 files changed, 345 insertions(+), 133 deletions(-) create mode 100644 Sources/Spezi/Spezi/KnowledgeSources/ImplicitlyCreatedModulesKey.swift rename Sources/Spezi/Spezi/{ => KnowledgeSources}/SpeziAnchor.swift (100%) create mode 100644 Sources/Spezi/Spezi/KnowledgeSources/SpeziStorage.swift create mode 100644 Sources/Spezi/Spezi/KnowledgeSources/StoredModulesKey.swift create mode 100644 Sources/Spezi/Utilities/DynamicReference.swift diff --git a/Sources/Spezi/Dependencies/DependencyManager.swift b/Sources/Spezi/Dependencies/DependencyManager.swift index 13a5b6ae..18e2664e 100644 --- a/Sources/Spezi/Dependencies/DependencyManager.swift +++ b/Sources/Spezi/Dependencies/DependencyManager.swift @@ -143,8 +143,9 @@ public class DependencyManager: Sendable { /// - optional: Flag indicating if it is a optional return. /// - Returns: Returns the Module instance. Only optional, if `optional` is set to `true` and no Module was found. func retrieve(module: M.Type = M.self, optional: Bool = false) -> M? { - guard let candidate = initializedModules.first(where: { type(of: $0) == M.self }) ?? existingModules.first(where: { type(of: $0) == M.self }), - let module = candidate as? M else { + guard let candidate = existingModules.first(where: { type(of: $0) == M.self }) + ?? initializedModules.first(where: { type(of: $0) == M.self }), + let module = candidate as? M else { precondition(optional, "Could not located dependency of type \(M.self)!") return nil } diff --git a/Sources/Spezi/Dependencies/Property/DependencyCollection.swift b/Sources/Spezi/Dependencies/Property/DependencyCollection.swift index cebb9452..2a83d9b2 100644 --- a/Sources/Spezi/Dependencies/Property/DependencyCollection.swift +++ b/Sources/Spezi/Dependencies/Property/DependencyCollection.swift @@ -11,12 +11,6 @@ public struct DependencyCollection: DependencyDeclaration { let entries: [AnyDependencyContext] - var injectedDependencies: [any Module] { - entries.reduce(into: []) { result, dependencies in - result.append(contentsOf: dependencies.injectedDependencies) - } - } - init(_ entries: [AnyDependencyContext]) { self.entries = entries } @@ -33,7 +27,7 @@ public struct DependencyCollection: DependencyDeclaration { /// ### Usage /// /// The `SomeCustomDependencyBuilder` enforces certain type constraints (e.g., `SomeTypeConstraint`, more specific than ``Module``) during aggregation of ``Module/Dependency``s (``Module``s) via a result builder. - /// The individual dependency expressions within the result builder conforming to `SomeTypeConstraint` are then transformed to a ``DependencyCollection`` via ``DependencyCollection/init(for:singleEntry:)``. + /// The individual dependency expressions within the result builder conforming to `SomeTypeConstraint` are then transformed to a ``DependencyCollection`` via ``DependencyCollection/init(for:singleEntry:)-6ihsh``. /// /// ```swift /// @resultBuilder @@ -49,6 +43,30 @@ public struct DependencyCollection: DependencyDeclaration { self.init(DependencyContext(for: type, defaultValue: singleEntry)) } + /// Creates a ``DependencyCollection`` from a closure resulting in a single generic type conforming to the Spezi ``Module``. + /// - Parameters: + /// - type: The generic type resulting from the passed closure, has to conform to ``Module``. + /// - singleEntry: Closure returning a dependency conforming to ``Module``, stored within the ``DependencyCollection``. + /// + /// ### Usage + /// + /// The `SomeCustomDependencyBuilder` enforces certain type constraints (e.g., `SomeTypeConstraint`, more specific than ``Module``) during aggregation of ``Module/Dependency``s (``Module``s) via a result builder. + /// The individual dependency expressions within the result builder conforming to `SomeTypeConstraint` are then transformed to a ``DependencyCollection`` via ``DependencyCollection/init(for:singleEntry:)-6nzui``. + /// + /// ```swift + /// @resultBuilder + /// public enum SomeCustomDependencyBuilder: DependencyCollectionBuilder { + /// public static func buildExpression(_ expression: @escaping @autoclosure () -> T) -> DependencyCollection { + /// DependencyCollection(singleEntry: expression) + /// } + /// } + /// ``` + /// + /// See `_DependencyPropertyWrapper/init(using:)` for a continued example regarding the usage of the implemented result builder. + public init(for type: Dependency.Type = Dependency.self, singleEntry: @escaping @autoclosure (() -> Dependency)) { + self.init(singleEntry: singleEntry) + } + func dependencyRelation(to module: DependencyReference) -> DependencyRelation { let relations = entries.map { $0.dependencyRelation(to: module) } @@ -75,6 +93,12 @@ public struct DependencyCollection: DependencyDeclaration { } } + func inject(spezi: Spezi) { + for entry in entries { + entry.inject(spezi: spezi) + } + } + func uninjectDependencies(notifying spezi: Spezi) { for entry in entries { entry.uninjectDependencies(notifying: spezi) diff --git a/Sources/Spezi/Dependencies/Property/DependencyCollectionBuilder.swift b/Sources/Spezi/Dependencies/Property/DependencyCollectionBuilder.swift index f353e50f..55afb6f0 100644 --- a/Sources/Spezi/Dependencies/Property/DependencyCollectionBuilder.swift +++ b/Sources/Spezi/Dependencies/Property/DependencyCollectionBuilder.swift @@ -16,7 +16,7 @@ /// static func buildExpression(_ expression: @escaping @autoclosure () -> M) -> DependencyCollection /// ``` /// -/// See ``DependencyCollection/init(for:singleEntry:)`` for an example conformance implementation of the ``DependencyCollectionBuilder``. +/// See ``DependencyCollection/init(for:singleEntry:)-6nzui`` for an example conformance implementation of the ``DependencyCollectionBuilder``. public protocol DependencyCollectionBuilder {} diff --git a/Sources/Spezi/Dependencies/Property/DependencyContext.swift b/Sources/Spezi/Dependencies/Property/DependencyContext.swift index 11f9fe0e..aabd17e1 100644 --- a/Sources/Spezi/Dependencies/Property/DependencyContext.swift +++ b/Sources/Spezi/Dependencies/Property/DependencyContext.swift @@ -17,40 +17,34 @@ protocol AnyDependencyContext: DependencyDeclaration { class DependencyContext: AnyDependencyContext { - @MainActor - private enum StorageReference: Sendable { - case dependency(Dependency) - case weakDependency(WeaklyStoredModule) - - nonisolated var value: Dependency? { - switch self { - case let .dependency(module): - return module - case let .weakDependency(reference): - return reference.module - } - } - } - let defaultValue: (() -> Dependency)? - private var injectedDependency: StorageReference? + private weak var spezi: Spezi? + private var injectedDependency: DynamicReference? var isOptional: Bool { defaultValue == nil } - var injectedDependencies: [any Module] { + private var dependency: Dependency? { guard let injectedDependency else { - return [] + return nil + } + + if let module = injectedDependency.element { + return module } - guard let module = injectedDependency.value else { - self.injectedDependency = nil // clear the left over storage - return [] + // Otherwise, we have a weakly injected module that was de-initialized. + // See, if there are multiple modules of the same type and inject the "next" one. + if let replacement = spezi?.retrieveDependencyReplacement(for: Dependency.self) { + self.injectedDependency = .weakElement(replacement) // update injected dependency + return replacement } - return [module] + // clear the left over storage + self.injectedDependency = nil + return nil } init(for type: Dependency.Type = Dependency.self, defaultValue: (() -> Dependency)? = nil) { @@ -80,14 +74,18 @@ class DependencyContext: AnyDependencyContext { } if isOptional { - injectedDependency = .weakDependency(WeaklyStoredModule(dependency)) + injectedDependency = .weakElement(dependency) } else { - injectedDependency = .dependency(dependency) + injectedDependency = .element(dependency) } } + func inject(spezi: Spezi) { + self.spezi = spezi + } + func uninjectDependencies(notifying spezi: Spezi) { - let dependency = injectedDependency?.value + let dependency = injectedDependency?.element injectedDependency = nil if let dependency { @@ -101,7 +99,7 @@ class DependencyContext: AnyDependencyContext { if let injectedDependency { Task { @MainActor in - guard let dependency = injectedDependency.value else { + guard let dependency = injectedDependency.element else { return } spezi.handleDependencyUninjection(of: dependency) @@ -109,8 +107,8 @@ class DependencyContext: AnyDependencyContext { } } - func retrieve(dependency: M.Type) -> M { - guard let injectedDependency else { + func retrieve(dependency dependencyType: M.Type) -> M { + guard let dependency else { preconditionFailure( """ A `@Dependency` was accessed before the dependency was activated. \ @@ -118,14 +116,14 @@ class DependencyContext: AnyDependencyContext { """ ) } - guard let dependency = injectedDependency.value as? M else { + guard let dependencyM = dependency as? M else { preconditionFailure("A injected dependency of type \(type(of: injectedDependency)) didn't match the expected type \(M.self)!") } - return dependency + return dependencyM } func retrieveOptional(dependency: M.Type) -> M? { - guard let dependency = injectedDependency?.value as? M? else { + guard let dependency = self.dependency as? M? else { preconditionFailure("A injected dependency of type \(type(of: injectedDependency)) didn't match the expected type \(M?.self)!") } return dependency diff --git a/Sources/Spezi/Dependencies/Property/DependencyDeclaration.swift b/Sources/Spezi/Dependencies/Property/DependencyDeclaration.swift index 131d824d..11c6c461 100644 --- a/Sources/Spezi/Dependencies/Property/DependencyDeclaration.swift +++ b/Sources/Spezi/Dependencies/Property/DependencyDeclaration.swift @@ -21,9 +21,6 @@ enum DependencyRelation: Hashable { /// /// This protocol allows to communicate dependency requirements of a ``Module`` to the ``DependencyManager``. protocol DependencyDeclaration { - /// List of injected dependencies. - var injectedDependencies: [any Module] { get } - /// Request from the ``DependencyManager`` to collect all dependencies. Mark required by calling `DependencyManager/require(_:defaultValue:)`. @MainActor func collect(into dependencyManager: DependencyManager) @@ -31,6 +28,9 @@ protocol DependencyDeclaration { @MainActor func inject(from dependencyManager: DependencyManager) + @MainActor + func inject(spezi: Spezi) + /// Remove all dependency injections. @MainActor func uninjectDependencies(notifying spezi: Spezi) diff --git a/Sources/Spezi/Dependencies/Property/DependencyPropertyWrapper.swift b/Sources/Spezi/Dependencies/Property/DependencyPropertyWrapper.swift index c81ae2fe..c68afb08 100644 --- a/Sources/Spezi/Dependencies/Property/DependencyPropertyWrapper.swift +++ b/Sources/Spezi/Dependencies/Property/DependencyPropertyWrapper.swift @@ -70,6 +70,7 @@ public class _DependencyPropertyWrapper { // swiftlint:disable:this type_ extension _DependencyPropertyWrapper: SpeziPropertyWrapper { func inject(spezi: Spezi) { self.spezi = spezi + dependencies.inject(spezi: spezi) } func clear() { @@ -82,11 +83,6 @@ extension _DependencyPropertyWrapper: SpeziPropertyWrapper { extension _DependencyPropertyWrapper: DependencyDeclaration { - var injectedDependencies: [any Module] { - dependencies.injectedDependencies - } - - func dependencyRelation(to module: DependencyReference) -> DependencyRelation { dependencies.dependencyRelation(to: module) } diff --git a/Sources/Spezi/Module/Module.swift b/Sources/Spezi/Module/Module.swift index 043f5dc7..2dfef5c1 100644 --- a/Sources/Spezi/Module/Module.swift +++ b/Sources/Spezi/Module/Module.swift @@ -6,12 +6,10 @@ // SPDX-License-Identifier: MIT // -import SpeziFoundation - // note: detailed documentation is provided as an article extension in the DocC bundle /// A `Module` defines a software subsystem that can be configured as part of the ``SpeziAppDelegate/configuration``. -public protocol Module: AnyObject, KnowledgeSource { +public protocol Module: AnyObject { /// Called on the initialization of the Spezi instance to perform a lightweight configuration of the module. /// /// It is advised that longer setup tasks are done in an asynchronous task and started during the call of the configure method. diff --git a/Sources/Spezi/Spezi.docc/Module/Module.md b/Sources/Spezi/Spezi.docc/Module/Module.md index 950dcd0c..687332c6 100644 --- a/Sources/Spezi/Spezi.docc/Module/Module.md +++ b/Sources/Spezi/Spezi.docc/Module/Module.md @@ -10,10 +10,6 @@ SPDX-License-Identifier: MIT --> -@Metadata { - @DocumentationExtension(mergeBehavior: append) -} - ## Overview A ``Module``'s initializer can be used to configure its behavior as a subsystem in Spezi-based software. diff --git a/Sources/Spezi/Spezi.docc/Standard.md b/Sources/Spezi/Spezi.docc/Standard.md index 99b637dc..4ee123a6 100644 --- a/Sources/Spezi/Spezi.docc/Standard.md +++ b/Sources/Spezi/Spezi.docc/Standard.md @@ -10,10 +10,6 @@ SPDX-License-Identifier: MIT --> -@Metadata { - @DocumentationExtension(mergeBehavior: append) -} - Modules can use the constraint mechanism to enforce a set of requirements to the ``Standard`` used in the Spezi-based software where the module is used. This mechanism follows a two-step process detailed in the module documentation: ``Module``. diff --git a/Sources/Spezi/Spezi/KnowledgeSources/ImplicitlyCreatedModulesKey.swift b/Sources/Spezi/Spezi/KnowledgeSources/ImplicitlyCreatedModulesKey.swift new file mode 100644 index 00000000..34784563 --- /dev/null +++ b/Sources/Spezi/Spezi/KnowledgeSources/ImplicitlyCreatedModulesKey.swift @@ -0,0 +1,17 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import SpeziFoundation + + +struct ImplicitlyCreatedModulesKey: DefaultProvidingKnowledgeSource { + typealias Value = Set + typealias Anchor = SpeziAnchor + + static let defaultValue: Value = [] +} diff --git a/Sources/Spezi/Spezi/SpeziAnchor.swift b/Sources/Spezi/Spezi/KnowledgeSources/SpeziAnchor.swift similarity index 100% rename from Sources/Spezi/Spezi/SpeziAnchor.swift rename to Sources/Spezi/Spezi/KnowledgeSources/SpeziAnchor.swift diff --git a/Sources/Spezi/Spezi/KnowledgeSources/SpeziStorage.swift b/Sources/Spezi/Spezi/KnowledgeSources/SpeziStorage.swift new file mode 100644 index 00000000..a905a6c8 --- /dev/null +++ b/Sources/Spezi/Spezi/KnowledgeSources/SpeziStorage.swift @@ -0,0 +1,16 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import SpeziFoundation + + +/// A ``SharedRepository`` implementation that is anchored to ``SpeziAnchor``. +/// +/// This represents the central ``Spezi/Spezi`` storage module. +@_documentation(visibility: internal) +public typealias SpeziStorage = ValueRepository diff --git a/Sources/Spezi/Spezi/KnowledgeSources/StoredModulesKey.swift b/Sources/Spezi/Spezi/KnowledgeSources/StoredModulesKey.swift new file mode 100644 index 00000000..8f175617 --- /dev/null +++ b/Sources/Spezi/Spezi/KnowledgeSources/StoredModulesKey.swift @@ -0,0 +1,83 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import OrderedCollections +import SpeziFoundation + + +protocol AnyStoredModules { + var anyModules: [any Module] { get } + + func removeNilReferences>(in storage: inout Repository) +} + + +struct StoredModulesKey: KnowledgeSource { + typealias Anchor = SpeziAnchor + typealias Value = Self + + var modules: OrderedDictionary> + + var isEmpty: Bool { + modules.isEmpty + } + + init(_ module: DynamicReference, forKey key: ModuleReference) { + modules = [key: module] + } + + func contains(_ key: ModuleReference) -> Bool { + modules[key] != nil + } + + func retrieveFirstAvailable() -> M? { + for (_, value) in modules { + guard let element = value.element else { + continue + } + return element + } + return nil + } + + @discardableResult + mutating func updateValue(_ module: DynamicReference, forKey key: ModuleReference) -> DynamicReference? { + modules.updateValue(module, forKey: key) + } + + @discardableResult + mutating func removeValue(forKey key: ModuleReference) -> DynamicReference? { + modules.removeValue(forKey: key) + } +} + + +extension StoredModulesKey: AnyStoredModules { + var anyModules: [any Module] { + modules.reduce(into: []) { partialResult, entry in + guard let element = entry.value.element else { + return + } + partialResult.append(element) + } + } + + func removeNilReferences>(in storage: inout Repository) { + guard modules.contains(where: { $0.value.element == nil }) else { + return // no weak references + } + + var value = self + + value.modules.removeAll { _, value in + value.element == nil + } + + storage[Self.self] = value + } +} diff --git a/Sources/Spezi/Spezi/Spezi.swift b/Sources/Spezi/Spezi/Spezi.swift index 05ec20f3..06905844 100644 --- a/Sources/Spezi/Spezi/Spezi.swift +++ b/Sources/Spezi/Spezi/Spezi.swift @@ -14,42 +14,6 @@ import SwiftUI import XCTRuntimeAssertions -/// A ``SharedRepository`` implementation that is anchored to ``SpeziAnchor``. -/// -/// This represents the central ``Spezi/Spezi`` storage module. -@_documentation(visibility: internal) -public typealias SpeziStorage = ValueRepository - - -private struct ImplicitlyCreatedModulesKey: DefaultProvidingKnowledgeSource { - typealias Value = Set - typealias Anchor = SpeziAnchor - - static let defaultValue: Value = [] -} - - -private protocol AnyWeaklyStoredModule { - var anyModule: (any Module)? { get } - - @discardableResult - func retrievePurgingIfNil>(in storage: inout Repository) -> (any Module)? -} - - -struct WeaklyStoredModule: KnowledgeSource { - typealias Anchor = SpeziAnchor - typealias Value = Self - - weak var module: M? - - - init(_ module: M) { - self.module = module - } -} - - /// Open-source framework for rapid development of modern, interoperable digital health applications. /// /// Set up the Spezi framework in your `App` instance of your SwiftUI application using the ``SpeziAppDelegate`` and the `@ApplicationDelegateAdaptor` property wrapper. @@ -154,20 +118,28 @@ public final class Spezi: Sendable { ) @_spi(Spezi) public var lifecycleHandler: [LifecycleHandler] { - storage.collect(allOf: LifecycleHandler.self) + modules.compactMap { module in + module as? LifecycleHandler + } } var notificationTokenHandler: [NotificationTokenHandler] { - storage.collect(allOf: NotificationTokenHandler.self) + modules.compactMap { module in + module as? NotificationTokenHandler + } } var notificationHandler: [NotificationHandler] { - storage.collect(allOf: NotificationHandler.self) + modules.compactMap { module in + module as? NotificationHandler + } } var modules: [any Module] { - storage.collect(allOf: (any Module).self) - + storage.collect(allOf: (any AnyWeaklyStoredModule).self).compactMap { $0.retrievePurgingIfNil(in: &storage) } + storage.collect(allOf: (any AnyStoredModules).self) + .reduce(into: []) { partialResult, modules in + partialResult.append(contentsOf: modules.anyModules) + } } @MainActor private var implicitlyCreatedModules: Set { @@ -422,16 +394,27 @@ public final class Spezi: Sendable { } } + func retrieveDependencyReplacement(for type: M.Type) -> M? { + guard let storedModules = storage[StoredModulesKey.self] else { + return nil + } + + let replacement = storedModules.retrieveFirstAvailable() + storedModules.removeNilReferences(in: &storage) // if we ask for a replacement, there is opportunity to clean up weak reference objects + return replacement + } + /// Iterates through weakly referenced modules and purges any nil references. /// /// These references are purged lazily. This is generally no problem because the overall overhead will be linear. /// If you load `n` modules with self-managed storage policy and then all `n` modules will eventually be deallocated, there might be `n` weak references still stored /// till the next module interaction is performed (e.g., new module is loaded or unloaded). private func purgeWeaklyReferenced() { - let elements = storage.collect(allOf: (any AnyWeaklyStoredModule).self) - for reference in elements { - reference.retrievePurgingIfNil(in: &storage) - } + storage + .collect(allOf: (any AnyStoredModules).self) + .forEach { storedModules in + storedModules.removeNilReferences(in: &storage) + } } } @@ -439,48 +422,43 @@ public final class Spezi: Sendable { extension Module { @MainActor fileprivate func storeModule(into spezi: Spezi) { - guard let value = self as? Value else { - spezi.logger.warning("Could not store \(Self.self) in the SpeziStorage as the `Value` typealias was modified.") - return - } - spezi.storage[Self.self] = value + storeDynamicReference(.element(self), into: spezi) } @MainActor fileprivate func storeWeakly(into spezi: Spezi) { - guard self is Value else { - spezi.logger.warning("Could not store \(Self.self) in the SpeziStorage as the `Value` typealias was modified.") - return - } + storeDynamicReference(.weakElement(self), into: spezi) + } - spezi.storage[WeaklyStoredModule.self] = WeaklyStoredModule(self) + @MainActor + fileprivate func storeDynamicReference(_ module: DynamicReference, into spezi: Spezi) { + if spezi.storage.contains(StoredModulesKey.self) { + // swiftlint:disable:next force_unwrapping + spezi.storage[StoredModulesKey.self]!.updateValue(module, forKey: self.reference) + } else { + spezi.storage[StoredModulesKey.self] = StoredModulesKey(module, forKey: reference) + } } @MainActor fileprivate func isLoaded(in spezi: Spezi) -> Bool { - spezi.storage[Self.self] != nil - || spezi.storage[WeaklyStoredModule.self]?.retrievePurgingIfNil(in: &spezi.storage) != nil + guard let storedModules = spezi.storage[StoredModulesKey.self] else { + return false + } + return storedModules.contains(reference) } @MainActor fileprivate func clearModule(from spezi: Spezi) { - spezi.storage[Self.self] = nil - spezi.storage[WeaklyStoredModule.self] = nil - } -} - - -extension WeaklyStoredModule: AnyWeaklyStoredModule { - var anyModule: (any Module)? { - module - } - + guard var storedModules = spezi.storage[StoredModulesKey.self] else { + return + } + storedModules.removeValue(forKey: reference) - func retrievePurgingIfNil>(in storage: inout Repository) -> (any Module)? { - guard let module else { - storage[Self.self] = nil - return nil + if storedModules.isEmpty { + spezi.storage[StoredModulesKey.self] = nil + } else { + spezi.storage[StoredModulesKey.self] = storedModules } - return module } } diff --git a/Sources/Spezi/Utilities/DynamicReference.swift b/Sources/Spezi/Utilities/DynamicReference.swift new file mode 100644 index 00000000..3e6e6646 --- /dev/null +++ b/Sources/Spezi/Utilities/DynamicReference.swift @@ -0,0 +1,40 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + + +@MainActor +enum DynamicReference: Sendable { + case element(Element) + case weakElement(WeaklyStoredElement) + + + nonisolated var element: Element? { + switch self { + case let .element(element): + return element + case let .weakElement(reference): + return reference.element + } + } + + + static nonisolated func weakElement(_ element: Element) -> DynamicReference { + .weakElement(WeaklyStoredElement(element)) + } +} + + +extension DynamicReference { + struct WeaklyStoredElement { + private(set) nonisolated(unsafe) weak var element: Element? + + init(_ element: Element? = nil) { + self.element = element + } + } +} diff --git a/Tests/SpeziTests/DependenciesTests/DependencyTests.swift b/Tests/SpeziTests/DependenciesTests/DependencyTests.swift index 4e575488..41dc794a 100644 --- a/Tests/SpeziTests/DependenciesTests/DependencyTests.swift +++ b/Tests/SpeziTests/DependenciesTests/DependencyTests.swift @@ -126,6 +126,10 @@ private final class TestModule8: Module { init() {} } +private final class SimpleOptionalModuleDependency: Module { + @Dependency var testModule6: TestModule6? +} + final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_length @MainActor @@ -495,4 +499,69 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le let dut4Module = try XCTUnwrap(dut4.testModule3) XCTAssertEqual(dut4Module.state, 4) } + + @MainActor + func testMultipleDependenciesOfSameType() throws { + let first = TestModule5() + let second = TestModule5() + + let spezi = Spezi(standard: DefaultStandard(), modules: [first, second, TestModule4()]) + + let modules = spezi.modules + func getModule(_ module: M.Type = M.self) throws -> M { + try XCTUnwrap(modules.first(where: { $0 is M }) as? M) + } + + XCTAssertEqual(modules.count, 4) // 3 modules + standard + _ = try getModule(DefaultStandard.self) + + let testModule4 = try getModule(TestModule4.self) + + XCTAssertTrue(testModule4.testModule5 === first) + } + + @MainActor + func testUnloadingWeakDependencyOfSameType() async throws { + let spezi = Spezi(standard: DefaultStandard(), modules: [SimpleOptionalModuleDependency()]) + + let modules = spezi.modules + func getModule(_ module: M.Type = M.self) throws -> M { + try XCTUnwrap(modules.first(where: { $0 is M }) as? M) + } + + XCTAssertEqual(modules.count, 2) + _ = try getModule(DefaultStandard.self) + let module = try getModule(SimpleOptionalModuleDependency.self) + + XCTAssertNil(module.testModule6) + + let dynamicModule6 = TestModule6() + let baseModule6 = TestModule6() + + let scope = { + let weakModule6 = TestModule6() + + spezi.loadModule(weakModule6, ownership: .external) + spezi.loadModule(dynamicModule6) + spezi.loadModule(baseModule6) + + // should contain the first loaded dependency + XCTAssertNotNil(module.testModule6) + XCTAssertTrue(module.testModule6 === weakModule6) + } + + scope() + + // after externally managed dependency goes out of scope it should automatically switch to next dependency + XCTAssertNotNil(module.testModule6) + XCTAssertTrue(module.testModule6 === dynamicModule6) + + spezi.unloadModule(dynamicModule6) + + // after manual unload it should take the next available + XCTAssertNotNil(module.testModule6) + XCTAssertTrue(module.testModule6 === baseModule6) + } } + +// swiftlint:disable:this file_length