Skip to content

Commit 0ac8b37

Browse files
authoredSep 15, 2020
Add JSTimer implementation with tests (#46)
Depends on #45. This is also a prerequisite for a future `JSPromise` PR with tests. I intentionally didn't match the JS API in this PR, as a special care is needed to hold a reference to the timer closure and to call `.release()` on it. Here, a user is supposed to hold a reference to a `JSTimer` instance for it to stay valid. The API is also intentionally simple, the timer is started right away, and the only way to invalidate the timer is to bring its reference count to zero. If you see a better way to manage closures passed to `setTimeout` and `setInterval`, I'd be happy to consider that. Also, 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.
1 parent 3220b3c commit 0ac8b37

File tree

2 files changed

+88
-0
lines changed

2 files changed

+88
-0
lines changed
 

‎IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,36 @@ try test("Date") {
435435
try expectEqual(date3 < date1, true)
436436
}
437437

438+
// make the timers global to prevent early deallocation
439+
var timeout: JSTimer?
440+
var interval: JSTimer?
441+
442+
try test("Timer") {
443+
let start = JSDate().valueOf()
444+
let timeoutMilliseconds = 5.0
445+
446+
timeout = JSTimer(millisecondsDelay: timeoutMilliseconds, isRepeating: false) {
447+
// verify that at least `timeoutMilliseconds` passed since the `timeout` timer started
448+
try! expectEqual(start + timeoutMilliseconds <= JSDate().valueOf(), true)
449+
}
450+
451+
var count = 0.0
452+
let maxCount = 5.0
453+
interval = JSTimer(millisecondsDelay: 5, isRepeating: true) {
454+
// verify that at least `timeoutMilliseconds * count` passed since the `timeout`
455+
// timer started
456+
try! expectEqual(start + timeoutMilliseconds * count <= JSDate().valueOf(), true)
457+
458+
guard count < maxCount else {
459+
// stop the timer after `maxCount` reached
460+
interval = nil
461+
return
462+
}
463+
464+
count += 1
465+
}
466+
}
467+
438468
try test("Error") {
439469
let message = "test error"
440470
let error = JSError(message: message)
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/** This timer type hides [`setInterval`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setInterval)
2+
/ [`clearInterval`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/clearInterval) and
3+
[`setTimeout`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout)
4+
/ [`clearTimeout`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout)
5+
pairs of calls for you. It intentionally doesn't match the JavaScript API, as a special care is
6+
needed to hold a reference to the timer closure and to call `JSClosure.release()` on it when the
7+
timer is deallocated. As a user, you have to hold a reference to a `JSTimer` instance for it to stay
8+
valid. The `JSTimer` API is also intentionally trivial, the timer is started right away, and the
9+
only way to invalidate the timer is to bring the reference count of the `JSTimer` instance to zero,
10+
either by storing the timer in an optional property and assigning `nil` to it or by deallocating the
11+
object that owns it for invalidation.
12+
*/
13+
public final class JSTimer {
14+
/// Indicates whether this timer instance calls its callback repeatedly at a given delay.
15+
public let isRepeating: Bool
16+
17+
private let closure: JSClosure
18+
19+
/** Node.js and browser APIs are slightly different. `setTimeout`/`setInterval` return an object
20+
in Node.js, while browsers return a number. Fortunately, clearTimeout and clearInterval take
21+
corresponding types as their arguments, and we can store either as JSValue, so we can treat both
22+
cases uniformly.
23+
*/
24+
private let value: JSValue
25+
private let global = JSObject.global
26+
27+
/**
28+
Creates a new timer instance that calls `setInterval` or `setTimeout` JavaScript functions for you
29+
under the hood.
30+
- Parameters:
31+
- millisecondsDelay: the amount of milliseconds before the `callback` closure is executed.
32+
- isRepeating: when `true` the `callback` closure is executed repeatedly at given
33+
`millisecondsDelay` intervals indefinitely until the timer is deallocated.
34+
- callback: the closure to be executed after a given `millisecondsDelay` interval.
35+
*/
36+
public init(millisecondsDelay: Double, isRepeating: Bool = false, callback: @escaping () -> ()) {
37+
closure = JSClosure { _ in callback() }
38+
self.isRepeating = isRepeating
39+
if isRepeating {
40+
value = global.setInterval.function!(closure, millisecondsDelay)
41+
} else {
42+
value = global.setTimeout.function!(closure, millisecondsDelay)
43+
}
44+
}
45+
46+
/** Makes a corresponding `clearTimeout` or `clearInterval` call, depending on whether this timer
47+
instance is repeating. The `closure` instance is released manually here, as it is required for
48+
bridged closure instances.
49+
*/
50+
deinit {
51+
if isRepeating {
52+
global.clearInterval.function!(value)
53+
} else {
54+
global.clearTimeout.function!(value)
55+
}
56+
closure.release()
57+
}
58+
}

0 commit comments

Comments
 (0)
Please sign in to comment.