From 25b682ede4093561202fa9e1600ec78fc7771210 Mon Sep 17 00:00:00 2001 From: James Gore <83005220+thejamesgore@users.noreply.github.com> Date: Fri, 21 Jun 2024 00:05:19 +0100 Subject: [PATCH 1/6] fix: align timer functions' error handling with web standards Fixes issue #45085 by updating setTimeout, clearTimeout, setInterval, and clearInterval to match web behavior. Added corresponding tests to ensure compliance. --- .../Libraries/Core/Timers/JSTimers.js | 16 ++- .../Core/Timers/__tests__/JSTimers-test.js | 113 ++++++++++++++++++ 2 files changed, 127 insertions(+), 2 deletions(-) diff --git a/packages/react-native/Libraries/Core/Timers/JSTimers.js b/packages/react-native/Libraries/Core/Timers/JSTimers.js index f996f9c26d47..3044e9e6ebc3 100644 --- a/packages/react-native/Libraries/Core/Timers/JSTimers.js +++ b/packages/react-native/Libraries/Core/Timers/JSTimers.js @@ -206,14 +206,17 @@ const JSTimers = { */ setTimeout: function ( func: Function, - duration: number, + duration: number = 0, ...args: any ): number { + if (typeof func !== 'function') { + throw new TypeError('The first argument must be a function.'); + } const id = _allocateCallback( () => func.apply(undefined, args), 'setTimeout', ); - createTimer(id, duration || 0, Date.now(), /* recurring */ false); + createTimer(id, duration, Date.now(), /* recurring */ false); return id; }, @@ -226,6 +229,9 @@ const JSTimers = { duration: number, ...args: any ): number { + if (typeof func !== 'function') { + throw new TypeError('The first argument must be a function.'); + } const id = _allocateCallback( () => func.apply(undefined, args), 'setInterval', @@ -325,10 +331,16 @@ const JSTimers = { }, clearTimeout: function (timerID: number) { + if (typeof timerID !== 'number') { + return; + } _freeCallback(timerID); }, clearInterval: function (timerID: number) { + if (typeof timerID !== 'number') { + return; + } _freeCallback(timerID); }, diff --git a/packages/react-native/Libraries/Core/Timers/__tests__/JSTimers-test.js b/packages/react-native/Libraries/Core/Timers/__tests__/JSTimers-test.js index b92a981c3b0a..928050d675a1 100644 --- a/packages/react-native/Libraries/Core/Timers/__tests__/JSTimers-test.js +++ b/packages/react-native/Libraries/Core/Timers/__tests__/JSTimers-test.js @@ -65,6 +65,38 @@ describe('JSTimers', function () { expect(callCount).toBe(1); }); + it('should not throw an error if clearTimeout is called with an invalid ID', function () { + expect(() => JSTimers.clearTimeout(null)).not.toThrow(); + expect(() => JSTimers.clearTimeout(undefined)).not.toThrow(); + expect(() => JSTimers.clearTimeout('invalid')).not.toThrow(); + expect(() => JSTimers.clearTimeout({})).not.toThrow(); + }); + + it('should not call the callback if clearTimeout is called before the timer fires', function () { + const callback = jest.fn(); + const timerID = JSTimers.setTimeout(callback, 100); + JSTimers.clearTimeout(timerID); + JSTimers.callTimers([timerID]); + expect(callback).not.toBeCalled(); + }); + + it('should not warn if callback is called on cancelled timer', function () { + const callback = jest.fn(); + const timerID = JSTimers.setTimeout(callback, 10); + JSTimers.clearTimeout(timerID); + JSTimers.callTimers([timerID]); + expect(callback).not.toBeCalled(); + expect(console.warn).not.toBeCalled(); + }); + + it('should not call to native when clearing a null timer', function () { + const timerID = JSTimers.setTimeout(() => {}); + JSTimers.clearTimeout(timerID); + NativeTiming.deleteTimer = jest.fn(); + JSTimers.clearTimeout(null); + expect(NativeTiming.deleteTimer.mock.calls.length).toBe(0); + }); + it('should call nested queueReactNativeMicrotask when cleared', function () { let id1, id2, id3; let callCount = 0; @@ -132,6 +164,58 @@ describe('JSTimers', function () { expect(callback).toBeCalledTimes(1); }); + it('should throw TypeError with setInterval when the first argument is not a function', function () { + expect(() => JSTimers.setInterval(123, 100)).toThrow(TypeError); + expect(() => JSTimers.setInterval(null, 100)).toThrow(TypeError); + expect(() => JSTimers.setInterval(undefined, 100)).toThrow(TypeError); + }); + + it('should call function with setInterval and default duration', function () { + const callback = jest.fn(); + const id = JSTimers.setInterval(callback); + JSTimers.callTimers([id]); + expect(callback).toBeCalledTimes(1); + }); + + it('should pass additional arguments to callback', function () { + const callback = jest.fn(); + const id = JSTimers.setInterval(callback, 100, 1, 2); + JSTimers.callTimers([id]); + expect(callback).toBeCalledWith(1, 2); + }); + + it('should not throw an error if clearInterval is called with an invalid ID', function () { + expect(() => JSTimers.clearInterval(null)).not.toThrow(); + expect(() => JSTimers.clearInterval(undefined)).not.toThrow(); + expect(() => JSTimers.clearInterval('invalid')).not.toThrow(); + expect(() => JSTimers.clearInterval({})).not.toThrow(); + }); + + it('should not call the callback if clearInterval is called before the interval fires', function () { + const callback = jest.fn(); + const intervalID = JSTimers.setInterval(callback, 100); + JSTimers.clearInterval(intervalID); + JSTimers.callTimers([intervalID]); + expect(callback).not.toBeCalled(); + }); + + it('should not warn if callback is called on cancelled interval', function () { + const callback = jest.fn(); + const intervalID = JSTimers.setInterval(callback, 10); + JSTimers.clearInterval(intervalID); + JSTimers.callTimers([intervalID]); + expect(callback).not.toBeCalled(); + expect(console.warn).not.toBeCalled(); + }); + + it('should not call to native when clearing a null interval', function () { + const intervalID = JSTimers.setInterval(() => {}); + JSTimers.clearInterval(intervalID); + NativeTiming.deleteTimer = jest.fn(); + JSTimers.clearInterval(null); + expect(NativeTiming.deleteTimer.mock.calls.length).toBe(0); + }); + it('should call function with queueReactNativeMicrotask', function () { const callback = jest.fn(); JSTimers.queueReactNativeMicrotask(callback); @@ -211,6 +295,35 @@ describe('JSTimers', function () { expect(callback).not.toBeCalled(); }); + it('should throw TypeError with setTimeout when the first argument is not a function', function () { + expect(() => JSTimers.setTimeout(123, 100)).toThrow(TypeError); + expect(() => JSTimers.setTimeout(null, 100)).toThrow(TypeError); + expect(() => JSTimers.setTimeout(undefined, 100)).toThrow(TypeError); + }); + + it('should call function with setTimeout and default duration', function () { + let didCall = false; + const id = JSTimers.setTimeout(function () { + didCall = true; + }); + JSTimers.callTimers([id]); + expect(didCall).toBe(true); + }); + + it('should pass additional arguments to callback with setTimeout', function () { + let result; + const id = JSTimers.setTimeout( + (a, b) => { + result = a + b; + }, + 100, + 1, + 2, + ); + JSTimers.callTimers([id]); + expect(result).toBe(3); + }); + it('should call setInterval as many times as callTimers is called', function () { const callback = jest.fn(); const id = JSTimers.setInterval(callback, 10); From 3767c3310da039696a8409a37c311344ab427c2f Mon Sep 17 00:00:00 2001 From: James Gore <83005220+thejamesgore@users.noreply.github.com> Date: Fri, 21 Jun 2024 11:09:54 +0100 Subject: [PATCH 2/6] Revert "Log a SoftException on SurfaceMountingManager.addRootView (#34785)" This reverts commit 3429dc1ccf32b6b23f4f0ad3cfdf0d5697af665f. --- .../mounting/SurfaceMountingManager.java | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.java index 754bc8945747..63b79a4d2228 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.java @@ -211,22 +211,11 @@ private void addRootView(@NonNull final View rootView) { + " tag: [%d]", rootView.getId(), mSurfaceId); - // This behavior can not be guaranteed in hybrid apps that have a native android layer - // over - // which reactRootViews are added and the native views need to have ids on them in order - // to - // work. - // Hence this can cause unnecessary crashes at runtime for hybrid apps. - // So converting this to a soft exception such that pure react-native devs can still see - // the - // warning while hybrid apps continue to run without crashes - ReactSoftExceptionLogger.logSoftException( - TAG, - new IllegalViewOperationException( - "Trying to add a root view with an explicit id already set. React Native uses" - + " the id field to track react tags and will overwrite this field. If that" - + " is fine, explicitly overwrite the id field to View.NO_ID before calling" - + " addRootView.")); + throw new IllegalViewOperationException( + "Trying to add a root view with an explicit id already set. React Native uses the" + + " id field to track react tags and will overwrite this field. If that is" + + " fine, explicitly overwrite the id field to View.NO_ID before calling" + + " addRootView."); } rootView.setId(mSurfaceId); From 7cb4eb43abdab8396e4e2a93c49889ed3a0b20c8 Mon Sep 17 00:00:00 2001 From: James Gore <83005220+thejamesgore@users.noreply.github.com> Date: Fri, 21 Jun 2024 12:10:35 +0100 Subject: [PATCH 3/6] revert: Undo changes made to JSTimers.js and JSTimers-test.js based on feedback --- .../Libraries/Core/Timers/JSTimers.js | 16 +-- .../Core/Timers/__tests__/JSTimers-test.js | 113 ------------------ 2 files changed, 2 insertions(+), 127 deletions(-) diff --git a/packages/react-native/Libraries/Core/Timers/JSTimers.js b/packages/react-native/Libraries/Core/Timers/JSTimers.js index 3044e9e6ebc3..f996f9c26d47 100644 --- a/packages/react-native/Libraries/Core/Timers/JSTimers.js +++ b/packages/react-native/Libraries/Core/Timers/JSTimers.js @@ -206,17 +206,14 @@ const JSTimers = { */ setTimeout: function ( func: Function, - duration: number = 0, + duration: number, ...args: any ): number { - if (typeof func !== 'function') { - throw new TypeError('The first argument must be a function.'); - } const id = _allocateCallback( () => func.apply(undefined, args), 'setTimeout', ); - createTimer(id, duration, Date.now(), /* recurring */ false); + createTimer(id, duration || 0, Date.now(), /* recurring */ false); return id; }, @@ -229,9 +226,6 @@ const JSTimers = { duration: number, ...args: any ): number { - if (typeof func !== 'function') { - throw new TypeError('The first argument must be a function.'); - } const id = _allocateCallback( () => func.apply(undefined, args), 'setInterval', @@ -331,16 +325,10 @@ const JSTimers = { }, clearTimeout: function (timerID: number) { - if (typeof timerID !== 'number') { - return; - } _freeCallback(timerID); }, clearInterval: function (timerID: number) { - if (typeof timerID !== 'number') { - return; - } _freeCallback(timerID); }, diff --git a/packages/react-native/Libraries/Core/Timers/__tests__/JSTimers-test.js b/packages/react-native/Libraries/Core/Timers/__tests__/JSTimers-test.js index 928050d675a1..b92a981c3b0a 100644 --- a/packages/react-native/Libraries/Core/Timers/__tests__/JSTimers-test.js +++ b/packages/react-native/Libraries/Core/Timers/__tests__/JSTimers-test.js @@ -65,38 +65,6 @@ describe('JSTimers', function () { expect(callCount).toBe(1); }); - it('should not throw an error if clearTimeout is called with an invalid ID', function () { - expect(() => JSTimers.clearTimeout(null)).not.toThrow(); - expect(() => JSTimers.clearTimeout(undefined)).not.toThrow(); - expect(() => JSTimers.clearTimeout('invalid')).not.toThrow(); - expect(() => JSTimers.clearTimeout({})).not.toThrow(); - }); - - it('should not call the callback if clearTimeout is called before the timer fires', function () { - const callback = jest.fn(); - const timerID = JSTimers.setTimeout(callback, 100); - JSTimers.clearTimeout(timerID); - JSTimers.callTimers([timerID]); - expect(callback).not.toBeCalled(); - }); - - it('should not warn if callback is called on cancelled timer', function () { - const callback = jest.fn(); - const timerID = JSTimers.setTimeout(callback, 10); - JSTimers.clearTimeout(timerID); - JSTimers.callTimers([timerID]); - expect(callback).not.toBeCalled(); - expect(console.warn).not.toBeCalled(); - }); - - it('should not call to native when clearing a null timer', function () { - const timerID = JSTimers.setTimeout(() => {}); - JSTimers.clearTimeout(timerID); - NativeTiming.deleteTimer = jest.fn(); - JSTimers.clearTimeout(null); - expect(NativeTiming.deleteTimer.mock.calls.length).toBe(0); - }); - it('should call nested queueReactNativeMicrotask when cleared', function () { let id1, id2, id3; let callCount = 0; @@ -164,58 +132,6 @@ describe('JSTimers', function () { expect(callback).toBeCalledTimes(1); }); - it('should throw TypeError with setInterval when the first argument is not a function', function () { - expect(() => JSTimers.setInterval(123, 100)).toThrow(TypeError); - expect(() => JSTimers.setInterval(null, 100)).toThrow(TypeError); - expect(() => JSTimers.setInterval(undefined, 100)).toThrow(TypeError); - }); - - it('should call function with setInterval and default duration', function () { - const callback = jest.fn(); - const id = JSTimers.setInterval(callback); - JSTimers.callTimers([id]); - expect(callback).toBeCalledTimes(1); - }); - - it('should pass additional arguments to callback', function () { - const callback = jest.fn(); - const id = JSTimers.setInterval(callback, 100, 1, 2); - JSTimers.callTimers([id]); - expect(callback).toBeCalledWith(1, 2); - }); - - it('should not throw an error if clearInterval is called with an invalid ID', function () { - expect(() => JSTimers.clearInterval(null)).not.toThrow(); - expect(() => JSTimers.clearInterval(undefined)).not.toThrow(); - expect(() => JSTimers.clearInterval('invalid')).not.toThrow(); - expect(() => JSTimers.clearInterval({})).not.toThrow(); - }); - - it('should not call the callback if clearInterval is called before the interval fires', function () { - const callback = jest.fn(); - const intervalID = JSTimers.setInterval(callback, 100); - JSTimers.clearInterval(intervalID); - JSTimers.callTimers([intervalID]); - expect(callback).not.toBeCalled(); - }); - - it('should not warn if callback is called on cancelled interval', function () { - const callback = jest.fn(); - const intervalID = JSTimers.setInterval(callback, 10); - JSTimers.clearInterval(intervalID); - JSTimers.callTimers([intervalID]); - expect(callback).not.toBeCalled(); - expect(console.warn).not.toBeCalled(); - }); - - it('should not call to native when clearing a null interval', function () { - const intervalID = JSTimers.setInterval(() => {}); - JSTimers.clearInterval(intervalID); - NativeTiming.deleteTimer = jest.fn(); - JSTimers.clearInterval(null); - expect(NativeTiming.deleteTimer.mock.calls.length).toBe(0); - }); - it('should call function with queueReactNativeMicrotask', function () { const callback = jest.fn(); JSTimers.queueReactNativeMicrotask(callback); @@ -295,35 +211,6 @@ describe('JSTimers', function () { expect(callback).not.toBeCalled(); }); - it('should throw TypeError with setTimeout when the first argument is not a function', function () { - expect(() => JSTimers.setTimeout(123, 100)).toThrow(TypeError); - expect(() => JSTimers.setTimeout(null, 100)).toThrow(TypeError); - expect(() => JSTimers.setTimeout(undefined, 100)).toThrow(TypeError); - }); - - it('should call function with setTimeout and default duration', function () { - let didCall = false; - const id = JSTimers.setTimeout(function () { - didCall = true; - }); - JSTimers.callTimers([id]); - expect(didCall).toBe(true); - }); - - it('should pass additional arguments to callback with setTimeout', function () { - let result; - const id = JSTimers.setTimeout( - (a, b) => { - result = a + b; - }, - 100, - 1, - 2, - ); - JSTimers.callTimers([id]); - expect(result).toBe(3); - }); - it('should call setInterval as many times as callTimers is called', function () { const callback = jest.fn(); const id = JSTimers.setInterval(callback, 10); From b7143300d49eb284b4e87ede1378de1d0f58e9c6 Mon Sep 17 00:00:00 2001 From: James Gore <83005220+thejamesgore@users.noreply.github.com> Date: Fri, 21 Jun 2024 12:16:39 +0100 Subject: [PATCH 4/6] revert: Undo linting error --- .../mounting/SurfaceMountingManager.java | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.java index 63b79a4d2228..754bc8945747 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.java @@ -211,11 +211,22 @@ private void addRootView(@NonNull final View rootView) { + " tag: [%d]", rootView.getId(), mSurfaceId); - throw new IllegalViewOperationException( - "Trying to add a root view with an explicit id already set. React Native uses the" - + " id field to track react tags and will overwrite this field. If that is" - + " fine, explicitly overwrite the id field to View.NO_ID before calling" - + " addRootView."); + // This behavior can not be guaranteed in hybrid apps that have a native android layer + // over + // which reactRootViews are added and the native views need to have ids on them in order + // to + // work. + // Hence this can cause unnecessary crashes at runtime for hybrid apps. + // So converting this to a soft exception such that pure react-native devs can still see + // the + // warning while hybrid apps continue to run without crashes + ReactSoftExceptionLogger.logSoftException( + TAG, + new IllegalViewOperationException( + "Trying to add a root view with an explicit id already set. React Native uses" + + " the id field to track react tags and will overwrite this field. If that" + + " is fine, explicitly overwrite the id field to View.NO_ID before calling" + + " addRootView.")); } rootView.setId(mSurfaceId); From 6551a773b7ea7bd407c309e62026240370751752 Mon Sep 17 00:00:00 2001 From: James Gore <83005220+thejamesgore@users.noreply.github.com> Date: Fri, 21 Jun 2024 12:19:00 +0100 Subject: [PATCH 5/6] fix: Align timer functions' error handling with web standards in TimerManager.cpp based on feedback Updated setTimeout, clearTimeout, setInterval, and clearInterval functions in TimerManager.cpp to match web behavior. setTimeout and setInterval now only throw if the callback is not provided, and clearTimeout and clearInterval silently ignore invalid IDs. Adjusted related logic to ensure compliance with web standards. Fixes issue #45085. --- .../ReactCommon/react/runtime/TimerManager.cpp | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/packages/react-native/ReactCommon/react/runtime/TimerManager.cpp b/packages/react-native/ReactCommon/react/runtime/TimerManager.cpp index 3ebf425447ee..be0947b9c925 100644 --- a/packages/react-native/ReactCommon/react/runtime/TimerManager.cpp +++ b/packages/react-native/ReactCommon/react/runtime/TimerManager.cpp @@ -225,17 +225,12 @@ void TimerManager::attachGlobals(jsi::Runtime& runtime) { throw jsi::JSError( rt, "The first argument to setTimeout must be a function."); } + auto callback = args[0].getObject(rt).getFunction(rt); - if (count > 1 && !args[1].isNumber() && !args[1].isUndefined()) { - throw jsi::JSError( - rt, - "The second argument to setTimeout must be a number or undefined."); - } auto delay = - count > 1 && args[1].isNumber() ? args[1].getNumber() : 0; + (count > 1 && args[1].isNumber()) ? args[1].getNumber() : 0; - // Package up the remaining argument values into one place. std::vector moreArgs; for (size_t extraArgNum = 2; extraArgNum < count; extraArgNum++) { moreArgs.emplace_back(rt, args[extraArgNum]); @@ -258,7 +253,9 @@ void TimerManager::attachGlobals(jsi::Runtime& runtime) { size_t count) { if (count > 0 && args[0].isNumber()) { auto handle = (TimerHandle)args[0].asNumber(); - deleteTimer(rt, handle); + if (timers_.find(handle) != timers_.end()) { + deleteTimer(rt, handle); + } } return jsi::Value::undefined(); })); @@ -285,11 +282,11 @@ void TimerManager::attachGlobals(jsi::Runtime& runtime) { throw jsi::JSError( rt, "The first argument to setInterval must be a function."); } + auto callback = args[0].getObject(rt).getFunction(rt); auto delay = - count > 1 && args[1].isNumber() ? args[1].getNumber() : 0; + (count > 1 && args[1].isNumber()) ? args[1].getNumber() : 0; - // Package up the remaining argument values into one place. std::vector moreArgs; for (size_t extraArgNum = 2; extraArgNum < count; extraArgNum++) { moreArgs.emplace_back(rt, args[extraArgNum]); From 0612d386d23b036062637e746e7361430a5f8fdb Mon Sep 17 00:00:00 2001 From: James Gore <83005220+thejamesgore@users.noreply.github.com> Date: Fri, 21 Jun 2024 12:56:29 +0100 Subject: [PATCH 6/6] fix: Align timer functions' error handling with web standards in TimerManager.cpp based on feedback Updated setTimeout, clearTimeout, setInterval, and clearInterval functions in TimerManager.cpp to match web behavior. setTimeout and setInterval now only throw if the callback is not provided, and clearTimeout and clearInterval silently ignore invalid IDs. Adjusted related logic to ensure compliance with web standards. Fixes issue #45085. --- .../react/runtime/TimerManager.cpp | 43 ++++--------------- 1 file changed, 9 insertions(+), 34 deletions(-) diff --git a/packages/react-native/ReactCommon/react/runtime/TimerManager.cpp b/packages/react-native/ReactCommon/react/runtime/TimerManager.cpp index be0947b9c925..c62048f2fb6c 100644 --- a/packages/react-native/ReactCommon/react/runtime/TimerManager.cpp +++ b/packages/react-native/ReactCommon/react/runtime/TimerManager.cpp @@ -215,27 +215,15 @@ void TimerManager::attachGlobals(jsi::Runtime& runtime) { const jsi::Value& thisVal, const jsi::Value* args, size_t count) { - if (count == 0) { + if (count == 0 || !args[0].isObject() || + !args[0].asObject(rt).isFunction(rt)) { throw jsi::JSError( - rt, - "setTimeout must be called with at least one argument (the function to call)."); + rt, "setTimeout must be called with a function."); } - - if (!args[0].isObject() || !args[0].asObject(rt).isFunction(rt)) { - throw jsi::JSError( - rt, "The first argument to setTimeout must be a function."); - } - auto callback = args[0].getObject(rt).getFunction(rt); - auto delay = (count > 1 && args[1].isNumber()) ? args[1].getNumber() : 0; - - std::vector moreArgs; - for (size_t extraArgNum = 2; extraArgNum < count; extraArgNum++) { - moreArgs.emplace_back(rt, args[extraArgNum]); - } - + std::vector moreArgs(args + 2, args + count); return createTimer(std::move(callback), std::move(moreArgs), delay); })); @@ -253,9 +241,7 @@ void TimerManager::attachGlobals(jsi::Runtime& runtime) { size_t count) { if (count > 0 && args[0].isNumber()) { auto handle = (TimerHandle)args[0].asNumber(); - if (timers_.find(handle) != timers_.end()) { - deleteTimer(rt, handle); - } + deleteTimer(rt, handle); } return jsi::Value::undefined(); })); @@ -272,26 +258,15 @@ void TimerManager::attachGlobals(jsi::Runtime& runtime) { const jsi::Value& thisVal, const jsi::Value* args, size_t count) { - if (count == 0) { - throw jsi::JSError( - rt, - "setInterval must be called with at least one argument (the function to call)."); - } - - if (!args[0].isObject() || !args[0].asObject(rt).isFunction(rt)) { + if (count == 0 || !args[0].isObject() || + !args[0].asObject(rt).isFunction(rt)) { throw jsi::JSError( - rt, "The first argument to setInterval must be a function."); + rt, "setInterval must be called with a function."); } - auto callback = args[0].getObject(rt).getFunction(rt); auto delay = (count > 1 && args[1].isNumber()) ? args[1].getNumber() : 0; - - std::vector moreArgs; - for (size_t extraArgNum = 2; extraArgNum < count; extraArgNum++) { - moreArgs.emplace_back(rt, args[extraArgNum]); - } - + std::vector moreArgs(args + 2, args + count); return createRecurringTimer( std::move(callback), std::move(moreArgs), delay); }));