Skip to content

Commit 480a464

Browse files
kittenfacebook-github-bot
authored andcommitted
fix: Align TimerManager sequential ids and function error handling with web standard (#51500)
Summary: Calls to create timers should return sequential ids (integers greater than zero in the spec's words). This regressed in the `TimerManager` implementation, which instead starts at zero inclusively. This has two side-effects for code assuming a spec-compliant implementation of `setTimeout` and `setInterval`: - Calls to `clearTimeout(0)` or `clearInterval(0)` will potentially cancel scheduled timers, although it's supposed to be a noop - Predicates like `if (timeoutId)` will fail since they assume non-negative ids The change in this PR is to align with WHATWG HTML 8.6.2 (Timers): https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#timers > otherwise, let id be an [implementation-defined](https://infra.spec.whatwg.org/#implementation-defined) integer that is **greater than zero** and does not already [exist](https://infra.spec.whatwg.org/#map-exists) in global's [map of setTimeout and setInterval IDs](https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#map-of-settimeout-and-setinterval-ids). Specifically, - we should return `0` to indicate that no timer was scheduled - we should start generating timer IDs at `1` instead of `0` This was previously raised in review comments here: https://github.com/facebook/react-native/pull/45092/files#r1650790008 The spec-incompliant behaviour was raised in an issue here: apollographql/apollo-client#12632 (comment) This PR does not, - add bounds checking on `timerIndex_` and add a search of an available id that isn't in the unordered map - exclude `0` from being an accepted `TimerHandle` in `TimerManager::createTimer` or `TimerManager::deleteTimer` since the above bounds checking hasn't been added either ## Changelog: [GENERAL] [FIXED] - Align timer IDs and timer function argument error handling with web standards. Pull Request resolved: #51500 Test Plan: - Run `setTimeout` / `setInterval`; before applied changes the timeout for the first timer will be `0` - Run `setTimeout(null)`; before applied changes the timer ID will be non-zero - Run `setInterval(null)`; before applied changes an error will be thrown rather than `0` being returned Reviewed By: cipolleschi Differential Revision: D75145909 Pulled By: rshest fbshipit-source-id: 6646439abd29cf3cfa9e5cf0a57448e3b7cd1b48
1 parent 44b0f55 commit 480a464

File tree

3 files changed

+18
-10
lines changed

3 files changed

+18
-10
lines changed

packages/react-native/ReactCommon/react/runtime/TimerManager.cpp

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -242,8 +242,9 @@ void TimerManager::attachGlobals(jsi::Runtime& runtime) {
242242
}
243243

244244
if (!args[0].isObject() || !args[0].asObject(rt).isFunction(rt)) {
245-
// Do not throw any error to match web spec
246-
return timerIndex_++;
245+
// Do not throw any error to match web spec; instead return 0, an
246+
// invalid timer id
247+
return 0;
247248
}
248249

249250
auto callback = args[0].getObject(rt).getFunction(rt);
@@ -300,8 +301,9 @@ void TimerManager::attachGlobals(jsi::Runtime& runtime) {
300301
}
301302

302303
if (!args[0].isObject() || !args[0].asObject(rt).isFunction(rt)) {
303-
throw jsi::JSError(
304-
rt, "The first argument to setInterval must be a function.");
304+
// Do not throw any error to match web spec; instead return 0, an
305+
// invalid timer id
306+
return 0;
305307
}
306308
auto callback = args[0].getObject(rt).getFunction(rt);
307309
auto delay = count > 1

packages/react-native/ReactCommon/react/runtime/TimerManager.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,9 @@ class TimerManager {
9393

9494
// Each timeout that is registered on this queue gets a sequential id. This
9595
// is the global count from which those are assigned.
96-
TimerHandle timerIndex_{0};
96+
// As per WHATWG HTML 8.6.1 (Timers) ids must be greater than zero, i.e. start
97+
// at 1
98+
TimerHandle timerIndex_{1};
9799

98100
// The React Native microtask queue is used to back public APIs including
99101
// `queueMicrotask`, `clearImmediate`, and `setImmediate` (which is used by

packages/react-native/ReactCommon/react/runtime/tests/cxx/ReactInstanceTest.cpp

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,9 @@ TEST_F(ReactInstanceTest, testSetTimeoutWithoutDelay) {
267267
EXPECT_CALL(
268268
*mockRegistry_,
269269
createTimer(_, 0)); // If delay is not provided, it should use 0
270-
eval("setTimeout(() => {});");
270+
auto val = eval("setTimeout(() => {});");
271+
expectNoError();
272+
EXPECT_EQ(val.asNumber(), 1); // First timer id should start at 1
271273
}
272274

273275
TEST_F(ReactInstanceTest, testSetTimeoutWithPassThroughArgs) {
@@ -299,8 +301,9 @@ TEST_F(ReactInstanceTest, testSetTimeoutWithInvalidArgs) {
299301
getErrorMessage("setTimeout();"),
300302
"setTimeout must be called with at least one argument (the function to call).");
301303

302-
eval("setTimeout('invalid');");
304+
auto val = eval("setTimeout('invalid')");
303305
expectNoError();
306+
EXPECT_EQ(val.asNumber(), 0);
304307

305308
eval("setTimeout(() => {}, 'invalid');");
306309
expectNoError();
@@ -417,9 +420,10 @@ TEST_F(ReactInstanceTest, testSetIntervalWithInvalidArgs) {
417420
EXPECT_EQ(
418421
getErrorMessage("setInterval();"),
419422
"setInterval must be called with at least one argument (the function to call).");
420-
EXPECT_EQ(
421-
getErrorMessage("setInterval('invalid', 100);"),
422-
"The first argument to setInterval must be a function.");
423+
424+
auto val = eval("setInterval('invalid', 100)");
425+
expectNoError();
426+
EXPECT_EQ(val.asNumber(), 0);
423427
}
424428

425429
TEST_F(ReactInstanceTest, testClearInterval) {

0 commit comments

Comments
 (0)