Skip to content

Commit

Permalink
Update 2.1.5 with timeToLive
Browse files Browse the repository at this point in the history
  • Loading branch information
hmlongco committed Apr 21, 2023
1 parent 18f02d7 commit 3f4efc8
Show file tree
Hide file tree
Showing 8 changed files with 87 additions and 22 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Factory Changelog

## 2.1.5

* Adds TimeToLive to scoped instances.

## 2.1.4

* Fix singletons in multiple modules keyed with same type and name. Issue - #99
Expand Down
2 changes: 1 addition & 1 deletion Factory.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "Factory"
s.version = "2.1.4"
s.version = "2.1.5"
s.summary = "A Modern Dependency Injection / Service Locator framework for Swift on iOS."
s.homepage = "https://github.com/hmlongco/Factory"
s.license = "MIT"
Expand Down
22 changes: 21 additions & 1 deletion Sources/Factory/Factory.docc/Basics/Scopes.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,9 +205,29 @@ Scope is managed by the container.
See the "Releasing a Container" discussion in <doc:Containers> for more information.

## TimeToLive

Factory provides a "time to live" option for scoped dependencies.

```swift
extension Container {
var authenticatedUser: Factory<AuthenticatedUser> {
self { AuthenticatedUser() }
.scope(.session)
.timeToLive(60 * 20) // (60 seconds * 20) = 20 minutes
}
}
```

As shown above, set a time to live for 20 minutes and any new request for that dependency that occurs *after* that period will discard the previously cached item, caching and returning a new instance instead.

Requesting a cached item before the timeout period ends returns the currently cached item and effectively restarts the clock for that item.

Like registrations, setting a time to live on a dependency only affects the *next* resolution for that item. Anything already resolved and referenced stays resolved and referenced.

## Reset

As shown above, individual scope caches on a container can be reset (cleared) if needed.
As mentioned earlier in the discussion on custom scopes, individual scope caches on a container can be reset (cleared) if needed.
```swift
// clear the default cached scope
Container.shared.manager.reset(scope: .cached)
Expand Down
10 changes: 10 additions & 0 deletions Sources/Factory/Factory/Modifiers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,16 @@ extension FactoryModifying {
registration.scope(.unique)
return self
}

/// Adds time to live option for scopes. If the dependency has been cached for longer than the timeToLive period the
/// cached item will be discarded and a new instance created.
@discardableResult
public func timeToLive(_ seconds: TimeInterval) -> Self {
registration.options { options in
options.ttl = seconds
}
return self
}
}

// FactoryModifying Decorator Functionality
Expand Down
5 changes: 4 additions & 1 deletion Sources/Factory/Factory/Registrations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ public struct FactoryRegistration<P,T> {
let manager = container.manager
let options = manager.options[id]
let scope = options?.scope ?? manager.defaultScope
let ttl = options?.ttl

var factory: (P) -> T = factoryForCurrentContext(using: options)

Expand All @@ -99,7 +100,7 @@ public struct FactoryRegistration<P,T> {
#endif

globalGraphResolutionDepth += 1
let instance = scope?.resolve(using: manager.cache, id: id, factory: { factory(parameters) }) ?? factory(parameters)
let instance = scope?.resolve(using: manager.cache, id: id, ttl: ttl, factory: { factory(parameters) }) ?? factory(parameters)
globalGraphResolutionDepth -= 1

if globalGraphResolutionDepth == 0 {
Expand Down Expand Up @@ -328,6 +329,8 @@ public enum FactoryResetOptions {
internal struct FactoryOptions {
/// Managed scope for this factory instance
var scope: Scope?
/// Time to live option for scopes
var ttl: TimeInterval?
/// Contexts
var argumentContexts: [String:AnyFactory]?
/// Contexts
Expand Down
48 changes: 31 additions & 17 deletions Sources/Factory/Factory/Scopes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,31 +53,39 @@ public class Scope {
fileprivate init() {}

/// Internal function returns cached value if it exists. Otherwise it creates a new instance and caches that value for later reference.
internal func resolve<T>(using cache: Cache, id: String, factory: () -> T) -> T {
if let cached: T = unboxed(box: cache.value(forKey: id)) {
return cached
internal func resolve<T>(using cache: Cache, id: String, ttl: TimeInterval?, factory: () -> T) -> T {
if let box = cache.value(forKey: id), let cached: T = unboxed(box: box) {
if let ttl = ttl {
let now = CFAbsoluteTimeGetCurrent()
if (box.timestamp + ttl) > now {
cache.set(timestamp: now, forKey: id)
return cached
}
} else {
return cached
}
}
let instance = factory()
if let box = box(instance) {
cache.set(value: box, forKey: id)
cache.set(value: box, forKey: id)
}
return instance
}

/// Internal function returns unboxed value if it exists
fileprivate func unboxed<T>(box: AnyBox?) -> T? {
if let box = box as? StrongBox<T> {
return box.boxed
}
return nil
(box as? StrongBox<T>)?.boxed
}

/// Internal function correctly boxes value depending upon scope type
fileprivate func box<T>(_ instance: T) -> AnyBox? {
if let optional = instance as? OptionalProtocol {
return optional.hasWrappedValue ? StrongBox<T>(scopeID: scopeID, boxed: instance) : nil
if optional.hasWrappedValue {
return StrongBox<T>(scopeID: scopeID, timestamp: CFAbsoluteTimeGetCurrent(), boxed: instance)
}
return nil
}
return StrongBox<T>(scopeID: scopeID, boxed: instance)
return StrongBox<T>(scopeID: scopeID, timestamp: CFAbsoluteTimeGetCurrent(), boxed: instance)
}

internal let scopeID: UUID = UUID()
Expand Down Expand Up @@ -106,9 +114,9 @@ extension Scope {
public override init() {
super.init()
}
internal override func resolve<T>(using cache: Cache, id: String, factory: () -> T) -> T {
internal override func resolve<T>(using cache: Cache, id: String, ttl: TimeInterval?, factory: () -> T) -> T {
// ignore container's cache in favor of our own
return super.resolve(using: self.cache, id: id, factory: factory)
return super.resolve(using: self.cache, id: id, ttl: ttl, factory: factory)
}
/// Private shared cache
internal var cache = Cache()
Expand Down Expand Up @@ -138,10 +146,10 @@ extension Scope {
fileprivate override func box<T>(_ instance: T) -> AnyBox? {
if let optional = instance as? OptionalProtocol {
if let unwrapped = optional.wrappedValue, type(of: unwrapped) is AnyObject.Type {
return WeakBox(scopeID: scopeID, boxed: unwrapped as AnyObject)
return WeakBox(scopeID: scopeID, timestamp: CFAbsoluteTimeGetCurrent(), boxed: unwrapped as AnyObject)
}
} else if type(of: instance as Any) is AnyObject.Type {
return WeakBox(scopeID: scopeID, boxed: instance as AnyObject)
return WeakBox(scopeID: scopeID, timestamp: CFAbsoluteTimeGetCurrent(), boxed: instance as AnyObject)
}
return nil
}
Expand All @@ -154,9 +162,9 @@ extension Scope {
public override init() {
super.init()
}
internal override func resolve<T>(using cache: Cache, id: String, factory: () -> T) -> T {
internal override func resolve<T>(using cache: Cache, id: String, ttl: TimeInterval?, factory: () -> T) -> T {
// ignore container's cache in favor of our own
return super.resolve(using: self.cache, id: id, factory: factory)
return super.resolve(using: self.cache, id: id, ttl: ttl, factory: factory)
}
/// Private shared cache
internal var cache = Cache()
Expand All @@ -177,7 +185,7 @@ extension Scope {
public override init() {
super.init()
}
internal override func resolve<T>(using cache: Cache, id: String, factory: () -> T) -> T {
internal override func resolve<T>(using cache: Cache, id: String, ttl: TimeInterval?, factory: () -> T) -> T {
factory()
}
}
Expand All @@ -197,6 +205,9 @@ extension Scope {
@inlinable func set(value: AnyBox, forKey key: String) {
cache[key] = value
}
@inlinable func set(timestamp: Double, forKey key: String) {
cache[key]?.timestamp = timestamp
}
@inlinable func removeValue(forKey key: String) {
cache.removeValue(forKey: key)
}
Expand Down Expand Up @@ -245,16 +256,19 @@ extension Optional: OptionalProtocol {
/// Internal box protocol for scope functionality
internal protocol AnyBox {
var scopeID: UUID { get }
var timestamp: Double { get set }
}

/// Strong box for strong references to a type
internal struct StrongBox<T>: AnyBox {
let scopeID: UUID
var timestamp: Double
let boxed: T
}

/// Weak box for shared scope
internal struct WeakBox: AnyBox {
let scopeID: UUID
var timestamp: Double
weak var boxed: AnyObject?
}
4 changes: 2 additions & 2 deletions Tests/FactoryTests/FactoryComponentTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ final class FactoryComponentTests: XCTestCase {
func testScopeCache () {
let cache = Scope.Cache()
let scopeID = UUID()
let strongBox = StrongBox(scopeID: scopeID, boxed: { MyService() })
let anotherBox = StrongBox(scopeID: UUID(), boxed: { MyService() })
let strongBox = StrongBox(scopeID: scopeID, timestamp: 0, boxed: { MyService() })
let anotherBox = StrongBox(scopeID: UUID(), timestamp: 0, boxed: { MyService() })
// Finds nothing
XCTAssertNil(cache.value(forKey: key1))
XCTAssertNil(cache.value(forKey: key2))
Expand Down
14 changes: 14 additions & 0 deletions Tests/FactoryTests/FactoryScopeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,20 @@ final class FactoryScopeTests: XCTestCase {
container1.manager.trace.toggle()
}

func testSingletonScopeTimeToLive() throws {
let service1 = Container.shared.singletonService()
let service2 = Container.shared.singletonService()
XCTAssertTrue(service1.id == service2.id)
Container.shared.singletonService.timeToLive(-60)
let service3 = Container.shared.singletonService()
// should fail ttl test and return new instance
XCTAssertTrue(service2.id != service3.id)
Container.shared.singletonService.timeToLive(60)
let service4 = Container.shared.singletonService()
// should passs ttl test and return old instance
XCTAssertTrue(service3.id == service4.id)
}

}

extension SharedContainer {
Expand Down

0 comments on commit 3f4efc8

Please sign in to comment.