Skip to content

Commit

Permalink
Lifetime observation API. Internal refactoring.
Browse files Browse the repository at this point in the history
  • Loading branch information
andersio committed Jan 14, 2017
1 parent 59c9e1e commit 5c7e576
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 22 deletions.
87 changes: 85 additions & 2 deletions Sources/Lifetime.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import enum Result.NoError
/// Represents the lifetime of an object, and provides a hook to observe when
/// the object deinitializes.
public final class Lifetime {
private enum Backing {
case token(Signal<(), NoError>)
case signal(() -> Signal<(), NoError>, (@escaping () -> Void) -> Disposable?)
}

/// MARK: Type properties and methods

/// A `Lifetime` that has already ended.
Expand All @@ -13,17 +18,52 @@ public final class Lifetime {

/// MARK: Instance properties

private let storage: Backing

/// A signal that sends a `completed` event when the lifetime ends.
public let ended: Signal<(), NoError>
public var ended: Signal<(), NoError> {
switch storage {
case let .token(signal): return signal
case let .signal(factory, _): return factory()
}
}

/// MARK: Initializers

/// Initialize a `Lifetime` for the given `Signal`.
///
/// - parameters:
/// - signal: The `Signal`.
internal init<U, E: Swift.Error>(_ signal: Signal<U, E>) {
let factory: () -> Signal<(), NoError> = { [weak signal] in
return signal.map { signal in
return Signal { observer in
return signal.observe { event in
if event.isTerminating {
observer.sendCompleted()
}
}
}
} ?? .empty
}

let observe: (@escaping () -> Void) -> Disposable? = { [weak signal] action in
return signal?.observe { event in
if event.isTerminating {
action()
}
}
}

self.storage = .signal(factory, observe)
}

/// Initialize a `Lifetime` object with the supplied ended signal.
///
/// - parameters:
/// - signal: The ended signal.
private init(ended signal: Signal<(), NoError>) {
ended = signal
storage = .token(signal)
}

/// Initialize a `Lifetime` from a lifetime token, which is expected to be
Expand All @@ -39,6 +79,49 @@ public final class Lifetime {
self.init(ended: token.ended)
}

/// Dispose of the given disposable when `self` ends.
///
/// - parameters:
/// - disposable: The disposable to be disposed of.
///
/// - returns: A disposable that detaches `action` from the lifetime, or `nil`
/// if `lifetime` has already ended.
@discardableResult
public func attach(_ disposable: Disposable?) -> Disposable? {
return disposable.flatMap { observeEnded($0.dispose) }
}

/// Observe the termination of `self`.
///
/// - parameters:
/// - action: The action to be invoked when `self` ends.
///
/// - returns: A disposable that detaches `action` from the lifetime, or `nil`
/// if `lifetime` has already ended.
@discardableResult
public func observeEnded(_ action: @escaping () -> Void) -> Disposable? {
switch storage {
case let .token(signal):
return signal.observeCompleted(action)

case let .signal(_, observe):
return observe(action)
}
}

/// Attach a `Disposable` to the lifetime.
///
/// - parameters:
/// - lifetime: The lifetime to attach to.
/// - disposable: The disposable to be attached.
///
/// - returns: A disposable that detaches `disposable` from the lifetime, or
/// `nil` if `lifetime` has already ended.
@discardableResult
public static func +=(lifetime: Lifetime, disposable: Disposable?) -> Disposable? {
return lifetime.attach(disposable)
}

/// A token object which completes its signal when it deinitializes.
///
/// It is generally used in conjuncion with `Lifetime` as a private
Expand Down
4 changes: 1 addition & 3 deletions Sources/Property.swift
Original file line number Diff line number Diff line change
Expand Up @@ -580,7 +580,6 @@ public final class Property<Value>: PropertyProtocol {
///
/// Instances of this class are thread-safe.
public final class MutableProperty<Value>: MutablePropertyProtocol {
private let token: Lifetime.Token
private let observer: Signal<Value, NoError>.Observer
private let atomic: RecursiveAtomic<Value>

Expand Down Expand Up @@ -628,8 +627,7 @@ public final class MutableProperty<Value>: MutablePropertyProtocol {
/// - initialValue: Starting value for the mutable property.
public init(_ initialValue: Value) {
(signal, observer) = Signal.pipe()
token = Lifetime.Token()
lifetime = Lifetime(token)
lifetime = Lifetime(signal)

/// Need a recursive lock around `value` to allow recursive access to
/// `value`. Note that recursive sets will still deadlock because the
Expand Down
7 changes: 6 additions & 1 deletion Sources/Signal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1261,7 +1261,12 @@ extension SignalProtocol {
///
/// - returns: A signal that will deliver events until `lifetime` ends.
public func take(during lifetime: Lifetime) -> Signal<Value, Error> {
return take(until: lifetime.ended)
return Signal { observer in
let disposable = CompositeDisposable()
disposable += self.observe(observer)
disposable += lifetime.observeEnded(observer.sendCompleted)
return disposable
}
}

/// Forward events from `self` until `trigger` sends a `value` or
Expand Down
2 changes: 1 addition & 1 deletion Sources/SignalProducer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -804,7 +804,7 @@ extension SignalProducerProtocol {
///
/// - returns: A producer that will deliver events until `lifetime` ends.
public func take(during lifetime: Lifetime) -> SignalProducer<Value, Error> {
return take(until: lifetime.ended)
return lift { $0.take(during: lifetime) }
}

/// Forward events from `self` until `trigger` sends a `value` or `completed`
Expand Down
86 changes: 71 additions & 15 deletions Tests/ReactiveSwiftTests/LifetimeSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,85 @@ import Result
final class LifetimeSpec: QuickSpec {
override func spec() {
describe("Lifetime") {
it("should complete its lifetime ended signal when the it deinitializes") {
let object = MutableReference(TestObject())
describe("ended") {
it("should complete its lifetime ended signal when the it deinitializes") {
let object = MutableReference(TestObject())

var isCompleted = false
var isCompleted = false

object.value!.lifetime.ended.observeCompleted { isCompleted = true }
expect(isCompleted) == false
object.value!.lifetime.ended.observeCompleted { isCompleted = true }
expect(isCompleted) == false

object.value = nil
expect(isCompleted) == true
object.value = nil
expect(isCompleted) == true
}

it("should complete its lifetime ended signal even if the lifetime object is being retained") {
let object = MutableReference(TestObject())
let lifetime = object.value!.lifetime

var isCompleted = false

lifetime.ended.observeCompleted { isCompleted = true }
expect(isCompleted) == false

object.value = nil
expect(isCompleted) == true
}
}

it("should complete its lifetime ended signal even if the lifetime object is being retained") {
let object = MutableReference(TestObject())
let lifetime = object.value!.lifetime
describe("observeEnded") {
it("should complete its lifetime ended signal when the it deinitializes") {
let object = MutableReference(TestObject())

var isCompleted = false

object.value!.lifetime.observeEnded { isCompleted = true }
expect(isCompleted) == false

object.value = nil
expect(isCompleted) == true
}

it("should complete its lifetime ended signal even if the lifetime object is being retained") {
let object = MutableReference(TestObject())
let lifetime = object.value!.lifetime

var isCompleted = false

lifetime.observeEnded { isCompleted = true }
expect(isCompleted) == false

object.value = nil
expect(isCompleted) == true
}
}

describe("attach") {
it("should complete its lifetime ended signal when the it deinitializes") {
let object = MutableReference(TestObject())

let disposable = SimpleDisposable()

object.value!.lifetime.attach(disposable)
expect(disposable.isDisposed) == false

object.value = nil
expect(disposable.isDisposed) == true
}

it("should complete its lifetime ended signal even if the lifetime object is being retained") {
let object = MutableReference(TestObject())
let lifetime = object.value!.lifetime

var isCompleted = false
let disposable = SimpleDisposable()

lifetime.ended.observeCompleted { isCompleted = true }
expect(isCompleted) == false
lifetime.attach(disposable)
expect(disposable.isDisposed) == false

object.value = nil
expect(isCompleted) == true
object.value = nil
expect(disposable.isDisposed) == true
}
}
}
}
Expand Down

0 comments on commit 5c7e576

Please sign in to comment.