Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d6d41df
feat: async shared instances
DanielCech Jan 15, 2026
4b0a8cc
Merge branch 'feat/swift-testing' into feat/async-shared-instances
DanielCech Jan 15, 2026
af55e8e
feat: another async test
DanielCech Jan 15, 2026
114321a
Merge branch 'feat/swift-testing' into feat/async-shared-instances
DanielCech Jan 19, 2026
5f54fdf
feat: changelog
DanielCech Jan 19, 2026
e413f54
Merge branch 'feat/swift-testing' into feat/async-shared-instances
DanielCech Jan 19, 2026
5713fdf
feat: changelog
DanielCech Jan 19, 2026
ac70699
Merge branch 'feat/swift-testing' into feat/async-shared-instances
DanielCech Jan 20, 2026
20ea8c9
Merge branch 'feat/swift-testing' into feat/async-shared-instances
DanielCech Feb 9, 2026
cccd226
feat: update
DanielCech Feb 9, 2026
8f6090d
Merge branch 'feat/swift-testing' into feat/async-shared-instances
DanielCech Feb 9, 2026
6b09e01
feat: arguments
DanielCech Feb 9, 2026
8e38d21
feat: registration changes
DanielCech Feb 9, 2026
0f858eb
feat: explicit scope
DanielCech Feb 12, 2026
3d7adac
Merge branch 'feat/swift-testing' into feat/async-shared-instances
DanielCech Feb 20, 2026
42c4875
Merge branch 'feat/swift-testing' into feat/async-shared-instances
DanielCech Feb 24, 2026
ec701be
Merge branch 'feat/swift-testing' into feat/async-shared-instances
DanielCech Mar 2, 2026
755e4e3
remove: scripts
DanielCech Mar 5, 2026
7782152
Merge branch 'feat/swift-testing' into feat/async-shared-instances
DanielCech Mar 5, 2026
037890f
feat: update
DanielCech Mar 6, 2026
393fa5a
Merge branch 'master' into feat/async-shared-instances
DanielCech Mar 6, 2026
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ __Sections__
- Support for multiple arguments. Code clean up.
- Using parameter packs for multiple arguments
- Tests are converted to the new Swift Testing framework
- Fix of async resolve of shared dependencies

## [1.0.4]

Expand Down
828 changes: 828 additions & 0 deletions DependencyInjection.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 24 additions & 0 deletions Derived/InfoPlists/DependencyInjection-Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright ©. All rights reserved.</string>
</dict>
</plist>
45 changes: 37 additions & 8 deletions Sources/Container/Async/AsyncContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public actor AsyncContainer: AsyncDependencyResolving, AsyncDependencyRegisterin

private var registrations = [RegistrationIdentifier: AsyncRegistration]()
private var sharedInstances = [RegistrationIdentifier: any Sendable]()
private var sharedTasks = [RegistrationIdentifier: Task<any Sendable, Error>]()

/// Create new instance of ``AsyncContainer``
public init() {}
Expand All @@ -28,6 +29,7 @@ public actor AsyncContainer: AsyncDependencyResolving, AsyncDependencyRegisterin
/// Remove already instantiated shared instances from the container
public func releaseSharedInstances() {
sharedInstances.removeAll()
sharedTasks.removeAll()
}

// MARK: Register dependency
Expand All @@ -50,6 +52,7 @@ public actor AsyncContainer: AsyncDependencyResolving, AsyncDependencyRegisterin
// With a new registration we should clean all shared instances
// because the new registered factory most likely returns different objects and we have no way to tell
sharedInstances[registration.identifier] = nil
sharedTasks[registration.identifier] = nil
}

// MARK: Register dependency with arguments
Expand Down Expand Up @@ -132,28 +135,54 @@ private extension AsyncContainer {
)
}

func getDependency<Dependency: Sendable>(from registration: AsyncRegistration, with argument: (any Sendable)? = nil) async throws -> Dependency {
func getDependency<Dependency: Sendable>(from registration: AsyncRegistration, with argument: (any Sendable)? = ()) async throws -> Dependency {
switch registration.scope {
case .shared:
if let dependency = sharedInstances[registration.identifier] as? Dependency {
return dependency
}

if let task = sharedTasks[registration.identifier] {
do {
return try await task.value as! Dependency
} catch {
sharedTasks[registration.identifier] = nil
throw error
}
}
case .new:
break
}

// We use force cast here because we are sure that the type-casting always succeed
// The reason why the `factory` closure returns ``Any`` is that we have to erase the generic type in order to store the registration
// When the registration is created it can be initialized just with a `factory` that returns the matching type
let dependency = try await registration.asyncRegistrationFactory(self, argument) as! Dependency
let task = Task<any Sendable, Error> {
try await registration.asyncRegistrationFactory(self, argument)
}

switch registration.scope {
case .shared:
sharedInstances[registration.identifier] = dependency
case .new:
break
if case .shared = registration.scope {
sharedTasks[registration.identifier] = task
}

do {
let dependency = try await task.value as! Dependency

switch registration.scope {
case .shared:
sharedInstances[registration.identifier] = dependency
sharedTasks[registration.identifier] = nil
case .new:
break
}

return dependency
} catch {
if case .shared = registration.scope {
sharedTasks[registration.identifier] = nil
}
throw error
}

return dependency
}
}
2 changes: 1 addition & 1 deletion Sources/Container/Sync/Container.swift
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ private extension Container {
)
}

func getDependency<Dependency>(from registration: Registration, with argument: Any? = nil) throws -> Dependency {
func getDependency<Dependency>(from registration: Registration, with argument: Any? = ()) throws -> Dependency {
switch registration.scope {
case .shared:
if let dependency = sharedInstances[registration.identifier] as? Dependency {
Expand Down
105 changes: 105 additions & 0 deletions Tests/Container/Async/AsyncBaseTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,109 @@ struct AsyncBaseTests {
}
}
}

@Test("Concurrent resolve of shared dependency")
func concurrentResolveSharedDependency() async {
// Given
let subject = AsyncContainer()
await subject.register(in: .shared) { _ -> SimpleDependency in
SimpleDependency()
}

// When
let resolvedDependencies = await withTaskGroup(of: SimpleDependency.self) { group in
for _ in 0..<50 {
group.addTask {
await subject.resolve()
}
}

var dependencies: [SimpleDependency] = []
for await dependency in group {
dependencies.append(dependency)
}

return dependencies
}

// Then
guard let firstDependency = resolvedDependencies.first else {
Issue.record("Expected to resolve at least one dependency")
return
}

#expect(resolvedDependencies.allSatisfy { $0 === firstDependency })
}

@Test("Concurrent register and resolve different types")
func concurrentRegisterAndResolveDifferentTypes() async {
// Given
let subject = AsyncContainer()

// When
let resolvedDependencies = await withTaskGroup(of: Any.self) { group in
group.addTask {
await subject.register(in: .shared) { _ -> SimpleDependency in
SimpleDependency()
}

let dependency: SimpleDependency = await subject.resolve()
return dependency
}

group.addTask {
await subject.register { _, argument -> DependencyWithValueTypeParameter in
DependencyWithValueTypeParameter(subDependency: argument)
}

let argument = StructureDependency(property1: "concurrent")
let dependency: DependencyWithValueTypeParameter = await subject.resolve(arguments: argument)
return dependency
}

group.addTask {
await subject.register { _, argument -> DependencyWithAsyncInitWithParameter in
await DependencyWithAsyncInitWithParameter(subDependency: argument)
}

let argument = StructureDependency(property1: "async")
let dependency: DependencyWithAsyncInitWithParameter = await subject.resolve(arguments: argument)
return dependency
}

var dependencies: [Any] = []
for await dependency in group {
dependencies.append(dependency)
}

return dependencies
}

// Then
#expect(resolvedDependencies.count == 3)

let hasSimpleDependency = resolvedDependencies.contains { $0 is SimpleDependency }
#expect(hasSimpleDependency)

let hasValueTypeParameter = resolvedDependencies.contains { $0 is DependencyWithValueTypeParameter }
#expect(hasValueTypeParameter)

let hasAsyncInitParameter = resolvedDependencies.contains { $0 is DependencyWithAsyncInitWithParameter }
#expect(hasAsyncInitParameter)
}

@Test("Register explicit type with resolver-only factory")
func registerExplicitTypeWithResolverOnlyFactory() async throws {
// Given
let subject = AsyncContainer()
await subject.register(type: SimpleDependency.self) { _ in
SimpleDependency()
}

// When
let resolvedDependency = try await subject.tryResolve(type: SimpleDependency.self)

// Then
#expect(resolvedDependency is SimpleDependency)
}
}
15 changes: 15 additions & 0 deletions Tests/Container/Sync/BaseTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,19 @@ struct BaseTests {
}
}
}

@Test("Register explicit type with resolver-only factory")
func registerExplicitTypeWithResolverOnlyFactory() throws {
// Given
let subject = Container()
subject.register(type: SimpleDependency.self) { _ in
SimpleDependency()
}

// When
let resolvedDependency: SimpleDependency = try subject.tryResolve(type: SimpleDependency.self)

// Then
#expect(resolvedDependency is SimpleDependency)
}
}