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

Introduce Weak Singleton. Provide dependencies based on arguments #451

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 18 additions & 0 deletions NeedleFoundation.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
/* End PBXAggregateTarget section */

/* Begin PBXBuildFile section */
3D35CB7C298A7FE800A374EC /* Nothing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D35CB75298A7FE800A374EC /* Nothing.swift */; };
3D35CB80298A7FE800A374EC /* AssemblyStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D35CB7B298A7FE800A374EC /* AssemblyStorage.swift */; };
OBJ_42 /* Bootstrap.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_9 /* Bootstrap.swift */; };
OBJ_43 /* Component.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_10 /* Component.swift */; };
OBJ_44 /* DependencyProviderRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_12 /* DependencyProviderRegistry.swift */; };
Expand Down Expand Up @@ -78,6 +80,8 @@
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
3D35CB75298A7FE800A374EC /* Nothing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Nothing.swift; sourceTree = "<group>"; };
3D35CB7B298A7FE800A374EC /* AssemblyStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssemblyStorage.swift; sourceTree = "<group>"; };
"NeedleFoundation::NeedleFoundation::Product" /* NeedleFoundation.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = NeedleFoundation.framework; sourceTree = BUILT_PRODUCTS_DIR; };
"NeedleFoundation::NeedleFoundationTest::Product" /* NeedleFoundationTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = NeedleFoundationTest.framework; sourceTree = BUILT_PRODUCTS_DIR; };
"NeedleFoundation::NeedleFoundationTestTests::Product" /* NeedleFoundationTestTests.xctest */ = {isa = PBXFileReference; lastKnownFileType = file; path = NeedleFoundationTestTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -134,6 +138,15 @@
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
7D1A478F298E7859004832D4 /* Misc */ = {
isa = PBXGroup;
children = (
3D35CB75298A7FE800A374EC /* Nothing.swift */,
3D35CB7B298A7FE800A374EC /* AssemblyStorage.swift */,
);
path = Misc;
sourceTree = "<group>";
};
OBJ_11 /* Internal */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -243,6 +256,7 @@
OBJ_8 /* NeedleFoundation */ = {
isa = PBXGroup;
children = (
7D1A478F298E7859004832D4 /* Misc */,
OBJ_9 /* Bootstrap.swift */,
OBJ_10 /* Component.swift */,
OBJ_11 /* Internal */,
Expand Down Expand Up @@ -378,7 +392,9 @@
OBJ_45 /* PluginExtensionProviderRegistry.swift in Sources */,
OBJ_46 /* NonCoreComponent.swift in Sources */,
OBJ_47 /* PluginizedComponent.swift in Sources */,
3D35CB80298A7FE800A374EC /* AssemblyStorage.swift in Sources */,
OBJ_48 /* PluginizedScopeLifecycle.swift in Sources */,
3D35CB7C298A7FE800A374EC /* Nothing.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -489,6 +505,7 @@
HEADER_SEARCH_PATHS = "$(inherited)";
INFOPLIST_FILE = NeedleFoundation.xcodeproj/NeedleFoundation_Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) $(TOOLCHAIN_DIR)/usr/lib/swift/macosx";
MACOSX_DEPLOYMENT_TARGET = 10.13;
OTHER_CFLAGS = "$(inherited)";
OTHER_LDFLAGS = "$(inherited)";
OTHER_SWIFT_FLAGS = "$(inherited)";
Expand Down Expand Up @@ -538,6 +555,7 @@
HEADER_SEARCH_PATHS = "$(inherited)";
INFOPLIST_FILE = NeedleFoundation.xcodeproj/NeedleFoundation_Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) $(TOOLCHAIN_DIR)/usr/lib/swift/macosx";
MACOSX_DEPLOYMENT_TARGET = 10.13;
OTHER_CFLAGS = "$(inherited)";
OTHER_LDFLAGS = "$(inherited)";
OTHER_SWIFT_FLAGS = "$(inherited)";
Expand Down
131 changes: 100 additions & 31 deletions Sources/NeedleFoundation/Component.swift
Original file line number Diff line number Diff line change
Expand Up @@ -130,27 +130,61 @@ open class Component<DependencyType>: Scope {
///
/// - parameter factory: The closure to construct the dependency object.
/// - returns: The dependency object instance.
public final func shared<T>(__function: String = #function, _ factory: () -> T) -> T {
// Use function name as the key, since this is unique per component
// class. At the same time, this is also 150 times faster than
// interpolating the type to convert to string, `"\(T.self)"`.
public final func shared<T>(__function: StaticString = #function, _ factory: () -> T) -> T {
return shared(__function: __function, args: Nothing(), factory)
}

/// Share the enclosed object as a singleton at this scope based on the `args` parameter. This allows
/// this scope as well as all child scopes to share a single instance of
/// the object, for as long as this component lives.
///
/// - note: Shared dependency's constructor should avoid switching threads as it may cause a deadlock.
///
/// - parameter args: A value that conforms to the `Hashable` protocol to differentiate between singleton instances.
/// - parameter factory: The closure to construct the dependency object.
/// - returns: The dependency object instance.
public final func shared<Args: Hashable, T>(__function: StaticString = #function, args: Args, _ factory: () -> T) -> T {
sharedInstanceLock.lock()
defer {
sharedInstanceLock.unlock()
}

// Additional nil coalescing is needed to mitigate a Swift bug appearing
// in Xcode 10. see https://bugs.swift.org/browse/SR-8704. Without this
// measure, calling `shared` from a function that returns an optional type
// will always pass the check below and return nil if the instance is not
// initialized.
if let instance = (sharedInstances[__function] as? T?) ?? nil {
return instance
return storage.shared(function: __function, args: args, factory: factory)
}

/// Share the enclosed object as a weak singleton at this scope. This allows
/// this scope as well as all child scopes to share a single instance of
/// the object, for as long as this component lives.
///
/// - note: Shared dependency's constructor should avoid switching threads as it may cause a deadlock.
///
/// - note: This function is only applicable for reference types (classes).
///
/// - parameter args: A value that conforms to the `Hashable` protocol to differentiate between singleton instances.
/// - parameter factory: The closure to construct the dependency object.
/// - returns: The dependency object instance.
public final func weakShared<T: AnyObject>(__function: StaticString = #function, _ factory: () -> T) -> T {
return weakShared(__function: __function, args: Nothing(), factory)
}

/// Share the enclosed object as a weak singleton at this scope based on the `args` parameter. This allows
/// this scope as well as all child scopes to share a single instance of
/// the object, for as long as this component lives.
///
/// - note: Shared dependency's constructor should avoid switching threads as it may cause a deadlock.
///
/// - note: This function is only applicable for reference types (classes).
///
/// - parameter args: A value that conforms to the `Hashable` protocol to differentiate between singleton instances.
/// - parameter factory: The closure to construct the dependency object.
/// - returns: The dependency object instance.
public final func weakShared<Args: Hashable, T: AnyObject>(__function: StaticString = #function, args: Args, _ factory: () -> T) -> T {
sharedInstanceLock.lock()
defer {
sharedInstanceLock.unlock()
}
let instance = factory()
sharedInstances[__function] = instance

return instance
return storage.weakShared(function: __function, args: args, factory: factory)
}

public func find<T>(property: String, skipThisLevel: Bool) -> T {
Expand All @@ -169,11 +203,12 @@ open class Component<DependencyType>: Scope {

public var localTable = [String:()->Any]()
public var keyPathToName = [PartialKeyPath<DependencyType>:String]()

// MARK: - Private

private let sharedInstanceLock = NSRecursiveLock()
private var sharedInstances = [String: Any]()

private let storage = AssemblyStorage()
private lazy var name: String = {
let fullyQualifiedSelfName = String(describing: self)
let parts = fullyQualifiedSelfName.components(separatedBy: ".")
Expand Down Expand Up @@ -239,27 +274,61 @@ open class Component<DependencyType>: Scope {
///
/// - parameter factory: The closure to construct the dependency object.
/// - returns: The dependency object instance.
public final func shared<T>(__function: String = #function, _ factory: () -> T) -> T {
// Use function name as the key, since this is unique per component
// class. At the same time, this is also 150 times faster than
// interpolating the type to convert to string, `"\(T.self)"`.
public final func shared<T>(__function: StaticString = #function, _ factory: () -> T) -> T {
return shared(__function: __function, args: Nothing(), factory)
}

/// Share the enclosed object as a singleton at this scope based on the `args` parameter. This allows
/// this scope as well as all child scopes to share a single instance of
/// the object, for as long as this component lives.
///
/// - note: Shared dependency's constructor should avoid switching threads as it may cause a deadlock.
///
/// - parameter args: A value that conforms to the `Hashable` protocol to differentiate between singleton instances.
/// - parameter factory: The closure to construct the dependency object.
/// - returns: The dependency object instance.
public final func shared<Args: Hashable, T>(__function: StaticString = #function, args: Args, _ factory: () -> T) -> T {
sharedInstanceLock.lock()
defer {
sharedInstanceLock.unlock()
}

// Additional nil coalescing is needed to mitigate a Swift bug appearing
// in Xcode 10. see https://bugs.swift.org/browse/SR-8704. Without this
// measure, calling `shared` from a function that returns an optional type
// will always pass the check below and return nil if the instance is not
// initialized.
if let instance = (sharedInstances[__function] as? T?) ?? nil {
return instance
return storage.shared(function: __function, args: args, factory: factory)
}

/// Share the enclosed object as a weak singleton at this scope. This allows
/// this scope as well as all child scopes to share a single instance of
/// the object, for as long as this component lives.
///
/// - note: Shared dependency's constructor should avoid switching threads as it may cause a deadlock.
///
/// - note: This function is only applicable for reference types (classes).
///
/// - parameter args: A value that conforms to the `Hashable` protocol to differentiate between singleton instances.
/// - parameter factory: The closure to construct the dependency object.
/// - returns: The dependency object instance.
public final func weakShared<T: AnyObject>(__function: StaticString = #function, _ factory: () -> T) -> T {
return weakShared(__function: __function, args: Nothing(), factory)
}

/// Share the enclosed object as a weak singleton at this scope based on the `args` parameter. This allows
/// this scope as well as all child scopes to share a single instance of
/// the object, for as long as this component lives.
///
/// - note: Shared dependency's constructor should avoid switching threads as it may cause a deadlock.
///
/// - note: This function is only applicable for reference types (classes).
///
/// - parameter args: A value that conforms to the `Hashable` protocol to differentiate between singleton instances.
/// - parameter factory: The closure to construct the dependency object.
/// - returns: The dependency object instance.
public final func weakShared<Args: Hashable, T: AnyObject>(__function: StaticString = #function, args: Args, _ factory: () -> T) -> T {
sharedInstanceLock.lock()
defer {
sharedInstanceLock.unlock()
}
let instance = factory()
sharedInstances[__function] = instance

return instance
return storage.weakShared(function: __function, args: args, factory: factory)
}

public subscript<T>(dynamicMember keyPath: KeyPath<DependencyType, T>) -> T {
Expand All @@ -269,7 +338,7 @@ open class Component<DependencyType>: Scope {
// MARK: - Private

private let sharedInstanceLock = NSRecursiveLock()
private var sharedInstances = [String: Any]()
private let storage = AssemblyStorage()
private lazy var name: String = {
let fullyQualifiedSelfName = String(describing: self)
let parts = fullyQualifiedSelfName.components(separatedBy: ".")
Expand Down
89 changes: 89 additions & 0 deletions Sources/NeedleFoundation/Misc/AssemblyStorage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
//
// AssemblyStorage.swift
// NeedleFoundation
//
// Created by Mikhail Maslo on 04.02.23.
//

final class AssemblyStorage {
final class WeakBox {
weak var object: AnyObject?

init<Object: AnyObject>(object: Object) {
self.object = object
}
}

private lazy var strongObjects: [Key: Any] = [:]
private lazy var weakObjects: [Key: WeakBox] = [:]

init() {}

func shared<Args: Hashable, Object>(function: StaticString, args: Args, factory: () -> Object) -> Object {
let key = Key(function: function, args: args)
if let object = strongObjects[key] {
return object as! Object
}

let object = factory()
strongObjects[key] = object
return object
}

func weakShared<Args: Hashable, Object: AnyObject>(function: StaticString, args: Args, factory: () -> Object) -> Object {
let key = Key(function: function, args: args)
if let object = weakObjects[key]?.object {
return object as! Object
}

let object = factory()
weakObjects[key] = WeakBox(object: object)
return object
}
}

final class Key: Hashable {
// MARK: - Private properties

private let function: StaticString
private let args: AnyHashable

// MARK: - Init

init<Args>(function: StaticString, args: Args) where Args: Hashable {
self.function = function
self.args = args
}

// MARK: - Hashable

static func == (lhs: Key, rhs: Key) -> Bool {
isSameFunction(function1: lhs.function, function2: rhs.function) && lhs.args == rhs.args
}

func hash(into hasher: inout Hasher) {
var hasher = Hasher()

if function.hasPointerRepresentation {
hasher.combine(function.utf8Start)
} else {
hasher.combine(function.unicodeScalar)
}

hasher.combine(args)
}

// MARK: - Private methods

private static func isSameFunction(function1: StaticString, function2: StaticString) -> Bool {
guard function1.hasPointerRepresentation == function2.hasPointerRepresentation else {
return false
}

if function1.hasPointerRepresentation {
return function1.utf8Start == function2.utf8Start
} else {
return function1.unicodeScalar == function2.unicodeScalar
}
}
}
22 changes: 22 additions & 0 deletions Sources/NeedleFoundation/Misc/Nothing.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// AssemblyStorage.swift
// NeedleFoundation
//
// Created by Mikhail Maslo on 04.02.23.
//

struct Nothing {
public init() {}
}

extension Nothing: Equatable {
public static func == (lhs: Nothing, rhs: Nothing) -> Bool {
true
}
}

extension Nothing: Hashable {
public func hash(into hasher: inout Hasher) {
// Do nothing
}
}
Loading