Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 30 additions & 6 deletions Sources/QLoop/QLAnchor+ConvenienceInit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,46 @@ public extension QLAnchor {
onError: QLAnchor.emptyErr)
}

convenience init(repeaters: QLAnchor...) {
convenience init(earlyRepeaters: QLAnchor...) {
self.init(echoFilter: QLAnchor.DefaultEchoFilter,
repeaters: repeaters)
earlyRepeaters: earlyRepeaters,
lateRepeaters: [])
}

convenience init(lateRepeaters: QLAnchor...) {
self.init(echoFilter: QLAnchor.DefaultEchoFilter,
earlyRepeaters: [],
lateRepeaters: lateRepeaters)
}

convenience init(earlyRepeaters: [QLAnchor],
lateRepeaters: [QLAnchor]) {
self.init(echoFilter: QLAnchor.DefaultEchoFilter,
earlyRepeaters: earlyRepeaters,
lateRepeaters: lateRepeaters)
}

convenience init(echoFilter: @escaping EchoFilter,
earlyRepeaters: QLAnchor...) {
self.init(echoFilter: echoFilter,
earlyRepeaters: earlyRepeaters,
lateRepeaters: [])
}

convenience init(echoFilter: @escaping EchoFilter,
repeaters: QLAnchor...) {
lateRepeaters: QLAnchor...) {
self.init(echoFilter: echoFilter,
repeaters: repeaters)
earlyRepeaters: [],
lateRepeaters: lateRepeaters)
}

convenience init(echoFilter: @escaping EchoFilter,
repeaters: [QLAnchor]) {
earlyRepeaters: [QLAnchor],
lateRepeaters: [QLAnchor]) {
self.init(onChange: QLAnchor.emptyIn,
onError: QLAnchor.emptyErr)
self.repeaters = repeaters
self.addRepeaters(earlyRepeaters, timing: .early)
self.addRepeaters(lateRepeaters, timing: .late)
self.echoFilter = echoFilter
}
}
33 changes: 25 additions & 8 deletions Sources/QLoop/QLAnchor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,28 @@ public final class QLAnchor<Input>: AnyAnchor {
public typealias EchoFilter = (Input?, QLAnchor<Input>) -> (Bool)
internal static var DefaultEchoFilter: EchoFilter { return { _, _ in return true } }

public enum Timing {
case early, late
}

internal final class Repeater {

weak var anchor: QLAnchor?
init(_ anchor: QLAnchor) {

let timing: Timing

init(_ anchor: QLAnchor, timing: Timing) {
self.timing = timing
self.anchor = anchor
}
func echo(value: Input?, filter: EchoFilter) {

func echo(value: Input?, filter: EchoFilter, timing: Timing) {
if let repeater = self.anchor,
filter(value, repeater) {
repeater.value = value
}
}

func echo(error: Error) {
anchor?.error = error
}
Expand All @@ -42,9 +53,13 @@ public final class QLAnchor<Input>: AnyAnchor {

public var inputSegment: AnySegment?

public var repeaters: [QLAnchor] {
get { return _repeaters.compactMap { $0.anchor } }
set { self._repeaters = newValue.map { Repeater($0) } }
public func addRepeaters(_ repeaters: [QLAnchor], timing: Timing) {
let repeaters = repeaters.map { Repeater($0, timing: timing) }
self._repeaters.append(contentsOf: repeaters)
}

public func getRepeaters(timing: Timing) -> [QLAnchor] {
return _repeaters.compactMap { ($0.timing == timing) ? $0.anchor : nil }
}

internal var _repeaters: [Repeater] = []
Expand All @@ -64,8 +79,9 @@ public final class QLAnchor<Input>: AnyAnchor {
if let err = getReroutableError(newValue) {
self.error = err
} else {
echo(value: newValue, timing: .early)
dispatch(value: newValue)
echo(value: newValue)
echo(value: newValue, timing: .late)
}

if (QLCommon.Config.Anchor.releaseValues) {
Expand Down Expand Up @@ -113,9 +129,10 @@ public final class QLAnchor<Input>: AnyAnchor {
}
}

private func echo(value: Input?) {
private func echo(value: Input?, timing: Timing) {
for repeater in _repeaters {
repeater.echo(value: value, filter: echoFilter)
guard repeater.timing == timing else { continue }
repeater.echo(value: value, filter: echoFilter, timing: timing)
}
}

Expand Down
62 changes: 57 additions & 5 deletions Tests/QLoopTests/QLAnchorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ final class QLAnchorTests: XCTestCase {
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() {
func test_given_it_has_early_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
Expand All @@ -90,7 +90,30 @@ final class QLAnchorTests: XCTestCase {
let expectRepeater2 = expectation(description: "should echo value to repeater2")
let repeater1 = QLAnchor<Int>(onChange: { receivedVal1 = $0!; expectRepeater1.fulfill() })
let repeater2 = QLAnchor<Int>(onChange: { receivedVal2 = $0!; expectRepeater2.fulfill() })
let subject = QLAnchor<Int>(repeaters: repeater1, repeater2)
let subject = QLAnchor<Int>(earlyRepeaters: repeater1, repeater2)
XCTAssertEqual(subject.getRepeaters(timing: .early).count, 2)

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_late_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<Int>(onChange: { receivedVal1 = $0!; expectRepeater1.fulfill() })
let repeater2 = QLAnchor<Int>(onChange: { receivedVal2 = $0!; expectRepeater2.fulfill() })
let subject = QLAnchor<Int>(lateRepeaters: repeater1, repeater2)
XCTAssertEqual(subject.getRepeaters(timing: .late).count, 2)

subject.onChange = { receivedVal0 = $0!; expectOriginal0.fulfill() }

Expand All @@ -113,7 +136,9 @@ final class QLAnchorTests: XCTestCase {
onError: { receivedErr1 = $0; expectRepeater1.fulfill() })
let repeater2 = QLAnchor<Int>(onChange: { _ in },
onError: { receivedErr2 = $0; expectRepeater2.fulfill() })
let subject = QLAnchor<Int>(repeaters: repeater1, repeater2)
let subject = QLAnchor<Int>(earlyRepeaters: [repeater1], lateRepeaters: [repeater2])
XCTAssertEqual(subject.getRepeaters(timing: .early).count, 1)
XCTAssertEqual(subject.getRepeaters(timing: .late).count, 1)

subject.onError = { receivedErr0 = $0; expectOriginal0.fulfill() }

Expand All @@ -125,7 +150,34 @@ final class QLAnchorTests: XCTestCase {
XCTAssertNotNil(receivedErr2)
}

func test_given_it_has_repeaters_with_custom_filter_when_input_set_then_it_dispatches_then_echoes_to_them_conditionally() {
func test_given_it_has_earlyRepeaters_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<Int>(onChange: { receivedVal1 = $0! })
let repeater2 = QLAnchor<Int>(onChange: { receivedVal2 = $0!; expectRepeater2.fulfill() })

let subject = QLAnchor<Int>(
echoFilter: ({ val, repeater in
return (val == 11 && repeater === repeater1)
|| (val == 22 && repeater === repeater2)
}),
earlyRepeaters: 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)
}

func test_given_it_has_lateRepeaters_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
Expand All @@ -139,7 +191,7 @@ final class QLAnchorTests: XCTestCase {
return (val == 11 && repeater === repeater1)
|| (val == 22 && repeater === repeater2)
}),
repeaters: repeater1, repeater2
lateRepeaters: repeater1, repeater2
)

subject.onChange = { receivedVal0 = $0!; expectOriginal0.fulfill() }
Expand Down
6 changes: 6 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@
## Change Log


<br />

### 0.1.7

- `QLAnchor` repeater timing (early/late)

<br />

### 0.1.6
Expand Down
10 changes: 6 additions & 4 deletions docs/reference/QLAnchor.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@

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

- init(repeaters: `QLAnchor.Repeater`, `...` )
- init(earlyRepeaters: `QLAnchor.Repeater`, `...` )

- init(lateRepeaters: `QLAnchor.Repeater`, `...` )


<br />
Expand Down Expand Up @@ -75,9 +77,9 @@ 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.
By default, it forwards all changes to all repeaters. In order to make it conditional,
include an `EchoFilter`. Return `false` from the EchoFilter to block that repeater
from receiving the change.


##### EchoFilter
Expand Down