Skip to content

Commit 70ad4da

Browse files
authored
Unserialized and Reentrant Signals (#797)
* Offer Signal variants for advanced users. * Switch `flatMapEvent` to use `nonSerializing`. * Test coverage. * Changelog * Expand the changelog. * Fix indentation. * Extend `nonSerializing()` to properties and producers too. * Make `on()` and `withLatest` non-serializing * Signals made by Producer can be non-serializing too * NonSerializing -> Unserialized * Remove use of @_specialize * reentrant -> reentrantUnserialized * Update changelog * Revive @_specialize
1 parent e1922f3 commit 70ad4da

File tree

6 files changed

+306
-32
lines changed

6 files changed

+306
-32
lines changed

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
11
# master
22
*Please add new entries at the top.*
33

4+
1. `Signal` offers two special variants for advanced users: unserialized and reentrant-unserialized. (#797)
5+
6+
The input observer of these variants assume that mutual exclusion has been enforced by its callers.
7+
8+
You can create these variants through four `Signal` static methods: `unserialized(_:)`, `unserializedPipe(_:)`, `reentrantUnserialized(_:)` and `reentrantUnserializedPipe(_:)`. These would be adopted by ReactiveCocoa UIKit bindings to improve interoperability with Loop, to tackle some legitimate recursive delivery scenarios (e.g. around first responder management), and also to reduce fine-grained locking in ReactiveCocoa.
9+
10+
Note that the default behavior of `Signal` has not been changed — event serialization remains the default behavior.
11+
12+
1. `SignalProducer` offers an unserialized variant via `SignalProducer.unserialized(_:)`. (#797)
13+
14+
1. `Signal` and Properties now use fewer locks, which should translate into minor performance improvements. (#797)
15+
16+
1. Added the `interval` operator (#810, kudos to @mluisbrown)
17+
418
1. `TestScheduler` can `advance` by `TimeInterval`. (#828)
519

620
1. Fixed spelling error in `Token` class documentation.
@@ -19,6 +33,7 @@
1933
1. Bumped deployment target to iOS 9.0, per Xcode 12 warnings. (#818, kudos to @harleyjcooper)
2034

2135
1. Fixed a few deprecation warning when the project is being built. (#819, kudos to @apps4everyone)
36+
>>>>>>> origin/master
2237
2338
# 6.5.0
2439

Sources/Atomic.swift

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ internal struct UnsafeAtomicState<State: RawRepresentable> where State.RawValue
104104

105105
/// `Lock` exposes `os_unfair_lock` on supported platforms, with pthread mutex as the
106106
/// fallback.
107-
internal class Lock {
107+
internal class Lock: LockProtocol {
108108
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
109109
@available(iOS 10.0, *)
110110
@available(macOS 10.12, *)
@@ -195,14 +195,14 @@ internal class Lock {
195195
}
196196
}
197197

198-
static func make() -> Lock {
198+
static func make() -> Self {
199199
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
200200
if #available(*, iOS 10.0, macOS 10.12, tvOS 10.0, watchOS 3.0) {
201-
return UnfairLock()
201+
return UnfairLock() as! Self
202202
}
203203
#endif
204204

205-
return PthreadLock()
205+
return PthreadLock() as! Self
206206
}
207207

208208
private init() {}
@@ -212,6 +212,22 @@ internal class Lock {
212212
func `try`() -> Bool { fatalError() }
213213
}
214214

215+
internal protocol LockProtocol {
216+
static func make() -> Self
217+
218+
func lock()
219+
func unlock()
220+
func `try`() -> Bool
221+
}
222+
223+
internal struct NoLock: LockProtocol {
224+
static func make() -> NoLock { NoLock() }
225+
226+
func lock() {}
227+
func unlock() {}
228+
func `try`() -> Bool { true }
229+
}
230+
215231
/// An atomic variable.
216232
public final class Atomic<Value> {
217233
private let lock: Lock

Sources/Property.swift

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,11 @@ extension PropertyProtocol {
8585
return Property(unsafeProducer: transform(producer))
8686
}
8787

88+
/// Lifts a unary SignalProducer operator to operate upon PropertyProtocol instead.
89+
fileprivate func liftUnserialized<U>(_ transform: @escaping (SignalProducer<Value, Never>) -> SignalProducer<U, Never>) -> Property<U> {
90+
return Property(unsafeProducer: transform(producer), unserialized: true)
91+
}
92+
8893
/// Lifts a binary SignalProducer operator to operate upon PropertyProtocol instead.
8994
fileprivate func lift<P: PropertyProtocol, U>(_ transform: @escaping (SignalProducer<Value, Never>) -> (SignalProducer<P.Value, Never>) -> SignalProducer<U, Never>) -> (P) -> Property<U> {
9095
return { other in
@@ -102,7 +107,7 @@ extension PropertyProtocol {
102107
///
103108
/// - returns: A property that holds a mapped value from `self`.
104109
public func map<U>(_ transform: @escaping (Value) -> U) -> Property<U> {
105-
return lift { $0.map(transform) }
110+
return liftUnserialized { $0.map(transform) }
106111
}
107112

108113
/// Map the current value and all susequent values to a new constant property.
@@ -112,7 +117,7 @@ extension PropertyProtocol {
112117
///
113118
/// - returns: A property that holds a mapped value from `self`.
114119
public func map<U>(value: U) -> Property<U> {
115-
return lift { $0.map(value: value) }
120+
return liftUnserialized { $0.map(value: value) }
116121
}
117122

118123
/// Maps the current value and all subsequent values to a new property
@@ -123,7 +128,7 @@ extension PropertyProtocol {
123128
///
124129
/// - returns: A property that holds a mapped value from `self`.
125130
public func map<U>(_ keyPath: KeyPath<Value, U>) -> Property<U> {
126-
return lift { $0.map(keyPath) }
131+
return liftUnserialized { $0.map(keyPath) }
127132
}
128133

129134
/// Passes only the values of the property that pass the given predicate
@@ -176,7 +181,7 @@ extension PropertyProtocol {
176181
/// - returns: A property that holds tuples that contain previous and
177182
/// current values of `self`.
178183
public func combinePrevious(_ initial: Value) -> Property<(Value, Value)> {
179-
return lift { $0.combinePrevious(initial) }
184+
return liftUnserialized { $0.combinePrevious(initial) }
180185
}
181186

182187
/// Forward only values from `self` that are not considered equivalent to its
@@ -189,7 +194,7 @@ extension PropertyProtocol {
189194
///
190195
/// - returns: A property which conditionally forwards values from `self`.
191196
public func skipRepeats(_ isEquivalent: @escaping (Value, Value) -> Bool) -> Property<Value> {
192-
return lift { $0.skipRepeats(isEquivalent) }
197+
return liftUnserialized { $0.skipRepeats(isEquivalent) }
193198
}
194199
}
195200

@@ -200,7 +205,7 @@ extension PropertyProtocol where Value: Equatable {
200205
///
201206
/// - returns: A property which conditionally forwards values from `self`.
202207
public func skipRepeats() -> Property<Value> {
203-
return lift { $0.skipRepeats() }
208+
return liftUnserialized { $0.skipRepeats() }
204209
}
205210
}
206211

@@ -243,7 +248,7 @@ extension PropertyProtocol {
243248
///
244249
/// - returns: A property that sends unique values during its lifetime.
245250
public func uniqueValues<Identity: Hashable>(_ transform: @escaping (Value) -> Identity) -> Property<Value> {
246-
return lift { $0.uniqueValues(transform) }
251+
return liftUnserialized { $0.uniqueValues(transform) }
247252
}
248253
}
249254

@@ -257,7 +262,7 @@ extension PropertyProtocol where Value: Hashable {
257262
///
258263
/// - returns: A property that sends unique values during its lifetime.
259264
public func uniqueValues() -> Property<Value> {
260-
return lift { $0.uniqueValues() }
265+
return liftUnserialized { $0.uniqueValues() }
261266
}
262267
}
263268

@@ -420,7 +425,7 @@ extension PropertyProtocol where Value == Bool {
420425
///
421426
/// - returns: A property that contains the logical NOT results.
422427
public func negate() -> Property<Value> {
423-
return self.lift { $0.negate() }
428+
return liftUnserialized { $0.negate() }
424429
}
425430

426431
/// Create a property that computes a logical AND between the latest values of `self`
@@ -618,7 +623,7 @@ public final class Property<Value>: PropertyProtocol {
618623
///
619624
/// - parameters:
620625
/// - unsafeProducer: The composed producer for creating the property.
621-
fileprivate init(unsafeProducer: SignalProducer<Value, Never>) {
626+
fileprivate init(unsafeProducer: SignalProducer<Value, Never>, unserialized: Bool = false) {
622627
// The ownership graph:
623628
//
624629
// ------------ weak ----------- strong ------------------
@@ -634,7 +639,9 @@ public final class Property<Value>: PropertyProtocol {
634639
// A composed property tracks its active consumers through its relay signal, and
635640
// interrupts `unsafeProducer` if the relay signal terminates.
636641
let disposable = SerialDisposable()
637-
let (relay, observer) = Signal<Value, Never>.pipe(disposable: disposable)
642+
let (relay, observer) = unserialized
643+
? Signal<Value, Never>.unserializedPipe(disposable: disposable)
644+
: Signal<Value, Never>.pipe(disposable: disposable)
638645

639646
disposable.inner = unsafeProducer.start { [weak box] event in
640647
// `observer` receives `interrupted` only as a result of the termination of
@@ -666,7 +673,7 @@ public final class Property<Value>: PropertyProtocol {
666673
_value = { box.value! }
667674
signal = relay
668675

669-
producer = SignalProducer { [box, relay] observer, lifetime in
676+
producer = SignalProducer.unserialized { [box, relay] observer, lifetime in
670677
box.withValue { value in
671678
observer.send(value: value!)
672679
lifetime += relay.observe(Signal.Observer(mappingInterruptedToCompleted: observer))
@@ -739,7 +746,7 @@ public final class MutableProperty<Value>: ComposableMutablePropertyProtocol {
739746
/// followed by all changes over time, then complete when the property has
740747
/// deinitialized.
741748
public var producer: SignalProducer<Value, Never> {
742-
return SignalProducer { [box, signal] observer, lifetime in
749+
return SignalProducer.unserialized { [box, signal] observer, lifetime in
743750
box.withValue { value in
744751
observer.send(value: value)
745752
lifetime += signal.observe(Signal.Observer(mappingInterruptedToCompleted: observer))
@@ -752,7 +759,7 @@ public final class MutableProperty<Value>: ComposableMutablePropertyProtocol {
752759
/// - parameters:
753760
/// - initialValue: Starting value for the mutable property.
754761
public init(_ initialValue: Value) {
755-
(signal, observer) = Signal.pipe()
762+
(signal, observer) = Signal.unserializedPipe()
756763
(lifetime, token) = Lifetime.make()
757764

758765
/// Need a recursive lock around `value` to allow recursive access to

0 commit comments

Comments
 (0)