From 7ae89405da7a47f423cecb3d7faae740bf696f0b Mon Sep 17 00:00:00 2001 From: Gijs Weterings Date: Thu, 10 Apr 2025 04:24:11 -0700 Subject: [PATCH 1/3] Apply nullsafe FIXMEs for xplat/js/react-native-github/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.java Summary: Added nullsafe FIXMEs for easier reviewing of next diff, where we fix them Changelog: [Internal] Differential Revision: D72384065 --- .../facebook/react/runtime/ReactHostImpl.java | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.java index 53d01cb135f9..22f9b9790280 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.java @@ -506,8 +506,10 @@ public TaskInterface reload(String reason) { if (task.isFaulted()) { final Exception ex = task.getError(); if (mUseDevSupport) { + // NULLSAFE_FIXME[Parameter Not Nullable] mDevSupportManager.handleException(ex); } else { + // NULLSAFE_FIXME[Parameter Not Nullable] mReactHostDelegate.handleInstanceException(ex); } return getOrCreateDestroyTask("Reload failed", ex); @@ -993,14 +995,18 @@ private Task getOrCreateStartTask() { if (task.isFaulted()) { Exception ex = task.getError(); if (mUseDevSupport) { + // NULLSAFE_FIXME[Parameter Not Nullable] mDevSupportManager.handleException(ex); } else { + // NULLSAFE_FIXME[Parameter Not Nullable] mReactHostDelegate.handleInstanceException(ex); } // Wait for destroy to finish return getOrCreateDestroyTask( + // NULLSAFE_FIXME[Nullable Dereference] "getOrCreateStartTask() failure: " + task.getError().getMessage(), task.getError()) + // NULLSAFE_FIXME[Parameter Not Nullable] .continueWithTask(destroyTask -> Task.forError(task.getError())) .makeVoid(); } @@ -1083,6 +1089,7 @@ private Task callAfterGetOrCreateReactInstance( .continueWith( task -> { if (task.isFaulted()) { + // NULLSAFE_FIXME[Parameter Not Nullable] handleHostException(task.getError()); } return null; @@ -1202,6 +1209,7 @@ private Task getOrCreateReactInstanceTask() { instance.initializeEagerTurboModules(); log(method, "Loading JS Bundle"); + // NULLSAFE_FIXME[Parameter Not Nullable] instance.loadJSBundle(bundleLoader); log( @@ -1225,8 +1233,11 @@ private Task getOrCreateReactInstanceTask() { Continuation lifecycleUpdateTask = task -> { + // NULLSAFE_FIXME[Nullable Dereference] final ReactInstance reactInstance = task.getResult().mInstance; + // NULLSAFE_FIXME[Nullable Dereference] final ReactContext reactContext = task.getResult().mContext; + // NULLSAFE_FIXME[Nullable Dereference] final boolean isReloading = task.getResult().mIsReloading; final boolean isManagerResumed = mReactLifecycleStateManager.getLifecycleState() == LifecycleState.RESUMED; @@ -1273,7 +1284,9 @@ private Task getOrCreateReactInstanceTask() { creationTask.onSuccess(lifecycleUpdateTask, mUIExecutor); return creationTask.onSuccess( - task -> task.getResult().mInstance, Task.IMMEDIATE_EXECUTOR); + // NULLSAFE_FIXME[Nullable Dereference] + task -> task.getResult().mInstance, + Task.IMMEDIATE_EXECUTOR); }); } @@ -1285,6 +1298,7 @@ private Task getJsBundleLoader() { return isMetroRunning() .onSuccessTask( task -> { + // NULLSAFE_FIXME[Nullable Dereference] boolean isMetroRunning = task.getResult(); if (isMetroRunning) { // Since metro is running, fetch the JS bundle from the server @@ -1409,6 +1423,7 @@ private ReactInstanceTaskUnwrapper createReactInstanceUnwrapper( final String reasonLabel = tag + " reason: " + reason; if (task.isFaulted()) { final Exception ex = task.getError(); + // NULLSAFE_FIXME[Nullable Dereference] final String faultLabel = "Fault reason: " + ex.getMessage(); raiseSoftException( method, @@ -1588,6 +1603,7 @@ private Task getOrCreateReloadTask(String reason) { raiseSoftException( method, "Error during reload. ReactInstance task faulted. Fault reason: " + // NULLSAFE_FIXME[Nullable Dereference] + fault.getMessage() + ". Reload reason: " + reason, @@ -1761,6 +1777,7 @@ private Task getOrCreateDestroyTask(final String reason, @Nullable Excepti raiseSoftException( method, "React destruction failed. ReactInstance task faulted. Fault reason: " + // NULLSAFE_FIXME[Nullable Dereference] + fault.getMessage() + ". Destroy reason: " + reason, From b753dc6a2bce2fa941cbcb70f4d532083ef57f2d Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Thu, 10 Apr 2025 04:24:17 -0700 Subject: [PATCH 2/3] Convert com.facebook.react.runtime.internal.bolts.Task to Kotlin (#50548) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/50548 Changelog: [Internal] Reviewed By: cortinico Differential Revision: D72578962 --- .../react/interfaces/TaskInterface.kt | 2 +- .../facebook/react/runtime/ReactHostImpl.java | 12 +- .../runtime/internal/bolts/Continuation.kt | 4 +- .../react/runtime/internal/bolts/Task.java | 553 ------------------ .../react/runtime/internal/bolts/Task.kt | 473 +++++++++++++++ .../internal/bolts/UnobservedErrorNotifier.kt | 4 +- 6 files changed, 482 insertions(+), 566 deletions(-) delete mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/internal/bolts/Task.java create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/internal/bolts/Task.kt diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/interfaces/TaskInterface.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/interfaces/TaskInterface.kt index c7d968023a24..30c60b534c25 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/interfaces/TaskInterface.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/interfaces/TaskInterface.kt @@ -23,7 +23,7 @@ public interface TaskInterface { * @return true if the task completed (has a result, an error, or was cancelled). false otherwise. */ @Throws(InterruptedException::class) - public fun waitForCompletion(duration: Long, timeUnit: TimeUnit?): Boolean + public fun waitForCompletion(duration: Long, timeUnit: TimeUnit): Boolean /** @return The result of the task, if set. null otherwise. */ public fun getResult(): TResult? diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.java index 22f9b9790280..017c17201e29 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.java @@ -18,7 +18,6 @@ import android.net.Uri; import android.nfc.NfcAdapter; import android.os.Bundle; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.facebook.common.logging.FLog; @@ -569,18 +568,15 @@ private void loadNetworkResource(String url, InspectorNetworkRequestListener lis * callback will run on a background thread. * @return A task that completes when React Native gets destroyed. */ - @NonNull @Override public TaskInterface destroy( - @NonNull String reason, - @Nullable Exception ex, - @NonNull Function1 onDestroyFinished) { + String reason, @Nullable Exception ex, Function1 onDestroyFinished) { Task task = (Task) destroy(reason, ex); return task.continueWith( new Continuation() { @Nullable @Override - public Void then(@NonNull Task task) throws Exception { + public Void then(Task task) throws Exception { boolean instanceDestroyedSuccessfully = task.isCompleted() && !task.isFaulted(); onDestroyFinished.invoke(instanceDestroyedSuccessfully); return null; @@ -954,12 +950,12 @@ DefaultHardwareBackBtnHandler getDefaultBackButtonHandler() { } @Override - public void addBeforeDestroyListener(@NonNull Function0 onBeforeDestroy) { + public void addBeforeDestroyListener(Function0 onBeforeDestroy) { mBeforeDestroyListeners.add(onBeforeDestroy); } @Override - public void removeBeforeDestroyListener(@NonNull Function0 onBeforeDestroy) { + public void removeBeforeDestroyListener(Function0 onBeforeDestroy) { mBeforeDestroyListeners.remove(onBeforeDestroy); } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/internal/bolts/Continuation.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/internal/bolts/Continuation.kt index d19b016b1703..8dd5c0885fd6 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/internal/bolts/Continuation.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/internal/bolts/Continuation.kt @@ -15,6 +15,6 @@ package com.facebook.react.runtime.internal.bolts * * @see Task */ -internal interface Continuation { - @Throws(Exception::class) fun then(task: Task): TContinuationResult? +public fun interface Continuation { + @Throws(Exception::class) public fun then(task: Task): TContinuationResult? } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/internal/bolts/Task.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/internal/bolts/Task.java deleted file mode 100644 index 5446df15cfca..000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/internal/bolts/Task.java +++ /dev/null @@ -1,553 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -package com.facebook.react.runtime.internal.bolts; - -import androidx.annotation.Nullable; -import com.facebook.react.interfaces.TaskInterface; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.CancellationException; -import java.util.concurrent.Executor; -import java.util.concurrent.TimeUnit; - -/** - * Represents the result of an asynchronous operation. - * - * @param The type of the result of the task. - */ -public class Task implements TaskInterface { - /** - * An {@link java.util.concurrent.Executor} that executes tasks in the current thread unless the - * stack runs too deep, at which point it will delegate to {@link Task#BACKGROUND_EXECUTOR} in - * order to trim the stack. - */ - public static final Executor IMMEDIATE_EXECUTOR = Executors.IMMEDIATE; - - /** An {@link java.util.concurrent.Executor} that executes tasks on the UI thread. */ - public static final Executor UI_THREAD_EXECUTOR = Executors.UI_THREAD; - - /** - * Interface for handlers invoked when a failed {@code Task} is about to be finalized, but the - * exception has not been consumed. - * - *

The handler will execute in the GC thread, so if the handler needs to do anything time - * consuming or complex it is a good idea to fire off a {@code Task} to handle the exception. - * - * @see #getUnobservedExceptionHandler - * @see #setUnobservedExceptionHandler - */ - public interface UnobservedExceptionHandler { - /** - * Method invoked when the given task has an unobserved exception. - * - *

Any exception thrown by this method will be ignored. - * - * @param t the task - * @param e the exception - */ - void unobservedException(Task t, UnobservedTaskException e); - } - - // null unless explicitly set - private static volatile UnobservedExceptionHandler unobservedExceptionHandler; - - /** Returns the handler invoked when a task has an unobserved exception or {@code null}. */ - public static @Nullable UnobservedExceptionHandler getUnobservedExceptionHandler() { - return unobservedExceptionHandler; - } - - /** - * Set the handler invoked when a task has an unobserved exception. - * - * @param eh the object to use as an unobserved exception handler. If null then - * unobserved exceptions will be ignored. - */ - public static void setUnobservedExceptionHandler(UnobservedExceptionHandler eh) { - unobservedExceptionHandler = eh; - } - - private final Object lock = new Object(); - private boolean complete; - private boolean cancelled; - // NULLSAFE_FIXME[Field Not Initialized] - private TResult result; - // NULLSAFE_FIXME[Field Not Initialized] - private Exception error; - private boolean errorHasBeenObserved; - // NULLSAFE_FIXME[Field Not Initialized] - private UnobservedErrorNotifier unobservedErrorNotifier; - private List> continuations = new ArrayList<>(); - - /* package */ Task() {} - - private Task(TResult result) { - trySetResult(result); - } - - private Task(boolean cancelled) { - if (cancelled) { - trySetCancelled(); - } else { - // NULLSAFE_FIXME[Parameter Not Nullable] - trySetResult(null); - } - } - - public static TaskCompletionSource create() { - Task task = new Task<>(); - return new TaskCompletionSource(); - } - - /** - * @return {@code true} if the task completed (has a result, an error, or was cancelled. {@code - * false} otherwise. - */ - @Override - public boolean isCompleted() { - synchronized (lock) { - return complete; - } - } - - /** - * @return {@code true} if the task was cancelled, {@code false} otherwise. - */ - @Override - public boolean isCancelled() { - synchronized (lock) { - return cancelled; - } - } - - /** - * @return {@code true} if the task has an error, {@code false} otherwise. - */ - @Override - public boolean isFaulted() { - synchronized (lock) { - return getError() != null; - } - } - - /** - * @return The result of the task, if set. {@code null} otherwise. - */ - @Override - public TResult getResult() { - synchronized (lock) { - return result; - } - } - - /** - * @return The error for the task, if set. {@code null} otherwise. - */ - @Override - public Exception getError() { - synchronized (lock) { - if (error != null) { - errorHasBeenObserved = true; - if (unobservedErrorNotifier != null) { - unobservedErrorNotifier.setObserved(); - // NULLSAFE_FIXME[Field Not Nullable] - unobservedErrorNotifier = null; - } - } - return error; - } - } - - /** Blocks until the task is complete. */ - @Override - public void waitForCompletion() throws InterruptedException { - synchronized (lock) { - if (!isCompleted()) { - lock.wait(); - } - } - } - - /** - * Blocks until the task is complete or times out. - * - * @return {@code true} if the task completed (has a result, an error, or was cancelled). {@code - * false} otherwise. - */ - @Override - // NULLSAFE_FIXME[Inconsistent Subclass Parameter Annotation] - public boolean waitForCompletion(long duration, TimeUnit timeUnit) throws InterruptedException { - synchronized (lock) { - if (!isCompleted()) { - lock.wait(timeUnit.toMillis(duration)); - } - return isCompleted(); - } - } - - /** Creates a completed task with the given value. */ - @SuppressWarnings("unchecked") - public static Task forResult(@Nullable TResult value) { - if (value == null) { - return (Task) TASK_NULL; - } - if (value instanceof Boolean) { - return (Task) ((Boolean) value ? TASK_TRUE : TASK_FALSE); - } - TaskCompletionSource tcs = new TaskCompletionSource<>(); - tcs.setResult(value); - return tcs.getTask(); - } - - /** Creates a faulted task with the given error. */ - public static Task forError(Exception error) { - TaskCompletionSource tcs = new TaskCompletionSource<>(); - tcs.setError(error); - return tcs.getTask(); - } - - /** Creates a cancelled task. */ - @SuppressWarnings("unchecked") - public static Task cancelled() { - return (Task) TASK_CANCELLED; - } - - /** Turns a Task into a Task, dropping any result. */ - public Task makeVoid() { - return this.continueWithTask( - new Continuation>() { - @Override - public Task then(Task task) throws Exception { - if (task.isCancelled()) { - return Task.cancelled(); - } - if (task.isFaulted()) { - return Task.forError(task.getError()); - } - return Task.forResult(null); - } - }); - } - - /** Invokes the callable using the given executor, returning a Task to represent the operation. */ - public static Task call(final Callable callable, Executor executor) { - final TaskCompletionSource tcs = new TaskCompletionSource<>(); - try { - executor.execute( - new Runnable() { - @Override - public void run() { - try { - tcs.setResult(callable.call()); - } catch (CancellationException e) { - tcs.setCancelled(); - } catch (Exception e) { - tcs.setError(e); - } - } - }); - } catch (Exception e) { - tcs.setError(new ExecutorException(e)); - } - - return tcs.getTask(); - } - - /** - * Adds a continuation that will be scheduled using the executor, returning a new task that - * completes after the continuation has finished running. This allows the continuation to be - * scheduled on different thread. - */ - public Task continueWith( - final Continuation continuation, final Executor executor) { - boolean completed; - final TaskCompletionSource tcs = new TaskCompletionSource<>(); - synchronized (lock) { - completed = this.isCompleted(); - if (!completed) { - continuations.add( - new Continuation() { - @Override - public Void then(Task task) { - completeImmediately(tcs, continuation, task, executor); - return null; - } - }); - } - } - if (completed) { - completeImmediately(tcs, continuation, this, executor); - } - return tcs.getTask(); - } - - /** - * Adds a synchronous continuation to this task, returning a new task that completes after the - * continuation has finished running. - */ - public Task continueWith( - Continuation continuation) { - return continueWith(continuation, IMMEDIATE_EXECUTOR); - } - - /** - * Adds an Task-based continuation to this task that will be scheduled using the executor, - * returning a new task that completes after the task returned by the continuation has completed. - */ - public Task continueWithTask( - final Continuation> continuation, - final Executor executor) { - boolean completed; - final TaskCompletionSource tcs = new TaskCompletionSource<>(); - synchronized (lock) { - completed = this.isCompleted(); - if (!completed) { - continuations.add( - new Continuation() { - @Override - public Void then(Task task) { - completeAfterTask(tcs, continuation, task, executor); - return null; - } - }); - } - } - if (completed) { - completeAfterTask(tcs, continuation, this, executor); - } - return tcs.getTask(); - } - - /** - * Adds an asynchronous continuation to this task, returning a new task that completes after the - * task returned by the continuation has completed. - */ - public Task continueWithTask( - Continuation> continuation) { - return continueWithTask(continuation, IMMEDIATE_EXECUTOR); - } - - /** - * Runs a continuation when a task completes successfully, forwarding along {@link - * java.lang.Exception} or cancellation. - */ - public Task onSuccess( - final Continuation continuation, Executor executor) { - return continueWithTask( - new Continuation>() { - @Override - public Task then(Task task) { - if (task.isFaulted()) { - return Task.forError(task.getError()); - } else if (task.isCancelled()) { - return Task.cancelled(); - } else { - return task.continueWith(continuation); - } - } - }, - executor); - } - - /** - * Runs a continuation when a task completes successfully, forwarding along {@link - * java.lang.Exception}s or cancellation. - */ - public Task onSuccess( - final Continuation continuation) { - return onSuccess(continuation, IMMEDIATE_EXECUTOR); - } - - /** - * Runs a continuation when a task completes successfully, forwarding along {@link - * java.lang.Exception}s or cancellation. - */ - public Task onSuccessTask( - final Continuation> continuation, Executor executor) { - return continueWithTask( - new Continuation>() { - @Override - public Task then(Task task) { - if (task.isFaulted()) { - return Task.forError(task.getError()); - } else if (task.isCancelled()) { - return Task.cancelled(); - } else { - return task.continueWithTask(continuation); - } - } - }, - executor); - } - - /** - * Runs a continuation when a task completes successfully, forwarding along {@link - * java.lang.Exception}s or cancellation. - */ - public Task onSuccessTask( - final Continuation> continuation) { - return onSuccessTask(continuation, IMMEDIATE_EXECUTOR); - } - - /** - * Handles the non-async (i.e. the continuation doesn't return a Task) continuation case, passing - * the results of the given Task through to the given continuation and using the results of that - * call to set the result of the TaskContinuationSource. - * - * @param tcs The TaskContinuationSource that will be orchestrated by this call. - * @param continuation The non-async continuation. - * @param task The task being completed. - * @param executor The executor to use when running the continuation (allowing the continuation to - * be scheduled on a different thread). - */ - private static void completeImmediately( - final TaskCompletionSource tcs, - final Continuation continuation, - final Task task, - Executor executor) { - try { - executor.execute( - new Runnable() { - @Override - public void run() { - try { - TContinuationResult result = continuation.then(task); - tcs.setResult(result); - } catch (CancellationException e) { - tcs.setCancelled(); - } catch (Exception e) { - tcs.setError(e); - } - } - }); - } catch (Exception e) { - tcs.setError(new ExecutorException(e)); - } - } - - /** - * Handles the async (i.e. the continuation does return a Task) continuation case, passing the - * results of the given Task through to the given continuation to get a new Task. The - * TaskCompletionSource's results are only set when the new Task has completed, unwrapping the - * results of the task returned by the continuation. - * - * @param tcs The TaskContinuationSource that will be orchestrated by this call. - * @param continuation The async continuation. - * @param task The task being completed. - * @param executor The executor to use when running the continuation (allowing the continuation to - * be scheduled on a different thread). - */ - private static void completeAfterTask( - final TaskCompletionSource tcs, - final Continuation> continuation, - final Task task, - final Executor executor) { - try { - executor.execute( - new Runnable() { - @Override - public void run() { - try { - Task result = continuation.then(task); - if (result == null) { - tcs.setResult(null); - } else { - result.continueWith( - new Continuation() { - @Override - public Void then(Task task) { - if (task.isCancelled()) { - tcs.setCancelled(); - } else if (task.isFaulted()) { - tcs.setError(task.getError()); - } else { - tcs.setResult(task.getResult()); - } - return null; - } - }); - } - } catch (CancellationException e) { - tcs.setCancelled(); - } catch (Exception e) { - tcs.setError(e); - } - } - }); - } catch (Exception e) { - tcs.setError(new ExecutorException(e)); - } - } - - private void runContinuations() { - synchronized (lock) { - for (Continuation continuation : continuations) { - try { - continuation.then(this); - } catch (RuntimeException e) { - throw e; - } catch (Exception e) { - throw new RuntimeException(e); - } - } - // NULLSAFE_FIXME[Field Not Nullable] - continuations = null; - } - } - - /** Sets the cancelled flag on the Task if the Task hasn't already been completed. */ - /* package */ boolean trySetCancelled() { - synchronized (lock) { - if (complete) { - return false; - } - complete = true; - cancelled = true; - lock.notifyAll(); - runContinuations(); - return true; - } - } - - /** Sets the result on the Task if the Task hasn't already been completed. */ - /* package */ boolean trySetResult(TResult result) { - synchronized (lock) { - if (complete) { - return false; - } - complete = true; - Task.this.result = result; - lock.notifyAll(); - runContinuations(); - return true; - } - } - - /** Sets the error on the Task if the Task hasn't already been completed. */ - /* package */ boolean trySetError(Exception error) { - synchronized (lock) { - if (complete) { - return false; - } - complete = true; - Task.this.error = error; - errorHasBeenObserved = false; - lock.notifyAll(); - runContinuations(); - if (!errorHasBeenObserved && getUnobservedExceptionHandler() != null) - unobservedErrorNotifier = new UnobservedErrorNotifier(this); - return true; - } - } - - // NULLSAFE_FIXME[Parameter Not Nullable] - private static Task TASK_NULL = new Task<>(null); - private static Task TASK_TRUE = new Task<>((Boolean) true); - private static Task TASK_FALSE = new Task<>((Boolean) false); - private static Task TASK_CANCELLED = new Task(true); -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/internal/bolts/Task.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/internal/bolts/Task.kt new file mode 100644 index 000000000000..3a61ee3ad02c --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/internal/bolts/Task.kt @@ -0,0 +1,473 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.runtime.internal.bolts + +import com.facebook.react.interfaces.TaskInterface +import java.util.concurrent.Callable +import java.util.concurrent.CancellationException +import java.util.concurrent.Executor +import java.util.concurrent.TimeUnit + +/** + * Represents the result of an asynchronous operation. + * + * @param The type of the result of the task. + */ +public class Task : TaskInterface { + + /** + * Interface for handlers invoked when a failed `Task` is about to be finalized, but the exception + * has not been consumed. + * + * The handler will execute in the GC thread, so if the handler needs to do anything time + * consuming or complex it is a good idea to fire off a `Task` to handle the exception. + * + * @see [unobservedExceptionHandler] + */ + internal fun interface UnobservedExceptionHandler { + /** + * Method invoked when the given task has an unobserved exception. + * + * Any exception thrown by this method will be ignored. + * + * @param t the task + * @param e the exception + */ + public fun unobservedException(t: Task<*>, e: UnobservedTaskException) + } + + private val lock = Object() + private var complete = false + private var cancelled = false + + private var result: TResult? = null + + private var error: Exception? = null + private var errorHasBeenObserved = false + + private var unobservedErrorNotifier: UnobservedErrorNotifier? = null + private val continuations: MutableList> = mutableListOf() + + internal constructor() + + private constructor(result: TResult?) { + trySetResult(result) + } + + private constructor(cancelled: Boolean) { + if (cancelled) { + trySetCancelled() + } else { + trySetResult(null) + } + } + + /** + * @return `true` if the task completed (has a result, an error, or was cancelled. `false` + * otherwise. + */ + override fun isCompleted(): Boolean = + synchronized(lock) { + return complete + } + + /** @return `true` if the task was cancelled, `false` otherwise. */ + override fun isCancelled(): Boolean = + synchronized(lock) { + return cancelled + } + + /** @return `true` if the task has an error, `false` otherwise. */ + override fun isFaulted(): Boolean = + synchronized(lock) { + return getError() != null + } + + /** @return The result of the task, if set. `null` otherwise. */ + override fun getResult(): TResult? = + synchronized(lock) { + return result + } + + /** @return The error for the task, if set. `null` otherwise. */ + override fun getError(): Exception? = + synchronized(lock) { + if (error != null) { + this.errorHasBeenObserved = true + if (unobservedErrorNotifier != null) { + unobservedErrorNotifier!!.setObserved() + this.unobservedErrorNotifier = null + } + } + return error + } + + /** Blocks until the task is complete. */ + @Throws(InterruptedException::class) + override fun waitForCompletion(): Unit = + synchronized(lock) { + if (!isCompleted()) { + lock.wait() + } + } + + /** + * Blocks until the task is complete or times out. + * + * @return `true` if the task completed (has a result, an error, or was cancelled). `false` + * otherwise. + */ + @Throws(InterruptedException::class) + override fun waitForCompletion(duration: Long, timeUnit: TimeUnit): Boolean = + synchronized(lock) { + if (!isCompleted()) { + lock.wait(timeUnit.toMillis(duration)) + } + return isCompleted() + } + + /** Turns a Task into a Task, dropping any result */ + public fun makeVoid(): Task = continueWithTask { task -> + when { + task.isCancelled() -> cancelled() + task.isFaulted() -> forError(task.getError()) + else -> TASK_NULL + } + } + + /** + * Adds a continuation that will be scheduled using the executor, returning a new task that + * completes after the continuation has finished running. This allows the continuation to be + * scheduled on different thread. + */ + public fun continueWith( + continuation: Continuation, + executor: Executor + ): Task { + val completed: Boolean + val tcs = TaskCompletionSource() + synchronized(lock) { + completed = this.isCompleted() + if (!completed) { + continuations.add( + Continuation { task -> completeImmediately(tcs, continuation, task, executor) }) + } + } + if (completed) { + completeImmediately(tcs, continuation, this, executor) + } + return tcs.task + } + + /** + * Adds a synchronous continuation to this task, returning a new task that completes after the + * continuation has finished running. + */ + public fun continueWith( + continuation: Continuation + ): Task = continueWith(continuation, IMMEDIATE_EXECUTOR) + + /** + * Adds an Task-based continuation to this task that will be scheduled using the executor, + * returning a new task that completes after the task returned by the continuation has completed. + */ + public fun continueWithTask( + continuation: Continuation>, + executor: Executor + ): Task { + val completed: Boolean + val tcs = TaskCompletionSource() + synchronized(lock) { + completed = this.isCompleted() + if (!completed) { + continuations.add( + Continuation { task -> completeAfterTask(tcs, continuation, task, executor) }) + } + } + if (completed) { + completeAfterTask(tcs, continuation, this, executor) + } + return tcs.task + } + + /** + * Adds an asynchronous continuation to this task, returning a new task that completes after the + * task returned by the continuation has completed. + */ + public fun continueWithTask( + continuation: Continuation> + ): Task = continueWithTask(continuation, IMMEDIATE_EXECUTOR) + + /** + * Runs a continuation when a task completes successfully, forwarding along [java.lang.Exception] + * or cancellation. + */ + public fun onSuccess( + continuation: Continuation, + executor: Executor + ): Task = + continueWithTask( + { task -> + when { + task.isCancelled() -> cancelled() + task.isFaulted() -> forError(task.getError()) + else -> task.continueWith(continuation) + } + }, + executor) + + /** + * Runs a continuation when a task completes successfully, forwarding along [java.lang.Exception]s + * or cancellation. + */ + public fun onSuccess( + continuation: Continuation + ): Task = onSuccess(continuation, IMMEDIATE_EXECUTOR) + + /** + * Runs a continuation when a task completes successfully, forwarding along [java.lang.Exception]s + * or cancellation. + */ + public fun onSuccessTask( + continuation: Continuation>, + executor: Executor + ): Task = + continueWithTask( + { task -> + when { + task.isCancelled() -> cancelled() + task.isFaulted() -> forError(task.getError()) + else -> task.continueWithTask(continuation) + } + }, + executor) + + /** + * Runs a continuation when a task completes successfully, forwarding along [java.lang.Exception]s + * or cancellation. + */ + public fun onSuccessTask( + continuation: Continuation> + ): Task = onSuccessTask(continuation, IMMEDIATE_EXECUTOR) + + private fun runContinuations() = + synchronized(lock) { + for (continuation in continuations) { + try { + continuation.then(this) + } catch (e: RuntimeException) { + throw e + } catch (e: Exception) { + throw RuntimeException(e) + } + } + continuations.clear() + } + + /** Sets the cancelled flag on the Task if the Task hasn't already been completed. */ + internal fun trySetCancelled(): Boolean = + synchronized(lock) { + if (complete) { + return false + } + this.complete = true + this.cancelled = true + lock.notifyAll() + runContinuations() + return true + } + + /** Sets the result on the Task if the Task hasn't already been completed. */ + internal fun trySetResult(result: TResult?): Boolean = + synchronized(lock) { + if (complete) { + return false + } + this.complete = true + this.result = result + lock.notifyAll() + runContinuations() + return true + } + + /** Sets the error on the Task if the Task hasn't already been completed. */ + internal fun trySetError(error: Exception?): Boolean = + synchronized(lock) { + if (complete) { + return false + } + this.complete = true + this.error = error + this.errorHasBeenObserved = false + lock.notifyAll() + runContinuations() + if (!errorHasBeenObserved && unobservedExceptionHandler != null) + this.unobservedErrorNotifier = UnobservedErrorNotifier(this) + return true + } + + public companion object { + /** + * An [java.util.concurrent.Executor] that executes tasks in the current thread unless the stack + * runs too deep, at which point it will delegate to [Task#BACKGROUND_EXECUTOR] in order to trim + * the stack. + */ + @JvmField public val IMMEDIATE_EXECUTOR: Executor = Executors.IMMEDIATE + + /** An [java.util.concurrent.Executor] that executes tasks on the UI thread. */ + @JvmField public val UI_THREAD_EXECUTOR: Executor = Executors.UI_THREAD + + /** + * Handler invoked when a task has an unobserved exception. Null, unless explicitly set. + * + * @param eh the object to use as an unobserved exception handler. If null then + * unobserved exceptions will be ignored. + */ + internal var unobservedExceptionHandler: UnobservedExceptionHandler? = null + + @JvmStatic + internal fun create(): TaskCompletionSource { + return TaskCompletionSource() + } + + /** Creates a completed task with the given value. */ + @JvmStatic + @Suppress("UNCHECKED_CAST") + public fun forResult(value: TResult?): Task { + if (value == null) { + return TASK_NULL as Task + } + if (value is Boolean) { + return (if (value) TASK_TRUE else TASK_FALSE) as Task + } + val tcs = TaskCompletionSource() + tcs.setResult(value) + return tcs.task + } + + /** Creates a faulted task with the given error. */ + @JvmStatic + public fun forError(error: Exception?): Task { + val tcs = TaskCompletionSource() + tcs.setError(error) + return tcs.task + } + + /** Creates a cancelled task. */ + @JvmStatic + public fun cancelled(): Task { + @Suppress("UNCHECKED_CAST") + return TASK_CANCELLED as Task + } + + /** + * Invokes the callable using the given executor, returning a Task to represent the operation. + */ + @JvmStatic + public fun call(callable: Callable, executor: Executor): Task { + val tcs = TaskCompletionSource() + try { + executor.execute { + try { + tcs.setResult(callable.call()) + } catch (e: CancellationException) { + tcs.setCancelled() + } catch (e: Exception) { + tcs.setError(e) + } + } + } catch (e: Exception) { + tcs.setError(ExecutorException(e)) + } + + return tcs.task + } + + /** + * Handles the non-async (i.e. the continuation doesn't return a Task) continuation case, + * passing the results of the given Task through to the given continuation and using the results + * of that call to set the result of the TaskContinuationSource. + * + * @param tcs The TaskContinuationSource that will be orchestrated by this call. + * @param continuation The non-async continuation. + * @param task The task being completed. + * @param executor The executor to use when running the continuation (allowing the continuation + * to be scheduled on a different thread). + */ + private fun completeImmediately( + tcs: TaskCompletionSource, + continuation: Continuation, + task: Task, + executor: Executor + ) { + try { + executor.execute { + try { + val result = continuation.then(task) + tcs.setResult(result) + } catch (e: CancellationException) { + tcs.setCancelled() + } catch (e: Exception) { + tcs.setError(e) + } + } + } catch (e: Exception) { + tcs.setError(ExecutorException(e)) + } + } + + /** + * Handles the async (i.e. the continuation does return a Task) continuation case, passing the + * results of the given Task through to the given continuation to get a new Task. The + * TaskCompletionSource's results are only set when the new Task has completed, unwrapping the + * results of the task returned by the continuation. + * + * @param tcs The TaskContinuationSource that will be orchestrated by this call. + * @param continuation The async continuation. + * @param task The task being completed. + * @param executor The executor to use when running the continuation (allowing the continuation + * to be scheduled on a different thread). + */ + private fun completeAfterTask( + tcs: TaskCompletionSource, + continuation: Continuation>, + task: Task, + executor: Executor + ) { + try { + executor.execute { + try { + val result = continuation.then(task) + if (result == null) { + tcs.setResult(null) + } else { + result.continueWith { task -> + when { + task.isCancelled() -> tcs.setCancelled() + task.isFaulted() -> tcs.setError(task.getError()) + else -> tcs.setResult(task.getResult()) + } + } + } + } catch (e: CancellationException) { + tcs.setCancelled() + } catch (e: Exception) { + tcs.setError(e) + } + } + } catch (e: Exception) { + tcs.setError(ExecutorException(e)) + } + } + + private val TASK_NULL: Task = Task(null) + private val TASK_TRUE = Task(result = true) + private val TASK_FALSE = Task(result = false) + private val TASK_CANCELLED: Task = Task(cancelled = true) + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/internal/bolts/UnobservedErrorNotifier.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/internal/bolts/UnobservedErrorNotifier.kt index 550224246264..e759546a2e08 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/internal/bolts/UnobservedErrorNotifier.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/internal/bolts/UnobservedErrorNotifier.kt @@ -16,8 +16,8 @@ internal class UnobservedErrorNotifier(private var task: Task<*>?) { protected fun finalize() { task?.let { faultedTask -> - Task.getUnobservedExceptionHandler() - ?.unobservedException(faultedTask, UnobservedTaskException(faultedTask.getError())) + Task.unobservedExceptionHandler?.unobservedException( + faultedTask, UnobservedTaskException(faultedTask.getError())) } } From c8802eb2f5b6938d1167d52d0be54612fc26a832 Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Thu, 10 Apr 2025 06:16:30 -0700 Subject: [PATCH 3/3] Use @JvmOverloads in Task (#50547) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/50547 Remove duplication and instead use JvmOverloads to expose all variants to Java Changelog: [Internal] Reviewed By: alanleedev Differential Revision: D72600052 --- .../react/runtime/internal/bolts/Task.kt | 61 +++++-------------- 1 file changed, 16 insertions(+), 45 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/internal/bolts/Task.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/internal/bolts/Task.kt index 3a61ee3ad02c..7f8074f15466 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/internal/bolts/Task.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/internal/bolts/Task.kt @@ -132,22 +132,24 @@ public class Task : TaskInterface { } /** Turns a Task into a Task, dropping any result */ - public fun makeVoid(): Task = continueWithTask { task -> - when { - task.isCancelled() -> cancelled() - task.isFaulted() -> forError(task.getError()) - else -> TASK_NULL - } - } + public fun makeVoid(): Task = + continueWithTask({ task -> + when { + task.isCancelled() -> cancelled() + task.isFaulted() -> forError(task.getError()) + else -> TASK_NULL + } + }) /** * Adds a continuation that will be scheduled using the executor, returning a new task that * completes after the continuation has finished running. This allows the continuation to be * scheduled on different thread. */ + @JvmOverloads public fun continueWith( continuation: Continuation, - executor: Executor + executor: Executor = IMMEDIATE_EXECUTOR ): Task { val completed: Boolean val tcs = TaskCompletionSource() @@ -164,21 +166,14 @@ public class Task : TaskInterface { return tcs.task } - /** - * Adds a synchronous continuation to this task, returning a new task that completes after the - * continuation has finished running. - */ - public fun continueWith( - continuation: Continuation - ): Task = continueWith(continuation, IMMEDIATE_EXECUTOR) - /** * Adds an Task-based continuation to this task that will be scheduled using the executor, * returning a new task that completes after the task returned by the continuation has completed. */ + @JvmOverloads public fun continueWithTask( continuation: Continuation>, - executor: Executor + executor: Executor = IMMEDIATE_EXECUTOR, ): Task { val completed: Boolean val tcs = TaskCompletionSource() @@ -195,21 +190,13 @@ public class Task : TaskInterface { return tcs.task } - /** - * Adds an asynchronous continuation to this task, returning a new task that completes after the - * task returned by the continuation has completed. - */ - public fun continueWithTask( - continuation: Continuation> - ): Task = continueWithTask(continuation, IMMEDIATE_EXECUTOR) - /** * Runs a continuation when a task completes successfully, forwarding along [java.lang.Exception] * or cancellation. */ public fun onSuccess( continuation: Continuation, - executor: Executor + executor: Executor = IMMEDIATE_EXECUTOR, ): Task = continueWithTask( { task -> @@ -221,21 +208,13 @@ public class Task : TaskInterface { }, executor) - /** - * Runs a continuation when a task completes successfully, forwarding along [java.lang.Exception]s - * or cancellation. - */ - public fun onSuccess( - continuation: Continuation - ): Task = onSuccess(continuation, IMMEDIATE_EXECUTOR) - /** * Runs a continuation when a task completes successfully, forwarding along [java.lang.Exception]s * or cancellation. */ public fun onSuccessTask( continuation: Continuation>, - executor: Executor + executor: Executor = IMMEDIATE_EXECUTOR, ): Task = continueWithTask( { task -> @@ -247,14 +226,6 @@ public class Task : TaskInterface { }, executor) - /** - * Runs a continuation when a task completes successfully, forwarding along [java.lang.Exception]s - * or cancellation. - */ - public fun onSuccessTask( - continuation: Continuation> - ): Task = onSuccessTask(continuation, IMMEDIATE_EXECUTOR) - private fun runContinuations() = synchronized(lock) { for (continuation in continuations) { @@ -446,13 +417,13 @@ public class Task : TaskInterface { if (result == null) { tcs.setResult(null) } else { - result.continueWith { task -> + result.continueWith({ task -> when { task.isCancelled() -> tcs.setCancelled() task.isFaulted() -> tcs.setError(task.getError()) else -> tcs.setResult(task.getResult()) } - } + }) } } catch (e: CancellationException) { tcs.setCancelled()