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
19 changes: 19 additions & 0 deletions Sources/QLoop/QLAnchor+ConvenienceInit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
87 changes: 72 additions & 15 deletions Sources/QLoop/QLAnchor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,25 @@ public final class QLAnchor<Input>: AnyAnchor {
public typealias OnChange = (Input?)->()
public typealias OnError = (Error)->()

public typealias EchoFilter = (Input?, QLAnchor<Input>) -> (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)

Expand All @@ -17,6 +36,22 @@ public final class QLAnchor<Input>: 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
Expand All @@ -25,24 +60,21 @@ public final class QLAnchor<Input>: 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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🏅

self.error = err
} else {
DispatchQueue.main.async {
self.onChange(newValue)
}
dispatch(value: newValue)
echo(value: newValue)
}

if (QLCommon.Config.Anchor.releaseValues) {
inputQueue.sync { self._value = nil }
}
}
}
private var _value: Input?


private var _error: Error?
public var error: Error? {
get {
var safeError: Error? = nil
Expand All @@ -52,19 +84,44 @@ public final class QLAnchor<Input>: 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)
}
}
}
71 changes: 71 additions & 0 deletions Tests/QLoopTests/QLAnchorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Int>(onChange: { receivedVal1 = $0!; expectRepeater1.fulfill() })
let repeater2 = QLAnchor<Int>(onChange: { receivedVal2 = $0!; expectRepeater2.fulfill() })
let subject = QLAnchor<Int>(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<Int>(onChange: { _ in },
onError: { receivedErr1 = $0; expectRepeater1.fulfill() })
let repeater2 = QLAnchor<Int>(onChange: { _ in },
onError: { receivedErr2 = $0; expectRepeater2.fulfill() })
let subject = QLAnchor<Int>(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<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)
}),
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)
}
}
39 changes: 39 additions & 0 deletions docs/reference/QLAnchor.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

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

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


<br />

Expand Down Expand Up @@ -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<DownloadStatus>(
echoFilter: ({ obj, repeater in
return (obj.isProgress && repeater === progressRepeater)
|| (obj.isFinal && repeater === finalRepeater)
}),
repeaters: progressRepeater, finalRepeater
)
```