diff --git a/Sources/QLoop/QLAnchor+ConvenienceInit.swift b/Sources/QLoop/QLAnchor+ConvenienceInit.swift
index bc47c3e..942f091 100644
--- a/Sources/QLoop/QLAnchor+ConvenienceInit.swift
+++ b/Sources/QLoop/QLAnchor+ConvenienceInit.swift
@@ -11,4 +11,23 @@ public extension QLAnchor {
self.init(onChange: onChange,
onError: QLAnchor.emptyErr)
}
+
+ convenience init(repeaters: QLAnchor...) {
+ self.init(echoFilter: QLAnchor.DefaultEchoFilter,
+ repeaters: repeaters)
+ }
+
+ convenience init(echoFilter: @escaping EchoFilter,
+ repeaters: QLAnchor...) {
+ self.init(echoFilter: echoFilter,
+ repeaters: repeaters)
+ }
+
+ convenience init(echoFilter: @escaping EchoFilter,
+ repeaters: [QLAnchor]) {
+ self.init(onChange: QLAnchor.emptyIn,
+ onError: QLAnchor.emptyErr)
+ self.repeaters = repeaters
+ self.echoFilter = echoFilter
+ }
}
diff --git a/Sources/QLoop/QLAnchor.swift b/Sources/QLoop/QLAnchor.swift
index 4564d53..9a81fe8 100644
--- a/Sources/QLoop/QLAnchor.swift
+++ b/Sources/QLoop/QLAnchor.swift
@@ -8,6 +8,25 @@ public final class QLAnchor: AnyAnchor {
public typealias OnChange = (Input?)->()
public typealias OnError = (Error)->()
+ public typealias EchoFilter = (Input?, QLAnchor) -> (Bool)
+ internal static var DefaultEchoFilter: EchoFilter { return { _, _ in return true } }
+
+ internal final class Repeater {
+ weak var anchor: QLAnchor?
+ init(_ anchor: QLAnchor) {
+ self.anchor = anchor
+ }
+ func echo(value: Input?, filter: EchoFilter) {
+ if let repeater = self.anchor,
+ filter(value, repeater) {
+ repeater.value = value
+ }
+ }
+ func echo(error: Error) {
+ anchor?.error = error
+ }
+ }
+
lazy var inputQueue = DispatchQueue(label: "\(self).inputQueue",
qos: .userInitiated)
@@ -17,6 +36,22 @@ public final class QLAnchor: AnyAnchor {
self.onError = onError
}
+ public var onChange: OnChange
+
+ public var onError: OnError
+
+ public var inputSegment: AnySegment?
+
+ public var repeaters: [QLAnchor] {
+ get { return _repeaters.compactMap { $0.anchor } }
+ set { self._repeaters = newValue.map { Repeater($0) } }
+ }
+
+ internal var _repeaters: [Repeater] = []
+
+ public var echoFilter: EchoFilter = DefaultEchoFilter
+
+ private var _value: Input?
public var value: Input? {
get {
var safeInput: Input? = nil
@@ -25,14 +60,12 @@ public final class QLAnchor: AnyAnchor {
}
set {
inputQueue.sync { self._value = newValue }
- if QLCommon.Config.Anchor.autoThrowResultFailures,
- let errGettable = newValue as? ErrorGettable,
- let err = errGettable.getError() {
+
+ if let err = getReroutableError(newValue) {
self.error = err
} else {
- DispatchQueue.main.async {
- self.onChange(newValue)
- }
+ dispatch(value: newValue)
+ echo(value: newValue)
}
if (QLCommon.Config.Anchor.releaseValues) {
@@ -40,9 +73,8 @@ public final class QLAnchor: AnyAnchor {
}
}
}
- private var _value: Input?
-
+ private var _error: Error?
public var error: Error? {
get {
var safeError: Error? = nil
@@ -52,19 +84,44 @@ public final class QLAnchor: AnyAnchor {
set {
let err: Error = newValue ?? QLCommon.Error.ThrownButNotSet
inputQueue.sync { self._error = err }
- DispatchQueue.main.async {
- self.onError(err)
- }
+ dispatch(error: err)
+ echo(error: err)
if (QLCommon.Config.Anchor.releaseValues) {
inputQueue.sync { self._error = nil }
}
}
}
- private var _error: Error?
- public var onChange: OnChange
- public var onError: OnError
+ private func getReroutableError(_ newValue: Input?) -> Error? {
+ guard QLCommon.Config.Anchor.autoThrowResultFailures,
+ let errGettable = newValue as? ErrorGettable,
+ let err = errGettable.getError()
+ else { return nil }
+ return err
+ }
- public var inputSegment: AnySegment?
+ private func dispatch(value: Input?) {
+ DispatchQueue.main.async {
+ self.onChange(value)
+ }
+ }
+
+ private func dispatch(error: Error) {
+ DispatchQueue.main.async {
+ self.onError(error)
+ }
+ }
+
+ private func echo(value: Input?) {
+ for repeater in _repeaters {
+ repeater.echo(value: value, filter: echoFilter)
+ }
+ }
+
+ private func echo(error: Error) {
+ for repeater in _repeaters {
+ repeater.echo(error: error)
+ }
+ }
}
diff --git a/Tests/QLoopTests/QLAnchorTests.swift b/Tests/QLoopTests/QLAnchorTests.swift
index 22b1b0e..57297ba 100644
--- a/Tests/QLoopTests/QLAnchorTests.swift
+++ b/Tests/QLoopTests/QLAnchorTests.swift
@@ -80,4 +80,75 @@ final class QLAnchorTests: XCTestCase {
wait(for: [expect], timeout: 8.0)
XCTAssert((receivedError as? QLCommon.Error) == QLCommon.Error.ThrownButNotSet)
}
+
+ func test_given_it_has_repeaters_with_default_filter_when_input_set_then_it_echoes_to_them_as_well() {
+ var receivedVal0: Int = -1
+ var receivedVal1: Int = -1
+ var receivedVal2: Int = -1
+ let expectOriginal0 = expectation(description: "should dispatch value")
+ let expectRepeater1 = expectation(description: "should echo value to repeater1")
+ let expectRepeater2 = expectation(description: "should echo value to repeater2")
+ let repeater1 = QLAnchor(onChange: { receivedVal1 = $0!; expectRepeater1.fulfill() })
+ let repeater2 = QLAnchor(onChange: { receivedVal2 = $0!; expectRepeater2.fulfill() })
+ let subject = QLAnchor(repeaters: repeater1, repeater2)
+
+ subject.onChange = { receivedVal0 = $0!; expectOriginal0.fulfill() }
+
+ subject.value = 99
+
+ wait(for: [expectOriginal0, expectRepeater1, expectRepeater2], timeout: 8.0)
+ XCTAssertEqual(receivedVal0, 99)
+ XCTAssertEqual(receivedVal1, 99)
+ XCTAssertEqual(receivedVal2, 99)
+ }
+
+ func test_given_it_has_repeaters_with_default_filter_when_error_set_then_it_echoes_to_them_as_well() {
+ var receivedErr0: Error? = nil
+ var receivedErr1: Error? = nil
+ var receivedErr2: Error? = nil
+ let expectOriginal0 = expectation(description: "should dispatch error")
+ let expectRepeater1 = expectation(description: "should echo error to repeater1")
+ let expectRepeater2 = expectation(description: "should echo error to repeater2")
+ let repeater1 = QLAnchor(onChange: { _ in },
+ onError: { receivedErr1 = $0; expectRepeater1.fulfill() })
+ let repeater2 = QLAnchor(onChange: { _ in },
+ onError: { receivedErr2 = $0; expectRepeater2.fulfill() })
+ let subject = QLAnchor(repeaters: repeater1, repeater2)
+
+ subject.onError = { receivedErr0 = $0; expectOriginal0.fulfill() }
+
+ subject.error = QLCommon.Error.Unknown
+
+ wait(for: [expectOriginal0, expectRepeater1, expectRepeater2], timeout: 8.0)
+ XCTAssertNotNil(receivedErr0)
+ XCTAssertNotNil(receivedErr1)
+ XCTAssertNotNil(receivedErr2)
+ }
+
+ func test_given_it_has_repeaters_with_custom_filter_when_input_set_then_it_dispatches_then_echoes_to_them_conditionally() {
+ var receivedVal0: Int = -1
+ var receivedVal1: Int = -1
+ var receivedVal2: Int = -1
+ let expectOriginal0 = expectation(description: "should dispatch value")
+ let expectRepeater2 = expectation(description: "should echo value to repeater2")
+ let repeater1 = QLAnchor(onChange: { receivedVal1 = $0! })
+ let repeater2 = QLAnchor(onChange: { receivedVal2 = $0!; expectRepeater2.fulfill() })
+
+ let subject = QLAnchor(
+ echoFilter: ({ val, repeater in
+ return (val == 11 && repeater === repeater1)
+ || (val == 22 && repeater === repeater2)
+ }),
+ repeaters: repeater1, repeater2
+ )
+
+ subject.onChange = { receivedVal0 = $0!; expectOriginal0.fulfill() }
+
+ subject.value = 22
+
+ wait(for: [expectOriginal0, expectRepeater2], timeout: 8.0)
+ XCTAssertEqual(receivedVal0, 22)
+ XCTAssertEqual(receivedVal1, -1)
+ XCTAssertEqual(receivedVal2, 22)
+ }
}
diff --git a/docs/reference/QLAnchor.md b/docs/reference/QLAnchor.md
index a064a20..9cf1d9e 100644
--- a/docs/reference/QLAnchor.md
+++ b/docs/reference/QLAnchor.md
@@ -20,6 +20,8 @@
- init(onChange: `(Input?)->()`, onError: `(Error)->()` )
+- init(repeaters: `QLAnchor.Repeater`, `...` )
+
@@ -64,3 +66,40 @@ An `anchor` :
`QLAnchor` implements a type of **semaphore** that makes use of synchronous dispatch
queues around its `value` and `error` nodes. Inputs can safely arrive on any thread,
and the events are guaranteed to arrive in serial fashion, although their order is not.
+
+
+##### Repeaters
+
+Repeaters offer a way to fork multiple streams off of the main path.
+
+When an Anchor has repeaters applied, then it will `echo` any `value` and `error` changes
+to each of them.
+
+By default, it forwards all changes to all repeaters. In order to make it conditional, we can
+set an `EchoFilter`, which gets called prior to forwarding to each repeater. Return `false`
+from the EchoFilter to block that repeater from receiving the change.
+
+
+##### EchoFilter
+
+- `(Input?, QLAnchor) -> (Bool)`
+
+Default filter returns `true`. You can evaluate the input value and decide whether or not the
+particular `anchor` (repeater) should receive the new value.
+
+To identify the anchor, you will need to do so using the object reference.
+
+example:
+
+```
+let progressRepeater = viewController.progressAnchor
+let finalRepeater = viewController.downloadCompleteAnchor
+
+let baseAnchor = QLAnchor(
+ echoFilter: ({ obj, repeater in
+ return (obj.isProgress && repeater === progressRepeater)
+ || (obj.isFinal && repeater === finalRepeater)
+ }),
+ repeaters: progressRepeater, finalRepeater
+)
+```