diff --git a/.travis.yml b/.travis.yml index 5f0caaabc938ee..0cbf424aacd8f7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ install: - $yarn install script: - - if [[ "$TEST_TYPE" = objc-ios ]]; then travis_retry travis_wait ./scripts/objc-test-ios.sh; fi + - if [[ "$TEST_TYPE" = objc-ios ]]; then travis_retry travis_wait ./scripts/objc-test-ios.sh test; fi - if [[ "$TEST_TYPE" = objc-tvos ]]; then travis_retry travis_wait ./scripts/objc-test-tvos.sh; fi - if [[ "$TEST_TYPE" = e2e-objc ]]; then node ./scripts/run-ci-e2e-tests.js --ios --js --retries 3; fi - if [[ ( "$TEST_TYPE" = podspecs ) && ( "$TRAVIS_PULL_REQUEST" = "false" ) ]]; then gem install cocoapods && ./scripts/process-podspecs.sh; fi diff --git a/Libraries/Animated/src/AnimatedImplementation.js b/Libraries/Animated/src/AnimatedImplementation.js index 1c06b9a6e4ab13..769b3b0164a64e 100644 --- a/Libraries/Animated/src/AnimatedImplementation.js +++ b/Libraries/Animated/src/AnimatedImplementation.js @@ -1508,32 +1508,48 @@ class AnimatedStyle extends AnimatedWithChildren { this._style = style; } - __getValue(): Object { - var style = {}; - for (var key in this._style) { - var value = this._style[key]; + // Recursively get values for nested styles (like iOS's shadowOffset) + __walkStyleAndGetValues(style) { + let updatedStyle = {}; + for (let key in style) { + let value = style[key]; if (value instanceof Animated) { if (!value.__isNative) { // We cannot use value of natively driven nodes this way as the value we have access from // JS may not be up to date. - style[key] = value.__getValue(); + updatedStyle[key] = value.__getValue(); } + } else if (value && !Array.isArray(value) && typeof value === 'object') { + // Support animating nested values (for example: shadowOffset.height) + updatedStyle[key] = this.__walkStyleAndGetValues(value); } else { - style[key] = value; + updatedStyle[key] = value; } } - return style; + return updatedStyle; } - __getAnimatedValue(): Object { - var style = {}; - for (var key in this._style) { - var value = this._style[key]; + __getValue(): Object { + return this.__walkStyleAndGetValues(this._style); + } + + // Recursively get animated values for nested styles (like iOS's shadowOffset) + __walkStyleAndGetAnimatedValues(style) { + let updatedStyle = {}; + for (let key in style) { + let value = style[key]; if (value instanceof Animated) { - style[key] = value.__getAnimatedValue(); + updatedStyle[key] = value.__getAnimatedValue(); + } else if (value && !Array.isArray(value) && typeof value === 'object') { + // Support animating nested values (for example: shadowOffset.height) + updatedStyle[key] = this.__walkStyleAndGetAnimatedValues(value); } } - return style; + return updatedStyle; + } + + __getAnimatedValue(): Object { + return this.__walkStyleAndGetAnimatedValues(this._style); } __attach(): void { diff --git a/Libraries/Animated/src/__tests__/Animated-test.js b/Libraries/Animated/src/__tests__/Animated-test.js index c89d2979995a21..c081f1e9b7a67a 100644 --- a/Libraries/Animated/src/__tests__/Animated-test.js +++ b/Libraries/Animated/src/__tests__/Animated-test.js @@ -33,7 +33,11 @@ describe('Animated tests', () => { outputRange: [100, 200], })}, {scale: anim}, - ] + ], + shadowOffset: { + width: anim, + height: anim, + }, } }, callback); @@ -47,6 +51,10 @@ describe('Animated tests', () => { {translateX: 100}, {scale: 0}, ], + shadowOffset: { + width: 0, + height: 0, + }, }, }); @@ -62,6 +70,10 @@ describe('Animated tests', () => { {translateX: 150}, {scale: 0.5}, ], + shadowOffset: { + width: 0.5, + height: 0.5, + }, }, }); diff --git a/Libraries/Core/Timers/JSTimers.js b/Libraries/Core/Timers/JSTimers.js index 842b47ba5455fc..967552c29ec833 100644 --- a/Libraries/Core/Timers/JSTimers.js +++ b/Libraries/Core/Timers/JSTimers.js @@ -15,6 +15,7 @@ // in dependencies. NativeModules > BatchedBridge > MessageQueue > JSTimersExecution const RCTTiming = require('NativeModules').Timing; const JSTimersExecution = require('JSTimersExecution'); +const Platform = require('Platform'); const parseErrorStack = require('parseErrorStack'); @@ -64,6 +65,14 @@ function _freeCallback(timerID: number) { } } +const MAX_TIMER_DURATION_MS = 60 * 1000; +const IS_ANDROID = Platform.OS === 'android'; +const ANDROID_LONG_TIMER_MESSAGE = + 'Setting a timer for a long period of time, i.e. multiple minutes, is a ' + + 'performance and correctness issue on Android as it keeps the timer ' + + 'module awake, and timers can only be called when the app is in the foreground. ' + + 'See https://github.com/facebook/react-native/issues/12981 for more info.'; + /** * JS implementation of timer functions. Must be completely driven by an * external clock signal, all that's stored here is timerID, timer type, and @@ -75,6 +84,11 @@ const JSTimers = { * @param {number} duration Number of milliseconds. */ setTimeout: function(func: Function, duration: number, ...args?: any): number { + if (IS_ANDROID && duration > MAX_TIMER_DURATION_MS) { + console.warn( + ANDROID_LONG_TIMER_MESSAGE + '\n' + '(Saw setTimeout with duration ' + + duration + 'ms)'); + } const id = _allocateCallback(() => func.apply(undefined, args), 'setTimeout'); RCTTiming.createTimer(id, duration || 0, Date.now(), /* recurring */ false); return id; @@ -85,6 +99,11 @@ const JSTimers = { * @param {number} duration Number of milliseconds. */ setInterval: function(func: Function, duration: number, ...args?: any): number { + if (IS_ANDROID && duration > MAX_TIMER_DURATION_MS) { + console.warn( + ANDROID_LONG_TIMER_MESSAGE + '\n' + '(Saw setInterval with duration ' + + duration + 'ms)'); + } const id = _allocateCallback(() => func.apply(undefined, args), 'setInterval'); RCTTiming.createTimer(id, duration || 0, Date.now(), /* recurring */ true); return id; diff --git a/React/CxxModule/RCTCxxUtils.h b/React/CxxModule/RCTCxxUtils.h index 0f361233836d2e..2278cf42de8371 100644 --- a/React/CxxModule/RCTCxxUtils.h +++ b/React/CxxModule/RCTCxxUtils.h @@ -33,7 +33,7 @@ template <> struct ValueEncoder { static Value toValue(JSGlobalContextRef ctx, NSArray *const __strong array) { - JSValue *value = [JSValue valueWithObject:array inContext:contextForGlobalContextRef(ctx)]; + JSValue *value = [JSC_JSValue(ctx) valueWithObject:array inContext:contextForGlobalContextRef(ctx)]; return {ctx, [value JSValueRef]}; } }; diff --git a/React/Profiler/RCTProfile.m b/React/Profiler/RCTProfile.m index b51a9bcc2bbd65..d2b0f479d136a7 100644 --- a/React/Profiler/RCTProfile.m +++ b/React/Profiler/RCTProfile.m @@ -153,6 +153,20 @@ static dispatch_group_t RCTProfileGetUnhookGroup(void) return unhookGroup; } +// Used by RCTProfileTrampoline assembly file to call libc`malloc +RCT_EXTERN void *RCTProfileMalloc(size_t size); +void *RCTProfileMalloc(size_t size) +{ + return malloc(size); +} + +// Used by RCTProfileTrampoline assembly file to call libc`free +RCT_EXTERN void RCTProfileFree(void *buf); +void RCTProfileFree(void *buf) +{ + free(buf); +} + RCT_EXTERN IMP RCTProfileGetImplementation(id obj, SEL cmd); IMP RCTProfileGetImplementation(id obj, SEL cmd) { diff --git a/React/Profiler/RCTProfileTrampoline-arm.S b/React/Profiler/RCTProfileTrampoline-arm.S index 99ac6498b2748a..aa622bbd0d7c6b 100644 --- a/React/Profiler/RCTProfileTrampoline-arm.S +++ b/React/Profiler/RCTProfileTrampoline-arm.S @@ -35,12 +35,7 @@ SYMBOL_NAME(RCTProfileTrampoline): * profile */ mov r0, #0xc - movw ip, :lower16:(L_malloc-(LPC1_0+4)) - movt ip, :upper16:(L_malloc-(LPC1_0+4)) -LPC1_0: - add ip, pc - ldr ip, [ip] - blx ip + bl SYMBOL_NAME(RCTProfileMalloc) /** * r4 is the callee saved register we'll use to refer to the allocated memory, * store its initial value, so we can restore it later @@ -92,12 +87,7 @@ LPC1_0: ldr r1, [r4, #0x8] ldr r4, [r4] push {r1} // save the caller on the stack - movw ip, :lower16:(L_free-(LPC1_1+4)) - movt ip, :upper16:(L_free-(LPC1_1+4)) -LPC1_1: - add ip, pc - ldr ip, [ip] - blx ip + bl SYMBOL_NAME(RCTProfileFree) pop {lr} // pop the caller pop {r0} // pop the return value @@ -105,11 +95,4 @@ LPC1_1: trap - .data - .p2align 2 -L_malloc: - .long SYMBOL_NAME(malloc) -L_free: - .long SYMBOL_NAME(free) - #endif diff --git a/React/Profiler/RCTProfileTrampoline-arm64.S b/React/Profiler/RCTProfileTrampoline-arm64.S index 30ce4a04828662..e513696870551e 100644 --- a/React/Profiler/RCTProfileTrampoline-arm64.S +++ b/React/Profiler/RCTProfileTrampoline-arm64.S @@ -48,7 +48,7 @@ SYMBOL_NAME(RCTProfileTrampoline): * the implementation and the caller address. */ mov x0, #0x10 - bl SYMBOL_NAME(malloc) + bl SYMBOL_NAME(RCTProfileMalloc) // store the initial value of r19, the callee saved register we'll use str x19, [x0] mov x19, x0 @@ -111,7 +111,7 @@ SYMBOL_NAME(RCTProfileTrampoline): ldr x10, [x19, #0x8] // load the caller address ldr x19, [x19] // restore x19 str x10, [sp, #0x18] // store x10 on the stack space allocated above - bl SYMBOL_NAME(free) + bl SYMBOL_NAME(RCTProfileFree) // Load both return values and link register from the stack ldr q0, [sp, #0x0] diff --git a/React/Profiler/RCTProfileTrampoline-i386.S b/React/Profiler/RCTProfileTrampoline-i386.S index 7faba7742656d1..d1adf1a0898ecd 100644 --- a/React/Profiler/RCTProfileTrampoline-i386.S +++ b/React/Profiler/RCTProfileTrampoline-i386.S @@ -30,7 +30,7 @@ SYMBOL_NAME(RCTProfileTrampoline): */ subl $0x8, %esp // stack padding (16-byte alignment for function calls) pushl $0xc // allocate 12-bytes - calll SYMBOL_NAME(malloc) + calll SYMBOL_NAME(RCTProfileMalloc) addl $0xc, %esp // restore stack (8-byte padding + 4-byte argument) /** @@ -85,7 +85,7 @@ SYMBOL_NAME(RCTProfileTrampoline): * the stack has already been padded and the first and only argument, the * memory address, is already in the bottom of the stack. */ - calll SYMBOL_NAME(free) + calll SYMBOL_NAME(RCTProfileFree) addl $0x8, %esp /** diff --git a/React/Profiler/RCTProfileTrampoline-x86_64.S b/React/Profiler/RCTProfileTrampoline-x86_64.S index 0325fb0f5496fe..21072d84241906 100644 --- a/React/Profiler/RCTProfileTrampoline-x86_64.S +++ b/React/Profiler/RCTProfileTrampoline-x86_64.S @@ -90,7 +90,7 @@ SYMBOL_NAME(RCTProfileTrampoline): // allocate 16 bytes movq $0x10, %rdi - callq SYMBOL_NAME_PIC(malloc) + callq SYMBOL_NAME_PIC(RCTProfileMalloc) // store the initial value of calle saved registers %r13 and %r14 movq %r13, 0x0(%rax) @@ -169,7 +169,7 @@ SYMBOL_NAME(RCTProfileTrampoline): andq $-0x10, %rsp // Free the memory allocated to stash callee saved registers - callq SYMBOL_NAME_PIC(free) + callq SYMBOL_NAME_PIC(RCTProfileFree) // unalign stack and restore %r12 movq %r12, %rsp diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java index 32a4f92c93802a..b5990c1b2134fc 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java @@ -56,6 +56,7 @@ import com.facebook.react.devsupport.DevSupportManagerFactory; import com.facebook.react.devsupport.ReactInstanceDevCommandsHandler; import com.facebook.react.devsupport.RedBoxHandler; +import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener; import com.facebook.react.devsupport.interfaces.DevSupportManager; import com.facebook.react.devsupport.interfaces.PackagerStatusCallback; import com.facebook.react.modules.appregistry.AppRegistry; @@ -310,7 +311,8 @@ public static ReactInstanceManagerBuilder builder() { JSCConfig jscConfig, @Nullable RedBoxHandler redBoxHandler, boolean lazyNativeModulesEnabled, - boolean lazyViewManagersEnabled) { + boolean lazyViewManagersEnabled, + @Nullable DevBundleDownloadListener devBundleDownloadListener) { initializeSoLoaderIfNecessary(applicationContext); @@ -330,7 +332,8 @@ public static ReactInstanceManagerBuilder builder() { mDevInterface, mJSMainModuleName, useDeveloperSupport, - redBoxHandler); + redBoxHandler, + devBundleDownloadListener); mBridgeIdleDebugListener = bridgeIdleDebugListener; mLifecycleState = initialLifecycleState; mUIImplementationProvider = uiImplementationProvider; diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManagerBuilder.java b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManagerBuilder.java index d77020bb4615e1..e653b1da6c5a1a 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManagerBuilder.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManagerBuilder.java @@ -15,6 +15,7 @@ import com.facebook.react.bridge.NotThreadSafeBridgeIdleDebugListener; import com.facebook.react.common.LifecycleState; import com.facebook.react.cxxbridge.JSBundleLoader; +import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener; import com.facebook.react.devsupport.interfaces.DevSupportManager; import com.facebook.react.devsupport.RedBoxHandler; import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; @@ -42,6 +43,7 @@ public class ReactInstanceManagerBuilder { protected @Nullable RedBoxHandler mRedBoxHandler; protected boolean mLazyNativeModulesEnabled; protected boolean mLazyViewManagersEnabled; + protected @Nullable DevBundleDownloadListener mDevBundleDownloadListener; /* package protected */ ReactInstanceManagerBuilder() { } @@ -186,6 +188,11 @@ public ReactInstanceManagerBuilder setLazyViewManagersEnabled(boolean lazyViewMa return this; } + public ReactInstanceManagerBuilder setDevBundleDownloadListener(@Nullable DevBundleDownloadListener listener) { + mDevBundleDownloadListener = listener; + return this; + } + /** * Instantiates a new {@link ReactInstanceManager}. * Before calling {@code build}, the following must be called: @@ -230,6 +237,7 @@ public ReactInstanceManager build() { mJSCConfig, mRedBoxHandler, mLazyNativeModulesEnabled, - mLazyViewManagersEnabled); + mLazyViewManagersEnabled, + mDevBundleDownloadListener); } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java index 5c3750dfe1ea87..9603ac2400627b 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java @@ -30,6 +30,7 @@ import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.common.ReactConstants; import com.facebook.react.common.network.OkHttpCallUtil; +import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener; import com.facebook.react.devsupport.interfaces.PackagerStatusCallback; import com.facebook.react.modules.systeminfo.AndroidInfoHelpers; import com.facebook.react.packagerconnection.FileIoHandler; @@ -84,12 +85,6 @@ public class DevServerHelper { private static final int LONG_POLL_FAILURE_DELAY_MS = 5000; private static final int HTTP_CONNECT_TIMEOUT_MS = 5000; - public interface BundleDownloadCallback { - void onSuccess(); - void onProgress(@Nullable String status, @Nullable Integer done, @Nullable Integer total); - void onFailure(Exception cause); - } - public interface OnServerContentChangeListener { void onServerContentChanged(); } @@ -302,7 +297,7 @@ public String getDevServerBundleURL(final String jsModulePath) { } public void downloadBundleFromURL( - final BundleDownloadCallback callback, + final DevBundleDownloadListener callback, final File outputFile, final String bundleURL) { final Request request = new Request.Builder() @@ -400,7 +395,7 @@ private void processBundleResult( int statusCode, BufferedSource body, File outputFile, - BundleDownloadCallback callback) throws IOException { + DevBundleDownloadListener callback) throws IOException { // Check for server errors. If the server error has the expected form, fail with more info. if (statusCode != 200) { String bodyString = body.readUtf8(); diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerFactory.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerFactory.java index e3ba2e6b49f2da..efe673b9ec3364 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerFactory.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerFactory.java @@ -15,6 +15,7 @@ import android.content.Context; +import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener; import com.facebook.react.devsupport.interfaces.DevSupportManager; /** @@ -39,6 +40,7 @@ public static DevSupportManager create( reactInstanceCommandsHandler, packagerPathForJSBundleName, enableOnCreate, + null, null); } @@ -47,7 +49,8 @@ public static DevSupportManager create( ReactInstanceDevCommandsHandler reactInstanceCommandsHandler, @Nullable String packagerPathForJSBundleName, boolean enableOnCreate, - @Nullable RedBoxHandler redBoxHandler) { + @Nullable RedBoxHandler redBoxHandler, + @Nullable DevBundleDownloadListener devBundleDownloadListener) { if (!enableOnCreate) { return new DisabledDevSupportManager(); } @@ -68,13 +71,15 @@ public static DevSupportManager create( ReactInstanceDevCommandsHandler.class, String.class, boolean.class, - RedBoxHandler.class); + RedBoxHandler.class, + DevBundleDownloadListener.class); return (DevSupportManager) constructor.newInstance( applicationContext, reactInstanceCommandsHandler, packagerPathForJSBundleName, true, - redBoxHandler); + redBoxHandler, + devBundleDownloadListener); } catch (Exception e) { throw new RuntimeException( "Requested enabled DevSupportManager, but DevSupportManagerImpl class was not found" + diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java index d141dc9325b585..015bd24ee59abe 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java @@ -38,6 +38,7 @@ import com.facebook.react.common.ShakeDetector; import com.facebook.react.common.futures.SimpleSettableFuture; import com.facebook.react.devsupport.DevServerHelper.PackagerCommandListener; +import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener; import com.facebook.react.devsupport.interfaces.DevOptionHandler; import com.facebook.react.devsupport.interfaces.DevSupportManager; import com.facebook.react.devsupport.interfaces.PackagerStatusCallback; @@ -131,6 +132,7 @@ private static enum ErrorType { private @Nullable StackFrame[] mLastErrorStack; private int mLastErrorCookie = 0; private @Nullable ErrorType mLastErrorType; + private @Nullable DevBundleDownloadListener mBundleDownloadListener; private static class JscProfileTask extends AsyncTask { private static final MediaType JSON = @@ -175,6 +177,7 @@ public DevSupportManagerImpl( reactInstanceCommandsHandler, packagerPathForJSBundleName, enableOnCreate, + null, null); } @@ -183,13 +186,15 @@ public DevSupportManagerImpl( ReactInstanceDevCommandsHandler reactInstanceCommandsHandler, @Nullable String packagerPathForJSBundleName, boolean enableOnCreate, - @Nullable RedBoxHandler redBoxHandler) { + @Nullable RedBoxHandler redBoxHandler, + @Nullable DevBundleDownloadListener devBundleDownloadListener) { mReactInstanceCommandsHandler = reactInstanceCommandsHandler; mApplicationContext = applicationContext; mJSAppBundleName = packagerPathForJSBundleName; mDevSettings = new DevInternalSettings(applicationContext, this); mDevServerHelper = new DevServerHelper(mDevSettings); + mBundleDownloadListener = devBundleDownloadListener; // Prepare shake gesture detector (will be started/stopped from #reload) mShakeDetector = new ShakeDetector(new ShakeDetector.ShakeListener() { @@ -804,11 +809,14 @@ public void reloadJSFromServer(final String bundleURL) { mDevLoadingViewVisible = true; mDevServerHelper.downloadBundleFromURL( - new DevServerHelper.BundleDownloadCallback() { + new DevBundleDownloadListener() { @Override public void onSuccess() { mDevLoadingViewController.hide(); mDevLoadingViewVisible = false; + if (mBundleDownloadListener != null) { + mBundleDownloadListener.onSuccess(); + } UiThreadUtil.runOnUiThread( new Runnable() { @Override @@ -821,12 +829,18 @@ public void run() { @Override public void onProgress(@Nullable final String status, @Nullable final Integer done, @Nullable final Integer total) { mDevLoadingViewController.updateProgress(status, done, total); + if (mBundleDownloadListener != null) { + mBundleDownloadListener.onProgress(status, done, total); + } } @Override public void onFailure(final Exception cause) { mDevLoadingViewController.hide(); mDevLoadingViewVisible = false; + if (mBundleDownloadListener != null) { + mBundleDownloadListener.onFailure(cause); + } FLog.e(ReactConstants.TAG, "Unable to download JS bundle", cause); UiThreadUtil.runOnUiThread( new Runnable() { diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/DevBundleDownloadListener.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/DevBundleDownloadListener.java new file mode 100644 index 00000000000000..3f45d3e911e5df --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/DevBundleDownloadListener.java @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.devsupport.interfaces; + +import javax.annotation.Nullable; + +public interface DevBundleDownloadListener { + void onSuccess(); + void onProgress(@Nullable String status, @Nullable Integer done, @Nullable Integer total); + void onFailure(Exception cause); +} diff --git a/ReactCommon/cxxreact/NativeToJsBridge.cpp b/ReactCommon/cxxreact/NativeToJsBridge.cpp index 5c4399766c0704..8c6babfbf4e842 100644 --- a/ReactCommon/cxxreact/NativeToJsBridge.cpp +++ b/ReactCommon/cxxreact/NativeToJsBridge.cpp @@ -52,6 +52,8 @@ class JsToNativeBridge : public react::ExecutorDelegate { "native module calls cannot be completed with no native modules"; ExecutorToken token = m_nativeToJs->getTokenForExecutor(executor); m_nativeQueue->runOnQueue([this, token, calls=std::move(calls), isEndOfBatch] () mutable { + m_batchHadNativeModuleCalls = m_batchHadNativeModuleCalls || !calls.empty(); + // An exception anywhere in here stops processing of the batch. This // was the behavior of the Android bridge, and since exception handling // terminates the whole bridge, there's not much point in continuing. @@ -60,7 +62,10 @@ class JsToNativeBridge : public react::ExecutorDelegate { token, call.moduleId, call.methodId, std::move(call.arguments), call.callId); } if (isEndOfBatch) { - m_callback->onBatchComplete(); + if (m_batchHadNativeModuleCalls) { + m_callback->onBatchComplete(); + m_batchHadNativeModuleCalls = false; + } m_callback->decrementPendingJSCalls(); } }); @@ -88,6 +93,7 @@ class JsToNativeBridge : public react::ExecutorDelegate { std::shared_ptr m_registry; std::unique_ptr m_nativeQueue; std::shared_ptr m_callback; + bool m_batchHadNativeModuleCalls = false; }; NativeToJsBridge::NativeToJsBridge( diff --git a/docs/Testing.md b/docs/Testing.md index ace4db3efc4958..eb7121788fdc80 100644 --- a/docs/Testing.md +++ b/docs/Testing.md @@ -97,7 +97,7 @@ See the following for example usage and integration points: You can run integration tests locally with cmd+U in the IntegrationTest and UIExplorer apps in Xcode, or by running the following in the command line on macOS: $ cd react-native - $ ./scripts/objc-test-ios.sh + $ ./scripts/objc-test-ios.sh test > Your Xcode install will come with a variety of Simulators running the latest OS. You may need to manually create a new Simulator to match what the `XCODE_DESTINATION` param in the test script. diff --git a/packager/src/lib/GlobalTransformCache.js b/packager/src/lib/GlobalTransformCache.js index 70b5979d927ea1..b74a1981cc2330 100644 --- a/packager/src/lib/GlobalTransformCache.js +++ b/packager/src/lib/GlobalTransformCache.js @@ -12,18 +12,19 @@ 'use strict'; const BatchProcessor = require('./BatchProcessor'); +const FetchError = require('node-fetch/lib/fetch-error'); const crypto = require('crypto'); +const fetch = require('node-fetch'); const imurmurhash = require('imurmurhash'); const jsonStableStringify = require('json-stable-stringify'); const path = require('path'); -const request = require('request'); import type {Options as TransformOptions} from '../JSTransformer/worker/worker'; import type {CachedResult, GetTransformCacheKey} from './TransformCache'; -import type {Reporter} from './reporting'; type FetchResultURIs = (keys: Array) => Promise>; +type FetchResultFromURI = (uri: string) => Promise; type StoreResults = (resultsByKey: Map) => Promise; type FetchProps = { @@ -33,9 +34,6 @@ type FetchProps = { transformOptions: TransformOptions, }; -type FetchCallback = (error?: Error, result?: ?CachedResult) => mixed; -type FetchURICallback = (error?: Error, resultURI?: ?string) => mixed; - type URI = string; /** @@ -46,7 +44,6 @@ class KeyURIFetcher { _batchProcessor: BatchProcessor; _fetchResultURIs: FetchResultURIs; - _processError: (error: Error) => mixed; /** * When a batch request fails for some reason, we process the error locally @@ -54,31 +51,21 @@ class KeyURIFetcher { * a build will not fail just because of the cache. */ async _processKeys(keys: Array): Promise> { - let URIsByKey; - try { - URIsByKey = await this._fetchResultURIs(keys); - } catch (error) { - this._processError(error); - return new Array(keys.length); - } + const URIsByKey = await this._fetchResultURIs(keys); return keys.map(key => URIsByKey.get(key)); } - fetch(key: string, callback: FetchURICallback) { - this._batchProcessor.queue(key).then( - res => process.nextTick(callback.bind(undefined, undefined, res)), - err => process.nextTick(callback.bind(undefined, err)), - ); + async fetch(key: string): Promise { + return await this._batchProcessor.queue(key); } - constructor(fetchResultURIs: FetchResultURIs, processError: (error: Error) => mixed) { + constructor(fetchResultURIs: FetchResultURIs) { this._fetchResultURIs = fetchResultURIs; this._batchProcessor = new BatchProcessor({ maximumDelayMs: 10, maximumItems: 500, concurrency: 25, }, this._processKeys.bind(this)); - this._processError = processError; } } @@ -111,21 +98,6 @@ class KeyResultStore { } -function validateCachedResult(cachedResult: mixed): ?CachedResult { - if ( - cachedResult != null && - typeof cachedResult === 'object' && - typeof cachedResult.code === 'string' && - Array.isArray(cachedResult.dependencies) && - cachedResult.dependencies.every(dep => typeof dep === 'string') && - Array.isArray(cachedResult.dependencyOffsets) && - cachedResult.dependencyOffsets.every(offset => typeof offset === 'number') - ) { - return (cachedResult: any); - } - return undefined; -} - /** * The transform options contain absolute paths. This can contain, for * example, the username if someone works their home directory (very likely). @@ -173,28 +145,32 @@ class TransformProfileSet { } } +/** + * For some reason the result stored by the server for a key might mismatch what + * we expect a result to be. So we need to verify carefully the data. + */ +function validateCachedResult(cachedResult: mixed): ?CachedResult { + if ( + cachedResult != null && + typeof cachedResult === 'object' && + typeof cachedResult.code === 'string' && + Array.isArray(cachedResult.dependencies) && + cachedResult.dependencies.every(dep => typeof dep === 'string') && + Array.isArray(cachedResult.dependencyOffsets) && + cachedResult.dependencyOffsets.every(offset => typeof offset === 'number') + ) { + return (cachedResult: any); + } + return null; +} + class GlobalTransformCache { _fetcher: KeyURIFetcher; + _fetchResultFromURI: FetchResultFromURI; _profileSet: TransformProfileSet; - _reporter: Reporter; - _retries: number; _store: ?KeyResultStore; - /** - * If too many errors already happened, we just drop the additional errors. - */ - _processError(error: Error) { - if (this._retries <= 0) { - return; - } - this._reporter.update({type: 'global_cache_error', error}); - --this._retries; - if (this._retries <= 0) { - this._reporter.update({type: 'global_cache_disabled', reason: 'too_many_errors'}); - } - } - /** * For using the global cache one needs to have some kind of central key-value * store that gets prefilled using keyOf() and the transformed results. The @@ -205,14 +181,13 @@ class GlobalTransformCache { */ constructor( fetchResultURIs: FetchResultURIs, + fetchResultFromURI: FetchResultFromURI, storeResults: ?StoreResults, profiles: Iterable, - reporter: Reporter, ) { - this._fetcher = new KeyURIFetcher(fetchResultURIs, this._processError.bind(this)); + this._fetcher = new KeyURIFetcher(fetchResultURIs); this._profileSet = new TransformProfileSet(profiles); - this._reporter = reporter; - this._retries = 4; + this._fetchResultFromURI = fetchResultFromURI; if (storeResults != null) { this._store = new KeyResultStore(storeResults); } @@ -236,57 +211,45 @@ class GlobalTransformCache { * blob of transformed results. However the results are generally only a few * megabytes each. */ - _fetchFromURI(uri: string, callback: FetchCallback) { - request.get({uri, json: true, timeout: 8000}, (error, response, unvalidatedResult) => { - if (error != null) { - callback(error); - return; - } - if (response.statusCode !== 200) { - callback(new Error( - `Unexpected HTTP status code: ${response.statusCode}`, - )); - return; - } - const result = validateCachedResult(unvalidatedResult); - if (result == null) { - callback(new Error('Invalid result returned by server.')); - return; - } - callback(undefined, result); - }); + static async _fetchResultFromURI(uri: string): Promise { + const response = await fetch(uri, {method: 'GET', timeout: 8000}); + if (response.status !== 200) { + throw new Error(`Unexpected HTTP status: ${response.status} ${response.statusText} `); + } + const unvalidatedResult = await response.json(); + const result = validateCachedResult(unvalidatedResult); + if (result == null) { + throw new Error('Server returned invalid result.'); + } + return result; } /** - * Wrap `_fetchFromURI` with error logging, and return an empty result instead - * of errors. This is because the global cache is not critical to the normal - * packager operation. + * It happens from time to time that a fetch timeouts, we want to try these + * again a second time. */ - _tryFetchingFromURI(uri: string, callback: FetchCallback) { - this._fetchFromURI(uri, (error, result) => { - if (error != null) { - this._processError(error); + static fetchResultFromURI(uri: string): Promise { + return GlobalTransformCache._fetchResultFromURI(uri).catch(error => { + if (!(error instanceof FetchError && error.type === 'request-timeout')) { + throw error; } - callback(undefined, result); + return this._fetchResultFromURI(uri); }); } - fetch(props: FetchProps, callback: FetchCallback) { - if (this._retries <= 0 || !this._profileSet.has(props.transformOptions)) { - process.nextTick(callback); - return; + /** + * This may return `null` if either the cache doesn't have a value for that + * key yet, or an error happened, processed separately. + */ + async fetch(props: FetchProps): Promise { + if (!this._profileSet.has(props.transformOptions)) { + return null; } - this._fetcher.fetch(GlobalTransformCache.keyOf(props), (error, uri) => { - if (error != null) { - callback(error); - } else { - if (uri == null) { - callback(); - return; - } - this._tryFetchingFromURI(uri, callback); - } - }); + const uri = await this._fetcher.fetch(GlobalTransformCache.keyOf(props)); + if (uri == null) { + return null; + } + return await this._fetchResultFromURI(uri); } store(props: FetchProps, result: CachedResult) { diff --git a/packager/src/node-haste/Module.js b/packager/src/node-haste/Module.js index 6f05271016d714..0f1892ca56a231 100644 --- a/packager/src/node-haste/Module.js +++ b/packager/src/node-haste/Module.js @@ -307,17 +307,16 @@ class Module { this._transformCodeForCallback(cacheProps, callback); return; } - _globalCache.fetch(cacheProps, (globalCacheError, globalCachedResult) => { - if (globalCacheError) { - callback(globalCacheError); - return; - } - if (globalCachedResult == null) { - this._transformAndStoreCodeGlobally(cacheProps, _globalCache, callback); - return; - } - callback(undefined, globalCachedResult); - }); + _globalCache.fetch(cacheProps).then( + globalCachedResult => process.nextTick(() => { + if (globalCachedResult == null) { + this._transformAndStoreCodeGlobally(cacheProps, _globalCache, callback); + return; + } + callback(undefined, globalCachedResult); + }), + globalCacheError => process.nextTick(() => callback(globalCacheError)), + ); } _getAndCacheTransformedCode( diff --git a/packager/src/node-haste/__tests__/DependencyGraph-test.js b/packager/src/node-haste/__tests__/DependencyGraph-test.js index 4a60d0b55c450c..f55ee0a9137123 100644 --- a/packager/src/node-haste/__tests__/DependencyGraph-test.js +++ b/packager/src/node-haste/__tests__/DependencyGraph-test.js @@ -5181,6 +5181,69 @@ describe('DependencyGraph', function() { expect(deps).toBeDefined(); }); }); + + it('should recover from multiple modules with the same name (but this is broken right now)', async () => { + const root = '/root'; + console.warn = jest.fn(); + const filesystem = setMockFileSystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require(\'a\')', + 'require(\'b\')', + ].join('\n'), + 'a.js': [ + '/**', + ' * @providesModule a', + ' */', + ].join('\n'), + 'b.js': [ + '/**', + ' * @providesModule b', + ' */', + ].join('\n'), + }, + }); + + const dgraph = DependencyGraph.load({...defaults, roots: [root]}); + await getOrderedDependenciesAsJSON(dgraph, root + '/index.js'); + filesystem.root['b.js'] = [ + '/**', + ' * @providesModule a', + ' */', + ].join('\n'); + await triggerAndProcessWatchEvent(dgraph, 'change', root + '/b.js'); + try { + await getOrderedDependenciesAsJSON(dgraph, root + '/index.js'); + throw new Error('expected `getOrderedDependenciesAsJSON` to fail'); + } catch (error) { + if (error.type !== 'UnableToResolveError') { + throw error; + } + expect(console.warn).toBeCalled(); + filesystem.root['b.js'] = [ + '/**', + ' * @providesModule b', + ' */', + ].join('\n'); + await triggerAndProcessWatchEvent(dgraph, 'change', root + '/b.js'); + } + + // This verifies that it is broken right now. Instead of throwing it should + // return correct results. Once this is fixed in `jest-haste`, remove + // the whole try catch and verify results are matching a snapshot. + try { + await getOrderedDependenciesAsJSON(dgraph, root + '/index.js'); + throw new Error('expected `getOrderedDependenciesAsJSON` to fail'); + } catch (error) { + if (error.type !== 'UnableToResolveError') { + throw error; + } + } + }); + }); describe('Extensions', () => { diff --git a/scripts/objc-test-ios.sh b/scripts/objc-test-ios.sh index a85caa073af04f..3f909f7a23a0c4 100755 --- a/scripts/objc-test-ios.sh +++ b/scripts/objc-test-ios.sh @@ -26,6 +26,12 @@ function cleanup { } trap cleanup INT TERM EXIT +# If first argument is "test", actually start the packager and run tests. +# Otherwise, just build UIExplorer for tvOS and exit + +if [ "$1" = "test" ]; +then + # Start the packager (exec "./packager/launchPackager.command" || echo "Can't start packager automatically") & (exec "./IntegrationTests/launchWebSocketServer.command" || echo "Can't start web socket server automatically") & @@ -60,6 +66,7 @@ rm temp.bundle curl 'http://localhost:8081/IntegrationTests/RCTRootViewIntegrationTestApp.bundle?platform=ios&dev=true' -o temp.bundle rm temp.bundle +# Build and test for iOS # TODO: We use xcodebuild because xctool would stall when collecting info about # the tests before running them. Switch back when this issue with xctool has # been resolved. @@ -70,3 +77,13 @@ xcodebuild \ -destination "platform=iOS Simulator,name=iPhone 5s,OS=10.1" \ build test +else + +# Only build for iOS (check there are no missing files in the Xcode project) +xcodebuild \ + -project "Examples/UIExplorer/UIExplorer.xcodeproj" \ + -scheme "UIExplorer" \ + -sdk "iphonesimulator" \ + build + +fi diff --git a/scripts/objc-test-tvos.sh b/scripts/objc-test-tvos.sh index 2266a5ba6fb40f..943d510914eece 100755 --- a/scripts/objc-test-tvos.sh +++ b/scripts/objc-test-tvos.sh @@ -23,7 +23,7 @@ function cleanup { } trap cleanup EXIT -# If first argument is "test", actually start the packager and run tests as in the iOS script +# If first argument is "test", actually start the packager and run tests. # Otherwise, just build UIExplorer for tvOS and exit if [ "$1" = "test" ]; @@ -57,12 +57,11 @@ xcodebuild \ else -# Build only (no test) for tvOS, to make sure there are no missing files +# Only build for tvOS (check there are no missing files in the Xcode project) xcodebuild \ -project "Examples/UIExplorer/UIExplorer.xcodeproj" \ -scheme "UIExplorer-tvOS" \ -sdk "appletvsimulator" \ - -destination "platform=tvOS Simulator,name=Apple TV 1080p,OS=10.1" \ build fi