Skip to content

Add JSTimer implementation with tests #46

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

Merged
merged 9 commits into from
Sep 15, 2020
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
30 changes: 30 additions & 0 deletions IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,36 @@ try test("Date") {
try expectEqual(date3 < date1, true)
}

// make the timers global to prevent early deallocation
var timeout: JSTimer?
var interval: JSTimer?

try test("Timer") {
let start = JSDate().valueOf()
let timeoutMilliseconds = 5.0

timeout = JSTimer(millisecondsDelay: timeoutMilliseconds, isRepeating: false) {
// verify that at least `timeoutMilliseconds` passed since the `timeout` timer started
try! expectEqual(start + timeoutMilliseconds <= JSDate().valueOf(), true)
}

var count = 0.0
let maxCount = 5.0
interval = JSTimer(millisecondsDelay: 5, isRepeating: true) {
// verify that at least `timeoutMilliseconds * count` passed since the `timeout`
// timer started
try! expectEqual(start + timeoutMilliseconds * count <= JSDate().valueOf(), true)

guard count < maxCount else {
// stop the timer after `maxCount` reached
interval = nil
return
}

count += 1
}
}

try test("Error") {
let message = "test error"
let error = JSError(message: message)
Expand Down
58 changes: 58 additions & 0 deletions Sources/JavaScriptKit/BasicObjects/JSTimer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/** This timer type hides [`setInterval`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setInterval)
/ [`clearInterval`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/clearInterval) and
[`setTimeout`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout)
/ [`clearTimeout`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout)
pairs of calls for you. It intentionally doesn't match the JavaScript API, as a special care is
needed to hold a reference to the timer closure and to call `JSClosure.release()` on it when the
timer is deallocated. As a user, you have to hold a reference to a `JSTimer` instance for it to stay
valid. The `JSTimer` API is also intentionally trivial, the timer is started right away, and the
only way to invalidate the timer is to bring the reference count of the `JSTimer` instance to zero,
either by storing the timer in an optional property and assigning `nil` to it or by deallocating the
object that owns it for invalidation.
*/
public final class JSTimer {
/// Indicates whether this timer instance calls its callback repeatedly at a given delay.
public let isRepeating: Bool

private let closure: JSClosure

/** Node.js and browser APIs are slightly different. `setTimeout`/`setInterval` return an object
in Node.js, while browsers return a number. Fortunately, clearTimeout and clearInterval take
corresponding types as their arguments, and we can store either as JSValue, so we can treat both
cases uniformly.
*/
private let value: JSValue
private let global = JSObject.global

/**
Creates a new timer instance that calls `setInterval` or `setTimeout` JavaScript functions for you
under the hood.
- Parameters:
- millisecondsDelay: the amount of milliseconds before the `callback` closure is executed.
- isRepeating: when `true` the `callback` closure is executed repeatedly at given
`millisecondsDelay` intervals indefinitely until the timer is deallocated.
- callback: the closure to be executed after a given `millisecondsDelay` interval.
*/
public init(millisecondsDelay: Double, isRepeating: Bool = false, callback: @escaping () -> ()) {
closure = JSClosure { _ in callback() }
self.isRepeating = isRepeating
if isRepeating {
value = global.setInterval.function!(closure, millisecondsDelay)
} else {
value = global.setTimeout.function!(closure, millisecondsDelay)
}
}

/** Makes a corresponding `clearTimeout` or `clearInterval` call, depending on whether this timer
instance is repeating. The `closure` instance is released manually here, as it is required for
bridged closure instances.
*/
deinit {
if isRepeating {
global.clearInterval.function!(value)
} else {
global.clearTimeout.function!(value)
}
closure.release()
}
}