Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use DispatchTimeInterval to offset Date values #48

Merged
merged 8 commits into from
Nov 6, 2016
18 changes: 18 additions & 0 deletions Sources/Deprecations+Removals.swift
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,12 @@ extension DateSchedulerProtocol {

@available(*, unavailable, renamed:"schedule(after:interval:leeway:)")
func scheduleAfter(date: Date, repeatingEvery: TimeInterval, withLeeway: TimeInterval, action: () -> Void) -> Disposable? { fatalError() }

@available(*, unavailable, message:"schedule(after:interval:leeway:action:) now uses DispatchTimeInterval")
func schedule(after date: Date, interval: TimeInterval, leeway: TimeInterval, action: @escaping () -> Void) -> Disposable? { fatalError() }

@available(*, unavailable, message:"schedule(after:interval:action:) now uses DispatchTimeInterval")
public func schedule(after date: Date, interval: TimeInterval, action: @escaping () -> Void) -> Disposable? { fatalError() }
}

extension TestScheduler {
Expand All @@ -323,6 +329,12 @@ extension TestScheduler {

@available(*, unavailable, renamed:"advance(to:)")
public func advanceToDate(_ date: Date) { fatalError() }

@available(*, unavailable, message:"advance(by:) now uses DispatchTimeInterval")
public func advance(by interval: TimeInterval) { fatalError() }

@available(*, unavailable, message:"rewind(by:) now uses DispatchTimeInterval")
public func rewind(by interval: TimeInterval) { fatalError() }
}

extension QueueScheduler {
Expand All @@ -342,6 +354,12 @@ extension URLSession {

// Free functions

@available(*, unavailable, message:"timer(interval:on:) now uses DispatchTimeInterval")
public func timer(interval: TimeInterval, on scheduler: DateSchedulerProtocol) -> SignalProducer<Date, NoError> { fatalError() }

@available(*, unavailable, message:"timer(interval:on:leeway:) now uses DispatchTimeInterval")
public func timer(interval: TimeInterval, on scheduler: DateSchedulerProtocol, leeway: TimeInterval) -> SignalProducer<Date, NoError> { fatalError() }

@available(*, unavailable, renamed:"Signal.combineLatest")
public func combineLatest<A, B, Error>(_ a: Signal<A, Error>, _ b: Signal<B, Error>) -> Signal<(A, B), Error> { fatalError() }

Expand Down
64 changes: 64 additions & 0 deletions Sources/FoundationExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,14 @@
//

import Foundation
import Dispatch
import enum Result.NoError

#if os(Linux)
import let CDispatch.NSEC_PER_USEC
import let CDispatch.NSEC_PER_SEC
#endif

extension NotificationCenter: ReactiveExtensionsProvider {}

extension Reactive where Base: NotificationCenter {
Expand Down Expand Up @@ -73,3 +79,61 @@ extension Reactive where Base: URLSession {
}
}
}

extension Date {
internal func addingTimeInterval(_ interval: DispatchTimeInterval) -> Date {
return addingTimeInterval(interval.timeInterval)
}
}

extension DispatchTimeInterval {
internal var timeInterval: TimeInterval {
switch self {
case let .seconds(s):
return TimeInterval(s)
case let .milliseconds(ms):
return TimeInterval(TimeInterval(ms) / 1000.0)
case let .microseconds(us):
return TimeInterval( UInt64(us) * NSEC_PER_USEC ) / TimeInterval(NSEC_PER_SEC)
case let .nanoseconds(ns):
return TimeInterval(ns) / TimeInterval(NSEC_PER_SEC)
}
}

// This was added purely so that our test scheduler to "go backwards" in
// time. See `TestScheduler.rewind(by interval: DispatchTimeInterval)`.
internal static prefix func -(lhs: DispatchTimeInterval) -> DispatchTimeInterval {
switch lhs {
case let .seconds(s):
return .seconds(-s)
case let .milliseconds(ms):
return .milliseconds(-ms)
case let .microseconds(us):
return .microseconds(-us)
case let .nanoseconds(ns):
return .nanoseconds(-ns)
}
}

/// Scales a time interval by the given scalar specified in `rhs`.
///
/// - note: This method is only used internally to "scale down" a time
/// interval. Specifically it's used only to scale intervals to 10%
/// of their original value for the default `leeway` parameter in
/// `SchedulerProtocol.schedule(after:action:)` schedule and similar
/// other methods.
///
/// If seconds is over 200,000, 10% is ~2,000, and hence we end up
/// with a value of ~2,000,000,000. Not quite overflowing a signed
/// integer on 32-bit platforms, but close.
///
/// Even still, 200,000 seconds should be a rarely (if ever)
/// specified interval for our APIs. And even then, folks should be
/// smart and specify their own `leeway` parameter.
///
/// - returns: Scaled interval in microseconds
internal static func *(lhs: DispatchTimeInterval, rhs: Double) -> DispatchTimeInterval {
let seconds = lhs.timeInterval * rhs
return .microseconds(Int(seconds * 1000 * 1000))
}
}
45 changes: 27 additions & 18 deletions Sources/Scheduler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,14 @@ public protocol DateSchedulerProtocol: SchedulerProtocol {
/// - withLeeway: Some delta for repetition.
/// - action: Closure of the action to perform.
///
/// - note: If you plan to specify an `interval` value greater than 200,000
/// seconds, use `schedule(after:interval:leeway:action)` instead
/// and specify your own `leeway` value to avoid potential overflow.
///
Copy link
Member

Choose a reason for hiding this comment

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

Awesome. Can you add the deprecations for all these?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added them all in the "deprecations bucket file."

/// - returns: Optional `Disposable` that can be used to cancel the work
/// before it begins.
@discardableResult
func schedule(after date: Date, interval: TimeInterval, leeway: TimeInterval, action: @escaping () -> Void) -> Disposable?
func schedule(after date: Date, interval: DispatchTimeInterval, leeway: DispatchTimeInterval, action: @escaping () -> Void) -> Disposable?
}

/// A scheduler that performs all work synchronously.
Expand Down Expand Up @@ -263,10 +267,14 @@ public final class QueueScheduler: DateSchedulerProtocol {
/// - repeatingEvery: Repetition interval.
/// - action: Closure of the action to repeat.
///
/// - note: If you plan to specify an `interval` value greater than 200,000
/// seconds, use `schedule(after:interval:leeway:action)` instead
/// and specify your own `leeway` value to avoid potential overflow.
///
/// - returns: Optional disposable that can be used to cancel the work
/// before it begins.
@discardableResult
public func schedule(after date: Date, interval: TimeInterval, action: @escaping () -> Void) -> Disposable? {
public func schedule(after date: Date, interval: DispatchTimeInterval, action: @escaping () -> Void) -> Disposable? {
// Apple's "Power Efficiency Guide for Mac Apps" recommends a leeway of
// at least 10% of the timer interval.
return schedule(after: date, interval: interval, leeway: interval * 0.1, action: action)
Expand All @@ -284,20 +292,17 @@ public final class QueueScheduler: DateSchedulerProtocol {
/// - returns: Optional `Disposable` that can be used to cancel the work
/// before it begins.
@discardableResult
public func schedule(after date: Date, interval: TimeInterval, leeway: TimeInterval, action: @escaping () -> Void) -> Disposable? {
precondition(interval >= 0)
precondition(leeway >= 0)

let msecInterval = interval * 1000
let msecLeeway = leeway * 1000
public func schedule(after date: Date, interval: DispatchTimeInterval, leeway: DispatchTimeInterval, action: @escaping () -> Void) -> Disposable? {
precondition(interval.timeInterval >= 0)
precondition(leeway.timeInterval >= 0)

let timer = DispatchSource.makeTimerSource(
flags: DispatchSource.TimerFlags(rawValue: UInt(0)),
queue: queue
)
timer.scheduleRepeating(wallDeadline: wallTime(with: date),
interval: .milliseconds(Int(msecInterval)),
leeway: .milliseconds(Int(msecLeeway)))
interval: interval,
leeway: leeway)
timer.setEventHandler(handler: action)
timer.resume()

Expand Down Expand Up @@ -386,7 +391,7 @@ public final class TestScheduler: DateSchedulerProtocol {
/// - returns: Optional disposable that can be used to cancel the work
/// before it begins.
@discardableResult
public func schedule(after delay: TimeInterval, action: @escaping () -> Void) -> Disposable? {
public func schedule(after delay: DispatchTimeInterval, action: @escaping () -> Void) -> Disposable? {
return schedule(after: currentDate.addingTimeInterval(delay), action: action)
}

Expand All @@ -403,10 +408,14 @@ public final class TestScheduler: DateSchedulerProtocol {
/// - repeatingEvery: Repetition interval.
/// - action: Closure of the action to repeat.
///
/// - note: If you plan to specify an `interval` value greater than 200,000
/// seconds, use `schedule(after:interval:leeway:action)` instead
/// and specify your own `leeway` value to avoid potential overflow.
///
/// - returns: Optional `Disposable` that can be used to cancel the work
/// before it begins.
private func schedule(after date: Date, interval: TimeInterval, disposable: SerialDisposable, action: @escaping () -> Void) {
precondition(interval >= 0)
private func schedule(after date: Date, interval: DispatchTimeInterval, disposable: SerialDisposable, action: @escaping () -> Void) {
precondition(interval.timeInterval >= 0)

disposable.innerDisposable = schedule(after: date) { [unowned self] in
action()
Expand All @@ -426,7 +435,7 @@ public final class TestScheduler: DateSchedulerProtocol {
/// - returns: Optional `Disposable` that can be used to cancel the work
/// before it begins.
@discardableResult
public func schedule(after delay: TimeInterval, interval: TimeInterval, leeway: TimeInterval = 0, action: @escaping () -> Void) -> Disposable? {
public func schedule(after delay: DispatchTimeInterval, interval: DispatchTimeInterval, leeway: DispatchTimeInterval = .seconds(0), action: @escaping () -> Void) -> Disposable? {
return schedule(after: currentDate.addingTimeInterval(delay), interval: interval, leeway: leeway, action: action)
}

Expand All @@ -441,7 +450,7 @@ public final class TestScheduler: DateSchedulerProtocol {
///
/// - returns: Optional `Disposable` that can be used to cancel the work
/// before it begins.
public func schedule(after date: Date, interval: TimeInterval, leeway: TimeInterval = 0, action: @escaping () -> Void) -> Disposable? {
public func schedule(after date: Date, interval: DispatchTimeInterval, leeway: DispatchTimeInterval = .seconds(0), action: @escaping () -> Void) -> Disposable? {
let disposable = SerialDisposable()
schedule(after: date, interval: interval, disposable: disposable, action: action)
return disposable
Expand All @@ -453,15 +462,15 @@ public final class TestScheduler: DateSchedulerProtocol {
/// This is intended to be used as a way to execute actions that have been
/// scheduled to run as soon as possible.
public func advance() {
advance(by: Double.ulpOfOne)
advance(by: .nanoseconds(1))
}

/// Advances the virtualized clock by the given interval, dequeuing and
/// executing any actions along the way.
///
/// - parameters:
/// - interval: Interval by which the current date will be advanced.
public func advance(by interval: TimeInterval) {
public func advance(by interval: DispatchTimeInterval) {
lock.lock()
advance(to: currentDate.addingTimeInterval(interval))
lock.unlock()
Expand Down Expand Up @@ -504,7 +513,7 @@ public final class TestScheduler: DateSchedulerProtocol {
///
/// - parameters:
/// - interval: Interval by which the current date will be retreated.
public func rewind(by interval: TimeInterval) {
public func rewind(by interval: DispatchTimeInterval) {
lock.lock()

let newDate = currentDate.addingTimeInterval(-interval)
Expand Down
12 changes: 8 additions & 4 deletions Sources/SignalProducer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1919,12 +1919,16 @@ private struct ReplayState<Value, Error: Swift.Error> {
///
/// - precondition: Interval must be non-negative number.
///
/// - note: If you plan to specify an `interval` value greater than 200,000
/// seconds, use `timer(interval:on:leeway:)` instead
/// and specify your own `leeway` value to avoid potential overflow.
///
/// - parameters:
/// - interval: An interval between invocations.
/// - scheduler: A scheduler to deliver events on.
///
/// - returns: A producer that sends `NSDate` values every `interval` seconds.
public func timer(interval: TimeInterval, on scheduler: DateSchedulerProtocol) -> SignalProducer<Date, NoError> {
public func timer(interval: DispatchTimeInterval, on scheduler: DateSchedulerProtocol) -> SignalProducer<Date, NoError> {
// Apple's "Power Efficiency Guide for Mac Apps" recommends a leeway of
// at least 10% of the timer interval.
return timer(interval: interval, on: scheduler, leeway: interval * 0.1)
Expand All @@ -1947,9 +1951,9 @@ public func timer(interval: TimeInterval, on scheduler: DateSchedulerProtocol) -
/// recommends a leeway of at least 10% of the timer interval.
///
/// - returns: A producer that sends `NSDate` values every `interval` seconds.
public func timer(interval: TimeInterval, on scheduler: DateSchedulerProtocol, leeway: TimeInterval) -> SignalProducer<Date, NoError> {
precondition(interval >= 0)
precondition(leeway >= 0)
public func timer(interval: DispatchTimeInterval, on scheduler: DateSchedulerProtocol, leeway: DispatchTimeInterval) -> SignalProducer<Date, NoError> {
precondition(interval.timeInterval >= 0)
precondition(leeway.timeInterval >= 0)

return SignalProducer { observer, compositeDisposable in
compositeDisposable += scheduler.schedule(after: scheduler.currentDate.addingTimeInterval(interval),
Expand Down
31 changes: 29 additions & 2 deletions Tests/ReactiveSwiftTests/FoundationExtensionsSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
//

import Foundation

import Dispatch
import Result
import Nimble
import Quick
import ReactiveSwift
@testable import ReactiveSwift

extension Notification.Name {
static let racFirst = Notification.Name(rawValue: "rac_notifications_test")
Expand Down Expand Up @@ -85,5 +85,32 @@ class FoundationExtensionsSpec: QuickSpec {
expect(signal).toNot(beNil())
}
}

describe("DispatchTimeInterval") {
it("should scale time values as expected") {
expect((DispatchTimeInterval.seconds(1) * 0.1).timeInterval).to(beCloseTo(DispatchTimeInterval.milliseconds(100).timeInterval))
expect((DispatchTimeInterval.milliseconds(100) * 0.1).timeInterval).to(beCloseTo(DispatchTimeInterval.microseconds(10000).timeInterval))

expect((DispatchTimeInterval.seconds(5) * 0.5).timeInterval).to(beCloseTo(DispatchTimeInterval.milliseconds(2500).timeInterval))
expect((DispatchTimeInterval.seconds(1) * 0.25).timeInterval).to(beCloseTo(DispatchTimeInterval.milliseconds(250).timeInterval))
}

it("should produce the expected TimeInterval values") {
expect(DispatchTimeInterval.seconds(1).timeInterval).to(beCloseTo(1.0))
expect(DispatchTimeInterval.milliseconds(1).timeInterval).to(beCloseTo(0.001))
expect(DispatchTimeInterval.microseconds(1).timeInterval).to(beCloseTo(0.000001, within: 0.0000001))
expect(DispatchTimeInterval.nanoseconds(1).timeInterval).to(beCloseTo(0.000000001, within: 0.0000000001))

expect(DispatchTimeInterval.milliseconds(500).timeInterval).to(beCloseTo(0.5))
expect(DispatchTimeInterval.milliseconds(250).timeInterval).to(beCloseTo(0.25))
}

it("should negate as you'd hope") {
expect(-DispatchTimeInterval.seconds(1).timeInterval).to(beCloseTo(-1.0))
expect(-DispatchTimeInterval.milliseconds(1).timeInterval).to(beCloseTo(-0.001))
expect(-DispatchTimeInterval.microseconds(1).timeInterval).to(beCloseTo(-0.000001, within: 0.0000001))
expect(-DispatchTimeInterval.nanoseconds(1).timeInterval).to(beCloseTo(-0.000000001, within: 0.0000000001))
}
}
}
}
14 changes: 7 additions & 7 deletions Tests/ReactiveSwiftTests/SchedulerSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ class SchedulerSpec: QuickSpec {
var count = 0
let timesToRun = 3

disposable.innerDisposable = scheduler.schedule(after: Date(), interval: 0.01, leeway: 0) {
disposable.innerDisposable = scheduler.schedule(after: Date(), interval: .milliseconds(10), leeway: .seconds(0)) {
expect(Thread.isMainThread) == false

count += 1
Expand Down Expand Up @@ -259,38 +259,38 @@ class SchedulerSpec: QuickSpec {
it("should run actions when advanced past the target date") {
var string = ""

scheduler.schedule(after: 15) { [weak scheduler] in
scheduler.schedule(after: .seconds(15)) { [weak scheduler] in
string += "bar"
expect(Thread.isMainThread) == true
expect(scheduler?.currentDate).to(beCloseTo(startDate.addingTimeInterval(15), within: dateComparisonDelta))
}

scheduler.schedule(after: 5) { [weak scheduler] in
scheduler.schedule(after: .seconds(5)) { [weak scheduler] in
string += "foo"
expect(Thread.isMainThread) == true
expect(scheduler?.currentDate).to(beCloseTo(startDate.addingTimeInterval(5), within: dateComparisonDelta))
}

expect(string) == ""

scheduler.advance(by: 10)
scheduler.advance(by: .seconds(10))
expect(scheduler.currentDate).to(beCloseTo(startDate.addingTimeInterval(10), within: TimeInterval(dateComparisonDelta)))
expect(string) == "foo"

scheduler.advance(by: 10)
scheduler.advance(by: .seconds(10))
expect(scheduler.currentDate).to(beCloseTo(startDate.addingTimeInterval(20), within: dateComparisonDelta))
expect(string) == "foobar"
}

it("should run all remaining actions in order") {
var string = ""

scheduler.schedule(after: 15) {
scheduler.schedule(after: .seconds(15)) {
string += "bar"
expect(Thread.isMainThread) == true
}

scheduler.schedule(after: 5) {
scheduler.schedule(after: .seconds(5)) {
string += "foo"
expect(Thread.isMainThread) == true
}
Expand Down
Loading