Skip to content

Commit 9c6991d

Browse files
authored
Feature: Anchor repeaters (#8)
* rudimentary broadcast support with anchor repeaters * added typo * refactor internal repeater wrapper type
1 parent 45b6a15 commit 9c6991d

File tree

4 files changed

+201
-15
lines changed

4 files changed

+201
-15
lines changed

Sources/QLoop/QLAnchor+ConvenienceInit.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,23 @@ public extension QLAnchor {
1111
self.init(onChange: onChange,
1212
onError: QLAnchor.emptyErr)
1313
}
14+
15+
convenience init(repeaters: QLAnchor...) {
16+
self.init(echoFilter: QLAnchor.DefaultEchoFilter,
17+
repeaters: repeaters)
18+
}
19+
20+
convenience init(echoFilter: @escaping EchoFilter,
21+
repeaters: QLAnchor...) {
22+
self.init(echoFilter: echoFilter,
23+
repeaters: repeaters)
24+
}
25+
26+
convenience init(echoFilter: @escaping EchoFilter,
27+
repeaters: [QLAnchor]) {
28+
self.init(onChange: QLAnchor.emptyIn,
29+
onError: QLAnchor.emptyErr)
30+
self.repeaters = repeaters
31+
self.echoFilter = echoFilter
32+
}
1433
}

Sources/QLoop/QLAnchor.swift

Lines changed: 72 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,25 @@ public final class QLAnchor<Input>: AnyAnchor {
88
public typealias OnChange = (Input?)->()
99
public typealias OnError = (Error)->()
1010

11+
public typealias EchoFilter = (Input?, QLAnchor<Input>) -> (Bool)
12+
internal static var DefaultEchoFilter: EchoFilter { return { _, _ in return true } }
13+
14+
internal final class Repeater {
15+
weak var anchor: QLAnchor?
16+
init(_ anchor: QLAnchor) {
17+
self.anchor = anchor
18+
}
19+
func echo(value: Input?, filter: EchoFilter) {
20+
if let repeater = self.anchor,
21+
filter(value, repeater) {
22+
repeater.value = value
23+
}
24+
}
25+
func echo(error: Error) {
26+
anchor?.error = error
27+
}
28+
}
29+
1130
lazy var inputQueue = DispatchQueue(label: "\(self).inputQueue",
1231
qos: .userInitiated)
1332

@@ -17,6 +36,22 @@ public final class QLAnchor<Input>: AnyAnchor {
1736
self.onError = onError
1837
}
1938

39+
public var onChange: OnChange
40+
41+
public var onError: OnError
42+
43+
public var inputSegment: AnySegment?
44+
45+
public var repeaters: [QLAnchor] {
46+
get { return _repeaters.compactMap { $0.anchor } }
47+
set { self._repeaters = newValue.map { Repeater($0) } }
48+
}
49+
50+
internal var _repeaters: [Repeater] = []
51+
52+
public var echoFilter: EchoFilter = DefaultEchoFilter
53+
54+
private var _value: Input?
2055
public var value: Input? {
2156
get {
2257
var safeInput: Input? = nil
@@ -25,24 +60,21 @@ public final class QLAnchor<Input>: AnyAnchor {
2560
}
2661
set {
2762
inputQueue.sync { self._value = newValue }
28-
if QLCommon.Config.Anchor.autoThrowResultFailures,
29-
let errGettable = newValue as? ErrorGettable,
30-
let err = errGettable.getError() {
63+
64+
if let err = getReroutableError(newValue) {
3165
self.error = err
3266
} else {
33-
DispatchQueue.main.async {
34-
self.onChange(newValue)
35-
}
67+
dispatch(value: newValue)
68+
echo(value: newValue)
3669
}
3770

3871
if (QLCommon.Config.Anchor.releaseValues) {
3972
inputQueue.sync { self._value = nil }
4073
}
4174
}
4275
}
43-
private var _value: Input?
44-
4576

77+
private var _error: Error?
4678
public var error: Error? {
4779
get {
4880
var safeError: Error? = nil
@@ -52,19 +84,44 @@ public final class QLAnchor<Input>: AnyAnchor {
5284
set {
5385
let err: Error = newValue ?? QLCommon.Error.ThrownButNotSet
5486
inputQueue.sync { self._error = err }
55-
DispatchQueue.main.async {
56-
self.onError(err)
57-
}
87+
dispatch(error: err)
88+
echo(error: err)
5889

5990
if (QLCommon.Config.Anchor.releaseValues) {
6091
inputQueue.sync { self._error = nil }
6192
}
6293
}
6394
}
64-
private var _error: Error?
6595

66-
public var onChange: OnChange
67-
public var onError: OnError
96+
private func getReroutableError(_ newValue: Input?) -> Error? {
97+
guard QLCommon.Config.Anchor.autoThrowResultFailures,
98+
let errGettable = newValue as? ErrorGettable,
99+
let err = errGettable.getError()
100+
else { return nil }
101+
return err
102+
}
68103

69-
public var inputSegment: AnySegment?
104+
private func dispatch(value: Input?) {
105+
DispatchQueue.main.async {
106+
self.onChange(value)
107+
}
108+
}
109+
110+
private func dispatch(error: Error) {
111+
DispatchQueue.main.async {
112+
self.onError(error)
113+
}
114+
}
115+
116+
private func echo(value: Input?) {
117+
for repeater in _repeaters {
118+
repeater.echo(value: value, filter: echoFilter)
119+
}
120+
}
121+
122+
private func echo(error: Error) {
123+
for repeater in _repeaters {
124+
repeater.echo(error: error)
125+
}
126+
}
70127
}

Tests/QLoopTests/QLAnchorTests.swift

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,75 @@ final class QLAnchorTests: XCTestCase {
8080
wait(for: [expect], timeout: 8.0)
8181
XCTAssert((receivedError as? QLCommon.Error) == QLCommon.Error.ThrownButNotSet)
8282
}
83+
84+
func test_given_it_has_repeaters_with_default_filter_when_input_set_then_it_echoes_to_them_as_well() {
85+
var receivedVal0: Int = -1
86+
var receivedVal1: Int = -1
87+
var receivedVal2: Int = -1
88+
let expectOriginal0 = expectation(description: "should dispatch value")
89+
let expectRepeater1 = expectation(description: "should echo value to repeater1")
90+
let expectRepeater2 = expectation(description: "should echo value to repeater2")
91+
let repeater1 = QLAnchor<Int>(onChange: { receivedVal1 = $0!; expectRepeater1.fulfill() })
92+
let repeater2 = QLAnchor<Int>(onChange: { receivedVal2 = $0!; expectRepeater2.fulfill() })
93+
let subject = QLAnchor<Int>(repeaters: repeater1, repeater2)
94+
95+
subject.onChange = { receivedVal0 = $0!; expectOriginal0.fulfill() }
96+
97+
subject.value = 99
98+
99+
wait(for: [expectOriginal0, expectRepeater1, expectRepeater2], timeout: 8.0)
100+
XCTAssertEqual(receivedVal0, 99)
101+
XCTAssertEqual(receivedVal1, 99)
102+
XCTAssertEqual(receivedVal2, 99)
103+
}
104+
105+
func test_given_it_has_repeaters_with_default_filter_when_error_set_then_it_echoes_to_them_as_well() {
106+
var receivedErr0: Error? = nil
107+
var receivedErr1: Error? = nil
108+
var receivedErr2: Error? = nil
109+
let expectOriginal0 = expectation(description: "should dispatch error")
110+
let expectRepeater1 = expectation(description: "should echo error to repeater1")
111+
let expectRepeater2 = expectation(description: "should echo error to repeater2")
112+
let repeater1 = QLAnchor<Int>(onChange: { _ in },
113+
onError: { receivedErr1 = $0; expectRepeater1.fulfill() })
114+
let repeater2 = QLAnchor<Int>(onChange: { _ in },
115+
onError: { receivedErr2 = $0; expectRepeater2.fulfill() })
116+
let subject = QLAnchor<Int>(repeaters: repeater1, repeater2)
117+
118+
subject.onError = { receivedErr0 = $0; expectOriginal0.fulfill() }
119+
120+
subject.error = QLCommon.Error.Unknown
121+
122+
wait(for: [expectOriginal0, expectRepeater1, expectRepeater2], timeout: 8.0)
123+
XCTAssertNotNil(receivedErr0)
124+
XCTAssertNotNil(receivedErr1)
125+
XCTAssertNotNil(receivedErr2)
126+
}
127+
128+
func test_given_it_has_repeaters_with_custom_filter_when_input_set_then_it_dispatches_then_echoes_to_them_conditionally() {
129+
var receivedVal0: Int = -1
130+
var receivedVal1: Int = -1
131+
var receivedVal2: Int = -1
132+
let expectOriginal0 = expectation(description: "should dispatch value")
133+
let expectRepeater2 = expectation(description: "should echo value to repeater2")
134+
let repeater1 = QLAnchor<Int>(onChange: { receivedVal1 = $0! })
135+
let repeater2 = QLAnchor<Int>(onChange: { receivedVal2 = $0!; expectRepeater2.fulfill() })
136+
137+
let subject = QLAnchor<Int>(
138+
echoFilter: ({ val, repeater in
139+
return (val == 11 && repeater === repeater1)
140+
|| (val == 22 && repeater === repeater2)
141+
}),
142+
repeaters: repeater1, repeater2
143+
)
144+
145+
subject.onChange = { receivedVal0 = $0!; expectOriginal0.fulfill() }
146+
147+
subject.value = 22
148+
149+
wait(for: [expectOriginal0, expectRepeater2], timeout: 8.0)
150+
XCTAssertEqual(receivedVal0, 22)
151+
XCTAssertEqual(receivedVal1, -1)
152+
XCTAssertEqual(receivedVal2, 22)
153+
}
83154
}

docs/reference/QLAnchor.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020

2121
- init(onChange: `(Input?)->()`, onError: `(Error)->()` )
2222

23+
- init(repeaters: `QLAnchor.Repeater`, `...` )
24+
2325

2426
<br />
2527

@@ -64,3 +66,40 @@ An `anchor` :
6466
`QLAnchor` implements a type of **semaphore** that makes use of synchronous dispatch
6567
queues around its `value` and `error` nodes. Inputs can safely arrive on any thread,
6668
and the events are guaranteed to arrive in serial fashion, although their order is not.
69+
70+
71+
##### Repeaters
72+
73+
Repeaters offer a way to fork multiple streams off of the main path.
74+
75+
When an Anchor has repeaters applied, then it will `echo` any `value` and `error` changes
76+
to each of them.
77+
78+
By default, it forwards all changes to all repeaters. In order to make it conditional, we can
79+
set an `EchoFilter`, which gets called prior to forwarding to each repeater. Return `false`
80+
from the EchoFilter to block that repeater from receiving the change.
81+
82+
83+
##### EchoFilter
84+
85+
- `(Input?, QLAnchor) -> (Bool)`
86+
87+
Default filter returns `true`. You can evaluate the input value and decide whether or not the
88+
particular `anchor` (repeater) should receive the new value.
89+
90+
To identify the anchor, you will need to do so using the object reference.
91+
92+
example:
93+
94+
```
95+
let progressRepeater = viewController.progressAnchor
96+
let finalRepeater = viewController.downloadCompleteAnchor
97+
98+
let baseAnchor = QLAnchor<DownloadStatus>(
99+
echoFilter: ({ obj, repeater in
100+
return (obj.isProgress && repeater === progressRepeater)
101+
|| (obj.isFinal && repeater === finalRepeater)
102+
}),
103+
repeaters: progressRepeater, finalRepeater
104+
)
105+
```

0 commit comments

Comments
 (0)