aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Ciro Spaciari <ciro.spaciari@gmail.com> 2023-05-14 14:18:56 -0300
committerGravatar GitHub <noreply@github.com> 2023-05-14 10:18:56 -0700
commitbf9e40d5b49f1ed16ea9abed4e231456dcda99c0 (patch)
treea803971285c5f4329bbee4ef025151baaf91c817
parent2a66229b0f9aabf96891afe3063e07ec877527ca (diff)
downloadbun-bf9e40d5b49f1ed16ea9abed4e231456dcda99c0.tar.gz
bun-bf9e40d5b49f1ed16ea9abed4e231456dcda99c0.tar.zst
bun-bf9e40d5b49f1ed16ea9abed4e231456dcda99c0.zip
feat(Timer.refresh) add refresh support on Timer (#2874)
* add refresh support on Timer * fix this return * add refresh setTimeout tests * fix tests and add setInterval test * use setCached for arguments and callback --------- Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
-rw-r--r--src/bun.js/api/bun.zig155
-rw-r--r--src/bun.js/bindings/JSSink.cpp2
-rw-r--r--src/bun.js/bindings/JSSink.h2
-rw-r--r--src/bun.js/bindings/JSSinkLookupTable.h2
-rw-r--r--src/bun.js/bindings/ZigGeneratedClasses.cpp79
-rw-r--r--src/bun.js/bindings/ZigGeneratedClasses.h7
-rw-r--r--src/bun.js/bindings/generated_classes.zig47
-rw-r--r--src/bun.js/node/node.classes.ts5
-rw-r--r--test/js/web/timers/setInterval.test.js14
-rw-r--r--test/js/web/timers/setTimeout.test.js59
10 files changed, 345 insertions, 27 deletions
diff --git a/src/bun.js/api/bun.zig b/src/bun.js/api/bun.zig
index 5b965e969..da4e2945b 100644
--- a/src/bun.js/api/bun.zig
+++ b/src/bun.js/api/bun.zig
@@ -2990,13 +2990,123 @@ pub const Timer = struct {
id: i32 = -1,
kind: Timeout.Kind = .setTimeout,
ref_count: u16 = 1,
+ interval: i32 = 0,
+ // we do not allow the timer to be refreshed after we call clearInterval/clearTimeout
+ has_cleaned_up: bool = false,
pub usingnamespace JSC.Codegen.JSTimeout;
+ pub fn init(globalThis: *JSGlobalObject, id: i32, kind: Timeout.Kind, interval: i32, callback: JSValue, arguments: JSValue) JSValue {
+ var timer = globalThis.allocator().create(TimerObject) catch unreachable;
+ timer.* = .{
+ .id = id,
+ .kind = kind,
+ .interval = interval,
+ };
+ var timer_js = timer.toJS(globalThis);
+ timer_js.ensureStillAlive();
+ TimerObject.argumentsSetCached(timer_js, globalThis, arguments);
+ TimerObject.callbackSetCached(timer_js, globalThis, callback);
+ timer_js.ensureStillAlive();
+ return timer_js;
+ }
+
pub fn doRef(this: *TimerObject, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSValue {
if (this.ref_count > 0)
this.ref_count +|= 1;
+ return JSValue.jsUndefined();
+ }
+
+ pub fn doRefresh(this: *TimerObject, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSValue {
+ // TODO: this is not the optimal way to do this but it works, we should revisit this and optimize it
+ // like truly resetting the timer instead of removing and re-adding when possible
+ const this_value = callframe.this();
+
+ // setImmediate does not support refreshing and we do not support refreshing after cleanup
+ if (this.has_cleaned_up or this.id == -1 or this.kind == .setImmediate) {
+ return JSValue.jsUndefined();
+ }
+ const vm = globalThis.bunVM();
+ var map = vm.timer.maps.get(this.kind);
+
+ // reschedule the event
+ if (TimerObject.callbackGetCached(this_value)) |callback| {
+ callback.ensureStillAlive();
+
+ const id: Timeout.ID = .{
+ .id = this.id,
+ .kind = this.kind,
+ };
+
+ if (this.kind == .setTimeout and this.interval == 0) {
+ var cb: CallbackJob = .{
+ .callback = JSC.Strong.create(callback, globalThis),
+ .globalThis = globalThis,
+ .id = this.id,
+ .kind = this.kind,
+ };
+
+ if (TimerObject.argumentsGetCached(this_value)) |arguments| {
+ arguments.ensureStillAlive();
+ cb.arguments = JSC.Strong.create(arguments, globalThis);
+ }
+
+ var job = vm.allocator.create(CallbackJob) catch @panic(
+ "Out of memory while allocating Timeout",
+ );
+
+ job.* = cb;
+ job.task = CallbackJob.Task.init(job);
+ job.ref.ref(vm);
+
+ // cancel the current event if exists before re-adding it
+ if (map.fetchSwapRemove(this.id)) |timer| {
+ if (timer.value != null) {
+ var value = timer.value.?;
+ value.deinit();
+ }
+ }
+
+ vm.enqueueTask(JSC.Task.init(&job.task));
+
+ map.put(vm.allocator, this.id, null) catch unreachable;
+ return this_value;
+ }
+
+ var timeout = Timeout{
+ .callback = JSC.Strong.create(callback, globalThis),
+ .globalThis = globalThis,
+ .timer = uws.Timer.create(
+ vm.uws_event_loop.?,
+ id,
+ ),
+ };
+
+ if (TimerObject.argumentsGetCached(this_value)) |arguments| {
+ arguments.ensureStillAlive();
+ timeout.arguments = JSC.Strong.create(arguments, globalThis);
+ }
+
+ timeout.poll_ref.ref(vm);
+
+ // cancel the current event if exists before re-adding it
+ if (map.fetchSwapRemove(this.id)) |timer| {
+ if (timer.value != null) {
+ var value = timer.value.?;
+ value.deinit();
+ }
+ }
+
+ map.put(vm.allocator, this.id, timeout) catch unreachable;
+ timeout.timer.set(
+ id,
+ Timeout.run,
+ this.interval,
+ @as(i32, @boolToInt(this.kind == .setInterval)) * this.interval,
+ );
+ return this_value;
+ }
return JSValue.jsUndefined();
}
@@ -3022,6 +3132,10 @@ pub const Timer = struct {
return JSValue.jsNumber(this.id);
}
+ pub fn markHasClear(this: *TimerObject) void {
+ this.has_cleaned_up = true;
+ }
+
pub fn finalize(this: *TimerObject) callconv(.C) void {
bun.default_allocator.destroy(this);
}
@@ -3143,20 +3257,13 @@ pub const Timer = struct {
id: i32,
globalThis: *JSGlobalObject,
callback: JSValue,
- countdown: JSValue,
+ interval: i32,
arguments_array_or_zero: JSValue,
repeat: bool,
) !void {
JSC.markBinding(@src());
var vm = globalThis.bunVM();
- // We don't deal with nesting levels directly
- // but we do set the minimum timeout to be 1ms for repeating timers
- const interval: i32 = @max(
- countdown.coerce(i32, globalThis),
- if (repeat) @as(i32, 1) else 0,
- );
-
const kind: Timeout.Kind = if (repeat) .setInterval else .setTimeout;
var map = vm.timer.maps.get(kind);
@@ -3228,16 +3335,15 @@ pub const Timer = struct {
const id = globalThis.bunVM().timer.last_id;
globalThis.bunVM().timer.last_id +%= 1;
- Timer.set(id, globalThis, callback, countdown, arguments, false) catch
- return JSValue.jsUndefined();
+ const interval: i32 = @max(
+ countdown.coerce(i32, globalThis),
+ 0,
+ );
- var timer = globalThis.allocator().create(TimerObject) catch unreachable;
- timer.* = .{
- .id = id,
- .kind = .setTimeout,
- };
+ Timer.set(id, globalThis, callback, interval, arguments, false) catch
+ return JSValue.jsUndefined();
- return timer.toJS(globalThis);
+ return TimerObject.init(globalThis, id, .setTimeout, interval, callback, arguments);
}
pub fn setInterval(
globalThis: *JSGlobalObject,
@@ -3249,16 +3355,16 @@ pub const Timer = struct {
const id = globalThis.bunVM().timer.last_id;
globalThis.bunVM().timer.last_id +%= 1;
- Timer.set(id, globalThis, callback, countdown, arguments, true) catch
+ // We don't deal with nesting levels directly
+ // but we do set the minimum timeout to be 1ms for repeating timers
+ const interval: i32 = @max(
+ countdown.coerce(i32, globalThis),
+ 1,
+ );
+ Timer.set(id, globalThis, callback, interval, arguments, true) catch
return JSValue.jsUndefined();
- var timer = globalThis.allocator().create(TimerObject) catch unreachable;
- timer.* = .{
- .id = id,
- .kind = .setInterval,
- };
-
- return timer.toJS(globalThis);
+ return TimerObject.init(globalThis, id, .setInterval, interval, callback, arguments);
}
pub fn clearTimer(timer_id_value: JSValue, globalThis: *JSGlobalObject, repeats: bool) void {
@@ -3275,6 +3381,7 @@ pub const Timer = struct {
}
if (TimerObject.fromJS(timer_id_value)) |timer_obj| {
+ timer_obj.markHasClear();
break :brk timer_obj.id;
}
diff --git a/src/bun.js/bindings/JSSink.cpp b/src/bun.js/bindings/JSSink.cpp
index 31e648b8a..2fb03963e 100644
--- a/src/bun.js/bindings/JSSink.cpp
+++ b/src/bun.js/bindings/JSSink.cpp
@@ -1,6 +1,6 @@
// AUTO-GENERATED FILE. DO NOT EDIT.
-// Generated by 'make generate-sink' at 2023-04-27T21:24:10.276Z
+// Generated by 'make generate-sink' at 2023-05-14T13:28:43.914Z
// To regenerate this file, run:
//
// make generate-sink
diff --git a/src/bun.js/bindings/JSSink.h b/src/bun.js/bindings/JSSink.h
index 29c5b2a09..9726f68e9 100644
--- a/src/bun.js/bindings/JSSink.h
+++ b/src/bun.js/bindings/JSSink.h
@@ -1,6 +1,6 @@
// AUTO-GENERATED FILE. DO NOT EDIT.
-// Generated by 'make generate-sink' at 2023-04-27T21:24:10.274Z
+// Generated by 'make generate-sink' at 2023-05-14T13:28:43.910Z
//
#pragma once
diff --git a/src/bun.js/bindings/JSSinkLookupTable.h b/src/bun.js/bindings/JSSinkLookupTable.h
index 2f92be340..e4ed81629 100644
--- a/src/bun.js/bindings/JSSinkLookupTable.h
+++ b/src/bun.js/bindings/JSSinkLookupTable.h
@@ -1,4 +1,4 @@
-// Automatically generated from src/bun.js/bindings/JSSink.cpp using /home/will/dev/bun-test-universe/src/bun.js/WebKit/Source/JavaScriptCore/create_hash_table. DO NOT EDIT!
+// Automatically generated from src/bun.js/bindings/JSSink.cpp using /home/cirospaciari/Repos/bun/src/bun.js/WebKit/Source/JavaScriptCore/create_hash_table. DO NOT EDIT!
diff --git a/src/bun.js/bindings/ZigGeneratedClasses.cpp b/src/bun.js/bindings/ZigGeneratedClasses.cpp
index 137d94fe2..a8ec4c6b5 100644
--- a/src/bun.js/bindings/ZigGeneratedClasses.cpp
+++ b/src/bun.js/bindings/ZigGeneratedClasses.cpp
@@ -13556,6 +13556,9 @@ JSC_DECLARE_HOST_FUNCTION(TimeoutPrototype__hasRefCallback);
extern "C" EncodedJSValue TimeoutPrototype__doRef(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame);
JSC_DECLARE_HOST_FUNCTION(TimeoutPrototype__refCallback);
+extern "C" EncodedJSValue TimeoutPrototype__doRefresh(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame);
+JSC_DECLARE_HOST_FUNCTION(TimeoutPrototype__refreshCallback);
+
extern "C" EncodedJSValue TimeoutPrototype__doUnref(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame);
JSC_DECLARE_HOST_FUNCTION(TimeoutPrototype__unrefCallback);
@@ -13564,6 +13567,7 @@ STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSTimeoutPrototype, JSTimeoutPrototype::Base
static const HashTableValue JSTimeoutPrototypeTableValues[] = {
{ "hasRef"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, TimeoutPrototype__hasRefCallback, 0 } },
{ "ref"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, TimeoutPrototype__refCallback, 0 } },
+ { "refresh"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, TimeoutPrototype__refreshCallback, 0 } },
{ "unref"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, TimeoutPrototype__unrefCallback, 0 } }
};
@@ -13617,6 +13621,22 @@ JSC_DEFINE_HOST_FUNCTION(TimeoutPrototype__refCallback, (JSGlobalObject * lexica
return TimeoutPrototype__doRef(thisObject->wrapped(), lexicalGlobalObject, callFrame);
}
+JSC_DEFINE_HOST_FUNCTION(TimeoutPrototype__refreshCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame))
+{
+ auto& vm = lexicalGlobalObject->vm();
+
+ JSTimeout* thisObject = jsDynamicCast<JSTimeout*>(callFrame->thisValue());
+
+ if (UNLIKELY(!thisObject)) {
+ auto throwScope = DECLARE_THROW_SCOPE(vm);
+ return throwVMTypeError(lexicalGlobalObject, throwScope);
+ }
+
+ JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject);
+
+ return TimeoutPrototype__doRefresh(thisObject->wrapped(), lexicalGlobalObject, callFrame);
+}
+
JSC_DEFINE_HOST_FUNCTION(TimeoutPrototype__unrefCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame))
{
auto& vm = lexicalGlobalObject->vm();
@@ -13633,6 +13653,32 @@ JSC_DEFINE_HOST_FUNCTION(TimeoutPrototype__unrefCallback, (JSGlobalObject * lexi
return TimeoutPrototype__doUnref(thisObject->wrapped(), lexicalGlobalObject, callFrame);
}
+extern "C" void TimeoutPrototype__argumentsSetCachedValue(JSC::EncodedJSValue thisValue, JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue value)
+{
+ auto& vm = globalObject->vm();
+ auto* thisObject = jsCast<JSTimeout*>(JSValue::decode(thisValue));
+ thisObject->m_arguments.set(vm, thisObject, JSValue::decode(value));
+}
+
+extern "C" EncodedJSValue TimeoutPrototype__argumentsGetCachedValue(JSC::EncodedJSValue thisValue)
+{
+ auto* thisObject = jsCast<JSTimeout*>(JSValue::decode(thisValue));
+ return JSValue::encode(thisObject->m_arguments.get());
+}
+
+extern "C" void TimeoutPrototype__callbackSetCachedValue(JSC::EncodedJSValue thisValue, JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue value)
+{
+ auto& vm = globalObject->vm();
+ auto* thisObject = jsCast<JSTimeout*>(JSValue::decode(thisValue));
+ thisObject->m_callback.set(vm, thisObject, JSValue::decode(value));
+}
+
+extern "C" EncodedJSValue TimeoutPrototype__callbackGetCachedValue(JSC::EncodedJSValue thisValue)
+{
+ auto* thisObject = jsCast<JSTimeout*>(JSValue::decode(thisValue));
+ return JSValue::encode(thisObject->m_callback.get());
+}
+
void JSTimeoutPrototype::finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject)
{
Base::finishCreation(vm);
@@ -13717,6 +13763,39 @@ extern "C" EncodedJSValue Timeout__create(Zig::GlobalObject* globalObject, void*
return JSValue::encode(instance);
}
+
+template<typename Visitor>
+void JSTimeout::visitChildrenImpl(JSCell* cell, Visitor& visitor)
+{
+ JSTimeout* thisObject = jsCast<JSTimeout*>(cell);
+ ASSERT_GC_OBJECT_INHERITS(thisObject, info());
+ Base::visitChildren(thisObject, visitor);
+ visitor.append(thisObject->m_arguments);
+ visitor.append(thisObject->m_callback);
+}
+
+DEFINE_VISIT_CHILDREN(JSTimeout);
+
+template<typename Visitor>
+void JSTimeout::visitAdditionalChildren(Visitor& visitor)
+{
+ JSTimeout* thisObject = this;
+ ASSERT_GC_OBJECT_INHERITS(thisObject, info());
+ visitor.append(thisObject->m_arguments);
+ visitor.append(thisObject->m_callback);
+}
+
+DEFINE_VISIT_ADDITIONAL_CHILDREN(JSTimeout);
+
+template<typename Visitor>
+void JSTimeout::visitOutputConstraintsImpl(JSCell* cell, Visitor& visitor)
+{
+ JSTimeout* thisObject = jsCast<JSTimeout*>(cell);
+ ASSERT_GC_OBJECT_INHERITS(thisObject, info());
+ thisObject->visitAdditionalChildren<Visitor>(visitor);
+}
+
+DEFINE_VISIT_OUTPUT_CONSTRAINTS(JSTimeout);
class JSTranspilerPrototype final : public JSC::JSNonFinalObject {
public:
using Base = JSC::JSNonFinalObject;
diff --git a/src/bun.js/bindings/ZigGeneratedClasses.h b/src/bun.js/bindings/ZigGeneratedClasses.h
index fc9be7501..02fef6d3c 100644
--- a/src/bun.js/bindings/ZigGeneratedClasses.h
+++ b/src/bun.js/bindings/ZigGeneratedClasses.h
@@ -1575,6 +1575,13 @@ public:
}
void finishCreation(JSC::VM&);
+
+ DECLARE_VISIT_CHILDREN;
+ template<typename Visitor> void visitAdditionalChildren(Visitor&);
+ DECLARE_VISIT_OUTPUT_CONSTRAINTS;
+
+ mutable JSC::WriteBarrier<JSC::Unknown> m_arguments;
+ mutable JSC::WriteBarrier<JSC::Unknown> m_callback;
};
class JSTranspiler final : public JSC::JSDestructibleObject {
diff --git a/src/bun.js/bindings/generated_classes.zig b/src/bun.js/bindings/generated_classes.zig
index a53d9bbe4..841e65b21 100644
--- a/src/bun.js/bindings/generated_classes.zig
+++ b/src/bun.js/bindings/generated_classes.zig
@@ -3988,6 +3988,50 @@ pub const JSTimeout = struct {
return Timeout__fromJS(value);
}
+ extern fn TimeoutPrototype__argumentsSetCachedValue(JSC.JSValue, *JSC.JSGlobalObject, JSC.JSValue) void;
+
+ extern fn TimeoutPrototype__argumentsGetCachedValue(JSC.JSValue) JSC.JSValue;
+
+ /// `Timeout.arguments` setter
+ /// This value will be visited by the garbage collector.
+ pub fn argumentsSetCached(thisValue: JSC.JSValue, globalObject: *JSC.JSGlobalObject, value: JSC.JSValue) void {
+ JSC.markBinding(@src());
+ TimeoutPrototype__argumentsSetCachedValue(thisValue, globalObject, value);
+ }
+
+ /// `Timeout.arguments` getter
+ /// This value will be visited by the garbage collector.
+ pub fn argumentsGetCached(thisValue: JSC.JSValue) ?JSC.JSValue {
+ JSC.markBinding(@src());
+ const result = TimeoutPrototype__argumentsGetCachedValue(thisValue);
+ if (result == .zero)
+ return null;
+
+ return result;
+ }
+
+ extern fn TimeoutPrototype__callbackSetCachedValue(JSC.JSValue, *JSC.JSGlobalObject, JSC.JSValue) void;
+
+ extern fn TimeoutPrototype__callbackGetCachedValue(JSC.JSValue) JSC.JSValue;
+
+ /// `Timeout.callback` setter
+ /// This value will be visited by the garbage collector.
+ pub fn callbackSetCached(thisValue: JSC.JSValue, globalObject: *JSC.JSGlobalObject, value: JSC.JSValue) void {
+ JSC.markBinding(@src());
+ TimeoutPrototype__callbackSetCachedValue(thisValue, globalObject, value);
+ }
+
+ /// `Timeout.callback` getter
+ /// This value will be visited by the garbage collector.
+ pub fn callbackGetCached(thisValue: JSC.JSValue) ?JSC.JSValue {
+ JSC.markBinding(@src());
+ const result = TimeoutPrototype__callbackGetCachedValue(thisValue);
+ if (result == .zero)
+ return null;
+
+ return result;
+ }
+
/// Create a new instance of Timeout
pub fn toJS(this: *Timeout, globalObject: *JSC.JSGlobalObject) JSC.JSValue {
JSC.markBinding(@src());
@@ -4030,10 +4074,13 @@ pub const JSTimeout = struct {
@compileLog("Expected Timeout.hasRef to be a callback but received " ++ @typeName(@TypeOf(Timeout.hasRef)));
if (@TypeOf(Timeout.doRef) != CallbackType)
@compileLog("Expected Timeout.doRef to be a callback but received " ++ @typeName(@TypeOf(Timeout.doRef)));
+ if (@TypeOf(Timeout.doRefresh) != CallbackType)
+ @compileLog("Expected Timeout.doRefresh to be a callback but received " ++ @typeName(@TypeOf(Timeout.doRefresh)));
if (@TypeOf(Timeout.doUnref) != CallbackType)
@compileLog("Expected Timeout.doUnref to be a callback but received " ++ @typeName(@TypeOf(Timeout.doUnref)));
if (!JSC.is_bindgen) {
@export(Timeout.doRef, .{ .name = "TimeoutPrototype__doRef" });
+ @export(Timeout.doRefresh, .{ .name = "TimeoutPrototype__doRefresh" });
@export(Timeout.doUnref, .{ .name = "TimeoutPrototype__doUnref" });
@export(Timeout.finalize, .{ .name = "TimeoutClass__finalize" });
@export(Timeout.hasRef, .{ .name = "TimeoutPrototype__hasRef" });
diff --git a/src/bun.js/node/node.classes.ts b/src/bun.js/node/node.classes.ts
index 5bebabe72..f984077e4 100644
--- a/src/bun.js/node/node.classes.ts
+++ b/src/bun.js/node/node.classes.ts
@@ -14,6 +14,10 @@ export default [
fn: "doRef",
length: 0,
},
+ refresh: {
+ fn: "doRefresh",
+ length: 0,
+ },
unref: {
fn: "doUnref",
length: 0,
@@ -27,6 +31,7 @@ export default [
length: 1,
},
},
+ values: ["arguments", "callback"],
}),
define({
name: "Stats",
diff --git a/test/js/web/timers/setInterval.test.js b/test/js/web/timers/setInterval.test.js
index 7b03afba5..b4215eef2 100644
--- a/test/js/web/timers/setInterval.test.js
+++ b/test/js/web/timers/setInterval.test.js
@@ -59,3 +59,17 @@ it("async setInterval", async () => {
});
});
});
+
+it("setInterval if refreshed before run, should reschedule to run later", done => {
+ let start = Date.now();
+ let timer = setInterval(() => {
+ let end = Date.now();
+ clearInterval(timer);
+ expect(end - start).toBeGreaterThanOrEqual(150);
+ done();
+ }, 100);
+
+ setTimeout(() => {
+ timer.refresh();
+ }, 50);
+});
diff --git a/test/js/web/timers/setTimeout.test.js b/test/js/web/timers/setTimeout.test.js
index 88472adc7..dbe89dea8 100644
--- a/test/js/web/timers/setTimeout.test.js
+++ b/test/js/web/timers/setTimeout.test.js
@@ -171,3 +171,62 @@ it.skip("order of setTimeouts", done => {
setTimeout(maybeDone(() => nums.push(4), 1));
Promise.resolve().then(maybeDone(() => nums.push(1)));
});
+
+it("setTimeout should refresh N times", done => {
+ let count = 0;
+ let timer = setTimeout(() => {
+ count++;
+ timer.refresh();
+ }, 50);
+
+ setTimeout(() => {
+ clearTimeout(timer);
+ expect(count).toBeGreaterThanOrEqual(5);
+ done();
+ }, 300);
+});
+
+it("setTimeout if refreshed before run, should reschedule to run later", done => {
+ let start = Date.now();
+ let timer = setTimeout(() => {
+ let end = Date.now();
+ expect(end - start).toBeGreaterThanOrEqual(150);
+ done();
+ }, 100);
+
+ setTimeout(() => {
+ timer.refresh();
+ }, 50);
+});
+
+it("setTimeout should refresh after already been run", done => {
+ let count = 0;
+ let timer = setTimeout(() => {
+ count++;
+ }, 50);
+
+ setTimeout(() => {
+ timer.refresh();
+ }, 100);
+
+ setTimeout(() => {
+ expect(count).toBe(2);
+ done();
+ }, 300);
+});
+
+it("setTimeout should not refresh after clearTimeout", done => {
+ let count = 0;
+ let timer = setTimeout(() => {
+ count++;
+ }, 50);
+
+ clearTimeout(timer);
+
+ timer.refresh();
+
+ setTimeout(() => {
+ expect(count).toBe(0);
+ done();
+ }, 100);
+});