From 3d237b833c84235827ff260f8f8656496af97b66 Mon Sep 17 00:00:00 2001 From: Christopher Liscio Date: Fri, 30 Sep 2016 06:30:34 -0400 Subject: [PATCH 1/6] Use DispatchTimeInterval to offset Date values This gives us improved expressiveness when specifying time offsets in the scheduler --- Sources/Scheduler.swift | 75 ++++++++++++++----- Sources/SignalProducer.swift | 8 +- Tests/ReactiveSwiftTests/SchedulerSpec.swift | 14 ++-- .../SignalProducerLiftingSpec.swift | 18 ++--- .../SignalProducerSpec.swift | 22 +++--- Tests/ReactiveSwiftTests/SignalSpec.swift | 22 +++--- 6 files changed, 99 insertions(+), 60 deletions(-) diff --git a/Sources/Scheduler.swift b/Sources/Scheduler.swift index 4953db74c..734e2dd02 100644 --- a/Sources/Scheduler.swift +++ b/Sources/Scheduler.swift @@ -13,6 +13,48 @@ import Foundation import CDispatch #endif +extension Date { + internal func addingTimeInterval(_ interval: DispatchTimeInterval) -> Date { + return addingTimeInterval(interval.asTimeInterval()) + } +} + +extension DispatchTimeInterval { + internal func asTimeInterval() -> 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) + } + } + + 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`. + /// + /// - returns: Scaled interval in microseconds + internal static func *(lhs: DispatchTimeInterval, rhs: Double) -> DispatchTimeInterval { + let seconds = lhs.asTimeInterval() * rhs + return .microseconds(Int(seconds * 1000 * 1000)) + } +} + /// Represents a serial queue of work items. public protocol SchedulerProtocol { /// Enqueues an action on the scheduler. @@ -57,7 +99,7 @@ public protocol DateSchedulerProtocol: SchedulerProtocol { /// - 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. @@ -266,7 +308,7 @@ 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, 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) @@ -284,20 +326,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.asTimeInterval() >= 0) + precondition(leeway.asTimeInterval() >= 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() @@ -386,7 +425,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) } @@ -405,8 +444,8 @@ public final class TestScheduler: DateSchedulerProtocol { /// /// - 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.asTimeInterval() >= 0) disposable.innerDisposable = schedule(after: date) { [unowned self] in action() @@ -426,7 +465,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) } @@ -441,7 +480,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 @@ -453,7 +492,7 @@ 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 @@ -461,7 +500,7 @@ public final class TestScheduler: DateSchedulerProtocol { /// /// - 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() @@ -504,7 +543,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) diff --git a/Sources/SignalProducer.swift b/Sources/SignalProducer.swift index a924b015a..5869866b6 100644 --- a/Sources/SignalProducer.swift +++ b/Sources/SignalProducer.swift @@ -1908,7 +1908,7 @@ private struct ReplayState { /// - 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 { +public func timer(interval: DispatchTimeInterval, on scheduler: DateSchedulerProtocol) -> SignalProducer { // 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) @@ -1931,9 +1931,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 { - precondition(interval >= 0) - precondition(leeway >= 0) +public func timer(interval: DispatchTimeInterval, on scheduler: DateSchedulerProtocol, leeway: DispatchTimeInterval) -> SignalProducer { + precondition(interval.asTimeInterval() >= 0) + precondition(leeway.asTimeInterval() >= 0) return SignalProducer { observer, compositeDisposable in compositeDisposable += scheduler.schedule(after: scheduler.currentDate.addingTimeInterval(interval), diff --git a/Tests/ReactiveSwiftTests/SchedulerSpec.swift b/Tests/ReactiveSwiftTests/SchedulerSpec.swift index 2ea04a4d7..297c2c11a 100644 --- a/Tests/ReactiveSwiftTests/SchedulerSpec.swift +++ b/Tests/ReactiveSwiftTests/SchedulerSpec.swift @@ -190,7 +190,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 @@ -246,13 +246,13 @@ 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)) @@ -260,11 +260,11 @@ class SchedulerSpec: QuickSpec { 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" } @@ -272,12 +272,12 @@ class SchedulerSpec: QuickSpec { 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 } diff --git a/Tests/ReactiveSwiftTests/SignalProducerLiftingSpec.swift b/Tests/ReactiveSwiftTests/SignalProducerLiftingSpec.swift index 2c5287968..d3c691361 100644 --- a/Tests/ReactiveSwiftTests/SignalProducerLiftingSpec.swift +++ b/Tests/ReactiveSwiftTests/SignalProducerLiftingSpec.swift @@ -794,7 +794,7 @@ class SignalProducerLiftingSpec: QuickSpec { testScheduler.schedule { observer.send(value: 1) } - testScheduler.schedule(after: 5) { + testScheduler.schedule(after: .seconds(5)) { observer.send(value: 2) observer.sendCompleted() } @@ -816,14 +816,14 @@ class SignalProducerLiftingSpec: QuickSpec { } } - testScheduler.advance(by: 4) // send initial value + testScheduler.advance(by: .seconds(4)) // send initial value expect(result).to(beEmpty()) - testScheduler.advance(by: 10) // send second value and receive first + testScheduler.advance(by: .seconds(10)) // send second value and receive first expect(result) == [ 1 ] expect(completed) == false - testScheduler.advance(by: 10) // send second value and receive first + testScheduler.advance(by: .seconds(10)) // send second value and receive first expect(result) == [ 1, 2 ] expect(completed) == true } @@ -882,10 +882,10 @@ class SignalProducerLiftingSpec: QuickSpec { observer.send(value: 2) expect(values) == [ 0 ] - scheduler.advance(by: 1.5) + scheduler.advance(by: .milliseconds(1500)) expect(values) == [ 0, 2 ] - scheduler.advance(by: 3) + scheduler.advance(by: .seconds(3)) expect(values) == [ 0, 2 ] observer.send(value: 3) @@ -899,7 +899,7 @@ class SignalProducerLiftingSpec: QuickSpec { scheduler.advance() expect(values) == [ 0, 2, 3 ] - scheduler.rewind(by: 2) + scheduler.rewind(by: .seconds(2)) expect(values) == [ 0, 2, 3 ] observer.send(value: 6) @@ -1388,7 +1388,7 @@ class SignalProducerLiftingSpec: QuickSpec { } } - testScheduler.schedule(after: 1) { + testScheduler.schedule(after: .seconds(1)) { observer.sendCompleted() } @@ -1414,7 +1414,7 @@ class SignalProducerLiftingSpec: QuickSpec { } } - testScheduler.schedule(after: 3) { + testScheduler.schedule(after: .seconds(3)) { observer.sendCompleted() } diff --git a/Tests/ReactiveSwiftTests/SignalProducerSpec.swift b/Tests/ReactiveSwiftTests/SignalProducerSpec.swift index 4b5c780ba..054edb7bc 100644 --- a/Tests/ReactiveSwiftTests/SignalProducerSpec.swift +++ b/Tests/ReactiveSwiftTests/SignalProducerSpec.swift @@ -767,7 +767,7 @@ class SignalProducerSpec: QuickSpec { describe("timer") { it("should send the current date at the given interval") { let scheduler = TestScheduler() - let producer = timer(interval: 1, on: scheduler, leeway: 0) + let producer = timer(interval: .seconds(1), on: scheduler, leeway: .seconds(0)) let startDate = scheduler.currentDate let tick1 = startDate.addingTimeInterval(1) @@ -777,19 +777,19 @@ class SignalProducerSpec: QuickSpec { var dates: [Date] = [] producer.startWithValues { dates.append($0) } - scheduler.advance(by: 0.9) + scheduler.advance(by: .milliseconds(900)) expect(dates) == [] - scheduler.advance(by: 1) + scheduler.advance(by: .seconds(1)) expect(dates) == [tick1] scheduler.advance() expect(dates) == [tick1] - scheduler.advance(by: 0.2) + scheduler.advance(by: .milliseconds(200)) expect(dates) == [tick1, tick2] - scheduler.advance(by: 1) + scheduler.advance(by: .seconds(1)) expect(dates) == [tick1, tick2, tick3] } @@ -801,7 +801,7 @@ class SignalProducerSpec: QuickSpec { scheduler = QueueScheduler(queue: DispatchQueue(label: "\(#file):\(#line)")) } - let producer = timer(interval: 3, on: scheduler) + let producer = timer(interval: .seconds(3), on: scheduler) producer .start() .dispose() @@ -809,7 +809,7 @@ class SignalProducerSpec: QuickSpec { it("should release the signal when disposed") { let scheduler = TestScheduler() - let producer = timer(interval: 1, on: scheduler, leeway: 0) + let producer = timer(interval: .seconds(1), on: scheduler, leeway: .seconds(0)) var interrupted = false weak var weakSignal: Signal? @@ -938,18 +938,18 @@ class SignalProducerSpec: QuickSpec { let startScheduler = TestScheduler() let testScheduler = TestScheduler() - let producer = timer(interval: 2, on: testScheduler, leeway: 0) + let producer = timer(interval: .seconds(2), on: testScheduler, leeway: .seconds(0)) var value: Date? producer.start(on: startScheduler).startWithValues { value = $0 } - startScheduler.advance(by: 2) + startScheduler.advance(by: .seconds(2)) expect(value).to(beNil()) - testScheduler.advance(by: 1) + testScheduler.advance(by: .seconds(1)) expect(value).to(beNil()) - testScheduler.advance(by: 1) + testScheduler.advance(by: .seconds(1)) expect(value) == testScheduler.currentDate } } diff --git a/Tests/ReactiveSwiftTests/SignalSpec.swift b/Tests/ReactiveSwiftTests/SignalSpec.swift index e85294959..88479931c 100755 --- a/Tests/ReactiveSwiftTests/SignalSpec.swift +++ b/Tests/ReactiveSwiftTests/SignalSpec.swift @@ -1174,7 +1174,7 @@ class SignalSpec: QuickSpec { testScheduler.schedule { observer.send(value: 1) } - testScheduler.schedule(after: 5) { + testScheduler.schedule(after: .seconds(5)) { observer.send(value: 2) observer.sendCompleted() } @@ -1197,14 +1197,14 @@ class SignalSpec: QuickSpec { } } - testScheduler.advance(by: 4) // send initial value + testScheduler.advance(by: .seconds(4)) // send initial value expect(result).to(beEmpty()) - testScheduler.advance(by: 10) // send second value and receive first + testScheduler.advance(by: .seconds(10)) // send second value and receive first expect(result) == [ 1 ] expect(completed) == false - testScheduler.advance(by: 10) // send second value and receive first + testScheduler.advance(by: .seconds(10)) // send second value and receive first expect(result) == [ 1, 2 ] expect(completed) == true } @@ -1262,10 +1262,10 @@ class SignalSpec: QuickSpec { observer.send(value: 2) expect(values) == [ 0 ] - scheduler.advance(by: 1.5) + scheduler.advance(by: .milliseconds(1500)) expect(values) == [ 0, 2 ] - scheduler.advance(by: 3) + scheduler.advance(by: .seconds(3)) expect(values) == [ 0, 2 ] observer.send(value: 3) @@ -1279,7 +1279,7 @@ class SignalSpec: QuickSpec { scheduler.advance() expect(values) == [ 0, 2, 3 ] - scheduler.rewind(by: 2) + scheduler.rewind(by: .seconds(2)) expect(values) == [ 0, 2, 3 ] observer.send(value: 6) @@ -1361,10 +1361,10 @@ class SignalSpec: QuickSpec { observer.send(value: 2) expect(values) == [] - scheduler.advance(by: 1.5) + scheduler.advance(by: .milliseconds(1500)) expect(values) == [ 2 ] - scheduler.advance(by: 3) + scheduler.advance(by: .seconds(3)) expect(values) == [ 2 ] observer.send(value: 3) @@ -1854,7 +1854,7 @@ class SignalSpec: QuickSpec { } } - testScheduler.schedule(after: 1) { + testScheduler.schedule(after: .seconds(1)) { observer.sendCompleted() } @@ -1880,7 +1880,7 @@ class SignalSpec: QuickSpec { } } - testScheduler.schedule(after: 3) { + testScheduler.schedule(after: .seconds(3)) { observer.sendCompleted() } From c13c3e63bb221ee90060cde4e7eebcc76e70156e Mon Sep 17 00:00:00 2001 From: Christopher Liscio Date: Tue, 25 Oct 2016 12:35:43 -0400 Subject: [PATCH 2/6] Added removal notices, code comments, and tests. --- Sources/Deprecations+Removals.swift | 18 ++++++ Sources/FoundationExtensions.swift | 62 +++++++++++++++++++ Sources/Scheduler.swift | 60 +++++------------- Sources/SignalProducer.swift | 8 ++- .../FoundationExtensionsSpec.swift | 29 ++++++++- 5 files changed, 129 insertions(+), 48 deletions(-) diff --git a/Sources/Deprecations+Removals.swift b/Sources/Deprecations+Removals.swift index dac20df08..3243369bb 100644 --- a/Sources/Deprecations+Removals.swift +++ b/Sources/Deprecations+Removals.swift @@ -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 { @@ -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 { @@ -332,6 +344,12 @@ extension QueueScheduler { // Free functions +@available(*, unavailable, message:"timer(interval:on:) now uses DispatchTimeInterval") +public func timer(interval: TimeInterval, on scheduler: DateSchedulerProtocol) -> SignalProducer { fatalError() } + +@available(*, unavailable, message:"timer(interval:on:leeway:) now uses DispatchTimeInterval") +public func timer(interval: TimeInterval, on scheduler: DateSchedulerProtocol, leeway: TimeInterval) -> SignalProducer { fatalError() } + @available(*, unavailable, renamed:"Signal.combineLatest") public func combineLatest(_ a: Signal, _ b: Signal) -> Signal<(A, B), Error> { fatalError() } diff --git a/Sources/FoundationExtensions.swift b/Sources/FoundationExtensions.swift index 20d7499e0..2eabcae4a 100644 --- a/Sources/FoundationExtensions.swift +++ b/Sources/FoundationExtensions.swift @@ -9,6 +9,10 @@ import Foundation import enum Result.NoError +#if os(Linux) + import CDispatch +#endif + extension NotificationCenter { /// Returns a SignalProducer to observe posting of the specified /// notification. @@ -78,3 +82,61 @@ extension 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)) + } +} diff --git a/Sources/Scheduler.swift b/Sources/Scheduler.swift index 734e2dd02..6462b6557 100644 --- a/Sources/Scheduler.swift +++ b/Sources/Scheduler.swift @@ -13,48 +13,6 @@ import Foundation import CDispatch #endif -extension Date { - internal func addingTimeInterval(_ interval: DispatchTimeInterval) -> Date { - return addingTimeInterval(interval.asTimeInterval()) - } -} - -extension DispatchTimeInterval { - internal func asTimeInterval() -> 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) - } - } - - 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`. - /// - /// - returns: Scaled interval in microseconds - internal static func *(lhs: DispatchTimeInterval, rhs: Double) -> DispatchTimeInterval { - let seconds = lhs.asTimeInterval() * rhs - return .microseconds(Int(seconds * 1000 * 1000)) - } -} - /// Represents a serial queue of work items. public protocol SchedulerProtocol { /// Enqueues an action on the scheduler. @@ -96,6 +54,10 @@ 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. + /// /// - returns: Optional `Disposable` that can be used to cancel the work /// before it begins. @discardableResult @@ -305,6 +267,10 @@ 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 @@ -327,8 +293,8 @@ public final class QueueScheduler: DateSchedulerProtocol { /// before it begins. @discardableResult public func schedule(after date: Date, interval: DispatchTimeInterval, leeway: DispatchTimeInterval, action: @escaping () -> Void) -> Disposable? { - precondition(interval.asTimeInterval() >= 0) - precondition(leeway.asTimeInterval() >= 0) + precondition(interval.timeInterval >= 0) + precondition(leeway.timeInterval >= 0) let timer = DispatchSource.makeTimerSource( flags: DispatchSource.TimerFlags(rawValue: UInt(0)), @@ -442,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: DispatchTimeInterval, disposable: SerialDisposable, action: @escaping () -> Void) { - precondition(interval.asTimeInterval() >= 0) + precondition(interval.timeInterval >= 0) disposable.innerDisposable = schedule(after: date) { [unowned self] in action() diff --git a/Sources/SignalProducer.swift b/Sources/SignalProducer.swift index 5869866b6..72cdaa274 100644 --- a/Sources/SignalProducer.swift +++ b/Sources/SignalProducer.swift @@ -1903,6 +1903,10 @@ private struct ReplayState { /// /// - 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. @@ -1932,8 +1936,8 @@ public func timer(interval: DispatchTimeInterval, on scheduler: DateSchedulerPro /// /// - returns: A producer that sends `NSDate` values every `interval` seconds. public func timer(interval: DispatchTimeInterval, on scheduler: DateSchedulerProtocol, leeway: DispatchTimeInterval) -> SignalProducer { - precondition(interval.asTimeInterval() >= 0) - precondition(leeway.asTimeInterval() >= 0) + precondition(interval.timeInterval >= 0) + precondition(leeway.timeInterval >= 0) return SignalProducer { observer, compositeDisposable in compositeDisposable += scheduler.schedule(after: scheduler.currentDate.addingTimeInterval(interval), diff --git a/Tests/ReactiveSwiftTests/FoundationExtensionsSpec.swift b/Tests/ReactiveSwiftTests/FoundationExtensionsSpec.swift index fc1c145d5..9bd67b442 100644 --- a/Tests/ReactiveSwiftTests/FoundationExtensionsSpec.swift +++ b/Tests/ReactiveSwiftTests/FoundationExtensionsSpec.swift @@ -9,7 +9,7 @@ import Result import Nimble import Quick -import ReactiveSwift +@testable import ReactiveSwift extension Notification.Name { static let racFirst = Notification.Name(rawValue: "rac_notifications_test") @@ -55,5 +55,32 @@ class FoundationExtensionsSpec: QuickSpec { } } + + 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)) + } + } } } From b820e10b746bf090bcbf49ebd1755ad3a811fafa Mon Sep 17 00:00:00 2001 From: Christopher Liscio Date: Tue, 25 Oct 2016 15:04:52 -0400 Subject: [PATCH 3/6] Attempting to fix a Linux build issue According to /~https://github.com/AlwaysRightInstitute/CDispatch, there should no longer be a need to reference CDispatch. --- Sources/FoundationExtensions.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Sources/FoundationExtensions.swift b/Sources/FoundationExtensions.swift index f5431fc33..9660e66db 100644 --- a/Sources/FoundationExtensions.swift +++ b/Sources/FoundationExtensions.swift @@ -7,12 +7,9 @@ // import Foundation +import Dispatch import enum Result.NoError -#if os(Linux) - import CDispatch -#endif - extension NotificationCenter: ReactiveExtensionsProvider {} extension Reactive where Base: NotificationCenter { From c2f5d1a78d5b8b4626ed78182182ac0ff06c3258 Mon Sep 17 00:00:00 2001 From: Christopher Liscio Date: Thu, 27 Oct 2016 10:03:58 -0400 Subject: [PATCH 4/6] Importing NSEC_PER_SEC from CDispatch --- Sources/FoundationExtensions.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/FoundationExtensions.swift b/Sources/FoundationExtensions.swift index 9660e66db..01c244589 100644 --- a/Sources/FoundationExtensions.swift +++ b/Sources/FoundationExtensions.swift @@ -10,6 +10,10 @@ import Foundation import Dispatch import enum Result.NoError +#if os(Linux) + import let CDispatch.NSEC_PER_SEC +#endif + extension NotificationCenter: ReactiveExtensionsProvider {} extension Reactive where Base: NotificationCenter { From 57a04225d1efa4868915df794f55ac1c5e815a0b Mon Sep 17 00:00:00 2001 From: Christopher Liscio Date: Thu, 27 Oct 2016 19:23:18 -0400 Subject: [PATCH 5/6] Another attempt to appease the Linux build --- Sources/FoundationExtensions.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/FoundationExtensions.swift b/Sources/FoundationExtensions.swift index 01c244589..f63aedb6a 100644 --- a/Sources/FoundationExtensions.swift +++ b/Sources/FoundationExtensions.swift @@ -11,6 +11,7 @@ import Dispatch import enum Result.NoError #if os(Linux) + import let CDispatch.NSEC_PER_USEC import let CDispatch.NSEC_PER_SEC #endif From b39557679aba0be67360fe6193c5aa777fd7fb59 Mon Sep 17 00:00:00 2001 From: Christopher Liscio Date: Thu, 27 Oct 2016 21:50:27 -0400 Subject: [PATCH 6/6] Another attempt at fixing the Linux build --- Tests/ReactiveSwiftTests/FoundationExtensionsSpec.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ReactiveSwiftTests/FoundationExtensionsSpec.swift b/Tests/ReactiveSwiftTests/FoundationExtensionsSpec.swift index 1d1a7a9bb..51814ee8a 100644 --- a/Tests/ReactiveSwiftTests/FoundationExtensionsSpec.swift +++ b/Tests/ReactiveSwiftTests/FoundationExtensionsSpec.swift @@ -7,7 +7,7 @@ // import Foundation - +import Dispatch import Result import Nimble import Quick